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