A vibe coded tangled fork which supports pijul.
at sl/tap-appview 278 lines 6.3 kB view raw
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}