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