A vibe coded tangled fork which supports pijul.
at 6cc35b6ec737371cd0815fa8e1a678e07b993973 269 lines 6.2 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 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}