mirror of https://github.com/jlelse/GoBlog
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
5.8 KiB
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)
|
|
}
|