From 47a42e1c55cc5a6e9300288cbd581457d186fd67 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Mon, 26 Dec 2022 19:52:06 +0100 Subject: [PATCH] Improve ActivityPub replies --- activityPub.go | 93 ++++++++++++++++++++++++++++++++++++++------ activityStreams.go | 27 +++++++++++-- check.go | 2 +- indexnow.go | 2 +- postsDb.go | 4 ++ postsFuncs.go | 34 ++++++++++------ telegram.go | 2 +- tts.go | 4 +- ui.go | 6 +-- uiComponents.go | 2 +- webmentionSending.go | 9 +---- 11 files changed, 144 insertions(+), 41 deletions(-) diff --git a/activityPub.go b/activityPub.go index 0d0a467..0d7dbd7 100644 --- a/activityPub.go +++ b/activityPub.go @@ -23,6 +23,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-fed/httpsig" "github.com/google/uuid" + "github.com/samber/lo" "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -34,12 +35,16 @@ func (a *goBlog) initActivityPub() error { } // Add hooks a.pPostHooks = append(a.pPostHooks, func(p *post) { - if p.isPublishedSectionPost() { + if p.isPublishedSectionPost() && (p.Visibility == visibilityPublic || p.Visibility == visibilityUnlisted) { + a.apCheckMentions(p) + a.apCheckActivityPubReply(p) a.apPost(p) } }) a.pUpdateHooks = append(a.pUpdateHooks, func(p *post) { - if p.isPublishedSectionPost() { + if p.isPublishedSectionPost() && (p.Visibility == visibilityPublic || p.Visibility == visibilityUnlisted) { + a.apCheckMentions(p) + a.apCheckActivityPubReply(p) a.apUpdate(p) } }) @@ -47,7 +52,7 @@ func (a *goBlog) initActivityPub() error { a.apDelete(p) }) a.pUndeleteHooks = append(a.pUndeleteHooks, func(p *post) { - if p.isPublishedSectionPost() { + if p.isPublishedSectionPost() && (p.Visibility == visibilityPublic || p.Visibility == visibilityUnlisted) { a.apUndelete(p) } }) @@ -149,6 +154,57 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) { _ = a.min.Get().Minify(contenttype.JSON, w, buf) } +const activityPubMentionsParameter = "activitypubmentions" + +func (a *goBlog) apCheckMentions(p *post) { + contentBuf := bufferpool.Get() + a.postHtmlToWriter(contentBuf, &postHtmlOptions{p: p}) + links, err := allLinksFromHTML(contentBuf, a.fullPostURL(p)) + bufferpool.Put(contentBuf) + if err != nil { + log.Println("Failed to extract links from post: " + err.Error()) + return + } + apc := a.apHttpClients[p.Blog] + mentions := []string{} + for _, link := range lo.Uniq(links) { + act, err := apc.Actor(context.Background(), ap.IRI(link)) + if err != nil || act == nil || act.Type != ap.PersonType { + continue + } + mentions = append(mentions, link) + } + if p.Parameters == nil { + p.Parameters = map[string][]string{} + } + p.Parameters[activityPubMentionsParameter] = mentions + _ = a.db.replacePostParam(p.Path, activityPubMentionsParameter, mentions) +} + +const activityPubReplyActorParameter = "activitypubreplyactor" + +func (a *goBlog) apCheckActivityPubReply(p *post) { + replyLink := a.replyLink(p) + if replyLink == "" { + return + } + apc := a.apHttpClients[p.Blog] + item, err := apc.LoadIRI(ap.IRI(replyLink)) + if err != nil || item == nil || !ap.IsObject(item) { + return + } + obj, err := ap.ToObject(item) + if err != nil || obj == nil || obj.GetLink() == "" || obj.AttributedTo == nil || obj.AttributedTo.GetLink() == "" { + return + } + replyLinkActor := []string{obj.AttributedTo.GetLink().String()} + if p.Parameters == nil { + p.Parameters = map[string][]string{} + } + p.Parameters[activityPubReplyActorParameter] = replyLinkActor + _ = a.db.replacePostParam(p.Path, activityPubReplyActorParameter, replyLinkActor) +} + func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) { blogName := chi.URLParam(r, "blog") blog, ok := a.cfg.Blogs[blogName] @@ -400,7 +456,7 @@ func (a *goBlog) apPost(p *post) { c := ap.CreateNew(a.apNewID(blogConfig), a.toAPNote(p)) c.Actor = a.apAPIri(blogConfig) c.Published = time.Now() - a.apSendToAllFollowers(p.Blog, c) + a.apSendToAllFollowers(p.Blog, c, append(p.Parameters[activityPubMentionsParameter], p.firstParameter(activityPubReplyActorParameter))...) } func (a *goBlog) apUpdate(p *post) { @@ -408,7 +464,7 @@ func (a *goBlog) apUpdate(p *post) { u := ap.UpdateNew(a.apNewID(blogConfig), a.toAPNote(p)) u.Actor = a.apAPIri(blogConfig) u.Published = time.Now() - a.apSendToAllFollowers(p.Blog, u) + a.apSendToAllFollowers(p.Blog, u, append(p.Parameters[activityPubMentionsParameter], p.firstParameter(activityPubReplyActorParameter))...) } func (a *goBlog) apDelete(p *post) { @@ -416,7 +472,7 @@ func (a *goBlog) apDelete(p *post) { d := ap.DeleteNew(a.apNewID(blogConfig), a.activityPubId(p)) d.Actor = a.apAPIri(blogConfig) d.Published = time.Now() - a.apSendToAllFollowers(p.Blog, d) + a.apSendToAllFollowers(p.Blog, d, append(p.Parameters[activityPubMentionsParameter], p.firstParameter(activityPubReplyActorParameter))...) } func (a *goBlog) apUndelete(p *post) { @@ -470,21 +526,36 @@ func (a *goBlog) apSendProfileUpdates() { update := ap.UpdateNew(a.apNewID(config), person) update.Actor = a.apAPIri(config) update.Published = time.Now() + update.To.Append(ap.PublicNS, a.apGetFollowersCollectionId(blog, config)) a.apSendToAllFollowers(blog, update) } } -func (a *goBlog) apSendToAllFollowers(blog string, activity *ap.Activity) { +func (a *goBlog) apSendToAllFollowers(blog string, activity *ap.Activity, mentions ...string) { inboxes, err := a.db.apGetAllInboxes(blog) if err != nil { - log.Println("Failed to retrieve inboxes:", err.Error()) + log.Println("Failed to retrieve follower inboxes:", err.Error()) return } - a.apSendTo(a.apIri(a.cfg.Blogs[blog]), activity, inboxes) + for _, m := range mentions { + go func(m string) { + if m == "" { + return + } + apc := a.apHttpClients[blog] + actor, err := apc.Actor(context.Background(), ap.IRI(m)) + if err != nil || actor == nil || actor.Inbox == nil || actor.Inbox.GetLink() == "" { + return + } + inbox := actor.Inbox.GetLink().String() + a.apSendTo(a.apIri(a.cfg.Blogs[blog]), activity, inbox) + }(m) + } + a.apSendTo(a.apIri(a.cfg.Blogs[blog]), activity, inboxes...) } -func (a *goBlog) apSendTo(blogIri string, activity *ap.Activity, inboxes []string) { - for _, i := range inboxes { +func (a *goBlog) apSendTo(blogIri string, activity *ap.Activity, inboxes ...string) { + for _, i := range lo.Uniq(inboxes) { go func(inbox string) { _ = a.apQueueSendSigned(blogIri, inbox, activity) }(i) diff --git a/activityStreams.go b/activityStreams.go index 35777ec..de8ce72 100644 --- a/activityStreams.go +++ b/activityStreams.go @@ -44,18 +44,28 @@ func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter, r *htt func (a *goBlog) toAPNote(p *post) *ap.Note { // Create a Note object note := ap.ObjectNew(ap.NoteType) - note.To.Append(ap.PublicNS) - note.MediaType = ap.MimeType(contenttype.HTML) note.ID = a.activityPubId(p) note.URL = ap.IRI(a.fullPostURL(p)) note.AttributedTo = a.apAPIri(a.getBlogFromPost(p)) + // Audience + switch p.Visibility { + case visibilityPublic: + note.To.Append(ap.PublicNS, a.apGetFollowersCollectionId(p.Blog, a.getBlogFromPost(p))) + case visibilityUnlisted: + note.To.Append(a.apGetFollowersCollectionId(p.Blog, a.getBlogFromPost(p))) + note.CC.Append(ap.PublicNS) + } + for _, m := range p.Parameters[activityPubMentionsParameter] { + note.CC.Append(ap.IRI(m)) + } // Name and Type if title := p.RenderedTitle; title != "" { note.Type = ap.ArticleType note.Name.Add(ap.DefaultLangRef(title)) } // Content - note.Content.Add(ap.DefaultLangRef(a.postHtml(p, true))) + note.MediaType = ap.MimeType(contenttype.HTML) + note.Content.Add(ap.DefaultLangRef(a.postHtml(&postHtmlOptions{p: p, absolute: true, activityPub: true}))) // Attachments if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 { var attachments ap.ItemCollection @@ -75,6 +85,17 @@ func (a *goBlog) toAPNote(p *post) *ap.Note { note.Tag.Append(apTag) } } + // Mentions + for _, mention := range p.Parameters[activityPubMentionsParameter] { + apMention := ap.MentionNew(ap.IRI(mention)) + apMention.Href = ap.IRI(mention) + note.Tag.Append(apMention) + } + if replyLinkActor := p.firstParameter(activityPubReplyActorParameter); replyLinkActor != "" { + apMention := ap.MentionNew(ap.IRI(replyLinkActor)) + apMention.Href = ap.IRI(replyLinkActor) + note.Tag.Append(apMention) + } // Dates if p.Published != "" { if t, err := dateparse.ParseLocal(p.Published); err == nil { diff --git a/check.go b/check.go index ce45bb2..d8bbb45 100644 --- a/check.go +++ b/check.go @@ -129,7 +129,7 @@ func (a *goBlog) checkLinks(w io.Writer, posts ...*post) error { func (a *goBlog) allLinks(posts ...*post) (allLinks []*stringPair, err error) { for _, p := range posts { contentBuf := bufferpool.Get() - a.postHtmlToWriter(contentBuf, p, true) + a.postHtmlToWriter(contentBuf, &postHtmlOptions{p: p, absolute: true}) links, err := allLinksFromHTML(contentBuf, a.fullPostURL(p)) bufferpool.Put(contentBuf) if err != nil { diff --git a/indexnow.go b/indexnow.go index f102821..8b1a9a6 100644 --- a/indexnow.go +++ b/indexnow.go @@ -18,7 +18,7 @@ func (a *goBlog) initIndexNow() { // Add hooks hook := func(p *post) { // Check if post is published - if !p.isPublishedSectionPost() { + if !p.isPublicPublishedSectionPost() { return } // Send IndexNow request diff --git a/postsDb.go b/postsDb.go index 0d4fe3c..d42159e 100644 --- a/postsDb.go +++ b/postsDb.go @@ -20,6 +20,10 @@ func (a *goBlog) checkPost(p *post, new bool) (err error) { } now := time.Now().Local() nowString := now.Format(time.RFC3339) + // Add parameters map + if p.Parameters == nil { + p.Parameters = map[string][]string{} + } // Maybe add blog if p.Blog == "" { p.Blog = a.cfg.DefaultBlog diff --git a/postsFuncs.go b/postsFuncs.go index bea56de..ee29813 100644 --- a/postsFuncs.go +++ b/postsFuncs.go @@ -39,33 +39,41 @@ func (p *post) addParameter(parameter, value string) { p.Parameters[parameter] = append(p.Parameters[parameter], value) } -func (a *goBlog) postHtml(p *post, absolute bool) (res string) { +type postHtmlOptions struct { + p *post + absolute bool + activityPub bool +} + +func (a *goBlog) postHtml(o *postHtmlOptions) (res string) { buf := bufferpool.Get() - a.postHtmlToWriter(buf, p, absolute) + a.postHtmlToWriter(buf, o) res = buf.String() bufferpool.Put(buf) return } -func (a *goBlog) postHtmlToWriter(w io.Writer, p *post, absolute bool) { +func (a *goBlog) postHtmlToWriter(w io.Writer, o *postHtmlOptions) { // Build HTML hb := htmlbuilder.NewHtmlBuilder(w) // Add audio to the top - for _, a := range p.Parameters[a.cfg.Micropub.AudioParam] { + for _, a := range o.p.Parameters[a.cfg.Micropub.AudioParam] { hb.WriteElementOpen("audio", "controls", "preload", "none") hb.WriteElementOpen("source", "src", a) hb.WriteElementClose("source") hb.WriteElementClose("audio") } // Add IndieWeb context - a.renderPostReplyContext(hb, p) - a.renderPostLikeContext(hb, p) + if !o.activityPub || o.p.firstParameter(activityPubReplyActorParameter) == "" { + a.renderPostReplyContext(hb, o.p) + } + a.renderPostLikeContext(hb, o.p) // Render markdown hb.WriteElementOpen("div", "class", "e-content") - _ = a.renderMarkdownToWriter(w, p.Content, absolute) + _ = a.renderMarkdownToWriter(w, o.p.Content, o.absolute) hb.WriteElementClose("div") // Add bookmark links to the bottom - for _, l := range p.Parameters[a.cfg.Micropub.BookmarkParam] { + for _, l := range o.p.Parameters[a.cfg.Micropub.BookmarkParam] { hb.WriteElementOpen("p") hb.WriteElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer") hb.WriteEscaped(l) @@ -84,7 +92,7 @@ func (a *goBlog) feedHtml(w io.Writer, p *post) { hb.WriteElementClose("audio") } // Add post HTML - a.postHtmlToWriter(hb, p, true) + a.postHtmlToWriter(hb, &postHtmlOptions{p: p, absolute: true}) // Add link to interactions and comments blogConfig := a.getBlogFromPost(p) if cc := blogConfig.Comments; cc != nil && cc.Enabled { @@ -99,7 +107,7 @@ func (a *goBlog) feedHtml(w io.Writer, p *post) { func (a *goBlog) minFeedHtml(w io.Writer, p *post) { hb := htmlbuilder.NewHtmlBuilder(w) // Add post HTML - a.postHtmlToWriter(hb, p, true) + a.postHtmlToWriter(hb, &postHtmlOptions{p: p, absolute: true}) } const summaryDivider = "" @@ -145,7 +153,11 @@ func (a *goBlog) postTranslations(p *post) []*post { } func (p *post) isPublishedSectionPost() bool { - return p.Published != "" && p.Section != "" && p.Status == statusPublished && p.Visibility == visibilityPublic + return p.Section != "" && p.Status == statusPublished +} + +func (p *post) isPublicPublishedSectionPost() bool { + return p.isPublishedSectionPost() && p.Visibility == visibilityPublic } func (a *goBlog) postToMfItem(p *post) *microformatItem { diff --git a/telegram.go b/telegram.go index 3f7b434..cce491a 100644 --- a/telegram.go +++ b/telegram.go @@ -26,7 +26,7 @@ func (tg *configTelegram) enabled() bool { func (a *goBlog) tgPost(silent bool) func(*post) { return func(p *post) { - if tg := a.getBlogFromPost(p).Telegram; tg.enabled() && p.isPublishedSectionPost() { + if tg := a.getBlogFromPost(p).Telegram; tg.enabled() && p.isPublicPublishedSectionPost() { tgChat := p.firstParameter("telegramchat") tgMsg := p.firstParameter("telegrammsg") if tgChat != "" && tgMsg != "" { diff --git a/tts.go b/tts.go index 615a77f..e345093 100644 --- a/tts.go +++ b/tts.go @@ -28,7 +28,7 @@ func (a *goBlog) initTTS() { } createOrUpdate := func(p *post) { // Automatically create audio for published section posts only - if !p.isPublishedSectionPost() { + if !p.isPublicPublishedSectionPost() { return } // Check if there is already a tts audio file @@ -69,7 +69,7 @@ func (a *goBlog) createPostTTSAudio(p *post) error { parts = append(parts, a.renderMdTitle(title)) } // Add body split into paragraphs because of 5000 character limit - parts = append(parts, strings.Split(htmlText(a.postHtml(p, false)), "\n\n")...) + parts = append(parts, strings.Split(htmlText(a.postHtml(&postHtmlOptions{p: p})), "\n\n")...) // Create TTS audio for each part partWriters := make([]io.Writer, len(parts)) diff --git a/ui.go b/ui.go index c8ba250..890fbfb 100644 --- a/ui.go +++ b/ui.go @@ -38,7 +38,7 @@ func (a *goBlog) wrapUiPlugins(t plugintypes.RenderType, d plugintypes.RenderDat func (a *goBlog) renderEditorPreview(hb *htmlbuilder.HtmlBuilder, bc *configBlog, p *post) { a.renderPostTitle(hb, p) a.renderPostMeta(hb, p, bc, "preview") - a.postHtmlToWriter(hb, p, true) + a.postHtmlToWriter(hb, &postHtmlOptions{p: p, absolute: true}) // a.renderPostGPX(hb, p, bc) a.renderPostTax(hb, p, bc) } @@ -931,7 +931,7 @@ func (a *goBlog) renderPost(hb *htmlbuilder.HtmlBuilder, rd *renderData) { // Old content warning a.renderOldContentWarning(hb, p, rd.Blog) // Content - a.postHtmlToWriter(hb, p, false) + a.postHtmlToWriter(hb, &postHtmlOptions{p: p}) // External Videp a.renderPostVideo(hb, p) // GPS Track @@ -1008,7 +1008,7 @@ func (a *goBlog) renderStaticHome(hb *htmlbuilder.HtmlBuilder, rd *renderData) { // Content if p.Content != "" { // Content - a.postHtmlToWriter(hb, p, false) + a.postHtmlToWriter(hb, &postHtmlOptions{p: p}) } // Author a.renderAuthor(hb) diff --git a/uiComponents.go b/uiComponents.go index 2c9b8de..d22c818 100644 --- a/uiComponents.go +++ b/uiComponents.go @@ -53,7 +53,7 @@ func (a *goBlog) renderSummary(hb *htmlbuilder.HtmlBuilder, bc *configBlog, p *p a.renderPostMeta(hb, p, bc, "summary") if typ != photoSummary && a.showFull(p) { // Show full content - a.postHtmlToWriter(hb, p, false) + a.postHtmlToWriter(hb, &postHtmlOptions{p: p}) } else { // Show IndieWeb context a.renderPostReplyContext(hb, p) diff --git a/webmentionSending.go b/webmentionSending.go index 9599ce0..16056ab 100644 --- a/webmentionSending.go +++ b/webmentionSending.go @@ -32,18 +32,13 @@ func (a *goBlog) sendWebmentions(p *post) error { // Ignore this post return nil } - links := []string{} contentBuf := bufferpool.Get() - a.postHtmlToWriter(contentBuf, p, false) - contentLinks, err := allLinksFromHTML(contentBuf, a.fullPostURL(p)) + a.postHtmlToWriter(contentBuf, &postHtmlOptions{p: p}) + links, err := allLinksFromHTML(contentBuf, a.fullPostURL(p)) bufferpool.Put(contentBuf) if err != nil { return err } - links = append(links, contentLinks...) - if mpc := a.cfg.Micropub; mpc != nil { - links = append(links, p.firstParameter(a.cfg.Micropub.LikeParam), p.firstParameter(a.cfg.Micropub.ReplyParam), p.firstParameter(a.cfg.Micropub.BookmarkParam)) - } for _, link := range lo.Uniq(links) { if link == "" { continue