A vibe coded tangled fork which supports pijul.
1package db
2
3import (
4 "fmt"
5 "log"
6 "strings"
7 "time"
8
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 "tangled.org/core/appview/models"
11 "tangled.org/core/orm"
12)
13
14func AddFollow(e Execer, follow *models.Follow) error {
15 query := `insert or ignore into follows (did, subject_did, rkey) values (?, ?, ?)`
16 _, err := e.Exec(query, follow.UserDid, follow.SubjectDid, follow.Rkey)
17 return err
18}
19
20// Remove a follow
21func DeleteFollow(e Execer, did, subjectDid syntax.DID) ([]syntax.ATURI, error) {
22 var deleted []syntax.ATURI
23 rows, err := e.Query(
24 `delete from follows
25 where did = ? and subject_did = ?
26 returning at_uri`,
27 did,
28 subjectDid,
29 )
30 if err != nil {
31 return nil, fmt.Errorf("deleting stars: %w", err)
32 }
33 defer rows.Close()
34
35 for rows.Next() {
36 var aturi syntax.ATURI
37 if err := rows.Scan(&aturi); err != nil {
38 return nil, fmt.Errorf("scanning at_uri: %w", err)
39 }
40 deleted = append(deleted, aturi)
41 }
42 return deleted, nil
43}
44
45// Remove a follow
46func DeleteFollowByRkey(e Execer, userDid, rkey string) error {
47 _, err := e.Exec(`delete from follows where did = ? and rkey = ?`, userDid, rkey)
48 return err
49}
50
51func GetFollowerFollowingCount(e Execer, did string) (models.FollowStats, error) {
52 var followers, following int64
53 err := e.QueryRow(
54 `SELECT
55 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers,
56 COUNT(CASE WHEN did = ? THEN 1 END) AS following
57 FROM follows;`, did, did).Scan(&followers, &following)
58 if err != nil {
59 return models.FollowStats{}, err
60 }
61 return models.FollowStats{
62 Followers: followers,
63 Following: following,
64 }, nil
65}
66
67func GetFollowerFollowingCounts(e Execer, dids []string) (map[string]models.FollowStats, error) {
68 if len(dids) == 0 {
69 return nil, nil
70 }
71
72 placeholders := make([]string, len(dids))
73 for i := range placeholders {
74 placeholders[i] = "?"
75 }
76 placeholderStr := strings.Join(placeholders, ",")
77
78 args := make([]any, len(dids)*2)
79 for i, did := range dids {
80 args[i] = did
81 args[i+len(dids)] = did
82 }
83
84 query := fmt.Sprintf(`
85 select
86 coalesce(f.did, g.did) as did,
87 coalesce(f.followers, 0) as followers,
88 coalesce(g.following, 0) as following
89 from (
90 select subject_did as did, count(*) as followers
91 from follows
92 where subject_did in (%s)
93 group by subject_did
94 ) f
95 full outer join (
96 select did as did, count(*) as following
97 from follows
98 where did in (%s)
99 group by did
100 ) g on f.did = g.did`,
101 placeholderStr, placeholderStr)
102
103 result := make(map[string]models.FollowStats)
104
105 rows, err := e.Query(query, args...)
106 if err != nil {
107 return nil, err
108 }
109 defer rows.Close()
110
111 for rows.Next() {
112 var did string
113 var followers, following int64
114 if err := rows.Scan(&did, &followers, &following); err != nil {
115 return nil, err
116 }
117 result[did] = models.FollowStats{
118 Followers: followers,
119 Following: following,
120 }
121 }
122
123 for _, did := range dids {
124 if _, exists := result[did]; !exists {
125 result[did] = models.FollowStats{
126 Followers: 0,
127 Following: 0,
128 }
129 }
130 }
131
132 return result, nil
133}
134
135func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, error) {
136 var follows []models.Follow
137
138 var conditions []string
139 var args []any
140 for _, filter := range filters {
141 conditions = append(conditions, filter.Condition())
142 args = append(args, filter.Arg()...)
143 }
144
145 whereClause := ""
146 if conditions != nil {
147 whereClause = " where " + strings.Join(conditions, " and ")
148 }
149 limitClause := ""
150 if limit > 0 {
151 limitClause = " limit ?"
152 args = append(args, limit)
153 }
154
155 query := fmt.Sprintf(
156 `select did, subject_did, created, rkey
157 from follows
158 %s
159 order by created desc
160 %s
161 `, whereClause, limitClause)
162
163 rows, err := e.Query(query, args...)
164 if err != nil {
165 return nil, err
166 }
167 defer rows.Close()
168
169 for rows.Next() {
170 var follow models.Follow
171 var followedAt string
172 err := rows.Scan(
173 &follow.UserDid,
174 &follow.SubjectDid,
175 &followedAt,
176 &follow.Rkey,
177 )
178 if err != nil {
179 return nil, err
180 }
181 followedAtTime, err := time.Parse(time.RFC3339, followedAt)
182 if err != nil {
183 log.Println("unable to determine followed at time")
184 follow.FollowedAt = time.Now()
185 } else {
186 follow.FollowedAt = followedAtTime
187 }
188 follows = append(follows, follow)
189 }
190 return follows, nil
191}
192
193func GetFollowers(e Execer, did string) ([]models.Follow, error) {
194 return GetFollows(e, 0, orm.FilterEq("subject_did", did))
195}
196
197func GetFollowing(e Execer, did string) ([]models.Follow, error) {
198 return GetFollows(e, 0, orm.FilterEq("did", did))
199}
200
201func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
202 if len(subjectDids) == 0 || userDid == "" {
203 return make(map[string]models.FollowStatus), nil
204 }
205
206 result := make(map[string]models.FollowStatus)
207
208 for _, subjectDid := range subjectDids {
209 if userDid == subjectDid {
210 result[subjectDid] = models.IsSelf
211 } else {
212 result[subjectDid] = models.IsNotFollowing
213 }
214 }
215
216 var querySubjects []string
217 for _, subjectDid := range subjectDids {
218 if userDid != subjectDid {
219 querySubjects = append(querySubjects, subjectDid)
220 }
221 }
222
223 if len(querySubjects) == 0 {
224 return result, nil
225 }
226
227 placeholders := make([]string, len(querySubjects))
228 args := make([]any, len(querySubjects)+1)
229 args[0] = userDid
230
231 for i, subjectDid := range querySubjects {
232 placeholders[i] = "?"
233 args[i+1] = subjectDid
234 }
235
236 query := fmt.Sprintf(`
237 SELECT subject_did
238 FROM follows
239 WHERE did = ? AND subject_did IN (%s)
240 `, strings.Join(placeholders, ","))
241
242 rows, err := e.Query(query, args...)
243 if err != nil {
244 return nil, err
245 }
246 defer rows.Close()
247
248 for rows.Next() {
249 var subjectDid string
250 if err := rows.Scan(&subjectDid); err != nil {
251 return nil, err
252 }
253 result[subjectDid] = models.IsFollowing
254 }
255
256 return result, nil
257}
258
259func GetFollowStatus(e Execer, userDid, subjectDid string) models.FollowStatus {
260 statuses, err := getFollowStatuses(e, userDid, []string{subjectDid})
261 if err != nil {
262 return models.IsNotFollowing
263 }
264 return statuses[subjectDid]
265}
266
267func GetFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
268 return getFollowStatuses(e, userDid, subjectDids)
269}