Improve ActivityPub replies

This commit is contained in:
Jan-Lukas Else 2022-12-26 19:52:06 +01:00
parent d5e3d9e216
commit 47a42e1c55
11 changed files with 144 additions and 41 deletions

View File

@ -23,6 +23,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-fed/httpsig" "github.com/go-fed/httpsig"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/samber/lo"
"go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/contenttype" "go.goblog.app/app/pkgs/contenttype"
) )
@ -34,12 +35,16 @@ func (a *goBlog) initActivityPub() error {
} }
// Add hooks // Add hooks
a.pPostHooks = append(a.pPostHooks, func(p *post) { 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.apPost(p)
} }
}) })
a.pUpdateHooks = append(a.pUpdateHooks, func(p *post) { 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) a.apUpdate(p)
} }
}) })
@ -47,7 +52,7 @@ func (a *goBlog) initActivityPub() error {
a.apDelete(p) a.apDelete(p)
}) })
a.pUndeleteHooks = append(a.pUndeleteHooks, func(p *post) { a.pUndeleteHooks = append(a.pUndeleteHooks, func(p *post) {
if p.isPublishedSectionPost() { if p.isPublishedSectionPost() && (p.Visibility == visibilityPublic || p.Visibility == visibilityUnlisted) {
a.apUndelete(p) 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) _ = 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) { func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
blogName := chi.URLParam(r, "blog") blogName := chi.URLParam(r, "blog")
blog, ok := a.cfg.Blogs[blogName] 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 := ap.CreateNew(a.apNewID(blogConfig), a.toAPNote(p))
c.Actor = a.apAPIri(blogConfig) c.Actor = a.apAPIri(blogConfig)
c.Published = time.Now() 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) { 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 := ap.UpdateNew(a.apNewID(blogConfig), a.toAPNote(p))
u.Actor = a.apAPIri(blogConfig) u.Actor = a.apAPIri(blogConfig)
u.Published = time.Now() 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) { 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 := ap.DeleteNew(a.apNewID(blogConfig), a.activityPubId(p))
d.Actor = a.apAPIri(blogConfig) d.Actor = a.apAPIri(blogConfig)
d.Published = time.Now() 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) { func (a *goBlog) apUndelete(p *post) {
@ -470,21 +526,36 @@ func (a *goBlog) apSendProfileUpdates() {
update := ap.UpdateNew(a.apNewID(config), person) update := ap.UpdateNew(a.apNewID(config), person)
update.Actor = a.apAPIri(config) update.Actor = a.apAPIri(config)
update.Published = time.Now() update.Published = time.Now()
update.To.Append(ap.PublicNS, a.apGetFollowersCollectionId(blog, config))
a.apSendToAllFollowers(blog, update) 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) inboxes, err := a.db.apGetAllInboxes(blog)
if err != nil { if err != nil {
log.Println("Failed to retrieve inboxes:", err.Error()) log.Println("Failed to retrieve follower inboxes:", err.Error())
return 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) { func (a *goBlog) apSendTo(blogIri string, activity *ap.Activity, inboxes ...string) {
for _, i := range inboxes { for _, i := range lo.Uniq(inboxes) {
go func(inbox string) { go func(inbox string) {
_ = a.apQueueSendSigned(blogIri, inbox, activity) _ = a.apQueueSendSigned(blogIri, inbox, activity)
}(i) }(i)

View File

@ -44,18 +44,28 @@ func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter, r *htt
func (a *goBlog) toAPNote(p *post) *ap.Note { func (a *goBlog) toAPNote(p *post) *ap.Note {
// Create a Note object // Create a Note object
note := ap.ObjectNew(ap.NoteType) note := ap.ObjectNew(ap.NoteType)
note.To.Append(ap.PublicNS)
note.MediaType = ap.MimeType(contenttype.HTML)
note.ID = a.activityPubId(p) note.ID = a.activityPubId(p)
note.URL = ap.IRI(a.fullPostURL(p)) note.URL = ap.IRI(a.fullPostURL(p))
note.AttributedTo = a.apAPIri(a.getBlogFromPost(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 // Name and Type
if title := p.RenderedTitle; title != "" { if title := p.RenderedTitle; title != "" {
note.Type = ap.ArticleType note.Type = ap.ArticleType
note.Name.Add(ap.DefaultLangRef(title)) note.Name.Add(ap.DefaultLangRef(title))
} }
// Content // 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 // Attachments
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 { if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
var attachments ap.ItemCollection var attachments ap.ItemCollection
@ -75,6 +85,17 @@ func (a *goBlog) toAPNote(p *post) *ap.Note {
note.Tag.Append(apTag) 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 // Dates
if p.Published != "" { if p.Published != "" {
if t, err := dateparse.ParseLocal(p.Published); err == nil { if t, err := dateparse.ParseLocal(p.Published); err == nil {

View File

@ -129,7 +129,7 @@ func (a *goBlog) checkLinks(w io.Writer, posts ...*post) error {
func (a *goBlog) allLinks(posts ...*post) (allLinks []*stringPair, err error) { func (a *goBlog) allLinks(posts ...*post) (allLinks []*stringPair, err error) {
for _, p := range posts { for _, p := range posts {
contentBuf := bufferpool.Get() contentBuf := bufferpool.Get()
a.postHtmlToWriter(contentBuf, p, true) a.postHtmlToWriter(contentBuf, &postHtmlOptions{p: p, absolute: true})
links, err := allLinksFromHTML(contentBuf, a.fullPostURL(p)) links, err := allLinksFromHTML(contentBuf, a.fullPostURL(p))
bufferpool.Put(contentBuf) bufferpool.Put(contentBuf)
if err != nil { if err != nil {

View File

@ -18,7 +18,7 @@ func (a *goBlog) initIndexNow() {
// Add hooks // Add hooks
hook := func(p *post) { hook := func(p *post) {
// Check if post is published // Check if post is published
if !p.isPublishedSectionPost() { if !p.isPublicPublishedSectionPost() {
return return
} }
// Send IndexNow request // Send IndexNow request

View File

@ -20,6 +20,10 @@ func (a *goBlog) checkPost(p *post, new bool) (err error) {
} }
now := time.Now().Local() now := time.Now().Local()
nowString := now.Format(time.RFC3339) nowString := now.Format(time.RFC3339)
// Add parameters map
if p.Parameters == nil {
p.Parameters = map[string][]string{}
}
// Maybe add blog // Maybe add blog
if p.Blog == "" { if p.Blog == "" {
p.Blog = a.cfg.DefaultBlog p.Blog = a.cfg.DefaultBlog

View File

@ -39,33 +39,41 @@ func (p *post) addParameter(parameter, value string) {
p.Parameters[parameter] = append(p.Parameters[parameter], value) 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() buf := bufferpool.Get()
a.postHtmlToWriter(buf, p, absolute) a.postHtmlToWriter(buf, o)
res = buf.String() res = buf.String()
bufferpool.Put(buf) bufferpool.Put(buf)
return return
} }
func (a *goBlog) postHtmlToWriter(w io.Writer, p *post, absolute bool) { func (a *goBlog) postHtmlToWriter(w io.Writer, o *postHtmlOptions) {
// Build HTML // Build HTML
hb := htmlbuilder.NewHtmlBuilder(w) hb := htmlbuilder.NewHtmlBuilder(w)
// Add audio to the top // 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("audio", "controls", "preload", "none")
hb.WriteElementOpen("source", "src", a) hb.WriteElementOpen("source", "src", a)
hb.WriteElementClose("source") hb.WriteElementClose("source")
hb.WriteElementClose("audio") hb.WriteElementClose("audio")
} }
// Add IndieWeb context // Add IndieWeb context
a.renderPostReplyContext(hb, p) if !o.activityPub || o.p.firstParameter(activityPubReplyActorParameter) == "" {
a.renderPostLikeContext(hb, p) a.renderPostReplyContext(hb, o.p)
}
a.renderPostLikeContext(hb, o.p)
// Render markdown // Render markdown
hb.WriteElementOpen("div", "class", "e-content") hb.WriteElementOpen("div", "class", "e-content")
_ = a.renderMarkdownToWriter(w, p.Content, absolute) _ = a.renderMarkdownToWriter(w, o.p.Content, o.absolute)
hb.WriteElementClose("div") hb.WriteElementClose("div")
// Add bookmark links to the bottom // 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("p")
hb.WriteElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer") hb.WriteElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer")
hb.WriteEscaped(l) hb.WriteEscaped(l)
@ -84,7 +92,7 @@ func (a *goBlog) feedHtml(w io.Writer, p *post) {
hb.WriteElementClose("audio") hb.WriteElementClose("audio")
} }
// Add post HTML // Add post HTML
a.postHtmlToWriter(hb, p, true) a.postHtmlToWriter(hb, &postHtmlOptions{p: p, absolute: true})
// Add link to interactions and comments // Add link to interactions and comments
blogConfig := a.getBlogFromPost(p) blogConfig := a.getBlogFromPost(p)
if cc := blogConfig.Comments; cc != nil && cc.Enabled { 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) { func (a *goBlog) minFeedHtml(w io.Writer, p *post) {
hb := htmlbuilder.NewHtmlBuilder(w) hb := htmlbuilder.NewHtmlBuilder(w)
// Add post HTML // Add post HTML
a.postHtmlToWriter(hb, p, true) a.postHtmlToWriter(hb, &postHtmlOptions{p: p, absolute: true})
} }
const summaryDivider = "<!--more-->" const summaryDivider = "<!--more-->"
@ -145,7 +153,11 @@ func (a *goBlog) postTranslations(p *post) []*post {
} }
func (p *post) isPublishedSectionPost() bool { 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 { func (a *goBlog) postToMfItem(p *post) *microformatItem {

View File

@ -26,7 +26,7 @@ func (tg *configTelegram) enabled() bool {
func (a *goBlog) tgPost(silent bool) func(*post) { func (a *goBlog) tgPost(silent bool) func(*post) {
return func(p *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") tgChat := p.firstParameter("telegramchat")
tgMsg := p.firstParameter("telegrammsg") tgMsg := p.firstParameter("telegrammsg")
if tgChat != "" && tgMsg != "" { if tgChat != "" && tgMsg != "" {

4
tts.go
View File

@ -28,7 +28,7 @@ func (a *goBlog) initTTS() {
} }
createOrUpdate := func(p *post) { createOrUpdate := func(p *post) {
// Automatically create audio for published section posts only // Automatically create audio for published section posts only
if !p.isPublishedSectionPost() { if !p.isPublicPublishedSectionPost() {
return return
} }
// Check if there is already a tts audio file // 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)) parts = append(parts, a.renderMdTitle(title))
} }
// Add body split into paragraphs because of 5000 character limit // 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 // Create TTS audio for each part
partWriters := make([]io.Writer, len(parts)) partWriters := make([]io.Writer, len(parts))

6
ui.go
View File

@ -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) { func (a *goBlog) renderEditorPreview(hb *htmlbuilder.HtmlBuilder, bc *configBlog, p *post) {
a.renderPostTitle(hb, p) a.renderPostTitle(hb, p)
a.renderPostMeta(hb, p, bc, "preview") 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.renderPostGPX(hb, p, bc)
a.renderPostTax(hb, p, bc) a.renderPostTax(hb, p, bc)
} }
@ -931,7 +931,7 @@ func (a *goBlog) renderPost(hb *htmlbuilder.HtmlBuilder, rd *renderData) {
// Old content warning // Old content warning
a.renderOldContentWarning(hb, p, rd.Blog) a.renderOldContentWarning(hb, p, rd.Blog)
// Content // Content
a.postHtmlToWriter(hb, p, false) a.postHtmlToWriter(hb, &postHtmlOptions{p: p})
// External Videp // External Videp
a.renderPostVideo(hb, p) a.renderPostVideo(hb, p)
// GPS Track // GPS Track
@ -1008,7 +1008,7 @@ func (a *goBlog) renderStaticHome(hb *htmlbuilder.HtmlBuilder, rd *renderData) {
// Content // Content
if p.Content != "" { if p.Content != "" {
// Content // Content
a.postHtmlToWriter(hb, p, false) a.postHtmlToWriter(hb, &postHtmlOptions{p: p})
} }
// Author // Author
a.renderAuthor(hb) a.renderAuthor(hb)

View File

@ -53,7 +53,7 @@ func (a *goBlog) renderSummary(hb *htmlbuilder.HtmlBuilder, bc *configBlog, p *p
a.renderPostMeta(hb, p, bc, "summary") a.renderPostMeta(hb, p, bc, "summary")
if typ != photoSummary && a.showFull(p) { if typ != photoSummary && a.showFull(p) {
// Show full content // Show full content
a.postHtmlToWriter(hb, p, false) a.postHtmlToWriter(hb, &postHtmlOptions{p: p})
} else { } else {
// Show IndieWeb context // Show IndieWeb context
a.renderPostReplyContext(hb, p) a.renderPostReplyContext(hb, p)

View File

@ -32,18 +32,13 @@ func (a *goBlog) sendWebmentions(p *post) error {
// Ignore this post // Ignore this post
return nil return nil
} }
links := []string{}
contentBuf := bufferpool.Get() contentBuf := bufferpool.Get()
a.postHtmlToWriter(contentBuf, p, false) a.postHtmlToWriter(contentBuf, &postHtmlOptions{p: p})
contentLinks, err := allLinksFromHTML(contentBuf, a.fullPostURL(p)) links, err := allLinksFromHTML(contentBuf, a.fullPostURL(p))
bufferpool.Put(contentBuf) bufferpool.Put(contentBuf)
if err != nil { if err != nil {
return err 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) { for _, link := range lo.Uniq(links) {
if link == "" { if link == "" {
continue continue