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