GoBlog/activityStreams.go

214 lines
6.4 KiB
Go

package main
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"github.com/araddon/dateparse"
ct "github.com/elnormous/contenttype"
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/contenttype"
)
const asContext = "https://www.w3.org/ns/activitystreams"
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),
}
}
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)
})
}
type asNote struct {
Context any `json:"@context,omitempty"`
To []string `json:"to,omitempty"`
InReplyTo string `json:"inReplyTo,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Content string `json:"content,omitempty"`
MediaType string `json:"mediaType,omitempty"`
Attachment []*asAttachment `json:"attachment,omitempty"`
Published string `json:"published,omitempty"`
Updated string `json:"updated,omitempty"`
ID string `json:"id,omitempty"`
URL string `json:"url,omitempty"`
AttributedTo string `json:"attributedTo,omitempty"`
Tag []*asTag `json:"tag,omitempty"`
}
type asPerson struct {
Context any `json:"@context,omitempty"`
ID string `json:"id,omitempty"`
URL string `json:"url,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Summary string `json:"summary,omitempty"`
PreferredUsername string `json:"preferredUsername,omitempty"`
Icon *asAttachment `json:"icon,omitempty"`
Inbox string `json:"inbox,omitempty"`
PublicKey *asPublicKey `json:"publicKey,omitempty"`
Endpoints *asEndpoints `json:"endpoints,omitempty"`
}
type asAttachment struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
}
type asTag struct {
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Href string `json:"href,omitempty"`
}
type asPublicKey struct {
ID string `json:"id,omitempty"`
Owner string `json:"owner,omitempty"`
PublicKeyPem string `json:"publicKeyPem,omitempty"`
}
type asEndpoints struct {
SharedInbox string `json:"sharedInbox,omitempty"`
}
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
buf := bufferpool.Get()
defer bufferpool.Put(buf)
if err := json.NewEncoder(buf).Encode(a.toASNote(p)); err != nil {
http.Error(w, "Encoding failed", http.StatusInternalServerError)
return
}
w.Header().Set(contentType, contenttype.ASUTF8)
_ = a.min.Get().Minify(contenttype.AS, w, buf)
}
func (a *goBlog) toASNote(p *post) *asNote {
// Create a Note object
as := &asNote{
Context: []string{asContext},
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
MediaType: contenttype.HTML,
ID: a.activityPubId(p),
URL: a.fullPostURL(p),
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
}
// Name and Type
if title := p.RenderedTitle; title != "" {
as.Name = title
as.Type = "Article"
} else {
as.Type = "Note"
}
// Content
as.Content = a.postHtml(p, true)
// Attachments
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
for _, image := range images {
as.Attachment = append(as.Attachment, &asAttachment{
Type: "Image",
URL: image,
})
}
}
// Tags
for _, tagTax := range a.cfg.ActivityPub.TagsTaxonomies {
for _, tag := range p.Parameters[tagTax] {
as.Tag = append(as.Tag, &asTag{
Type: "Hashtag",
Name: tag,
Href: a.getFullAddress(a.getRelativePath(p.Blog, fmt.Sprintf("/%s/%s", tagTax, urlize(tag)))),
})
}
}
// Dates
dateFormat := "2006-01-02T15:04:05-07:00"
if p.Published != "" {
if t, err := dateparse.ParseLocal(p.Published); err == nil {
as.Published = t.Format(dateFormat)
}
}
if p.Updated != "" {
if t, err := dateparse.ParseLocal(p.Updated); err == nil {
as.Updated = t.Format(dateFormat)
}
}
// Reply
if replyLink := p.firstParameter(a.cfg.Micropub.ReplyParam); replyLink != "" {
as.InReplyTo = replyLink
}
return as
}
const activityPubVersionParam = "activitypubversion"
func (a *goBlog) activityPubId(p *post) string {
fu := a.fullPostURL(p)
if version := p.firstParameter(activityPubVersionParam); version != "" {
return fu + "?activitypubversion=" + version
}
return fu
}
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
b := a.cfg.Blogs[blog]
publicKeyDer, err := x509.MarshalPKIXPublicKey(&(a.apPrivateKey.PublicKey))
if err != nil {
a.serveError(w, r, "Failed to marshal public key", http.StatusInternalServerError)
return
}
asBlog := &asPerson{
Context: []string{asContext},
Type: "Person",
ID: a.apIri(b),
URL: a.apIri(b),
Name: a.renderMdTitle(b.Title),
Summary: b.Description,
PreferredUsername: blog,
Inbox: a.getFullAddress("/activitypub/inbox/" + blog),
PublicKey: &asPublicKey{
Owner: a.apIri(b),
ID: a.apIri(b) + "#main-key",
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: publicKeyDer,
})),
},
}
// Add profile picture
if a.cfg.User.Picture != "" {
asBlog.Icon = &asAttachment{
Type: "Image",
URL: a.cfg.User.Picture,
}
}
// Encode
buf := bufferpool.Get()
defer bufferpool.Put(buf)
if err := json.NewEncoder(buf).Encode(asBlog); err != nil {
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
return
}
w.Header().Set(contentType, contenttype.ASUTF8)
_ = a.min.Get().Minify(contenttype.AS, w, buf)
}