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