Monorepo for Tangled
tangled.org
1package db
2
3import (
4 "database/sql"
5 "encoding/json"
6 "fmt"
7 "log"
8 "sort"
9 "strings"
10 "time"
11
12 "github.com/bluesky-social/indigo/api/atproto"
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "tangled.org/core/api/tangled"
15 "tangled.org/core/appview/models"
16 "tangled.org/core/orm"
17)
18
19func PutComment(tx *sql.Tx, c *models.Comment, references []syntax.ATURI) error {
20 if c.Collection == "" {
21 c.Collection = tangled.FeedCommentNSID
22 }
23
24 var bodyBlobs, replyToUri, replyToCid *string
25 if len(c.Body.Blobs) > 0 {
26 encoded, err := json.Marshal(c.Body.Blobs)
27 if err != nil {
28 return fmt.Errorf("encoding blobs to json: %w", err)
29 }
30 encodedStr := string(encoded)
31 bodyBlobs = &encodedStr
32 }
33 if c.ReplyTo != nil {
34 replyToUri = &c.ReplyTo.Uri
35 replyToCid = &c.ReplyTo.Cid
36 }
37 result, err := tx.Exec(
38 // users can change the 'created' date.
39 // skip update entirely if cid is unchanged.
40 `insert into comments (
41 did,
42 collection,
43 rkey,
44 cid,
45 subject_uri,
46 subject_cid,
47 body_text,
48 body_original,
49 body_blobs,
50 created,
51 reply_to_uri,
52 reply_to_cid,
53 pull_round_idx
54 )
55 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
56 on conflict(did, collection, rkey)
57 do update set
58 cid = excluded.cid,
59 subject_uri = excluded.subject_uri,
60 subject_cid = excluded.subject_cid,
61 body_text = excluded.body_text,
62 body_original = excluded.body_original,
63 body_blobs = excluded.body_blobs,
64 created = excluded.created,
65 reply_to_uri = excluded.reply_to_uri,
66 reply_to_cid = excluded.reply_to_cid,
67 pull_round_idx = excluded.pull_round_idx,
68 edited = ?
69 where comments.cid is not excluded.cid`,
70 c.Did,
71 c.Collection,
72 c.Rkey,
73 c.Cid,
74 c.Subject.Uri,
75 c.Subject.Cid,
76 c.Body.Text,
77 c.Body.Original,
78 bodyBlobs,
79 c.Created.Format(time.RFC3339),
80 replyToUri,
81 replyToCid,
82 c.PullRoundIdx,
83 time.Now().Format(time.RFC3339),
84 )
85 if err != nil {
86 return err
87 }
88
89 c.Id, err = result.LastInsertId()
90 if err != nil {
91 return err
92 }
93
94 affected, err := result.RowsAffected()
95 if err != nil {
96 return err
97 }
98
99 if affected < 1 {
100 log.Println("record is already stored. skipping operation")
101 return nil
102 }
103
104 // update references when comment is updated
105 if err := putReferences(tx, c.AtUri(), references); err != nil {
106 return fmt.Errorf("put reference_links: %w", err)
107 }
108
109 return nil
110}
111
112// PurgeComments actually purges a comment row from db instead of marking it as "deleted"
113func PurgeComments(e Execer, filters ...orm.Filter) error {
114 var conditions []string
115 var args []any
116 for _, filter := range filters {
117 conditions = append(conditions, filter.Condition())
118 args = append(args, filter.Arg()...)
119 }
120
121 whereClause := ""
122 if conditions != nil {
123 whereClause = " where " + strings.Join(conditions, " and ")
124 }
125
126 _, err := e.Exec(fmt.Sprintf(`delete from comments %s`, whereClause), args...)
127 return err
128}
129
130func DeleteComments(e Execer, filters ...orm.Filter) error {
131 var conditions []string
132 var args []any
133 for _, filter := range filters {
134 conditions = append(conditions, filter.Condition())
135 args = append(args, filter.Arg()...)
136 }
137
138 whereClause := ""
139 if conditions != nil {
140 whereClause = " where " + strings.Join(conditions, " and ")
141 }
142
143 query := fmt.Sprintf(
144 `update comments
145 set body_text = "",
146 body_original = null,
147 body_blobs = null,
148 deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now')
149 %s`,
150 whereClause,
151 )
152
153 _, err := e.Exec(query, args...)
154 return err
155}
156
157func GetComment(e Execer, filters ...orm.Filter) (models.Comment, error) {
158 comments, err := GetComments(e, filters...)
159 if err != nil {
160 return models.Comment{}, err
161 }
162 if len(comments) != 1 {
163 return models.Comment{}, fmt.Errorf("expected 1 comment, got %d", len(comments))
164 }
165 return comments[0], nil
166}
167
168func GetComments(e Execer, filters ...orm.Filter) ([]models.Comment, error) {
169 var comments []models.Comment
170
171 var conditions []string
172 var args []any
173 for _, filter := range filters {
174 conditions = append(conditions, filter.Condition())
175 args = append(args, filter.Arg()...)
176 }
177
178 whereClause := ""
179 if conditions != nil {
180 whereClause = " where " + strings.Join(conditions, " and ")
181 }
182
183 query := fmt.Sprintf(`
184 select
185 id,
186 did,
187 collection,
188 rkey,
189 cid,
190 subject_uri,
191 subject_cid,
192 body_text,
193 body_original,
194 body_blobs,
195 created,
196 reply_to_uri,
197 reply_to_cid,
198 pull_round_idx,
199 edited,
200 deleted
201 from
202 comments
203 %s
204 `, whereClause)
205
206 rows, err := e.Query(query, args...)
207 if err != nil {
208 return nil, err
209 }
210 defer rows.Close()
211
212 for rows.Next() {
213 var comment models.Comment
214 var created string
215 var cid, bodyBlobs, replyToUri, replyToCid, edited, deleted sql.Null[string]
216 err := rows.Scan(
217 &comment.Id,
218 &comment.Did,
219 &comment.Collection,
220 &comment.Rkey,
221 &cid,
222 &comment.Subject.Uri,
223 &comment.Subject.Cid,
224 &comment.Body.Text,
225 &comment.Body.Original,
226 &bodyBlobs,
227 &created,
228 &replyToUri,
229 &replyToCid,
230 &comment.PullRoundIdx,
231 &edited,
232 &deleted,
233 )
234 if err != nil {
235 return nil, err
236 }
237
238 if cid.Valid && cid.V != "" {
239 comment.Cid = syntax.CID(cid.V)
240 }
241
242 if bodyBlobs.Valid && bodyBlobs.V != "" {
243 if err := json.Unmarshal([]byte(bodyBlobs.V), &comment.Body.Blobs); err != nil {
244 return nil, fmt.Errorf("decoding blobs: %w", err)
245 }
246 }
247
248 if t, err := time.Parse(time.RFC3339, created); err == nil {
249 comment.Created = t
250 }
251
252 if replyToUri.Valid && replyToCid.Valid {
253 comment.ReplyTo = &atproto.RepoStrongRef{
254 Uri: replyToUri.V,
255 Cid: replyToCid.V,
256 }
257 }
258
259 if edited.Valid {
260 if t, err := time.Parse(time.RFC3339, edited.V); err == nil {
261 comment.Edited = &t
262 }
263 }
264
265 if deleted.Valid {
266 if t, err := time.Parse(time.RFC3339, deleted.V); err == nil {
267 comment.Deleted = &t
268 }
269 }
270
271 comments = append(comments, comment)
272 }
273
274 if err := rows.Err(); err != nil {
275 return nil, err
276 }
277
278 sort.Slice(comments, func(i, j int) bool {
279 return comments[i].Created.Before(comments[j].Created)
280 })
281
282 return comments, nil
283}