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-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)

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 {
// 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 {

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) {
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 {

View File

@ -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

View File

@ -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

View File

@ -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 = "<!--more-->"
@ -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 {

View File

@ -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 != "" {

4
tts.go
View File

@ -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))

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) {
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)

View File

@ -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)

View File

@ -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