This repository has no description
1package db
2
3import (
4 "fmt"
5 "log"
6 "time"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "tangled.org/core/appview/models"
10 "tangled.org/core/orm"
11)
12
13func UpsertReaction(e Execer, reaction models.Reaction) error {
14 _, err := e.Exec(
15 `insert into reactions (did, rkey, subject_at, kind, created)
16 values (?, ?, ?, ?, ?)
17 on conflict(did, rkey) do update set
18 subject_at = excluded.subject_at,
19 kind = excluded.kind,
20 created = excluded.created`,
21 reaction.ReactedByDid,
22 reaction.Rkey,
23 reaction.ThreadAt,
24 reaction.Kind,
25 reaction.Created.Format(time.RFC3339),
26 )
27 return err
28}
29
30// Get a reaction record
31func GetReaction(e Execer, did string, subjectAt syntax.ATURI, kind models.ReactionKind) (*models.Reaction, error) {
32 query := `
33 select did, subject_at, created, rkey
34 from reactions
35 where did = ? and subject_at = ? and kind = ?`
36 row := e.QueryRow(query, did, subjectAt, kind)
37
38 var reaction models.Reaction
39 var created string
40 err := row.Scan(&reaction.ReactedByDid, &reaction.ThreadAt, &created, &reaction.Rkey)
41 if err != nil {
42 return nil, err
43 }
44
45 createdAtTime, err := time.Parse(time.RFC3339, created)
46 if err != nil {
47 log.Println("unable to determine followed at time")
48 reaction.Created = time.Now()
49 } else {
50 reaction.Created = createdAtTime
51 }
52
53 return &reaction, nil
54}
55
56// Remove a reaction
57func DeleteReaction(e Execer, did syntax.DID, subjectAt syntax.ATURI, kind models.ReactionKind) ([]syntax.ATURI, error) {
58 var deleted []syntax.ATURI
59 rows, err := e.Query(
60 `delete from reactions
61 where did = ? and subject_at = ? and kind = ?
62 returning at_uri`,
63 did,
64 subjectAt,
65 kind,
66 )
67 if err != nil {
68 return nil, fmt.Errorf("deleting stars: %w", err)
69 }
70 defer rows.Close()
71
72 for rows.Next() {
73 var aturi syntax.ATURI
74 if err := rows.Scan(&aturi); err != nil {
75 return nil, fmt.Errorf("scanning at_uri: %w", err)
76 }
77 deleted = append(deleted, aturi)
78 }
79 return deleted, nil
80}
81
82// Remove a reaction
83func DeleteReactionByRkey(e Execer, did string, rkey string) error {
84 _, err := e.Exec(`delete from reactions where did = ? and rkey = ?`, did, rkey)
85 return err
86}
87
88func GetReactionCount(e Execer, subjectAt syntax.ATURI) (int, error) {
89 count := 0
90 err := e.QueryRow(`select count(did) from reactions where subject_at = ?`, subjectAt).Scan(&count)
91 if err != nil {
92 return 0, err
93 }
94 return count, nil
95}
96
97func GetReactionCountByKind(e Execer, subjectAt syntax.ATURI, kind models.ReactionKind) (int, error) {
98 count := 0
99 err := e.QueryRow(
100 `select count(did) from reactions where subject_at = ? and kind = ?`, subjectAt, kind).Scan(&count)
101 if err != nil {
102 return 0, err
103 }
104 return count, nil
105}
106
107// GetReactionDisplayDataMap returns map of [models.ReactionKind]->[models.ReactionDisplayData]
108func GetReactionMap(e Execer, userLimit int, subjectAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) {
109 reactionMaps, err := ListReactionDisplayDataMap(e, []syntax.ATURI{subjectAt}, userLimit)
110 return reactionMaps[subjectAt], err
111}
112
113// ListReactionDisplayDataMap returns map of [syntax.ATURI]->[models.ReactionKind]->[models.ReactionDisplayData]
114func ListReactionDisplayDataMap(e Execer, threads []syntax.ATURI, userLimit int) (map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData, error) {
115 if len(threads) == 0 {
116 return nil, nil
117 }
118
119 filter := orm.FilterIn("subject_at", threads)
120 args := filter.Arg()
121 args = append(args, userLimit)
122 rows, err := e.Query(
123 fmt.Sprintf(
124 `with ranked_reactions as (
125 select
126 subject_at,
127 kind,
128 did,
129 row_number() over (partition by subject_at, kind order by created asc) as rn,
130 count(*) over (partition by subject_at, kind) as total
131 from reactions
132 where %s
133 )
134 select subject_at, kind, did, total
135 from ranked_reactions
136 where rn <= ?
137 order by subject_at, kind, rn asc`,
138 filter.Condition(),
139 ),
140 args...,
141 )
142 if err != nil {
143 return nil, fmt.Errorf("querying: %w", err)
144 }
145 defer rows.Close()
146
147 // aturi -> kind -> {count,users}
148 result := make(map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData)
149
150 for rows.Next() {
151 var aturi syntax.ATURI
152 var kind models.ReactionKind
153 var did syntax.DID
154 var count int
155
156 if err := rows.Scan(&aturi, &kind, &did, &count); err != nil {
157 return nil, fmt.Errorf("scanning row: %w", err)
158 }
159
160 if _, ok := result[aturi]; !ok {
161 result[aturi] = make(map[models.ReactionKind]models.ReactionDisplayData)
162 }
163 data := result[aturi][kind]
164 data.Count = count
165 data.Users = append(data.Users, did.String())
166 result[aturi][kind] = data
167 }
168
169 if err := rows.Err(); err != nil {
170 return nil, fmt.Errorf("iterate rows: %w", err)
171 }
172
173 return result, nil
174}
175
176// GetReactionStatusMap returns map of [models.ReactionKind]->[bool]
177func GetReactionStatusMap(e Execer, userDid syntax.DID, subjectAt syntax.ATURI) (map[models.ReactionKind]bool, error) {
178 reactionMaps, err := ListReactionStatusMap(e, []syntax.ATURI{subjectAt}, userDid)
179 return reactionMaps[subjectAt], err
180}
181
182// ListReactionStatusMap returns map of [syntax.ATURI]->[models.ReactionKind]->[bool]
183func ListReactionStatusMap(e Execer, threads []syntax.ATURI, userDid syntax.DID) (map[syntax.ATURI]map[models.ReactionKind]bool, error) {
184 if len(threads) == 0 {
185 return nil, nil
186 }
187
188 filter := orm.FilterIn("subject_at", threads)
189 args := []any{userDid}
190 args = append(args, filter.Arg()...)
191 rows, err := e.Query(
192 fmt.Sprintf(
193 `select subject_at, kind from reactions
194 where did = ? and %s`,
195 filter.Condition(),
196 ),
197 args...,
198 )
199 if err != nil {
200 return nil, err
201 }
202 defer rows.Close()
203
204 // aturi -> kind -> bool
205 result := make(map[syntax.ATURI]map[models.ReactionKind]bool)
206
207 for rows.Next() {
208 var aturi syntax.ATURI
209 var kind models.ReactionKind
210
211 if err := rows.Scan(&aturi, &kind); err != nil {
212 return nil, fmt.Errorf("scanning row: %w", err)
213 }
214
215 if _, ok := result[aturi]; !ok {
216 result[aturi] = make(map[models.ReactionKind]bool)
217 }
218
219 result[aturi][kind] = true
220 }
221
222 return result, nil
223}