+172
appview/db/reference.go
+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
+10
-20
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"
28
27
"tangled.org/core/appview/pagination"
28
+
"tangled.org/core/appview/refresolver"
29
29
"tangled.org/core/appview/reporesolver"
30
30
"tangled.org/core/appview/validator"
31
31
"tangled.org/core/idresolver"
···
37
37
repoResolver *reporesolver.RepoResolver
38
38
pages *pages.Pages
39
39
idResolver *idresolver.Resolver
40
+
refResolver *refresolver.Resolver
40
41
db *db.DB
41
42
config *config.Config
42
43
notifier notify.Notifier
···
50
51
repoResolver *reporesolver.RepoResolver,
51
52
pages *pages.Pages,
52
53
idResolver *idresolver.Resolver,
54
+
refResolver *refresolver.Resolver,
53
55
db *db.DB,
54
56
config *config.Config,
55
57
notifier notify.Notifier,
···
62
64
repoResolver: repoResolver,
63
65
pages: pages,
64
66
idResolver: idResolver,
67
+
refResolver: refResolver,
65
68
db: db,
66
69
config: config,
67
70
notifier: notifier,
···
399
402
replyTo = &replyToUri
400
403
}
401
404
405
+
mentions, _ := rp.refResolver.Resolve(r.Context(), body)
406
+
402
407
comment := models.IssueComment{
403
408
Did: user.Did,
404
409
Rkey: tid.TID(),
···
455
460
// notify about the new comment
456
461
comment.Id = commentId
457
462
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
463
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
468
464
469
465
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId))
···
884
880
RepoInfo: f.RepoInfo(user),
885
881
})
886
882
case http.MethodPost:
883
+
body := r.FormValue("body")
884
+
mentions, _ := rp.refResolver.Resolve(r.Context(), body)
885
+
887
886
issue := &models.Issue{
888
887
RepoAt: f.RepoAt(),
889
888
Rkey: tid.TID(),
890
889
Title: r.FormValue("title"),
891
-
Body: r.FormValue("body"),
890
+
Body: body,
892
891
Open: true,
893
892
Did: user.Did,
894
893
Created: time.Now(),
···
960
959
// everything is successful, do not rollback the atproto record
961
960
atUri = ""
962
961
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
962
rp.notifier.NewIssue(r.Context(), issue, mentions)
973
963
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
974
964
return
+18
appview/models/reference.go
+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
+3
-3
appview/pages/markup/extension/atlink.go
···
16
16
17
17
// An AtNode struct represents an AtNode
18
18
type AtNode struct {
19
-
handle string
19
+
Handle string
20
20
ast.BaseInline
21
21
}
22
22
···
59
59
block.Advance(m[1])
60
60
node := &AtNode{}
61
61
node.AppendChild(node, ast.NewTextSegment(atSegment))
62
-
node.handle = string(atSegment.Value(block.Source())[1:])
62
+
node.Handle = string(atSegment.Value(block.Source())[1:])
63
63
return node
64
64
}
65
65
···
88
88
func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
89
89
if entering {
90
90
w.WriteString(`<a href="/@`)
91
-
w.WriteString(n.(*AtNode).handle)
91
+
w.WriteString(n.(*AtNode).Handle)
92
92
w.WriteString(`" class="mention">`)
93
93
} else {
94
94
w.WriteString("</a>")
+24
appview/pages/markup/markdown.go
+24
appview/pages/markup/markdown.go
···
77
77
return md
78
78
}
79
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
+
80
104
func (rctx *RenderContext) RenderMarkdown(source string) string {
81
105
md := NewMarkdown()
82
106
+124
appview/pages/markup/reference_link.go
+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
+
}
+42
appview/pages/markup/reference_link_test.go
+42
appview/pages/markup/reference_link_test.go
···
1
+
package markup_test
2
+
3
+
import (
4
+
"testing"
5
+
6
+
"github.com/stretchr/testify/assert"
7
+
"tangled.org/core/appview/models"
8
+
"tangled.org/core/appview/pages/markup"
9
+
)
10
+
11
+
func TestMarkupParsing(t *testing.T) {
12
+
tests := []struct {
13
+
name string
14
+
source string
15
+
wantHandles []string
16
+
wantRefLinks []models.ReferenceLink
17
+
}{
18
+
{
19
+
name: "normal link",
20
+
source: `[link](http://127.0.0.1:3000/alice.pds.tngl.boltless.dev/coolproj/issues/1)`,
21
+
wantHandles: make([]string, 0),
22
+
wantRefLinks: []models.ReferenceLink{
23
+
{Handle: "alice.pds.tngl.boltless.dev", Repo: "coolproj", Kind: models.RefKindIssue, SubjectId: 1, CommentId: nil},
24
+
},
25
+
},
26
+
{
27
+
name: "commonmark style autolink",
28
+
source: `<http://127.0.0.1:3000/alice.pds.tngl.boltless.dev/coolproj/issues/1>`,
29
+
wantHandles: make([]string, 0),
30
+
wantRefLinks: []models.ReferenceLink{
31
+
{Handle: "alice.pds.tngl.boltless.dev", Repo: "coolproj", Kind: models.RefKindIssue, SubjectId: 1, CommentId: nil},
32
+
},
33
+
},
34
+
}
35
+
for _, tt := range tests {
36
+
t.Run(tt.name, func(t *testing.T) {
37
+
handles, refLinks := markup.FindReferences("http://127.0.0.1:3000", tt.source)
38
+
assert.ElementsMatch(t, tt.wantHandles, handles)
39
+
assert.ElementsMatch(t, tt.wantRefLinks, refLinks)
40
+
})
41
+
}
42
+
}
+6
-10
appview/pulls/pulls.go
+6
-10
appview/pulls/pulls.go
···
23
23
"tangled.org/core/appview/oauth"
24
24
"tangled.org/core/appview/pages"
25
25
"tangled.org/core/appview/pages/markup"
26
+
"tangled.org/core/appview/refresolver"
26
27
"tangled.org/core/appview/reporesolver"
27
28
"tangled.org/core/appview/validator"
28
29
"tangled.org/core/appview/xrpcclient"
···
45
46
repoResolver *reporesolver.RepoResolver
46
47
pages *pages.Pages
47
48
idResolver *idresolver.Resolver
49
+
refResolver *refresolver.Resolver
48
50
db *db.DB
49
51
config *config.Config
50
52
notifier notify.Notifier
···
59
61
repoResolver *reporesolver.RepoResolver,
60
62
pages *pages.Pages,
61
63
resolver *idresolver.Resolver,
64
+
refResolver *refresolver.Resolver,
62
65
db *db.DB,
63
66
config *config.Config,
64
67
notifier notify.Notifier,
···
72
75
repoResolver: repoResolver,
73
76
pages: pages,
74
77
idResolver: resolver,
78
+
refResolver: refResolver,
75
79
db: db,
76
80
config: config,
77
81
notifier: notifier,
···
691
695
}
692
696
693
697
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
694
-
l := s.logger.With("handler", "PullComment")
695
698
user := s.oauth.GetUser(r)
696
699
f, err := s.repoResolver.Resolve(r)
697
700
if err != nil {
···
729
732
s.pages.Notice(w, "pull", "Comment body is required")
730
733
return
731
734
}
735
+
736
+
mentions, _ := s.refResolver.Resolve(r.Context(), body)
732
737
733
738
// Start a transaction
734
739
tx, err := s.db.BeginTx(r.Context(), nil)
···
789
794
return
790
795
}
791
796
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
797
s.notifier.NewPullComment(r.Context(), comment, mentions)
802
798
803
799
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
+65
appview/refresolver/resolver.go
+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
+2
appview/state/router.go
+5
appview/state/state.go
+5
appview/state/state.go
···
21
21
phnotify "tangled.org/core/appview/notify/posthog"
22
22
"tangled.org/core/appview/oauth"
23
23
"tangled.org/core/appview/pages"
24
+
"tangled.org/core/appview/refresolver"
24
25
"tangled.org/core/appview/reporesolver"
25
26
"tangled.org/core/appview/validator"
26
27
xrpcclient "tangled.org/core/appview/xrpcclient"
···
49
50
enforcer *rbac.Enforcer
50
51
pages *pages.Pages
51
52
idResolver *idresolver.Resolver
53
+
refResolver *refresolver.Resolver
52
54
posthog posthog.Client
53
55
jc *jetstream.JetstreamClient
54
56
config *config.Config
···
97
99
validator := validator.New(d, res, enforcer)
98
100
99
101
repoResolver := reporesolver.New(config, enforcer, res, d)
102
+
103
+
refResolver := refresolver.New(config, res, d, log.SubLogger(logger, "refResolver"))
100
104
101
105
wrapper := db.DbWrapper{Execer: d}
102
106
jc, err := jetstream.NewJetstreamClient(
···
178
182
enforcer,
179
183
pages,
180
184
res,
185
+
refResolver,
181
186
posthog,
182
187
jc,
183
188
config,