GoBlog/activityStreams.go

187 lines
5.8 KiB
Go

package main
import (
"bytes"
"context"
"encoding/pem"
"fmt"
"net/http"
"net/url"
"github.com/araddon/dateparse"
ct "github.com/elnormous/contenttype"
ap "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
"go.goblog.app/app/pkgs/contenttype"
)
const asRequestKey contextKey = "asRequest"
func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
if len(a.asCheckMediaTypes) == 0 {
a.asCheckMediaTypes = []ct.MediaType{
ct.NewMediaType(contenttype.HTML),
ct.NewMediaType(contenttype.AS),
ct.NewMediaType(contenttype.LDJSON),
ct.NewMediaType(contenttype.LDJSON + "; profile=\"https://www.w3.org/ns/activitystreams\""),
}
}
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled && !a.isPrivate() {
// Check if accepted media type is not HTML
if mt, _, err := ct.GetAcceptableMediaType(r, a.asCheckMediaTypes); err == nil && mt.String() != a.asCheckMediaTypes[0].String() {
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true)))
return
}
}
next.ServeHTTP(rw, r)
})
}
func (a *goBlog) serveActivityStreamsPost(w http.ResponseWriter, r *http.Request, status int, p *post) {
a.serveAPItem(w, r, status, a.toAPNote(p))
}
func (a *goBlog) toAPNote(p *post) *ap.Note {
// Create a Note object
note := ap.ObjectNew(ap.NoteType)
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.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
for _, image := range images {
apImage := ap.ObjectNew(ap.ImageType)
apImage.URL = ap.IRI(image)
attachments.Append(apImage)
}
note.Attachment = attachments
}
// Tags
for _, tagTax := range a.cfg.ActivityPub.TagsTaxonomies {
for _, tag := range p.Parameters[tagTax] {
apTag := &ap.Object{Type: "Hashtag"}
apTag.Name.Add(ap.DefaultLangRef(tag))
apTag.URL = ap.IRI(a.getFullAddress(a.getRelativePath(p.Blog, fmt.Sprintf("/%s/%s", tagTax, urlize(tag)))))
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 {
note.Published = t
}
}
if p.Updated != "" {
if t, err := dateparse.ParseLocal(p.Updated); err == nil {
note.Updated = t
}
}
// Reply
if replyLink := p.firstParameter(a.cfg.Micropub.ReplyParam); replyLink != "" {
note.InReplyTo = ap.IRI(replyLink)
}
return note
}
const activityPubVersionParam = "activitypubversion"
func (a *goBlog) activityPubId(p *post) ap.IRI {
fu := a.fullPostURL(p)
if version := p.firstParameter(activityPubVersionParam); version != "" {
return ap.IRI(fu + "?activitypubversion=" + version)
}
return ap.IRI(fu)
}
func (a *goBlog) toApPerson(blog string) *ap.Person {
b := a.cfg.Blogs[blog]
apIri := a.apAPIri(b)
apBlog := ap.PersonNew(apIri)
apBlog.URL = apIri
apBlog.Name.Set(ap.DefaultLang, ap.Content(a.renderMdTitle(b.Title)))
apBlog.Summary.Set(ap.DefaultLang, ap.Content(b.Description))
apBlog.PreferredUsername.Set(ap.DefaultLang, ap.Content(blog))
apBlog.Inbox = ap.IRI(a.getFullAddress("/activitypub/inbox/" + blog))
apBlog.Followers = ap.IRI(a.getFullAddress("/activitypub/followers/" + blog))
apBlog.PublicKey.Owner = apIri
apBlog.PublicKey.ID = ap.IRI(a.apIri(b) + "#main-key")
apBlog.PublicKey.PublicKeyPem = string(pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: a.apPubKeyBytes,
}))
if a.hasProfileImage() {
icon := &ap.Image{}
icon.Type = ap.ImageType
icon.MediaType = ap.MimeType(contenttype.JPEG)
icon.URL = ap.IRI(a.getFullAddress(a.profileImagePath(profileImageFormatJPEG, 0, 0)))
apBlog.Icon = icon
}
return apBlog
}
func (a *goBlog) serveActivityStreams(w http.ResponseWriter, r *http.Request, status int, blog string) {
a.serveAPItem(w, r, status, a.toApPerson(blog))
}
func (a *goBlog) serveAPItem(w http.ResponseWriter, r *http.Request, status int, item any) {
// Encode
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(item)
if err != nil {
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
return
}
// Send response
w.WriteHeader(status)
w.Header().Set(contentType, contenttype.ASUTF8)
_ = a.min.Get().Minify(contenttype.AS, w, bytes.NewReader(binary))
}
func apUsername(person *ap.Person) string {
preferredUsername := person.PreferredUsername.First().Value.String()
u, err := url.Parse(person.GetLink().String())
if err != nil || u == nil || u.Host == "" || preferredUsername == "" {
return person.GetLink().String()
}
return fmt.Sprintf("@%s@%s", preferredUsername, u.Host)
}