From bc693fa8664aa88c35c15567a1737b775cc9bf4a Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Sat, 26 Jul 2025 10:59:02 +0900 Subject: [PATCH] appview/notify: notify users mentioned in issues Change-Id: knkwrvolorspoxoqttxurpplqqxvoyxo pass mentioned DIDs on `NewIssue*` events Signed-off-by: Seongmin Lee --- appview/indexer/notifier.go | 2 +- appview/issues/issues.go | 25 +++++++++++++++++++++++-- appview/notify/db/db.go | 30 ++++++++++++++++++++++++------ appview/notify/merged_notifier.go | 8 ++++---- appview/notify/notifier.go | 9 +++++---- appview/notify/posthog/notifier.go | 6 ++++-- appview/pages/markup/markdown.go | 24 ++++++++++++++++++++++++ 7 files changed, 85 insertions(+), 19 deletions(-) diff --git a/appview/indexer/notifier.go b/appview/indexer/notifier.go index ebbdc567..e23e7e50 100644 --- a/appview/indexer/notifier.go +++ b/appview/indexer/notifier.go @@ -11,7 +11,7 @@ import ( var _ notify.Notifier = &Indexer{} -func (ix *Indexer) NewIssue(ctx context.Context, issue *models.Issue) { +func (ix *Indexer) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { l := log.FromContext(ctx).With("notifier", "indexer", "issue", issue) l.Debug("indexing new issue") err := ix.Issues.Index(ctx, *issue) diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 15b5fad1..5bf5199a 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -24,6 +24,7 @@ import ( "tangled.org/core/appview/notify" "tangled.org/core/appview/oauth" "tangled.org/core/appview/pages" + "tangled.org/core/appview/pages/markup" "tangled.org/core/appview/pagination" "tangled.org/core/appview/reporesolver" "tangled.org/core/appview/validator" @@ -453,7 +454,17 @@ func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { // notify about the new comment comment.Id = commentId - rp.notifier.NewIssueComment(r.Context(), &comment) + + rawMentions := markup.FindUserMentions(comment.Body) + idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions) + l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) + var mentions []syntax.DID + for _, ident := range idents { + if ident != nil && !ident.Handle.IsInvalidHandle() { + mentions = append(mentions, ident.DID) + } + } + rp.notifier.NewIssueComment(r.Context(), &comment, mentions) rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId)) } @@ -948,7 +959,17 @@ func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { // everything is successful, do not rollback the atproto record atUri = "" - rp.notifier.NewIssue(r.Context(), issue) + + rawMentions := markup.FindUserMentions(issue.Body) + idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions) + l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) + var mentions []syntax.DID + for _, ident := range idents { + if ident != nil && !ident.Handle.IsInvalidHandle() { + mentions = append(mentions, ident.DID) + } + } + rp.notifier.NewIssue(r.Context(), issue, mentions) rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) return } diff --git a/appview/notify/db/db.go b/appview/notify/db/db.go index ef29880f..fdf2f03f 100644 --- a/appview/notify/db/db.go +++ b/appview/notify/db/db.go @@ -64,7 +64,7 @@ func (n *databaseNotifier) DeleteStar(ctx context.Context, star *models.Star) { // no-op } -func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) { +func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { // build the recipients list // - owner of the repo @@ -81,7 +81,6 @@ func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) { } actorDid := syntax.DID(issue.Did) - eventType := models.NotificationTypeIssueCreated entityType := "issue" entityId := issue.AtUri().String() repoId := &issue.Repo.Id @@ -91,7 +90,17 @@ func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) { n.notifyEvent( actorDid, recipients, - eventType, + models.NotificationTypeIssueCreated, + entityType, + entityId, + repoId, + issueId, + pullId, + ) + n.notifyEvent( + actorDid, + mentions, + models.NotificationTypeUserMentioned, entityType, entityId, repoId, @@ -100,7 +109,7 @@ func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) { ) } -func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { +func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt)) if err != nil { log.Printf("NewIssueComment: failed to get issues: %v", err) @@ -132,7 +141,6 @@ func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models. } actorDid := syntax.DID(comment.Did) - eventType := models.NotificationTypeIssueCommented entityType := "issue" entityId := issue.AtUri().String() repoId := &issue.Repo.Id @@ -142,7 +150,17 @@ func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models. n.notifyEvent( actorDid, recipients, - eventType, + models.NotificationTypeIssueCommented, + entityType, + entityId, + repoId, + issueId, + pullId, + ) + n.notifyEvent( + actorDid, + mentions, + models.NotificationTypeUserMentioned, entityType, entityId, repoId, diff --git a/appview/notify/merged_notifier.go b/appview/notify/merged_notifier.go index 3d162815..93809863 100644 --- a/appview/notify/merged_notifier.go +++ b/appview/notify/merged_notifier.go @@ -54,12 +54,12 @@ func (m *mergedNotifier) DeleteStar(ctx context.Context, star *models.Star) { m.fanout("DeleteStar", ctx, star) } -func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue) { - m.fanout("NewIssue", ctx, issue) +func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { + m.fanout("NewIssue", ctx, issue, mentions) } -func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { - m.fanout("NewIssueComment", ctx, comment) +func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { + m.fanout("NewIssueComment", ctx, comment, mentions) } func (m *mergedNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { diff --git a/appview/notify/notifier.go b/appview/notify/notifier.go index 4b1d3d18..b7058188 100644 --- a/appview/notify/notifier.go +++ b/appview/notify/notifier.go @@ -13,8 +13,8 @@ type Notifier interface { NewStar(ctx context.Context, star *models.Star) DeleteStar(ctx context.Context, star *models.Star) - NewIssue(ctx context.Context, issue *models.Issue) - NewIssueComment(ctx context.Context, comment *models.IssueComment) + NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) + NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) DeleteIssue(ctx context.Context, issue *models.Issue) @@ -42,8 +42,9 @@ func (m *BaseNotifier) NewRepo(ctx context.Context, repo *models.Repo) {} func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {} func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} -func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {} -func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {} +func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} +func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { +} func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {} diff --git a/appview/notify/posthog/notifier.go b/appview/notify/posthog/notifier.go index 818fb257..aefd5d75 100644 --- a/appview/notify/posthog/notifier.go +++ b/appview/notify/posthog/notifier.go @@ -57,13 +57,14 @@ func (n *posthogNotifier) DeleteStar(ctx context.Context, star *models.Star) { } } -func (n *posthogNotifier) NewIssue(ctx context.Context, issue *models.Issue) { +func (n *posthogNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { err := n.client.Enqueue(posthog.Capture{ DistinctId: issue.Did, Event: "new_issue", Properties: posthog.Properties{ "repo_at": issue.RepoAt.String(), "issue_id": issue.IssueId, + "mentions": mentions, }, }) if err != nil { @@ -178,12 +179,13 @@ func (n *posthogNotifier) NewString(ctx context.Context, string *models.String) } } -func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) { +func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { err := n.client.Enqueue(posthog.Capture{ DistinctId: comment.Did, Event: "new_issue_comment", Properties: posthog.Properties{ "issue_at": comment.IssueAt, + "mentions": mentions, }, }) if err != nil { diff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go index 81bea562..4f290e44 100644 --- a/appview/pages/markup/markdown.go +++ b/appview/pages/markup/markdown.go @@ -302,6 +302,30 @@ func (rctx *RenderContext) actualPath(dst string) string { return path.Join(rctx.CurrentDir, dst) } +// FindUserMentions returns Set of user handles from given markup soruce. +// It doesn't guarntee unique DIDs +func FindUserMentions(source string) []string { + var ( + mentions []string + mentionsSet = make(map[string]struct{}) + md = NewMarkdown() + sourceBytes = []byte(source) + root = md.Parser().Parse(text.NewReader(sourceBytes)) + ) + ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering && n.Kind() == textension.KindAt { + handle := n.(*textension.AtNode).Handle + mentionsSet[handle] = struct{}{} + return ast.WalkSkipChildren, nil + } + return ast.WalkContinue, nil + }) + for handle := range mentionsSet { + mentions = append(mentions, handle) + } + return mentions +} + func isAbsoluteUrl(link string) bool { parsed, err := url.Parse(link) if err != nil { -- 2.43.0