From 66484fef88f70202ca254d5187734d4dfa4fe870 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Thu, 24 Nov 2022 21:18:12 +0100 Subject: [PATCH] Implement ActivityPub remote follow form --- activityPubTools.go | 70 ++++++++++++++++++++++++++++++++++++++++++++ httpRouters.go | 5 +++- strings/de.yaml | 2 ++ strings/default.yaml | 2 ++ ui.go | 27 +++++++++++++++++ 5 files changed, 105 insertions(+), 1 deletion(-) diff --git a/activityPubTools.go b/activityPubTools.go index 1a8fce6..59dd0cd 100644 --- a/activityPubTools.go +++ b/activityPubTools.go @@ -1,8 +1,14 @@ package main import ( + "encoding/json" + "fmt" + "io" "net/http" + "net/url" + "strings" + "github.com/carlmjohnson/requests" "github.com/go-chi/chi/v5" ) @@ -25,3 +31,67 @@ func (a *goBlog) apShowFollowers(w http.ResponseWriter, r *http.Request) { }, }) } + +func (a *goBlog) apRemoteFollow(w http.ResponseWriter, r *http.Request) { + blogName := chi.URLParam(r, "blog") + blog, ok := a.cfg.Blogs[blogName] + if !ok || blog == nil { + a.serveError(w, r, "Blog not found", http.StatusNotFound) + return + } + if user := r.FormValue("user"); user != "" { + // Parse instance + userParts := strings.Split(user, "@") + if len(userParts) < 2 { + a.serveError(w, r, "User must be of the form user@example.org or @user@example.org", http.StatusBadRequest) + return + } + user = userParts[len(userParts)-2] + instance := userParts[len(userParts)-1] + if user == "" || instance == "" { + a.serveError(w, r, "User or instance are empty", http.StatusBadRequest) + return + } + // Get webfinger + type webfingerLinkType struct { + Rel string `json:"rel"` + Template string `json:"template"` + } + type webfingerType struct { + Links []*webfingerLinkType `json:"links"` + } + webfinger := &webfingerType{} + err := requests.URL(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", instance, user, instance)). + Client(a.httpClient). + UserAgent(appUserAgent). + Handle(func(resp *http.Response) error { + defer resp.Body.Close() + return json.NewDecoder(io.LimitReader(resp.Body, 1000*1000)).Decode(webfinger) + }). + Fetch(r.Context()) + if err != nil { + a.serveError(w, r, "Failed to query webfinger", http.StatusInternalServerError) + return + } + // Check webfinger and find template + template := "" + for _, link := range webfinger.Links { + if link.Rel == "http://ostatus.org/schema/1.0/subscribe" { + template = link.Template + break + } + } + if template == "" { + a.serveError(w, r, "Instance does not support subscribe schema version 1.0", http.StatusInternalServerError) + return + } + // Build redirect + redirect := strings.ReplaceAll(template, "{uri}", url.PathEscape(a.apIri(blog))) + http.Redirect(w, r, redirect, http.StatusFound) + return + } + // Render remote follow form + a.render(w, r, a.renderActivityPubRemoteFollow, &renderData{ + BlogString: blogName, + }) +} diff --git a/httpRouters.go b/httpRouters.go index ff87cff..6acd461 100644 --- a/httpRouters.go +++ b/httpRouters.go @@ -42,8 +42,11 @@ func (a *goBlog) activityPubRouter(r chi.Router) { if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled { r.Route("/activitypub", func(r chi.Router) { r.Post("/inbox/{blog}", a.apHandleInbox) - r.Post("/{blog}/inbox", a.apHandleInbox) + r.Post("/{blog}/inbox", a.apHandleInbox) // old r.With(a.authMiddleware).Get("/{blog}/followers", a.apShowFollowers) + r.With(a.authMiddleware).Get("/followers/{blog}", a.apShowFollowers) // old + r.With(a.cacheMiddleware).Get("/remote_follow/{blog}", a.apRemoteFollow) + r.Post("/remote_follow/{blog}", a.apRemoteFollow) }) r.Group(func(r chi.Router) { r.Use(cacheLoggedIn, a.cacheMiddleware) diff --git a/strings/de.yaml b/strings/de.yaml index b520f15..77911cf 100644 --- a/strings/de.yaml +++ b/strings/de.yaml @@ -23,6 +23,8 @@ editorpostdesc: "💡 Leere Parameter werden automatisch entfernt. Mehr möglich editorusetemplate: "Benutze Vorlage" emailopt: "E-Mail (optional)" fileuses: "Datei-Verwendungen" +follow: "Folgen" +followusingactivitypub: "Mit ActivityPub folgen" general: "Allgemein" gentts: "Text-To-Speech-Audio erzeugen" gpxhelper: "GPX-Helfer" diff --git a/strings/default.yaml b/strings/default.yaml index 0c494ca..64e0266 100644 --- a/strings/default.yaml +++ b/strings/default.yaml @@ -30,6 +30,8 @@ editorusetemplate: "Use template" emailopt: "Email (optional)" feed: "Feed" fileuses: "file uses" +follow: "Follow" +followusingactivitypub: "Follow using ActivityPub" general: "General" gentts: "Generate Text-To-Speech audio" gpxhelper: "GPX helper" diff --git a/ui.go b/ui.go index 595b2b9..5b9aa23 100644 --- a/ui.go +++ b/ui.go @@ -1631,6 +1631,33 @@ func (a *goBlog) renderActivityPubFollowers(hb *htmlbuilder.HtmlBuilder, rd *ren } hb.WriteElementClose("tbody") hb.WriteElementClose("table") + + hb.WriteElementClose("main") + }, + ) +} + +func (a *goBlog) renderActivityPubRemoteFollow(hb *htmlbuilder.HtmlBuilder, rd *renderData) { + a.renderBase( + hb, rd, + func(hb *htmlbuilder.HtmlBuilder) { + a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "apfollowers")) + }, + func(hb *htmlbuilder.HtmlBuilder) { + hb.WriteElementOpen("main") + + // Title + hb.WriteElementOpen("h1") + hb.WriteEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "followusingactivitypub")) + hb.WriteElementClose("h1") + + // Form + hb.WriteElementOpen("form", "class", "fw p", "method", "post") + hb.WriteElementOpen("input", "type", "text", "name", "user", "placeholder", "user@example.org") + hb.WriteElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "follow")) + hb.WriteElementClose("form") + + hb.WriteElementClose("main") }, ) }