1package db
2
3import (
4 "fmt"
5 "log"
6 "strings"
7 "time"
8
9 "tangled.org/core/appview/models"
10)
11
12func AddFollow(e Execer, follow *models.Follow) error {
13 query := `insert or ignore into follows (user_did, subject_did, rkey) values (?, ?, ?)`
14 _, err := e.Exec(query, follow.UserDid, follow.SubjectDid, follow.Rkey)
15 return err
16}
17
18// Get a follow record
19func GetFollow(e Execer, userDid, subjectDid string) (*models.Follow, error) {
20 query := `select user_did, subject_did, followed_at, rkey from follows where user_did = ? and subject_did = ?`
21 row := e.QueryRow(query, userDid, subjectDid)
22
23 var follow models.Follow
24 var followedAt string
25 err := row.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.Rkey)
26 if err != nil {
27 return nil, err
28 }
29
30 followedAtTime, err := time.Parse(time.RFC3339, followedAt)
31 if err != nil {
32 log.Println("unable to determine followed at time")
33 follow.FollowedAt = time.Now()
34 } else {
35 follow.FollowedAt = followedAtTime
36 }
37
38 return &follow, nil
39}
40
41// Remove a follow
42func DeleteFollow(e Execer, userDid, subjectDid string) error {
43 _, err := e.Exec(`delete from follows where user_did = ? and subject_did = ?`, userDid, subjectDid)
44 return err
45}
46
47// Remove a follow
48func DeleteFollowByRkey(e Execer, userDid, rkey string) error {
49 _, err := e.Exec(`delete from follows where user_did = ? and rkey = ?`, userDid, rkey)
50 return err
51}
52
53func GetFollowerFollowingCount(e Execer, did string) (models.FollowStats, error) {
54 var followers, following int64
55 err := e.QueryRow(
56 `SELECT
57 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers,
58 COUNT(CASE WHEN user_did = ? THEN 1 END) AS following
59 FROM follows;`, did, did).Scan(&followers, &following)
60 if err != nil {
61 return models.FollowStats{}, err
62 }
63 return models.FollowStats{
64 Followers: followers,
65 Following: following,
66 }, nil
67}
68
69func GetFollowerFollowingCounts(e Execer, dids []string) (map[string]models.FollowStats, error) {
70 if len(dids) == 0 {
71 return nil, nil
72 }
73
74 placeholders := make([]string, len(dids))
75 for i := range placeholders {
76 placeholders[i] = "?"
77 }
78 placeholderStr := strings.Join(placeholders, ",")
79
80 args := make([]any, len(dids)*2)
81 for i, did := range dids {
82 args[i] = did
83 args[i+len(dids)] = did
84 }
85
86 query := fmt.Sprintf(`
87 select
88 coalesce(f.did, g.did) as did,
89 coalesce(f.followers, 0) as followers,
90 coalesce(g.following, 0) as following
91 from (
92 select subject_did as did, count(*) as followers
93 from follows
94 where subject_did in (%s)
95 group by subject_did
96 ) f
97 full outer join (
98 select user_did as did, count(*) as following
99 from follows
100 where user_did in (%s)
101 group by user_did
102 ) g on f.did = g.did`,
103 placeholderStr, placeholderStr)
104
105 result := make(map[string]models.FollowStats)
106
107 rows, err := e.Query(query, args...)
108 if err != nil {
109 return nil, err
110 }
111 defer rows.Close()
112
113 for rows.Next() {
114 var did string
115 var followers, following int64
116 if err := rows.Scan(&did, &followers, &following); err != nil {
117 return nil, err
118 }
119 result[did] = models.FollowStats{
120 Followers: followers,
121 Following: following,
122 }
123 }
124
125 for _, did := range dids {
126 if _, exists := result[did]; !exists {
127 result[did] = models.FollowStats{
128 Followers: 0,
129 Following: 0,
130 }
131 }
132 }
133
134 return result, nil
135}
136
137func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) {
138 var follows []models.Follow
139
140 var conditions []string
141 var args []any
142 for _, filter := range filters {
143 conditions = append(conditions, filter.Condition())
144 args = append(args, filter.Arg()...)
145 }
146
147 whereClause := ""
148 if conditions != nil {
149 whereClause = " where " + strings.Join(conditions, " and ")
150 }
151 limitClause := ""
152 if limit > 0 {
153 limitClause = " limit ?"
154 args = append(args, limit)
155 }
156
157 query := fmt.Sprintf(
158 `select user_did, subject_did, followed_at, rkey
159 from follows
160 %s
161 order by followed_at desc
162 %s
163 `, whereClause, limitClause)
164
165 rows, err := e.Query(query, args...)
166 if err != nil {
167 return nil, err
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, FilterEq("subject_did", did))
195}
196
197func GetFollowing(e Execer, did string) ([]models.Follow, error) {
198 return GetFollows(e, 0, FilterEq("user_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 user_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}