appview: parse reference links from markdown body #760

merged
opened by boltless.me targeting master from feat/mentions

Defined refResolver which will parse useful data from markdown body like @-mentions or issue/pr/comment mentions

Signed-off-by: Seongmin Lee git@boltless.me

Changed files
+470 -33
appview
+172
appview/db/reference.go
···
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "strings" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "tangled.org/core/api/tangled" 10 + "tangled.org/core/appview/models" 11 + ) 12 + 13 + // FindReferences resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 14 + // It will ignore missing refLinks. 15 + func FindReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 16 + var ( 17 + issueRefs []models.ReferenceLink 18 + pullRefs []models.ReferenceLink 19 + ) 20 + for _, ref := range refLinks { 21 + switch ref.Kind { 22 + case models.RefKindIssue: 23 + issueRefs = append(issueRefs, ref) 24 + case models.RefKindPull: 25 + pullRefs = append(pullRefs, ref) 26 + } 27 + } 28 + issueUris, err := findIssueReferences(e, issueRefs) 29 + if err != nil { 30 + return nil, err 31 + } 32 + pullUris, err := findPullReferences(e, pullRefs) 33 + if err != nil { 34 + return nil, err 35 + } 36 + 37 + return append(issueUris, pullUris...), nil 38 + } 39 + 40 + func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 41 + if len(refLinks) == 0 { 42 + return nil, nil 43 + } 44 + vals := make([]string, len(refLinks)) 45 + args := make([]any, 0, len(refLinks)*4) 46 + for i, ref := range refLinks { 47 + vals[i] = "(?, ?, ?, ?)" 48 + args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId) 49 + } 50 + query := fmt.Sprintf( 51 + `with input(owner_did, name, issue_id, comment_id) as ( 52 + values %s 53 + ) 54 + select 55 + i.did, i.rkey, 56 + c.did, c.rkey 57 + from input inp 58 + join repos r 59 + on r.did = inp.owner_did 60 + and r.name = inp.name 61 + join issues i 62 + on i.repo_at = r.at_uri 63 + and i.issue_id = inp.issue_id 64 + left join issue_comments c 65 + on inp.comment_id is not null 66 + and c.issue_at = i.at_uri 67 + and c.id = inp.comment_id 68 + `, 69 + strings.Join(vals, ","), 70 + ) 71 + rows, err := e.Query(query, args...) 72 + if err != nil { 73 + return nil, err 74 + } 75 + defer rows.Close() 76 + 77 + var uris []syntax.ATURI 78 + 79 + for rows.Next() { 80 + // Scan rows 81 + var issueOwner, issueRkey string 82 + var commentOwner, commentRkey sql.NullString 83 + var uri syntax.ATURI 84 + if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { 85 + return nil, err 86 + } 87 + if commentOwner.Valid && commentRkey.Valid { 88 + uri = syntax.ATURI(fmt.Sprintf( 89 + "at://%s/%s/%s", 90 + commentOwner.String, 91 + tangled.RepoIssueCommentNSID, 92 + commentRkey.String, 93 + )) 94 + } else { 95 + uri = syntax.ATURI(fmt.Sprintf( 96 + "at://%s/%s/%s", 97 + issueOwner, 98 + tangled.RepoIssueNSID, 99 + issueRkey, 100 + )) 101 + } 102 + uris = append(uris, uri) 103 + } 104 + return uris, nil 105 + } 106 + 107 + func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 108 + if len(refLinks) == 0 { 109 + return nil, nil 110 + } 111 + vals := make([]string, len(refLinks)) 112 + args := make([]any, 0, len(refLinks)*4) 113 + for i, ref := range refLinks { 114 + vals[i] = "(?, ?, ?, ?)" 115 + args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId) 116 + } 117 + query := fmt.Sprintf( 118 + `with input(owner_did, name, pull_id, comment_id) as ( 119 + values %s 120 + ) 121 + select 122 + p.owner_did, p.rkey, 123 + c.owner_did, c.rkey 124 + from input inp 125 + join repos r 126 + on r.did = inp.owner_did 127 + and r.name = inp.name 128 + join pulls p 129 + on p.repo_at = r.at_uri 130 + and p.pull_id = inp.pull_id 131 + left join pull_comments c 132 + on inp.comment_id is not null 133 + and c.repo_at = r.at_uri and c.pull_id = p.pull_id 134 + and c.id = inp.comment_id 135 + `, 136 + strings.Join(vals, ","), 137 + ) 138 + rows, err := e.Query(query, args...) 139 + if err != nil { 140 + return nil, err 141 + } 142 + defer rows.Close() 143 + 144 + var uris []syntax.ATURI 145 + 146 + for rows.Next() { 147 + // Scan rows 148 + var pullOwner, pullRkey string 149 + var commentOwner, commentRkey sql.NullString 150 + var uri syntax.ATURI 151 + if err := rows.Scan(&pullOwner, &pullRkey, &commentOwner, &commentRkey); err != nil { 152 + return nil, err 153 + } 154 + if commentOwner.Valid && commentRkey.Valid { 155 + uri = syntax.ATURI(fmt.Sprintf( 156 + "at://%s/%s/%s", 157 + commentOwner.String, 158 + tangled.RepoPullCommentNSID, 159 + commentRkey.String, 160 + )) 161 + } else { 162 + uri = syntax.ATURI(fmt.Sprintf( 163 + "at://%s/%s/%s", 164 + pullOwner, 165 + tangled.RepoPullNSID, 166 + pullRkey, 167 + )) 168 + } 169 + uris = append(uris, uri) 170 + } 171 + return uris, nil 172 + }
+10 -20
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/pages/markup" 28 "tangled.org/core/appview/pagination" 29 "tangled.org/core/appview/reporesolver" 30 "tangled.org/core/appview/validator" 31 "tangled.org/core/idresolver" ··· 37 repoResolver *reporesolver.RepoResolver 38 pages *pages.Pages 39 idResolver *idresolver.Resolver 40 db *db.DB 41 config *config.Config 42 notifier notify.Notifier ··· 50 repoResolver *reporesolver.RepoResolver, 51 pages *pages.Pages, 52 idResolver *idresolver.Resolver, 53 db *db.DB, 54 config *config.Config, 55 notifier notify.Notifier, ··· 62 repoResolver: repoResolver, 63 pages: pages, 64 idResolver: idResolver, 65 db: db, 66 config: config, 67 notifier: notifier, ··· 399 replyTo = &replyToUri 400 } 401 402 comment := models.IssueComment{ 403 Did: user.Did, 404 Rkey: tid.TID(), ··· 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)) ··· 884 RepoInfo: f.RepoInfo(user), 885 }) 886 case http.MethodPost: 887 issue := &models.Issue{ 888 RepoAt: f.RepoAt(), 889 Rkey: tid.TID(), 890 Title: r.FormValue("title"), 891 - Body: r.FormValue("body"), 892 Open: true, 893 Did: user.Did, 894 Created: time.Now(), ··· 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
··· 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/refresolver" 29 "tangled.org/core/appview/reporesolver" 30 "tangled.org/core/appview/validator" 31 "tangled.org/core/idresolver" ··· 37 repoResolver *reporesolver.RepoResolver 38 pages *pages.Pages 39 idResolver *idresolver.Resolver 40 + refResolver *refresolver.Resolver 41 db *db.DB 42 config *config.Config 43 notifier notify.Notifier ··· 51 repoResolver *reporesolver.RepoResolver, 52 pages *pages.Pages, 53 idResolver *idresolver.Resolver, 54 + refResolver *refresolver.Resolver, 55 db *db.DB, 56 config *config.Config, 57 notifier notify.Notifier, ··· 64 repoResolver: repoResolver, 65 pages: pages, 66 idResolver: idResolver, 67 + refResolver: refResolver, 68 db: db, 69 config: config, 70 notifier: notifier, ··· 402 replyTo = &replyToUri 403 } 404 405 + mentions, _ := rp.refResolver.Resolve(r.Context(), body) 406 + 407 comment := models.IssueComment{ 408 Did: user.Did, 409 Rkey: tid.TID(), ··· 460 // notify about the new comment 461 comment.Id = commentId 462 463 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 464 465 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId)) ··· 880 RepoInfo: f.RepoInfo(user), 881 }) 882 case http.MethodPost: 883 + body := r.FormValue("body") 884 + mentions, _ := rp.refResolver.Resolve(r.Context(), body) 885 + 886 issue := &models.Issue{ 887 RepoAt: f.RepoAt(), 888 Rkey: tid.TID(), 889 Title: r.FormValue("title"), 890 + Body: body, 891 Open: true, 892 Did: user.Did, 893 Created: time.Now(), ··· 959 // everything is successful, do not rollback the atproto record 960 atUri = "" 961 962 rp.notifier.NewIssue(r.Context(), issue, mentions) 963 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 964 return
+18
appview/models/reference.go
···
··· 1 + package models 2 + 3 + type RefKind int 4 + 5 + const ( 6 + RefKindIssue RefKind = iota 7 + RefKindPull 8 + ) 9 + 10 + // /@alice.com/cool-proj/issues/123 11 + // /@alice.com/cool-proj/issues/123#comment-321 12 + type ReferenceLink struct { 13 + Handle string 14 + Repo string 15 + Kind RefKind 16 + SubjectId int 17 + CommentId *int 18 + }
+3 -3
appview/pages/markup/extension/atlink.go
··· 16 17 // An AtNode struct represents an AtNode 18 type AtNode struct { 19 - handle string 20 ast.BaseInline 21 } 22 ··· 59 block.Advance(m[1]) 60 node := &AtNode{} 61 node.AppendChild(node, ast.NewTextSegment(atSegment)) 62 - node.handle = string(atSegment.Value(block.Source())[1:]) 63 return node 64 } 65 ··· 88 func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 89 if entering { 90 w.WriteString(`<a href="/@`) 91 - w.WriteString(n.(*AtNode).handle) 92 w.WriteString(`" class="mention">`) 93 } else { 94 w.WriteString("</a>")
··· 16 17 // An AtNode struct represents an AtNode 18 type AtNode struct { 19 + Handle string 20 ast.BaseInline 21 } 22 ··· 59 block.Advance(m[1]) 60 node := &AtNode{} 61 node.AppendChild(node, ast.NewTextSegment(atSegment)) 62 + node.Handle = string(atSegment.Value(block.Source())[1:]) 63 return node 64 } 65 ··· 88 func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 89 if entering { 90 w.WriteString(`<a href="/@`) 91 + w.WriteString(n.(*AtNode).Handle) 92 w.WriteString(`" class="mention">`) 93 } else { 94 w.WriteString("</a>")
+24
appview/pages/markup/markdown.go
··· 77 return md 78 } 79 80 func (rctx *RenderContext) RenderMarkdown(source string) string { 81 md := NewMarkdown() 82
··· 77 return md 78 } 79 80 + // FindUserMentions returns Set of user handles from given markup soruce. 81 + // It doesn't guarntee unique DIDs 82 + func FindUserMentions(source string) []string { 83 + var ( 84 + mentions []string 85 + mentionsSet = make(map[string]struct{}) 86 + md = NewMarkdown() 87 + sourceBytes = []byte(source) 88 + root = md.Parser().Parse(text.NewReader(sourceBytes)) 89 + ) 90 + ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 91 + if entering && n.Kind() == textension.KindAt { 92 + handle := n.(*textension.AtNode).Handle 93 + mentionsSet[handle] = struct{}{} 94 + return ast.WalkSkipChildren, nil 95 + } 96 + return ast.WalkContinue, nil 97 + }) 98 + for handle := range mentionsSet { 99 + mentions = append(mentions, handle) 100 + } 101 + return mentions 102 + } 103 + 104 func (rctx *RenderContext) RenderMarkdown(source string) string { 105 md := NewMarkdown() 106
+123
appview/pages/markup/reference_link.go
···
··· 1 + package markup 2 + 3 + import ( 4 + "maps" 5 + "net/url" 6 + "path" 7 + "slices" 8 + "strconv" 9 + "strings" 10 + 11 + "github.com/yuin/goldmark/ast" 12 + "github.com/yuin/goldmark/text" 13 + "tangled.org/core/appview/models" 14 + ) 15 + 16 + // FindReferences collects all links referencing tangled-related objects 17 + // like issues, PRs, comments or even @-mentions 18 + // This funciton doesn't actually check for the existence of records in the DB 19 + // or the PDS; it merely returns a list of what are presumed to be references. 20 + func FindReferences(baseUrl string, source string) ([]string, []models.ReferenceLink) { 21 + var ( 22 + refLinkSet = make(map[models.ReferenceLink]struct{}) 23 + mentionsSet = make(map[string]struct{}) 24 + md = NewMarkdown() 25 + sourceBytes = []byte(source) 26 + root = md.Parser().Parse(text.NewReader(sourceBytes)) 27 + ) 28 + // trim url scheme. the SSL shouldn't matter 29 + baseUrl = strings.TrimPrefix(baseUrl, "https://") 30 + baseUrl = strings.TrimPrefix(baseUrl, "http://") 31 + 32 + ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 33 + if !entering { 34 + return ast.WalkContinue, nil 35 + } 36 + switch n.Kind() { 37 + case KindAt: 38 + handle := n.(*AtNode).handle 39 + mentionsSet[handle] = struct{}{} 40 + return ast.WalkSkipChildren, nil 41 + case ast.KindLink: 42 + dest := string(n.(*ast.Link).Destination) 43 + ref := parseTangledLink(baseUrl, dest) 44 + if ref != nil { 45 + refLinkSet[*ref] = struct{}{} 46 + } 47 + return ast.WalkSkipChildren, nil 48 + case ast.KindAutoLink: 49 + an := n.(*ast.AutoLink) 50 + if an.AutoLinkType == ast.AutoLinkURL { 51 + dest := string(an.URL(sourceBytes)) 52 + ref := parseTangledLink(baseUrl, dest) 53 + if ref != nil { 54 + refLinkSet[*ref] = struct{}{} 55 + } 56 + } 57 + return ast.WalkSkipChildren, nil 58 + } 59 + return ast.WalkContinue, nil 60 + }) 61 + mentions := slices.Collect(maps.Keys(mentionsSet)) 62 + references := slices.Collect(maps.Keys(refLinkSet)) 63 + return mentions, references 64 + } 65 + 66 + func parseTangledLink(baseHost string, urlStr string) *models.ReferenceLink { 67 + u, err := url.Parse(urlStr) 68 + if err != nil { 69 + return nil 70 + } 71 + 72 + if u.Host != "" && !strings.EqualFold(u.Host, baseHost) { 73 + return nil 74 + } 75 + 76 + p := path.Clean(u.Path) 77 + parts := strings.FieldsFunc(p, func(r rune) bool { return r == '/' }) 78 + if len(parts) < 4 { 79 + // need at least: handle / repo / kind / id 80 + return nil 81 + } 82 + 83 + var ( 84 + handle = parts[0] 85 + repo = parts[1] 86 + kindSeg = parts[2] 87 + subjectSeg = parts[3] 88 + ) 89 + 90 + handle = strings.TrimPrefix(handle, "@") 91 + 92 + var kind models.RefKind 93 + switch kindSeg { 94 + case "issues": 95 + kind = models.RefKindIssue 96 + case "pulls": 97 + kind = models.RefKindPull 98 + default: 99 + return nil 100 + } 101 + 102 + subjectId, err := strconv.Atoi(subjectSeg) 103 + if err != nil { 104 + return nil 105 + } 106 + var commentId *int 107 + if u.Fragment != "" { 108 + if strings.HasPrefix(u.Fragment, "comment-") { 109 + commentIdStr := u.Fragment[len("comment-"):] 110 + if id, err := strconv.Atoi(commentIdStr); err == nil { 111 + commentId = &id 112 + } 113 + } 114 + } 115 + 116 + return &models.ReferenceLink{ 117 + Handle: handle, 118 + Repo: repo, 119 + Kind: kind, 120 + SubjectId: subjectId, 121 + CommentId: commentId, 122 + } 123 + }
+6 -10
appview/pulls/pulls.go
··· 23 "tangled.org/core/appview/oauth" 24 "tangled.org/core/appview/pages" 25 "tangled.org/core/appview/pages/markup" 26 "tangled.org/core/appview/reporesolver" 27 "tangled.org/core/appview/validator" 28 "tangled.org/core/appview/xrpcclient" ··· 45 repoResolver *reporesolver.RepoResolver 46 pages *pages.Pages 47 idResolver *idresolver.Resolver 48 db *db.DB 49 config *config.Config 50 notifier notify.Notifier ··· 59 repoResolver *reporesolver.RepoResolver, 60 pages *pages.Pages, 61 resolver *idresolver.Resolver, 62 db *db.DB, 63 config *config.Config, 64 notifier notify.Notifier, ··· 72 repoResolver: repoResolver, 73 pages: pages, 74 idResolver: resolver, 75 db: db, 76 config: config, 77 notifier: notifier, ··· 691 } 692 693 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 694 - l := s.logger.With("handler", "PullComment") 695 user := s.oauth.GetUser(r) 696 f, err := s.repoResolver.Resolve(r) 697 if err != nil { ··· 730 return 731 } 732 733 // Start a transaction 734 tx, err := s.db.BeginTx(r.Context(), nil) 735 if err != nil { ··· 789 return 790 } 791 792 - rawMentions := markup.FindUserMentions(comment.Body) 793 - idents := s.idResolver.ResolveIdents(r.Context(), rawMentions) 794 - l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 795 - var mentions []syntax.DID 796 - for _, ident := range idents { 797 - if ident != nil && !ident.Handle.IsInvalidHandle() { 798 - mentions = append(mentions, ident.DID) 799 - } 800 - } 801 s.notifier.NewPullComment(r.Context(), comment, mentions) 802 803 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
··· 23 "tangled.org/core/appview/oauth" 24 "tangled.org/core/appview/pages" 25 "tangled.org/core/appview/pages/markup" 26 + "tangled.org/core/appview/refresolver" 27 "tangled.org/core/appview/reporesolver" 28 "tangled.org/core/appview/validator" 29 "tangled.org/core/appview/xrpcclient" ··· 46 repoResolver *reporesolver.RepoResolver 47 pages *pages.Pages 48 idResolver *idresolver.Resolver 49 + refResolver *refresolver.Resolver 50 db *db.DB 51 config *config.Config 52 notifier notify.Notifier ··· 61 repoResolver *reporesolver.RepoResolver, 62 pages *pages.Pages, 63 resolver *idresolver.Resolver, 64 + refResolver *refresolver.Resolver, 65 db *db.DB, 66 config *config.Config, 67 notifier notify.Notifier, ··· 75 repoResolver: repoResolver, 76 pages: pages, 77 idResolver: resolver, 78 + refResolver: refResolver, 79 db: db, 80 config: config, 81 notifier: notifier, ··· 695 } 696 697 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 698 user := s.oauth.GetUser(r) 699 f, err := s.repoResolver.Resolve(r) 700 if err != nil { ··· 733 return 734 } 735 736 + mentions, _ := s.refResolver.Resolve(r.Context(), body) 737 + 738 // Start a transaction 739 tx, err := s.db.BeginTx(r.Context(), nil) 740 if err != nil { ··· 794 return 795 } 796 797 s.notifier.NewPullComment(r.Context(), comment, mentions) 798 799 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
+65
appview/refresolver/resolver.go
···
··· 1 + package refresolver 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.org/core/appview/config" 9 + "tangled.org/core/appview/db" 10 + "tangled.org/core/appview/models" 11 + "tangled.org/core/appview/pages/markup" 12 + "tangled.org/core/idresolver" 13 + ) 14 + 15 + type Resolver struct { 16 + config *config.Config 17 + idResolver *idresolver.Resolver 18 + execer db.Execer 19 + logger *slog.Logger 20 + } 21 + 22 + func New( 23 + config *config.Config, 24 + idResolver *idresolver.Resolver, 25 + execer db.Execer, 26 + logger *slog.Logger, 27 + ) *Resolver { 28 + return &Resolver{ 29 + config, 30 + idResolver, 31 + execer, 32 + logger, 33 + } 34 + } 35 + 36 + func (r *Resolver) Resolve(ctx context.Context, source string) ([]syntax.DID, []syntax.ATURI) { 37 + l := r.logger.With("method", "find_references") 38 + rawMentions, rawRefs := markup.FindReferences(r.config.Core.AppviewHost, source) 39 + l.Debug("found possible references", "mentions", rawMentions, "refs", rawRefs) 40 + idents := r.idResolver.ResolveIdents(ctx, rawMentions) 41 + var mentions []syntax.DID 42 + for _, ident := range idents { 43 + if ident != nil && !ident.Handle.IsInvalidHandle() { 44 + mentions = append(mentions, ident.DID) 45 + } 46 + } 47 + l.Debug("found mentions", "mentions", mentions) 48 + 49 + var resolvedRefs []models.ReferenceLink 50 + for _, rawRef := range rawRefs { 51 + ident, err := r.idResolver.ResolveIdent(ctx, rawRef.Handle) 52 + if err != nil || ident == nil || ident.Handle.IsInvalidHandle() { 53 + continue 54 + } 55 + rawRef.Handle = string(ident.DID) 56 + resolvedRefs = append(resolvedRefs, rawRef) 57 + } 58 + aturiRefs, err := db.FindReferences(r.execer, resolvedRefs) 59 + if err != nil { 60 + l.Error("failed running query", "err", err) 61 + } 62 + l.Debug("found references", "refs", aturiRefs) 63 + 64 + return mentions, aturiRefs 65 + }
+2
appview/state/router.go
··· 260 s.repoResolver, 261 s.pages, 262 s.idResolver, 263 s.db, 264 s.config, 265 s.notifier, ··· 276 s.repoResolver, 277 s.pages, 278 s.idResolver, 279 s.db, 280 s.config, 281 s.notifier,
··· 260 s.repoResolver, 261 s.pages, 262 s.idResolver, 263 + s.refResolver, 264 s.db, 265 s.config, 266 s.notifier, ··· 277 s.repoResolver, 278 s.pages, 279 s.idResolver, 280 + s.refResolver, 281 s.db, 282 s.config, 283 s.notifier,
+5
appview/state/state.go
··· 21 phnotify "tangled.org/core/appview/notify/posthog" 22 "tangled.org/core/appview/oauth" 23 "tangled.org/core/appview/pages" 24 "tangled.org/core/appview/reporesolver" 25 "tangled.org/core/appview/validator" 26 xrpcclient "tangled.org/core/appview/xrpcclient" ··· 49 enforcer *rbac.Enforcer 50 pages *pages.Pages 51 idResolver *idresolver.Resolver 52 posthog posthog.Client 53 jc *jetstream.JetstreamClient 54 config *config.Config ··· 98 99 repoResolver := reporesolver.New(config, enforcer, res, d) 100 101 wrapper := db.DbWrapper{Execer: d} 102 jc, err := jetstream.NewJetstreamClient( 103 config.Jetstream.Endpoint, ··· 178 enforcer, 179 pages, 180 res, 181 posthog, 182 jc, 183 config,
··· 21 phnotify "tangled.org/core/appview/notify/posthog" 22 "tangled.org/core/appview/oauth" 23 "tangled.org/core/appview/pages" 24 + "tangled.org/core/appview/refresolver" 25 "tangled.org/core/appview/reporesolver" 26 "tangled.org/core/appview/validator" 27 xrpcclient "tangled.org/core/appview/xrpcclient" ··· 50 enforcer *rbac.Enforcer 51 pages *pages.Pages 52 idResolver *idresolver.Resolver 53 + refResolver *refresolver.Resolver 54 posthog posthog.Client 55 jc *jetstream.JetstreamClient 56 config *config.Config ··· 100 101 repoResolver := reporesolver.New(config, enforcer, res, d) 102 103 + refResolver := refresolver.New(config, res, d, log.SubLogger(logger, "refResolver")) 104 + 105 wrapper := db.DbWrapper{Execer: d} 106 jc, err := jetstream.NewJetstreamClient( 107 config.Jetstream.Endpoint, ··· 182 enforcer, 183 pages, 184 res, 185 + refResolver, 186 posthog, 187 jc, 188 config,