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