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}