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