Monorepo for Tangled tangled.org

appview/notify: notify users mentioned in issues

pass mentioned DIDs on `NewIssue*` events

Signed-off-by: Seongmin Lee <boltlessengineer@proton.me>

Changed files
+85 -19
appview
indexer
issues
notify
pages
markup
+1 -1
appview/indexer/notifier.go
··· 11 11 12 12 var _ notify.Notifier = &Indexer{} 13 13 14 - func (ix *Indexer) NewIssue(ctx context.Context, issue *models.Issue) { 14 + func (ix *Indexer) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 15 15 l := log.FromContext(ctx).With("notifier", "indexer", "issue", issue) 16 16 l.Debug("indexing new issue") 17 17 err := ix.Issues.Index(ctx, *issue)
+23 -2
appview/issues/issues.go
··· 24 24 "tangled.org/core/appview/notify" 25 25 "tangled.org/core/appview/oauth" 26 26 "tangled.org/core/appview/pages" 27 + "tangled.org/core/appview/pages/markup" 27 28 "tangled.org/core/appview/pagination" 28 29 "tangled.org/core/appview/reporesolver" 29 30 "tangled.org/core/appview/validator" ··· 453 454 454 455 // notify about the new comment 455 456 comment.Id = commentId 456 - rp.notifier.NewIssueComment(r.Context(), &comment) 457 + 458 + rawMentions := markup.FindUserMentions(comment.Body) 459 + idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions) 460 + l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 461 + var mentions []syntax.DID 462 + for _, ident := range idents { 463 + if ident != nil && !ident.Handle.IsInvalidHandle() { 464 + mentions = append(mentions, ident.DID) 465 + } 466 + } 467 + rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 457 468 458 469 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId)) 459 470 } ··· 948 959 949 960 // everything is successful, do not rollback the atproto record 950 961 atUri = "" 951 - rp.notifier.NewIssue(r.Context(), issue) 962 + 963 + rawMentions := markup.FindUserMentions(issue.Body) 964 + idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions) 965 + l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 966 + var mentions []syntax.DID 967 + for _, ident := range idents { 968 + if ident != nil && !ident.Handle.IsInvalidHandle() { 969 + mentions = append(mentions, ident.DID) 970 + } 971 + } 972 + rp.notifier.NewIssue(r.Context(), issue, mentions) 952 973 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 953 974 return 954 975 }
+24 -6
appview/notify/db/db.go
··· 64 64 // no-op 65 65 } 66 66 67 - func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) { 67 + func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 68 68 69 69 // build the recipients list 70 70 // - owner of the repo ··· 81 81 } 82 82 83 83 actorDid := syntax.DID(issue.Did) 84 - eventType := models.NotificationTypeIssueCreated 85 84 entityType := "issue" 86 85 entityId := issue.AtUri().String() 87 86 repoId := &issue.Repo.Id ··· 91 90 n.notifyEvent( 92 91 actorDid, 93 92 recipients, 94 - eventType, 93 + models.NotificationTypeIssueCreated, 94 + entityType, 95 + entityId, 96 + repoId, 97 + issueId, 98 + pullId, 99 + ) 100 + n.notifyEvent( 101 + actorDid, 102 + mentions, 103 + models.NotificationTypeUserMentioned, 95 104 entityType, 96 105 entityId, 97 106 repoId, ··· 100 109 ) 101 110 } 102 111 103 - func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { 112 + func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 104 113 issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt)) 105 114 if err != nil { 106 115 log.Printf("NewIssueComment: failed to get issues: %v", err) ··· 132 141 } 133 142 134 143 actorDid := syntax.DID(comment.Did) 135 - eventType := models.NotificationTypeIssueCommented 136 144 entityType := "issue" 137 145 entityId := issue.AtUri().String() 138 146 repoId := &issue.Repo.Id ··· 142 150 n.notifyEvent( 143 151 actorDid, 144 152 recipients, 145 - eventType, 153 + models.NotificationTypeIssueCommented, 154 + entityType, 155 + entityId, 156 + repoId, 157 + issueId, 158 + pullId, 159 + ) 160 + n.notifyEvent( 161 + actorDid, 162 + mentions, 163 + models.NotificationTypeUserMentioned, 146 164 entityType, 147 165 entityId, 148 166 repoId,
+4 -4
appview/notify/merged_notifier.go
··· 54 54 m.fanout("DeleteStar", ctx, star) 55 55 } 56 56 57 - func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue) { 58 - m.fanout("NewIssue", ctx, issue) 57 + func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 58 + m.fanout("NewIssue", ctx, issue, mentions) 59 59 } 60 60 61 - func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { 62 - m.fanout("NewIssueComment", ctx, comment) 61 + func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 62 + m.fanout("NewIssueComment", ctx, comment, mentions) 63 63 } 64 64 65 65 func (m *mergedNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
+5 -4
appview/notify/notifier.go
··· 13 13 NewStar(ctx context.Context, star *models.Star) 14 14 DeleteStar(ctx context.Context, star *models.Star) 15 15 16 - NewIssue(ctx context.Context, issue *models.Issue) 17 - NewIssueComment(ctx context.Context, comment *models.IssueComment) 16 + NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) 17 + NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) 18 18 NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) 19 19 DeleteIssue(ctx context.Context, issue *models.Issue) 20 20 ··· 42 42 func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {} 43 43 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 44 44 45 - func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {} 46 - func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {} 45 + func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} 46 + func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 47 + } 47 48 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 48 49 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {} 49 50
+4 -2
appview/notify/posthog/notifier.go
··· 57 57 } 58 58 } 59 59 60 - func (n *posthogNotifier) NewIssue(ctx context.Context, issue *models.Issue) { 60 + func (n *posthogNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 61 61 err := n.client.Enqueue(posthog.Capture{ 62 62 DistinctId: issue.Did, 63 63 Event: "new_issue", 64 64 Properties: posthog.Properties{ 65 65 "repo_at": issue.RepoAt.String(), 66 66 "issue_id": issue.IssueId, 67 + "mentions": mentions, 67 68 }, 68 69 }) 69 70 if err != nil { ··· 178 179 } 179 180 } 180 181 181 - func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { 182 + func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 182 183 err := n.client.Enqueue(posthog.Capture{ 183 184 DistinctId: comment.Did, 184 185 Event: "new_issue_comment", 185 186 Properties: posthog.Properties{ 186 187 "issue_at": comment.IssueAt, 188 + "mentions": mentions, 187 189 }, 188 190 }) 189 191 if err != nil {
+24
appview/pages/markup/markdown.go
··· 302 302 return path.Join(rctx.CurrentDir, dst) 303 303 } 304 304 305 + // FindUserMentions returns Set of user handles from given markup soruce. 306 + // It doesn't guarntee unique DIDs 307 + func FindUserMentions(source string) []string { 308 + var ( 309 + mentions []string 310 + mentionsSet = make(map[string]struct{}) 311 + md = NewMarkdown() 312 + sourceBytes = []byte(source) 313 + root = md.Parser().Parse(text.NewReader(sourceBytes)) 314 + ) 315 + ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 316 + if entering && n.Kind() == textension.KindAt { 317 + handle := n.(*textension.AtNode).Handle 318 + mentionsSet[handle] = struct{}{} 319 + return ast.WalkSkipChildren, nil 320 + } 321 + return ast.WalkContinue, nil 322 + }) 323 + for handle := range mentionsSet { 324 + mentions = append(mentions, handle) 325 + } 326 + return mentions 327 + } 328 + 305 329 func isAbsoluteUrl(link string) bool { 306 330 parsed, err := url.Parse(link) 307 331 if err != nil {