This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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