Monorepo for Tangled tangled.org

appview/pages/markup: smart commit autolink renderer #1003

merged opened by boltless.me targeting master from sl/uupvwnkzxzom
Labels

None yet.

assignee

None yet.

Participants 3
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mcufej76qu22
+43 -37
Interdiff #1 โ†’ #2
+1 -1
appview/pages/markup/extension/tangledlink.go
··· 78 79 func (t *tangledLinkTransformer) parseLinkCommitSha(raw string) string { 80 u, err := url.Parse(raw) 81 - if err != nil || u.Host != "tangled.org" { 82 return "" 83 } 84
··· 78 79 func (t *tangledLinkTransformer) parseLinkCommitSha(raw string) string { 80 u, err := url.Parse(raw) 81 + if err != nil || u.Host != t.host { 82 return "" 83 } 84
+4 -3
appview/pages/markup/markdown.go
··· 46 CamoSecret string 47 repoinfo.RepoInfo 48 IsDev bool 49 RendererType RendererType 50 Sanitizer Sanitizer 51 Files fs.FS 52 } 53 54 - func NewMarkdown() goldmark.Markdown { 55 md := goldmark.New( 56 goldmark.WithExtensions( 57 extension.GFM, ··· 67 ), 68 callout.CalloutExtention, 69 textension.AtExt, 70 - textension.NewTangledLinkExt("tangled.org"), 71 emoji.Emoji, 72 ), 73 goldmark.WithParserOptions( ··· 79 } 80 81 func (rctx *RenderContext) RenderMarkdown(source string) string { 82 - return rctx.RenderMarkdownWith(source, NewMarkdown()) 83 } 84 85 func (rctx *RenderContext) RenderMarkdownWith(source string, md goldmark.Markdown) string {
··· 46 CamoSecret string 47 repoinfo.RepoInfo 48 IsDev bool 49 + Hostname string 50 RendererType RendererType 51 Sanitizer Sanitizer 52 Files fs.FS 53 } 54 55 + func NewMarkdown(hostname string) goldmark.Markdown { 56 md := goldmark.New( 57 goldmark.WithExtensions( 58 extension.GFM, ··· 68 ), 69 callout.CalloutExtention, 70 textension.AtExt, 71 + textension.NewTangledLinkExt(hostname), 72 emoji.Emoji, 73 ), 74 goldmark.WithParserOptions( ··· 80 } 81 82 func (rctx *RenderContext) RenderMarkdown(source string) string { 83 + return rctx.RenderMarkdownWith(source, NewMarkdown(rctx.Hostname)) 84 } 85 86 func (rctx *RenderContext) RenderMarkdownWith(source string, md goldmark.Markdown) string {
+11
appview/config/config.go
··· 25 TmpAltAppPassword string `env:"ALT_APP_PASSWORD"` 26 } 27 28 type OAuthConfig struct { 29 ClientSecret string `env:"CLIENT_SECRET"` 30 ClientKid string `env:"CLIENT_KID"`
··· 25 TmpAltAppPassword string `env:"ALT_APP_PASSWORD"` 26 } 27 28 + func (c *CoreConfig) UseTLS() bool { 29 + return !c.Dev 30 + } 31 + 32 + func (c *CoreConfig) Url() string { 33 + if c.UseTLS() { 34 + return "https://" + c.AppviewHost 35 + } 36 + return "http://" + c.AppviewHost 37 + } 38 + 39 type OAuthConfig struct { 40 ClientSecret string `env:"CLIENT_SECRET"` 41 ClientKid string `env:"CLIENT_KID"`
+2 -5
appview/oauth/oauth.go
··· 37 38 func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enforcer, res *idresolver.Resolver, logger *slog.Logger) (*OAuth, error) { 39 var oauthConfig oauth.ClientConfig 40 - var clientUri string 41 if config.Core.Dev { 42 - clientUri = "http://127.0.0.1:3000" 43 - callbackUri := clientUri + "/oauth/callback" 44 oauthConfig = oauth.NewLocalhostConfig(callbackUri, []string{"atproto", "transition:generic"}) 45 } else { 46 - clientUri = config.Core.AppviewHost 47 clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri) 48 - callbackUri := clientUri + "/oauth/callback" 49 oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, []string{"atproto", "transition:generic"}) 50 } 51
··· 37 38 func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enforcer, res *idresolver.Resolver, logger *slog.Logger) (*OAuth, error) { 39 var oauthConfig oauth.ClientConfig 40 + clientUri := config.Core.Url() 41 + callbackUri := clientUri + "/oauth/callback" 42 if config.Core.Dev { 43 oauthConfig = oauth.NewLocalhostConfig(callbackUri, []string{"atproto", "transition:generic"}) 44 } else { 45 clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri) 46 oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, []string{"atproto", "transition:generic"}) 47 } 48
+2 -2
appview/pages/markup/markdown_test.go
··· 50 51 for _, tt := range tests { 52 t.Run(tt.name, func(t *testing.T) { 53 - md := NewMarkdown() 54 55 var buf bytes.Buffer 56 if err := md.Convert([]byte(tt.markdown), &buf); err != nil { ··· 105 106 for _, tt := range tests { 107 t.Run(tt.name, func(t *testing.T) { 108 - md := NewMarkdown() 109 110 var buf bytes.Buffer 111 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
··· 50 51 for _, tt := range tests { 52 t.Run(tt.name, func(t *testing.T) { 53 + md := NewMarkdown("tangled.org") 54 55 var buf bytes.Buffer 56 if err := md.Convert([]byte(tt.markdown), &buf); err != nil { ··· 105 106 for _, tt := range tests { 107 t.Run(tt.name, func(t *testing.T) { 108 + md := NewMarkdown("tangled.org") 109 110 var buf bytes.Buffer 111 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
+4 -7
appview/pages/markup/reference_link.go
··· 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 { ··· 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 } ··· 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 }
··· 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(host string, source string) ([]string, []models.ReferenceLink) { 22 var ( 23 refLinkSet = make(map[models.ReferenceLink]struct{}) 24 mentionsSet = make(map[string]struct{}) 25 + md = NewMarkdown(host) 26 sourceBytes = []byte(source) 27 root = md.Parser().Parse(text.NewReader(sourceBytes)) 28 ) 29 30 ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 31 if !entering { ··· 38 return ast.WalkSkipChildren, nil 39 case ast.KindLink: 40 dest := string(n.(*ast.Link).Destination) 41 + ref := parseTangledLink(host, dest) 42 if ref != nil { 43 refLinkSet[*ref] = struct{}{} 44 } ··· 47 an := n.(*ast.AutoLink) 48 if an.AutoLinkType == ast.AutoLinkURL { 49 dest := string(an.URL(sourceBytes)) 50 + ref := parseTangledLink(host, dest) 51 if ref != nil { 52 refLinkSet[*ref] = struct{}{} 53 }
+1
appview/pages/pages.go
··· 53 // initialized with safe defaults, can be overriden per use 54 rctx := &markup.RenderContext{ 55 IsDev: config.Core.Dev, 56 CamoUrl: config.Camo.Host, 57 CamoSecret: config.Camo.SharedSecret, 58 Sanitizer: markup.NewSanitizer(),
··· 53 // initialized with safe defaults, can be overriden per use 54 rctx := &markup.RenderContext{ 55 IsDev: config.Core.Dev, 56 + Hostname: config.Core.AppviewHost, 57 CamoUrl: config.Camo.Host, 58 CamoSecret: config.Camo.SharedSecret, 59 Sanitizer: markup.NewSanitizer(),
+1 -1
appview/repo/archive.go
··· 66 if link := resp.Header.Get("Link"); link != "" { 67 if resolvedRef, err := extractImmutableLink(link); err == nil { 68 newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 - rp.config.Core.AppviewHost, f.DidSlashRepo(), resolvedRef) 70 w.Header().Set("Link", newLink) 71 } 72 }
··· 66 if link := resp.Header.Get("Link"); link != "" { 67 if resolvedRef, err := extractImmutableLink(link); err == nil { 68 newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 + rp.config.Core.Url(), f.DidSlashRepo(), resolvedRef) 70 w.Header().Set("Link", newLink) 71 } 72 }
+4 -4
appview/repo/feed.go
··· 37 38 feed := &feeds.Feed{ 39 Title: fmt.Sprintf("activity feed for @%s", ownerSlashRepo), 40 - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, ownerSlashRepo), Type: "text/html", Rel: "alternate"}, 41 Items: make([]*feeds.Item, 0), 42 Updated: time.UnixMilli(0), 43 } ··· 86 mainItem := &feeds.Item{ 87 Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), 88 Description: description, 89 - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId)}, 90 Created: pull.Created, 91 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 92 } ··· 100 roundItem := &feeds.Item{ 101 Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), 102 Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in @%s", owner.Handle, round.RoundNumber, pull.PullId, ownerSlashRepo), 103 - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId, round.RoundNumber)}, 104 Created: round.Created, 105 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 106 } ··· 124 return &feeds.Item{ 125 Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), 126 Description: fmt.Sprintf("@%s %s issue #%d in @%s", owner.Handle, state, issue.IssueId, ownerSlashRepo), 127 - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, ownerSlashRepo, issue.IssueId)}, 128 Created: issue.Created, 129 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 130 }, nil
··· 37 38 feed := &feeds.Feed{ 39 Title: fmt.Sprintf("activity feed for @%s", ownerSlashRepo), 40 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.Url(), ownerSlashRepo), Type: "text/html", Rel: "alternate"}, 41 Items: make([]*feeds.Item, 0), 42 Updated: time.UnixMilli(0), 43 } ··· 86 mainItem := &feeds.Item{ 87 Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), 88 Description: description, 89 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.Url(), ownerSlashRepo, pull.PullId)}, 90 Created: pull.Created, 91 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 92 } ··· 100 roundItem := &feeds.Item{ 101 Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), 102 Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in @%s", owner.Handle, round.RoundNumber, pull.PullId, ownerSlashRepo), 103 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.Url(), ownerSlashRepo, pull.PullId, round.RoundNumber)}, 104 Created: round.Created, 105 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 106 } ··· 124 return &feeds.Item{ 125 Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), 126 Description: fmt.Sprintf("@%s %s issue #%d in @%s", owner.Handle, state, issue.IssueId, ownerSlashRepo), 127 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.Url(), ownerSlashRepo, issue.IssueId)}, 128 Created: issue.Created, 129 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 130 }, nil
+7 -8
appview/settings/settings.go
··· 298 } 299 300 func (s *Settings) verifyUrl(did string, email string, code string) string { 301 - var appUrl string 302 - if s.Config.Core.Dev { 303 - appUrl = "http://" + s.Config.Core.ListenAddr 304 - } else { 305 - appUrl = s.Config.Core.AppviewHost 306 - } 307 - 308 - return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code)) 309 } 310 311 func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) {
··· 298 } 299 300 func (s *Settings) verifyUrl(did string, email string, code string) string { 301 + return fmt.Sprintf( 302 + "%s/settings/emails/verify?did=%s&email=%s&code=%s", 303 + s.Config.Core.Url(), 304 + url.QueryEscape(did), 305 + url.QueryEscape(email), 306 + url.QueryEscape(code), 307 + ) 308 } 309 310 func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) {
+4 -4
appview/state/profile.go
··· 415 416 feed := feeds.Feed{ 417 Title: fmt.Sprintf("%s's timeline", author.Name), 418 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, id.Handle), Type: "text/html", Rel: "alternate"}, 419 Items: make([]*feeds.Item, 0), 420 Updated: time.UnixMilli(0), 421 Author: author, ··· 483 func (s *State) createPullRequestItem(pull *models.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { 484 return &feeds.Item{ 485 Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 486 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 487 Created: pull.Created, 488 Author: author, 489 } ··· 492 func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { 493 return &feeds.Item{ 494 Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name), 495 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 496 Created: issue.Created, 497 Author: author, 498 } ··· 512 513 return &feeds.Item{ 514 Title: title, 515 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix 516 Created: repo.Repo.Created, 517 Author: author, 518 }, nil
··· 415 416 feed := feeds.Feed{ 417 Title: fmt.Sprintf("%s's timeline", author.Name), 418 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.Url(), id.Handle), Type: "text/html", Rel: "alternate"}, 419 Items: make([]*feeds.Item, 0), 420 Updated: time.UnixMilli(0), 421 Author: author, ··· 483 func (s *State) createPullRequestItem(pull *models.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { 484 return &feeds.Item{ 485 Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 486 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.Url(), owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 487 Created: pull.Created, 488 Author: author, 489 } ··· 492 func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { 493 return &feeds.Item{ 494 Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name), 495 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.Url(), owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 496 Created: issue.Created, 497 Author: author, 498 } ··· 512 513 return &feeds.Item{ 514 Title: title, 515 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.Url(), author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix 516 Created: repo.Repo.Created, 517 Author: author, 518 }, nil

History

5 rounds 11 comments
sign up or login to add to the discussion
2 commits
expand
appview/pages/markup: smart commit autolink renderer
appview: strip scheme from CoreConfig.AppviewHost
3/3 success
expand
expand 2 comments

@oppi.li makes sense. I completely forgot the indigo oauth behavior. I reverted the change.

lgtm, this bug is still present:

next, in appview/config/config.go, the default value for AppivewHost should be set to tangled.org

but should be a quick fix, i can apply that on master!

pull request successfully merged
2 commits
expand
appview/pages/markup: smart commit autolink renderer
appview: strip scheme from CoreConfig.AppviewHost
expand 3 comments

when using dev, the callback URL should always be http://127.0.0.1/oauth/callback (or http://localhost), regardless of what the AppviewHost is set to. the port is ignored iirc.

next, in appview/config/config.go, the default value for AppivewHost should be set to tangled.org (without the scheme).

i think BaseUrl is good naming choice. happy with that!

when using dev, the callback URL should always be http://127.0.0.1/oauth/callback (or http://localhost), regardless of what the AppviewHost is set to. the port is ignored iirc.

If someone set AppviewHost, I suspect they are testing on that host. I used to test on my own domain long ago. http://127.0.0.1:3000/oauth/callback is what was used in original code.

next, in appview/config/config.go, the default value for AppivewHost should be set to tangled.org

will do!

if you are using oauth.NewLocalhostConfig (as we do in dev), the redirect URL's host cannot be anything other than localhost or 127.0.0.1, if it is something else, you will get an error when performing the PAR request:

URL must use \"localhost\", \"127.0.0.1\" or \"[::1]\" as hostname at body.redirect_uri]"

when in dev, we should just ignore AppviewHost and use one of the predefined hosts. in the original code, the host is 127.0.0.1 which is one of the predefined hosts. the port is ignored anyway.

2 commits
expand
appview/pages/markup: smart commit autolink renderer
appview: strip scheme from CoreConfig.AppviewHost
3/3 success
expand
expand 5 comments
  • here, i think Url is a bit ambiguous to have on config, could use a better name here

rest of the changeset lgtm, will give this a test, thanks!

after some local testing, this seems to oauth, i am unable to login!

2026/01/21 06:25:06 WARN auth server request failed request=PAR statusCode=400 body="map[error:invalid_request error_description:Invalid authorization request: URL must use \"localhost\", \"127.0.0.1\" or \"[::1]\" as hostname at body.redirect_uri]"
2026/01/21 06:25:06 ERRO appview: failed to start auth handler=Login err="auth request failed: PAR request failed (HTTP 400): invalid_request"

@oppi.li I can't reproduce the oauth issue. Have you tried after setting TANGLED_APPVIEWHOST=127.0.0.1? you should remove the scheme now.

for Url() naming, would BaseUrl() be fine enough?

Also naming/consistency nit: tangledlink.go โ†’ tangled_link.go? Keeping in line with reference_link.go. Rest looks OK!

@anirudh.fi I'm matching with extension/atlink.go. Doesn't reference_link.go violates the golang conventions? yes, I'm the one who wrote both... inconsistencies all over the place ๐Ÿ™ˆ

1 commit
expand
appview/pages/markup: smart commit autolink renderer
3/3 success
expand
expand 1 comment

would be good to avoid hardcoding the appview host here!

1 commit
expand
appview/pages/markup: smart commit autolink renderer
1/3 failed, 2/3 success
expand
expand 0 comments