appview/notify: use sets to manage participant list #874

merged
opened by oppi.li targeting master from op/tqlkrqqqlpxu
Changed files
+63 -53
appview
notify
db
+63 -53
appview/notify/db/db.go
··· 3 import ( 4 "context" 5 "log" 6 - "maps" 7 "slices" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" ··· 12 "tangled.org/core/appview/models" 13 "tangled.org/core/appview/notify" 14 "tangled.org/core/idresolver" 15 ) 16 17 const ( 18 - maxMentions = 5 19 ) 20 21 type databaseNotifier struct { ··· 49 } 50 51 actorDid := syntax.DID(star.Did) 52 - recipients := []syntax.DID{syntax.DID(repo.Did)} 53 eventType := models.NotificationTypeRepoStarred 54 entityType := "repo" 55 entityId := star.RepoAt.String() ··· 74 } 75 76 func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 77 - 78 - // build the recipients list 79 - // - owner of the repo 80 - // - collaborators in the repo 81 - var recipients []syntax.DID 82 - recipients = append(recipients, syntax.DID(issue.Repo.Did)) 83 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt())) 84 if err != nil { 85 log.Printf("failed to fetch collaborators: %v", err) 86 return 87 } 88 for _, c := range collaborators { 89 - recipients = append(recipients, c.SubjectDid) 90 } 91 92 actorDid := syntax.DID(issue.Did) ··· 108 ) 109 n.notifyEvent( 110 actorDid, 111 - mentions, 112 models.NotificationTypeUserMentioned, 113 entityType, 114 entityId, ··· 130 } 131 issue := issues[0] 132 133 - var recipients []syntax.DID 134 - recipients = append(recipients, syntax.DID(issue.Repo.Did)) 135 136 if comment.IsReply() { 137 // if this comment is a reply, then notify everybody in that thread 138 parentAtUri := *comment.ReplyTo 139 - allThreads := issue.CommentList() 140 141 // find the parent thread, and add all DIDs from here to the recipient list 142 - for _, t := range allThreads { 143 if t.Self.AtUri().String() == parentAtUri { 144 - recipients = append(recipients, t.Participants()...) 145 } 146 } 147 } else { 148 // not a reply, notify just the issue author 149 - recipients = append(recipients, syntax.DID(issue.Did)) 150 } 151 152 actorDid := syntax.DID(comment.Did) ··· 168 ) 169 n.notifyEvent( 170 actorDid, 171 - mentions, 172 models.NotificationTypeUserMentioned, 173 entityType, 174 entityId, ··· 184 185 func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) { 186 actorDid := syntax.DID(follow.UserDid) 187 - recipients := []syntax.DID{syntax.DID(follow.SubjectDid)} 188 eventType := models.NotificationTypeFollowed 189 entityType := "follow" 190 entityId := follow.UserDid ··· 216 // build the recipients list 217 // - owner of the repo 218 // - collaborators in the repo 219 - var recipients []syntax.DID 220 - recipients = append(recipients, syntax.DID(repo.Did)) 221 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt())) 222 if err != nil { 223 log.Printf("failed to fetch collaborators: %v", err) 224 return 225 } 226 for _, c := range collaborators { 227 - recipients = append(recipients, c.SubjectDid) 228 } 229 230 actorDid := syntax.DID(pull.OwnerDid) ··· 267 // build up the recipients list: 268 // - repo owner 269 // - all pull participants 270 - var recipients []syntax.DID 271 - recipients = append(recipients, syntax.DID(repo.Did)) 272 for _, p := range pull.Participants() { 273 - recipients = append(recipients, syntax.DID(p)) 274 } 275 276 actorDid := syntax.DID(comment.OwnerDid) ··· 294 ) 295 n.notifyEvent( 296 actorDid, 297 - mentions, 298 models.NotificationTypeUserMentioned, 299 entityType, 300 entityId, ··· 321 } 322 323 func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { 324 - // build up the recipients list: 325 - // - repo owner 326 - // - repo collaborators 327 - // - all issue participants 328 - var recipients []syntax.DID 329 - recipients = append(recipients, syntax.DID(issue.Repo.Did)) 330 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt())) 331 if err != nil { 332 log.Printf("failed to fetch collaborators: %v", err) 333 return 334 } 335 for _, c := range collaborators { 336 - recipients = append(recipients, c.SubjectDid) 337 } 338 for _, p := range issue.Participants() { 339 - recipients = append(recipients, syntax.DID(p)) 340 } 341 342 entityType := "pull" ··· 372 return 373 } 374 375 - // build up the recipients list: 376 - // - repo owner 377 - // - all pull participants 378 - var recipients []syntax.DID 379 - recipients = append(recipients, syntax.DID(repo.Did)) 380 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt())) 381 if err != nil { 382 log.Printf("failed to fetch collaborators: %v", err) 383 return 384 } 385 for _, c := range collaborators { 386 - recipients = append(recipients, c.SubjectDid) 387 } 388 for _, p := range pull.Participants() { 389 - recipients = append(recipients, syntax.DID(p)) 390 } 391 392 entityType := "pull" ··· 422 423 func (n *databaseNotifier) notifyEvent( 424 actorDid syntax.DID, 425 - recipients []syntax.DID, 426 eventType models.NotificationType, 427 entityType string, 428 entityId string, ··· 430 issueId *int64, 431 pullId *int64, 432 ) { 433 - if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions { 434 - recipients = recipients[:maxMentions] 435 - } 436 - recipientSet := make(map[syntax.DID]struct{}) 437 - for _, did := range recipients { 438 - // everybody except actor themselves 439 - if did != actorDid { 440 - recipientSet[did] = struct{}{} 441 - } 442 } 443 444 prefMap, err := db.GetNotificationPreferences( 445 n.db, 446 - db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))), 447 ) 448 if err != nil { 449 // failed to get prefs for users ··· 459 defer tx.Rollback() 460 461 // filter based on preferences 462 - for recipientDid := range recipientSet { 463 prefs, ok := prefMap[recipientDid] 464 if !ok { 465 prefs = models.DefaultNotificationPreferences(recipientDid)
··· 3 import ( 4 "context" 5 "log" 6 "slices" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" ··· 11 "tangled.org/core/appview/models" 12 "tangled.org/core/appview/notify" 13 "tangled.org/core/idresolver" 14 + "tangled.org/oppi.li/sets" 15 ) 16 17 const ( 18 + maxMentions = 8 19 ) 20 21 type databaseNotifier struct { ··· 49 } 50 51 actorDid := syntax.DID(star.Did) 52 + recipients := sets.Singleton(syntax.DID(repo.Did)) 53 eventType := models.NotificationTypeRepoStarred 54 entityType := "repo" 55 entityId := star.RepoAt.String() ··· 74 } 75 76 func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 77 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt())) 78 if err != nil { 79 log.Printf("failed to fetch collaborators: %v", err) 80 return 81 } 82 + 83 + // build the recipients list 84 + // - owner of the repo 85 + // - collaborators in the repo 86 + // - remove users already mentioned 87 + recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 88 for _, c := range collaborators { 89 + recipients.Insert(c.SubjectDid) 90 + } 91 + for _, m := range mentions { 92 + recipients.Remove(m) 93 } 94 95 actorDid := syntax.DID(issue.Did) ··· 111 ) 112 n.notifyEvent( 113 actorDid, 114 + sets.Collect(slices.Values(mentions)), 115 models.NotificationTypeUserMentioned, 116 entityType, 117 entityId, ··· 133 } 134 issue := issues[0] 135 136 + // built the recipients list: 137 + // - the owner of the repo 138 + // - | if the comment is a reply -> everybody on that thread 139 + // | if the comment is a top level -> just the issue owner 140 + // - remove mentioned users from the recipients list 141 + recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 142 143 if comment.IsReply() { 144 // if this comment is a reply, then notify everybody in that thread 145 parentAtUri := *comment.ReplyTo 146 147 // find the parent thread, and add all DIDs from here to the recipient list 148 + for _, t := range issue.CommentList() { 149 if t.Self.AtUri().String() == parentAtUri { 150 + for _, p := range t.Participants() { 151 + recipients.Insert(p) 152 + } 153 } 154 } 155 } else { 156 // not a reply, notify just the issue author 157 + recipients.Insert(syntax.DID(issue.Did)) 158 + } 159 + 160 + for _, m := range mentions { 161 + recipients.Remove(m) 162 } 163 164 actorDid := syntax.DID(comment.Did) ··· 180 ) 181 n.notifyEvent( 182 actorDid, 183 + sets.Collect(slices.Values(mentions)), 184 models.NotificationTypeUserMentioned, 185 entityType, 186 entityId, ··· 196 197 func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) { 198 actorDid := syntax.DID(follow.UserDid) 199 + recipients := sets.Singleton(syntax.DID(follow.SubjectDid)) 200 eventType := models.NotificationTypeFollowed 201 entityType := "follow" 202 entityId := follow.UserDid ··· 228 // build the recipients list 229 // - owner of the repo 230 // - collaborators in the repo 231 + recipients := sets.Singleton(syntax.DID(repo.Did)) 232 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt())) 233 if err != nil { 234 log.Printf("failed to fetch collaborators: %v", err) 235 return 236 } 237 for _, c := range collaborators { 238 + recipients.Insert(c.SubjectDid) 239 } 240 241 actorDid := syntax.DID(pull.OwnerDid) ··· 278 // build up the recipients list: 279 // - repo owner 280 // - all pull participants 281 + // - remove those already mentioned 282 + recipients := sets.Singleton(syntax.DID(repo.Did)) 283 for _, p := range pull.Participants() { 284 + recipients.Insert(syntax.DID(p)) 285 + } 286 + for _, m := range mentions { 287 + recipients.Remove(m) 288 } 289 290 actorDid := syntax.DID(comment.OwnerDid) ··· 308 ) 309 n.notifyEvent( 310 actorDid, 311 + sets.Collect(slices.Values(mentions)), 312 models.NotificationTypeUserMentioned, 313 entityType, 314 entityId, ··· 335 } 336 337 func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { 338 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt())) 339 if err != nil { 340 log.Printf("failed to fetch collaborators: %v", err) 341 return 342 } 343 + 344 + // build up the recipients list: 345 + // - repo owner 346 + // - repo collaborators 347 + // - all issue participants 348 + recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 349 for _, c := range collaborators { 350 + recipients.Insert(c.SubjectDid) 351 } 352 for _, p := range issue.Participants() { 353 + recipients.Insert(syntax.DID(p)) 354 } 355 356 entityType := "pull" ··· 386 return 387 } 388 389 collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt())) 390 if err != nil { 391 log.Printf("failed to fetch collaborators: %v", err) 392 return 393 } 394 + 395 + // build up the recipients list: 396 + // - repo owner 397 + // - all pull participants 398 + recipients := sets.Singleton(syntax.DID(repo.Did)) 399 for _, c := range collaborators { 400 + recipients.Insert(c.SubjectDid) 401 } 402 for _, p := range pull.Participants() { 403 + recipients.Insert(syntax.DID(p)) 404 } 405 406 entityType := "pull" ··· 436 437 func (n *databaseNotifier) notifyEvent( 438 actorDid syntax.DID, 439 + recipients sets.Set[syntax.DID], 440 eventType models.NotificationType, 441 entityType string, 442 entityId string, ··· 444 issueId *int64, 445 pullId *int64, 446 ) { 447 + // if the user is attempting to mention >maxMentions users, this is probably spam, do not mention anybody 448 + if eventType == models.NotificationTypeUserMentioned && recipients.Len() > maxMentions { 449 + return 450 } 451 452 + recipients.Remove(actorDid) 453 + 454 prefMap, err := db.GetNotificationPreferences( 455 n.db, 456 + db.FilterIn("user_did", slices.Collect(recipients.All())), 457 ) 458 if err != nil { 459 // failed to get prefs for users ··· 469 defer tx.Rollback() 470 471 // filter based on preferences 472 + for recipientDid := range recipients.All() { 473 prefs, ok := prefMap[recipientDid] 474 if !ok { 475 prefs = models.DefaultNotificationPreferences(recipientDid)