mirror of https://github.com/jlelse/GoBlog
Improve ActivityPub replies
This commit is contained in:
parent
d5e3d9e216
commit
47a42e1c55
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
2
check.go
2
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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
4
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))
|
||||
|
|
6
ui.go
6
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue