forked from tangled.org/core
Monorepo for Tangled

appview: parse reference links from markdown body

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>

authored by boltless.me and committed by Tangled d2dcc711 4b0a917e

Changed files
+444 -54
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
··· 23 "tangled.org/core/appview/notify" 24 "tangled.org/core/appview/oauth" 25 "tangled.org/core/appview/pages" 26 - "tangled.org/core/appview/pages/markup" 27 "tangled.org/core/appview/pages/repoinfo" 28 "tangled.org/core/appview/pagination" 29 "tangled.org/core/appview/reporesolver" 30 "tangled.org/core/appview/validator" 31 "tangled.org/core/idresolver" ··· 39 enforcer *rbac.Enforcer 40 pages *pages.Pages 41 idResolver *idresolver.Resolver 42 db *db.DB 43 config *config.Config 44 notifier notify.Notifier ··· 53 enforcer *rbac.Enforcer, 54 pages *pages.Pages, 55 idResolver *idresolver.Resolver, 56 db *db.DB, 57 config *config.Config, 58 notifier notify.Notifier, ··· 66 enforcer: enforcer, 67 pages: pages, 68 idResolver: idResolver, 69 db: db, 70 config: config, 71 notifier: notifier, ··· 391 replyTo = &replyToUri 392 } 393 394 comment := models.IssueComment{ 395 Did: user.Did, 396 Rkey: tid.TID(), ··· 447 // notify about the new comment 448 comment.Id = commentId 449 450 - rawMentions := markup.FindUserMentions(comment.Body) 451 - idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions) 452 - l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 453 - var mentions []syntax.DID 454 - for _, ident := range idents { 455 - if ident != nil && !ident.Handle.IsInvalidHandle() { 456 - mentions = append(mentions, ident.DID) 457 - } 458 - } 459 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 460 461 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 870 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 871 }) 872 case http.MethodPost: 873 issue := &models.Issue{ 874 RepoAt: f.RepoAt(), 875 Rkey: tid.TID(), 876 Title: r.FormValue("title"), 877 - Body: r.FormValue("body"), 878 Open: true, 879 Did: user.Did, 880 Created: time.Now(), ··· 946 // everything is successful, do not rollback the atproto record 947 atUri = "" 948 949 - rawMentions := markup.FindUserMentions(issue.Body) 950 - idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions) 951 - l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 952 - var mentions []syntax.DID 953 - for _, ident := range idents { 954 - if ident != nil && !ident.Handle.IsInvalidHandle() { 955 - mentions = append(mentions, ident.DID) 956 - } 957 - } 958 rp.notifier.NewIssue(r.Context(), issue, mentions) 959 960 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
··· 23 "tangled.org/core/appview/notify" 24 "tangled.org/core/appview/oauth" 25 "tangled.org/core/appview/pages" 26 "tangled.org/core/appview/pages/repoinfo" 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" ··· 39 enforcer *rbac.Enforcer 40 pages *pages.Pages 41 idResolver *idresolver.Resolver 42 + refResolver *refresolver.Resolver 43 db *db.DB 44 config *config.Config 45 notifier notify.Notifier ··· 54 enforcer *rbac.Enforcer, 55 pages *pages.Pages, 56 idResolver *idresolver.Resolver, 57 + refResolver *refresolver.Resolver, 58 db *db.DB, 59 config *config.Config, 60 notifier notify.Notifier, ··· 68 enforcer: enforcer, 69 pages: pages, 70 idResolver: idResolver, 71 + refResolver: refResolver, 72 db: db, 73 config: config, 74 notifier: notifier, ··· 394 replyTo = &replyToUri 395 } 396 397 + mentions, _ := rp.refResolver.Resolve(r.Context(), body) 398 + 399 comment := models.IssueComment{ 400 Did: user.Did, 401 Rkey: tid.TID(), ··· 452 // notify about the new comment 453 comment.Id = commentId 454 455 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 456 457 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 866 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 867 }) 868 case http.MethodPost: 869 + body := r.FormValue("body") 870 + mentions, _ := rp.refResolver.Resolve(r.Context(), body) 871 + 872 issue := &models.Issue{ 873 RepoAt: f.RepoAt(), 874 Rkey: tid.TID(), 875 Title: r.FormValue("title"), 876 + Body: body, 877 Open: true, 878 Did: user.Did, 879 Created: time.Now(), ··· 945 // everything is successful, do not rollback the atproto record 946 atUri = "" 947 948 rp.notifier.NewIssue(r.Context(), issue, mentions) 949 950 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
+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 + }
-24
appview/pages/markup/markdown.go
··· 304 return path.Join(rctx.CurrentDir, dst) 305 } 306 307 - // FindUserMentions returns Set of user handles from given markup soruce. 308 - // It doesn't guarntee unique DIDs 309 - func FindUserMentions(source string) []string { 310 - var ( 311 - mentions []string 312 - mentionsSet = make(map[string]struct{}) 313 - md = NewMarkdown() 314 - sourceBytes = []byte(source) 315 - root = md.Parser().Parse(text.NewReader(sourceBytes)) 316 - ) 317 - ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 318 - if entering && n.Kind() == textension.KindAt { 319 - handle := n.(*textension.AtNode).Handle 320 - mentionsSet[handle] = struct{}{} 321 - return ast.WalkSkipChildren, nil 322 - } 323 - return ast.WalkContinue, nil 324 - }) 325 - for handle := range mentionsSet { 326 - mentions = append(mentions, handle) 327 - } 328 - return mentions 329 - } 330 - 331 func isAbsoluteUrl(link string) bool { 332 parsed, err := url.Parse(link) 333 if err != nil {
··· 304 return path.Join(rctx.CurrentDir, dst) 305 } 306 307 func isAbsoluteUrl(link string) bool { 308 parsed, err := url.Parse(link) 309 if err != nil {
+124
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 + textension "tangled.org/core/appview/pages/markup/extension" 15 + ) 16 + 17 + // FindReferences collects all links referencing tangled-related objects 18 + // like issues, PRs, comments or even @-mentions 19 + // This funciton doesn't actually check for the existence of records in the DB 20 + // or the PDS; it merely returns a list of what are presumed to be references. 21 + func FindReferences(baseUrl string, source string) ([]string, []models.ReferenceLink) { 22 + var ( 23 + refLinkSet = make(map[models.ReferenceLink]struct{}) 24 + mentionsSet = make(map[string]struct{}) 25 + md = NewMarkdown() 26 + sourceBytes = []byte(source) 27 + root = md.Parser().Parse(text.NewReader(sourceBytes)) 28 + ) 29 + // trim url scheme. the SSL shouldn't matter 30 + baseUrl = strings.TrimPrefix(baseUrl, "https://") 31 + baseUrl = strings.TrimPrefix(baseUrl, "http://") 32 + 33 + ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 34 + if !entering { 35 + return ast.WalkContinue, nil 36 + } 37 + switch n.Kind() { 38 + case textension.KindAt: 39 + handle := n.(*textension.AtNode).Handle 40 + mentionsSet[handle] = struct{}{} 41 + return ast.WalkSkipChildren, nil 42 + case ast.KindLink: 43 + dest := string(n.(*ast.Link).Destination) 44 + ref := parseTangledLink(baseUrl, dest) 45 + if ref != nil { 46 + refLinkSet[*ref] = struct{}{} 47 + } 48 + return ast.WalkSkipChildren, nil 49 + case ast.KindAutoLink: 50 + an := n.(*ast.AutoLink) 51 + if an.AutoLinkType == ast.AutoLinkURL { 52 + dest := string(an.URL(sourceBytes)) 53 + ref := parseTangledLink(baseUrl, dest) 54 + if ref != nil { 55 + refLinkSet[*ref] = struct{}{} 56 + } 57 + } 58 + return ast.WalkSkipChildren, nil 59 + } 60 + return ast.WalkContinue, nil 61 + }) 62 + mentions := slices.Collect(maps.Keys(mentionsSet)) 63 + references := slices.Collect(maps.Keys(refLinkSet)) 64 + return mentions, references 65 + } 66 + 67 + func parseTangledLink(baseHost string, urlStr string) *models.ReferenceLink { 68 + u, err := url.Parse(urlStr) 69 + if err != nil { 70 + return nil 71 + } 72 + 73 + if u.Host != "" && !strings.EqualFold(u.Host, baseHost) { 74 + return nil 75 + } 76 + 77 + p := path.Clean(u.Path) 78 + parts := strings.FieldsFunc(p, func(r rune) bool { return r == '/' }) 79 + if len(parts) < 4 { 80 + // need at least: handle / repo / kind / id 81 + return nil 82 + } 83 + 84 + var ( 85 + handle = parts[0] 86 + repo = parts[1] 87 + kindSeg = parts[2] 88 + subjectSeg = parts[3] 89 + ) 90 + 91 + handle = strings.TrimPrefix(handle, "@") 92 + 93 + var kind models.RefKind 94 + switch kindSeg { 95 + case "issues": 96 + kind = models.RefKindIssue 97 + case "pulls": 98 + kind = models.RefKindPull 99 + default: 100 + return nil 101 + } 102 + 103 + subjectId, err := strconv.Atoi(subjectSeg) 104 + if err != nil { 105 + return nil 106 + } 107 + var commentId *int 108 + if u.Fragment != "" { 109 + if strings.HasPrefix(u.Fragment, "comment-") { 110 + commentIdStr := u.Fragment[len("comment-"):] 111 + if id, err := strconv.Atoi(commentIdStr); err == nil { 112 + commentId = &id 113 + } 114 + } 115 + } 116 + 117 + return &models.ReferenceLink{ 118 + Handle: handle, 119 + Repo: repo, 120 + Kind: kind, 121 + SubjectId: subjectId, 122 + CommentId: commentId, 123 + } 124 + }
+6 -10
appview/pulls/pulls.go
··· 24 "tangled.org/core/appview/pages" 25 "tangled.org/core/appview/pages/markup" 26 "tangled.org/core/appview/pages/repoinfo" 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 db *db.DB 50 config *config.Config 51 notifier notify.Notifier ··· 60 repoResolver *reporesolver.RepoResolver, 61 pages *pages.Pages, 62 resolver *idresolver.Resolver, 63 db *db.DB, 64 config *config.Config, 65 notifier notify.Notifier, ··· 73 repoResolver: repoResolver, 74 pages: pages, 75 idResolver: resolver, 76 db: db, 77 config: config, 78 notifier: notifier, ··· 678 } 679 680 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 681 - l := s.logger.With("handler", "PullComment") 682 user := s.oauth.GetUser(r) 683 f, err := s.repoResolver.Resolve(r) 684 if err != nil { ··· 716 s.pages.Notice(w, "pull", "Comment body is required") 717 return 718 } 719 720 // Start a transaction 721 tx, err := s.db.BeginTx(r.Context(), nil) ··· 776 return 777 } 778 779 - rawMentions := markup.FindUserMentions(comment.Body) 780 - idents := s.idResolver.ResolveIdents(r.Context(), rawMentions) 781 - l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 782 - var mentions []syntax.DID 783 - for _, ident := range idents { 784 - if ident != nil && !ident.Handle.IsInvalidHandle() { 785 - mentions = append(mentions, ident.DID) 786 - } 787 - } 788 s.notifier.NewPullComment(r.Context(), comment, mentions) 789 790 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
··· 24 "tangled.org/core/appview/pages" 25 "tangled.org/core/appview/pages/markup" 26 "tangled.org/core/appview/pages/repoinfo" 27 + "tangled.org/core/appview/refresolver" 28 "tangled.org/core/appview/reporesolver" 29 "tangled.org/core/appview/validator" 30 "tangled.org/core/appview/xrpcclient" ··· 47 repoResolver *reporesolver.RepoResolver 48 pages *pages.Pages 49 idResolver *idresolver.Resolver 50 + refResolver *refresolver.Resolver 51 db *db.DB 52 config *config.Config 53 notifier notify.Notifier ··· 62 repoResolver *reporesolver.RepoResolver, 63 pages *pages.Pages, 64 resolver *idresolver.Resolver, 65 + refResolver *refresolver.Resolver, 66 db *db.DB, 67 config *config.Config, 68 notifier notify.Notifier, ··· 76 repoResolver: repoResolver, 77 pages: pages, 78 idResolver: resolver, 79 + refResolver: refResolver, 80 db: db, 81 config: config, 82 notifier: notifier, ··· 682 } 683 684 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 685 user := s.oauth.GetUser(r) 686 f, err := s.repoResolver.Resolve(r) 687 if err != nil { ··· 719 s.pages.Notice(w, "pull", "Comment body is required") 720 return 721 } 722 + 723 + mentions, _ := s.refResolver.Resolve(r.Context(), body) 724 725 // Start a transaction 726 tx, err := s.db.BeginTx(r.Context(), nil) ··· 781 return 782 } 783 784 s.notifier.NewPullComment(r.Context(), comment, mentions) 785 786 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
+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
··· 266 s.enforcer, 267 s.pages, 268 s.idResolver, 269 s.db, 270 s.config, 271 s.notifier, ··· 282 s.repoResolver, 283 s.pages, 284 s.idResolver, 285 s.db, 286 s.config, 287 s.notifier,
··· 266 s.enforcer, 267 s.pages, 268 s.idResolver, 269 + s.refResolver, 270 s.db, 271 s.config, 272 s.notifier, ··· 283 s.repoResolver, 284 s.pages, 285 s.idResolver, 286 + s.refResolver, 287 s.db, 288 s.config, 289 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 ··· 97 validator := validator.New(d, res, enforcer) 98 99 repoResolver := reporesolver.New(config, enforcer, d) 100 101 wrapper := db.DbWrapper{Execer: d} 102 jc, err := jetstream.NewJetstreamClient( ··· 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 ··· 99 validator := validator.New(d, res, enforcer) 100 101 repoResolver := reporesolver.New(config, enforcer, 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( ··· 182 enforcer, 183 pages, 184 res, 185 + refResolver, 186 posthog, 187 jc, 188 config,