forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview/notify: notify users mentioned in issues

pass mentioned DIDs on `NewIssue*` events

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

authored by boltless.me and committed by Tangled b02199b3 47c8c79d

Changed files
+85 -19
appview
indexer
issues
notify
pages
markup
+1 -1
appview/indexer/notifier.go
··· 11 12 var _ notify.Notifier = &Indexer{} 13 14 - func (ix *Indexer) NewIssue(ctx context.Context, issue *models.Issue) { 15 l := log.FromContext(ctx).With("notifier", "indexer", "issue", issue) 16 l.Debug("indexing new issue") 17 err := ix.Issues.Index(ctx, *issue)
··· 11 12 var _ notify.Notifier = &Indexer{} 13 14 + func (ix *Indexer) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 15 l := log.FromContext(ctx).With("notifier", "indexer", "issue", issue) 16 l.Debug("indexing new issue") 17 err := ix.Issues.Index(ctx, *issue)
+23 -2
appview/issues/issues.go
··· 24 "tangled.org/core/appview/notify" 25 "tangled.org/core/appview/oauth" 26 "tangled.org/core/appview/pages" 27 "tangled.org/core/appview/pagination" 28 "tangled.org/core/appview/reporesolver" 29 "tangled.org/core/appview/validator" ··· 453 454 // notify about the new comment 455 comment.Id = commentId 456 - rp.notifier.NewIssueComment(r.Context(), &comment) 457 458 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId)) 459 } ··· 948 949 // everything is successful, do not rollback the atproto record 950 atUri = "" 951 - rp.notifier.NewIssue(r.Context(), issue) 952 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 953 return 954 }
··· 24 "tangled.org/core/appview/notify" 25 "tangled.org/core/appview/oauth" 26 "tangled.org/core/appview/pages" 27 + "tangled.org/core/appview/pages/markup" 28 "tangled.org/core/appview/pagination" 29 "tangled.org/core/appview/reporesolver" 30 "tangled.org/core/appview/validator" ··· 454 455 // notify about the new comment 456 comment.Id = commentId 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) 468 469 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId)) 470 } ··· 959 960 // everything is successful, do not rollback the atproto record 961 atUri = "" 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) 973 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 974 return 975 }
+24 -6
appview/notify/db/db.go
··· 64 // no-op 65 } 66 67 - func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) { 68 69 // build the recipients list 70 // - owner of the repo ··· 81 } 82 83 actorDid := syntax.DID(issue.Did) 84 - eventType := models.NotificationTypeIssueCreated 85 entityType := "issue" 86 entityId := issue.AtUri().String() 87 repoId := &issue.Repo.Id ··· 91 n.notifyEvent( 92 actorDid, 93 recipients, 94 - eventType, 95 entityType, 96 entityId, 97 repoId, ··· 100 ) 101 } 102 103 - func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { 104 issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt)) 105 if err != nil { 106 log.Printf("NewIssueComment: failed to get issues: %v", err) ··· 132 } 133 134 actorDid := syntax.DID(comment.Did) 135 - eventType := models.NotificationTypeIssueCommented 136 entityType := "issue" 137 entityId := issue.AtUri().String() 138 repoId := &issue.Repo.Id ··· 142 n.notifyEvent( 143 actorDid, 144 recipients, 145 - eventType, 146 entityType, 147 entityId, 148 repoId,
··· 64 // no-op 65 } 66 67 + func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 68 69 // build the recipients list 70 // - owner of the repo ··· 81 } 82 83 actorDid := syntax.DID(issue.Did) 84 entityType := "issue" 85 entityId := issue.AtUri().String() 86 repoId := &issue.Repo.Id ··· 90 n.notifyEvent( 91 actorDid, 92 recipients, 93 + models.NotificationTypeIssueCreated, 94 + entityType, 95 + entityId, 96 + repoId, 97 + issueId, 98 + pullId, 99 + ) 100 + n.notifyEvent( 101 + actorDid, 102 + mentions, 103 + models.NotificationTypeUserMentioned, 104 entityType, 105 entityId, 106 repoId, ··· 109 ) 110 } 111 112 + func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 113 issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt)) 114 if err != nil { 115 log.Printf("NewIssueComment: failed to get issues: %v", err) ··· 141 } 142 143 actorDid := syntax.DID(comment.Did) 144 entityType := "issue" 145 entityId := issue.AtUri().String() 146 repoId := &issue.Repo.Id ··· 150 n.notifyEvent( 151 actorDid, 152 recipients, 153 + models.NotificationTypeIssueCommented, 154 + entityType, 155 + entityId, 156 + repoId, 157 + issueId, 158 + pullId, 159 + ) 160 + n.notifyEvent( 161 + actorDid, 162 + mentions, 163 + models.NotificationTypeUserMentioned, 164 entityType, 165 entityId, 166 repoId,
+4 -4
appview/notify/merged_notifier.go
··· 54 m.fanout("DeleteStar", ctx, star) 55 } 56 57 - func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue) { 58 - m.fanout("NewIssue", ctx, issue) 59 } 60 61 - func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { 62 - m.fanout("NewIssueComment", ctx, comment) 63 } 64 65 func (m *mergedNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
··· 54 m.fanout("DeleteStar", ctx, star) 55 } 56 57 + func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 58 + m.fanout("NewIssue", ctx, issue, mentions) 59 } 60 61 + func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 62 + m.fanout("NewIssueComment", ctx, comment, mentions) 63 } 64 65 func (m *mergedNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
+5 -4
appview/notify/notifier.go
··· 13 NewStar(ctx context.Context, star *models.Star) 14 DeleteStar(ctx context.Context, star *models.Star) 15 16 - NewIssue(ctx context.Context, issue *models.Issue) 17 - NewIssueComment(ctx context.Context, comment *models.IssueComment) 18 NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) 19 DeleteIssue(ctx context.Context, issue *models.Issue) 20 ··· 42 func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {} 43 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 44 45 - func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {} 46 - func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {} 47 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 48 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {} 49
··· 13 NewStar(ctx context.Context, star *models.Star) 14 DeleteStar(ctx context.Context, star *models.Star) 15 16 + NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) 17 + NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) 18 NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) 19 DeleteIssue(ctx context.Context, issue *models.Issue) 20 ··· 42 func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {} 43 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 44 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 + } 48 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 49 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {} 50
+4 -2
appview/notify/posthog/notifier.go
··· 57 } 58 } 59 60 - func (n *posthogNotifier) NewIssue(ctx context.Context, issue *models.Issue) { 61 err := n.client.Enqueue(posthog.Capture{ 62 DistinctId: issue.Did, 63 Event: "new_issue", 64 Properties: posthog.Properties{ 65 "repo_at": issue.RepoAt.String(), 66 "issue_id": issue.IssueId, 67 }, 68 }) 69 if err != nil { ··· 178 } 179 } 180 181 - func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { 182 err := n.client.Enqueue(posthog.Capture{ 183 DistinctId: comment.Did, 184 Event: "new_issue_comment", 185 Properties: posthog.Properties{ 186 "issue_at": comment.IssueAt, 187 }, 188 }) 189 if err != nil {
··· 57 } 58 } 59 60 + func (n *posthogNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 61 err := n.client.Enqueue(posthog.Capture{ 62 DistinctId: issue.Did, 63 Event: "new_issue", 64 Properties: posthog.Properties{ 65 "repo_at": issue.RepoAt.String(), 66 "issue_id": issue.IssueId, 67 + "mentions": mentions, 68 }, 69 }) 70 if err != nil { ··· 179 } 180 } 181 182 + func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 183 err := n.client.Enqueue(posthog.Capture{ 184 DistinctId: comment.Did, 185 Event: "new_issue_comment", 186 Properties: posthog.Properties{ 187 "issue_at": comment.IssueAt, 188 + "mentions": mentions, 189 }, 190 }) 191 if err != nil {
+24
appview/pages/markup/markdown.go
··· 302 return path.Join(rctx.CurrentDir, dst) 303 } 304 305 func isAbsoluteUrl(link string) bool { 306 parsed, err := url.Parse(link) 307 if err != nil {
··· 302 return path.Join(rctx.CurrentDir, dst) 303 } 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 + 329 func isAbsoluteUrl(link string) bool { 330 parsed, err := url.Parse(link) 331 if err != nil {