this repo has no description
1package db
2
3import (
4 "context"
5 "log"
6 "maps"
7 "slices"
8
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 "tangled.org/core/appview/db"
11 "tangled.org/core/appview/models"
12 "tangled.org/core/appview/notify"
13 "tangled.org/core/idresolver"
14)
15
16type databaseNotifier struct {
17 db *db.DB
18 res *idresolver.Resolver
19}
20
21func NewDatabaseNotifier(database *db.DB, resolver *idresolver.Resolver) notify.Notifier {
22 return &databaseNotifier{
23 db: database,
24 res: resolver,
25 }
26}
27
28var _ notify.Notifier = &databaseNotifier{}
29
30func (n *databaseNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
31 // no-op for now
32}
33
34func (n *databaseNotifier) NewStar(ctx context.Context, star *models.Star) {
35 var err error
36 repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt)))
37 if err != nil {
38 log.Printf("NewStar: failed to get repos: %v", err)
39 return
40 }
41
42 actorDid := syntax.DID(star.StarredByDid)
43 recipients := []syntax.DID{syntax.DID(repo.Did)}
44 eventType := models.NotificationTypeRepoStarred
45 entityType := "repo"
46 entityId := star.RepoAt.String()
47 repoId := &repo.Id
48 var issueId *int64
49 var pullId *int64
50
51 n.notifyEvent(
52 actorDid,
53 recipients,
54 eventType,
55 entityType,
56 entityId,
57 repoId,
58 issueId,
59 pullId,
60 )
61}
62
63func (n *databaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {
64 // no-op
65}
66
67func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
68
69 // build the recipients list
70 // - owner of the repo
71 // - collaborators in the repo
72 var recipients []syntax.DID
73 recipients = append(recipients, syntax.DID(issue.Repo.Did))
74 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
75 if err != nil {
76 log.Printf("failed to fetch collaborators: %v", err)
77 return
78 }
79 for _, c := range collaborators {
80 recipients = append(recipients, c.SubjectDid)
81 }
82
83 actorDid := syntax.DID(issue.Did)
84 eventType := models.NotificationTypeIssueCreated
85 entityType := "issue"
86 entityId := issue.AtUri().String()
87 repoId := &issue.Repo.Id
88 issueId := &issue.Id
89 var pullId *int64
90
91 n.notifyEvent(
92 actorDid,
93 recipients,
94 eventType,
95 entityType,
96 entityId,
97 repoId,
98 issueId,
99 pullId,
100 )
101}
102
103func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {
104 issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt))
105 if err != nil {
106 log.Printf("NewIssueComment: failed to get issues: %v", err)
107 return
108 }
109 if len(issues) == 0 {
110 log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt)
111 return
112 }
113 issue := issues[0]
114
115 var recipients []syntax.DID
116 recipients = append(recipients, syntax.DID(issue.Repo.Did))
117
118 if comment.IsReply() {
119 // if this comment is a reply, then notify everybody in that thread
120 parentAtUri := *comment.ReplyTo
121 allThreads := issue.CommentList()
122
123 // find the parent thread, and add all DIDs from here to the recipient list
124 for _, t := range allThreads {
125 if t.Self.AtUri().String() == parentAtUri {
126 recipients = append(recipients, t.Participants()...)
127 }
128 }
129 } else {
130 // not a reply, notify just the issue author
131 recipients = append(recipients, syntax.DID(issue.Did))
132 }
133
134 actorDid := syntax.DID(comment.Did)
135 eventType := models.NotificationTypeIssueCommented
136 entityType := "issue"
137 entityId := issue.AtUri().String()
138 repoId := &issue.Repo.Id
139 issueId := &issue.Id
140 var pullId *int64
141
142 n.notifyEvent(
143 actorDid,
144 recipients,
145 eventType,
146 entityType,
147 entityId,
148 repoId,
149 issueId,
150 pullId,
151 )
152}
153
154func (n *databaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {
155 // no-op for now
156}
157
158func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
159 actorDid := syntax.DID(follow.UserDid)
160 recipients := []syntax.DID{syntax.DID(follow.SubjectDid)}
161 eventType := models.NotificationTypeFollowed
162 entityType := "follow"
163 entityId := follow.UserDid
164 var repoId, issueId, pullId *int64
165
166 n.notifyEvent(
167 actorDid,
168 recipients,
169 eventType,
170 entityType,
171 entityId,
172 repoId,
173 issueId,
174 pullId,
175 )
176}
177
178func (n *databaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
179 // no-op
180}
181
182func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
183 repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
184 if err != nil {
185 log.Printf("NewPull: failed to get repos: %v", err)
186 return
187 }
188
189 // build the recipients list
190 // - owner of the repo
191 // - collaborators in the repo
192 var recipients []syntax.DID
193 recipients = append(recipients, syntax.DID(repo.Did))
194 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
195 if err != nil {
196 log.Printf("failed to fetch collaborators: %v", err)
197 return
198 }
199 for _, c := range collaborators {
200 recipients = append(recipients, c.SubjectDid)
201 }
202
203 actorDid := syntax.DID(pull.OwnerDid)
204 eventType := models.NotificationTypePullCreated
205 entityType := "pull"
206 entityId := pull.PullAt().String()
207 repoId := &repo.Id
208 var issueId *int64
209 p := int64(pull.ID)
210 pullId := &p
211
212 n.notifyEvent(
213 actorDid,
214 recipients,
215 eventType,
216 entityType,
217 entityId,
218 repoId,
219 issueId,
220 pullId,
221 )
222}
223
224func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) {
225 pull, err := db.GetPull(n.db,
226 syntax.ATURI(comment.RepoAt),
227 comment.PullId,
228 )
229 if err != nil {
230 log.Printf("NewPullComment: failed to get pulls: %v", err)
231 return
232 }
233
234 repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
235 if err != nil {
236 log.Printf("NewPullComment: failed to get repos: %v", err)
237 return
238 }
239
240 // build up the recipients list:
241 // - repo owner
242 // - all pull participants
243 var recipients []syntax.DID
244 recipients = append(recipients, syntax.DID(repo.Did))
245 for _, p := range pull.Participants() {
246 recipients = append(recipients, syntax.DID(p))
247 }
248
249 actorDid := syntax.DID(comment.OwnerDid)
250 eventType := models.NotificationTypePullCommented
251 entityType := "pull"
252 entityId := pull.PullAt().String()
253 repoId := &repo.Id
254 var issueId *int64
255 p := int64(pull.ID)
256 pullId := &p
257
258 n.notifyEvent(
259 actorDid,
260 recipients,
261 eventType,
262 entityType,
263 entityId,
264 repoId,
265 issueId,
266 pullId,
267 )
268}
269
270func (n *databaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
271 // no-op
272}
273
274func (n *databaseNotifier) DeleteString(ctx context.Context, did, rkey string) {
275 // no-op
276}
277
278func (n *databaseNotifier) EditString(ctx context.Context, string *models.String) {
279 // no-op
280}
281
282func (n *databaseNotifier) NewString(ctx context.Context, string *models.String) {
283 // no-op
284}
285
286func (n *databaseNotifier) NewIssueClosed(ctx context.Context, issue *models.Issue) {
287 // build up the recipients list:
288 // - repo owner
289 // - repo collaborators
290 // - all issue participants
291 var recipients []syntax.DID
292 recipients = append(recipients, syntax.DID(issue.Repo.Did))
293 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
294 if err != nil {
295 log.Printf("failed to fetch collaborators: %v", err)
296 return
297 }
298 for _, c := range collaborators {
299 recipients = append(recipients, c.SubjectDid)
300 }
301 for _, p := range issue.Participants() {
302 recipients = append(recipients, syntax.DID(p))
303 }
304
305 actorDid := syntax.DID(issue.Repo.Did)
306 eventType := models.NotificationTypeIssueClosed
307 entityType := "pull"
308 entityId := issue.AtUri().String()
309 repoId := &issue.Repo.Id
310 issueId := &issue.Id
311 var pullId *int64
312
313 n.notifyEvent(
314 actorDid,
315 recipients,
316 eventType,
317 entityType,
318 entityId,
319 repoId,
320 issueId,
321 pullId,
322 )
323}
324
325func (n *databaseNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
326 // Get repo details
327 repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
328 if err != nil {
329 log.Printf("NewPullMerged: failed to get repos: %v", err)
330 return
331 }
332
333 // build up the recipients list:
334 // - repo owner
335 // - all pull participants
336 var recipients []syntax.DID
337 recipients = append(recipients, syntax.DID(repo.Did))
338 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
339 if err != nil {
340 log.Printf("failed to fetch collaborators: %v", err)
341 return
342 }
343 for _, c := range collaborators {
344 recipients = append(recipients, c.SubjectDid)
345 }
346 for _, p := range pull.Participants() {
347 recipients = append(recipients, syntax.DID(p))
348 }
349
350 actorDid := syntax.DID(repo.Did)
351 eventType := models.NotificationTypePullMerged
352 entityType := "pull"
353 entityId := pull.PullAt().String()
354 repoId := &repo.Id
355 var issueId *int64
356 p := int64(pull.ID)
357 pullId := &p
358
359 n.notifyEvent(
360 actorDid,
361 recipients,
362 eventType,
363 entityType,
364 entityId,
365 repoId,
366 issueId,
367 pullId,
368 )
369}
370
371func (n *databaseNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
372 // Get repo details
373 repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
374 if err != nil {
375 log.Printf("NewPullMerged: failed to get repos: %v", err)
376 return
377 }
378
379 // build up the recipients list:
380 // - repo owner
381 // - all pull participants
382 var recipients []syntax.DID
383 recipients = append(recipients, syntax.DID(repo.Did))
384 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
385 if err != nil {
386 log.Printf("failed to fetch collaborators: %v", err)
387 return
388 }
389 for _, c := range collaborators {
390 recipients = append(recipients, c.SubjectDid)
391 }
392 for _, p := range pull.Participants() {
393 recipients = append(recipients, syntax.DID(p))
394 }
395
396 actorDid := syntax.DID(repo.Did)
397 eventType := models.NotificationTypePullClosed
398 entityType := "pull"
399 entityId := pull.PullAt().String()
400 repoId := &repo.Id
401 var issueId *int64
402 p := int64(pull.ID)
403 pullId := &p
404
405 n.notifyEvent(
406 actorDid,
407 recipients,
408 eventType,
409 entityType,
410 entityId,
411 repoId,
412 issueId,
413 pullId,
414 )
415}
416
417func (n *databaseNotifier) notifyEvent(
418 actorDid syntax.DID,
419 recipients []syntax.DID,
420 eventType models.NotificationType,
421 entityType string,
422 entityId string,
423 repoId *int64,
424 issueId *int64,
425 pullId *int64,
426) {
427 recipientSet := make(map[syntax.DID]struct{})
428 for _, did := range recipients {
429 // everybody except actor themselves
430 if did != actorDid {
431 recipientSet[did] = struct{}{}
432 }
433 }
434
435 prefMap, err := db.GetNotificationPreferences(
436 n.db,
437 db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
438 )
439 if err != nil {
440 // failed to get prefs for users
441 return
442 }
443
444 // create a transaction for bulk notification storage
445 tx, err := n.db.Begin()
446 if err != nil {
447 // failed to start tx
448 return
449 }
450 defer tx.Rollback()
451
452 // filter based on preferences
453 for recipientDid := range recipientSet {
454 prefs, ok := prefMap[recipientDid]
455 if !ok {
456 prefs = models.DefaultNotificationPreferences(recipientDid)
457 }
458
459 // skip users who don’t want this type
460 if !prefs.ShouldNotify(eventType) {
461 continue
462 }
463
464 // create notification
465 notif := &models.Notification{
466 RecipientDid: recipientDid.String(),
467 ActorDid: actorDid.String(),
468 Type: eventType,
469 EntityType: entityType,
470 EntityId: entityId,
471 RepoId: repoId,
472 IssueId: issueId,
473 PullId: pullId,
474 }
475
476 if err := db.CreateNotification(tx, notif); err != nil {
477 log.Printf("notifyEvent: failed to create notification for %s: %v", recipientDid, err)
478 }
479 }
480
481 if err := tx.Commit(); err != nil {
482 // failed to commit
483 return
484 }
485}