2020-10-26 16:37:31 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-02-24 12:16:33 +00:00
|
|
|
"context"
|
2021-01-21 16:59:47 +00:00
|
|
|
"encoding/json"
|
2020-10-26 16:37:31 +00:00
|
|
|
"encoding/pem"
|
2021-03-10 10:29:20 +00:00
|
|
|
"fmt"
|
2020-10-26 16:37:31 +00:00
|
|
|
"net/http"
|
|
|
|
|
|
|
|
"github.com/araddon/dateparse"
|
2021-06-18 12:32:03 +00:00
|
|
|
ct "github.com/elnormous/contenttype"
|
2022-04-10 09:46:35 +00:00
|
|
|
"go.goblog.app/app/pkgs/bufferpool"
|
2021-06-28 20:17:18 +00:00
|
|
|
"go.goblog.app/app/pkgs/contenttype"
|
2020-10-26 16:37:31 +00:00
|
|
|
)
|
|
|
|
|
2021-06-18 12:32:03 +00:00
|
|
|
const asContext = "https://www.w3.org/ns/activitystreams"
|
2021-03-10 10:29:20 +00:00
|
|
|
|
2021-06-29 15:07:08 +00:00
|
|
|
const asRequestKey contextKey = "asRequest"
|
2021-02-24 12:16:33 +00:00
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
|
2021-06-19 06:37:16 +00:00
|
|
|
if len(a.asCheckMediaTypes) == 0 {
|
|
|
|
a.asCheckMediaTypes = []ct.MediaType{
|
|
|
|
ct.NewMediaType(contenttype.HTML),
|
|
|
|
ct.NewMediaType(contenttype.AS),
|
|
|
|
ct.NewMediaType(contenttype.LDJSON),
|
|
|
|
}
|
|
|
|
}
|
2020-10-26 16:37:31 +00:00
|
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
2021-07-27 10:51:08 +00:00
|
|
|
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled && !a.isPrivate() {
|
2021-03-10 10:29:20 +00:00
|
|
|
// Check if accepted media type is not HTML
|
2021-06-19 06:37:16 +00:00
|
|
|
if mt, _, err := ct.GetAcceptableMediaType(r, a.asCheckMediaTypes); err == nil && mt.String() != a.asCheckMediaTypes[0].String() {
|
2021-02-24 12:16:33 +00:00
|
|
|
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true)))
|
|
|
|
return
|
2021-02-16 15:26:21 +00:00
|
|
|
}
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
next.ServeHTTP(rw, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
type asNote struct {
|
2022-03-16 07:28:03 +00:00
|
|
|
Context any `json:"@context,omitempty"`
|
2020-10-26 16:37:31 +00:00
|
|
|
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"`
|
2021-03-10 10:29:20 +00:00
|
|
|
Tag []*asTag `json:"tag,omitempty"`
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type asPerson struct {
|
2022-03-16 07:28:03 +00:00
|
|
|
Context any `json:"@context,omitempty"`
|
2020-10-26 16:37:31 +00:00
|
|
|
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"`
|
2020-11-25 10:29:36 +00:00
|
|
|
Endpoints *asEndpoints `json:"endpoints,omitempty"`
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type asAttachment struct {
|
|
|
|
Type string `json:"type,omitempty"`
|
|
|
|
URL string `json:"url,omitempty"`
|
|
|
|
}
|
|
|
|
|
2021-03-10 10:29:20 +00:00
|
|
|
type asTag struct {
|
|
|
|
Type string `json:"type,omitempty"`
|
|
|
|
Name string `json:"name,omitempty"`
|
|
|
|
Href string `json:"href,omitempty"`
|
|
|
|
}
|
|
|
|
|
2020-10-26 16:37:31 +00:00
|
|
|
type asPublicKey struct {
|
|
|
|
ID string `json:"id,omitempty"`
|
|
|
|
Owner string `json:"owner,omitempty"`
|
|
|
|
PublicKeyPem string `json:"publicKeyPem,omitempty"`
|
|
|
|
}
|
|
|
|
|
2020-11-25 10:29:36 +00:00
|
|
|
type asEndpoints struct {
|
|
|
|
SharedInbox string `json:"sharedInbox,omitempty"`
|
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
|
2022-04-10 09:46:35 +00:00
|
|
|
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
|
|
|
|
}
|
2021-06-18 12:32:03 +00:00
|
|
|
w.Header().Set(contentType, contenttype.ASUTF8)
|
2022-04-10 09:46:35 +00:00
|
|
|
_ = a.min.Get().Minify(contenttype.AS, w, buf)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) toASNote(p *post) *asNote {
|
2020-10-26 16:37:31 +00:00
|
|
|
// Create a Note object
|
|
|
|
as := &asNote{
|
2021-06-18 12:32:03 +00:00
|
|
|
Context: []string{asContext},
|
2020-10-26 16:37:31 +00:00
|
|
|
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
2021-06-18 12:32:03 +00:00
|
|
|
MediaType: contenttype.HTML,
|
2022-03-31 12:55:36 +00:00
|
|
|
ID: a.activityPubId(p),
|
2021-06-06 12:39:42 +00:00
|
|
|
URL: a.fullPostURL(p),
|
|
|
|
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
// Name and Type
|
2021-08-05 06:09:34 +00:00
|
|
|
if title := p.RenderedTitle; title != "" {
|
2020-10-26 16:37:31 +00:00
|
|
|
as.Name = title
|
|
|
|
as.Type = "Article"
|
|
|
|
} else {
|
|
|
|
as.Type = "Note"
|
|
|
|
}
|
|
|
|
// Content
|
2022-02-11 15:19:10 +00:00
|
|
|
as.Content = a.postHtml(p, true)
|
2020-10-26 16:37:31 +00:00
|
|
|
// Attachments
|
2021-06-06 12:39:42 +00:00
|
|
|
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
|
2020-10-26 16:37:31 +00:00
|
|
|
for _, image := range images {
|
|
|
|
as.Attachment = append(as.Attachment, &asAttachment{
|
|
|
|
Type: "Image",
|
|
|
|
URL: image,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-03-10 10:29:20 +00:00
|
|
|
// Tags
|
2021-06-06 12:39:42 +00:00
|
|
|
for _, tagTax := range a.cfg.ActivityPub.TagsTaxonomies {
|
2021-03-10 10:29:20 +00:00
|
|
|
for _, tag := range p.Parameters[tagTax] {
|
|
|
|
as.Tag = append(as.Tag, &asTag{
|
|
|
|
Type: "Hashtag",
|
|
|
|
Name: tag,
|
2021-06-10 20:09:50 +00:00
|
|
|
Href: a.getFullAddress(a.getRelativePath(p.Blog, fmt.Sprintf("/%s/%s", tagTax, urlize(tag)))),
|
2021-03-10 10:29:20 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-10-26 16:37:31 +00:00
|
|
|
// Dates
|
|
|
|
dateFormat := "2006-01-02T15:04:05-07:00"
|
|
|
|
if p.Published != "" {
|
2020-12-16 19:21:35 +00:00
|
|
|
if t, err := dateparse.ParseLocal(p.Published); err == nil {
|
2020-10-26 16:37:31 +00:00
|
|
|
as.Published = t.Format(dateFormat)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if p.Updated != "" {
|
2020-12-16 19:21:35 +00:00
|
|
|
if t, err := dateparse.ParseLocal(p.Updated); err == nil {
|
2020-10-26 16:37:31 +00:00
|
|
|
as.Updated = t.Format(dateFormat)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Reply
|
2021-06-06 12:39:42 +00:00
|
|
|
if replyLink := p.firstParameter(a.cfg.Micropub.ReplyParam); replyLink != "" {
|
2020-10-26 16:37:31 +00:00
|
|
|
as.InReplyTo = replyLink
|
|
|
|
}
|
|
|
|
return as
|
|
|
|
}
|
|
|
|
|
2022-03-31 12:55:36 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-08-07 10:46:49 +00:00
|
|
|
func (a *goBlog) toAsPerson(blog string) *asPerson {
|
2021-06-06 12:39:42 +00:00
|
|
|
b := a.cfg.Blogs[blog]
|
2020-10-26 16:37:31 +00:00
|
|
|
asBlog := &asPerson{
|
2021-06-18 12:32:03 +00:00
|
|
|
Context: []string{asContext},
|
2020-10-26 16:37:31 +00:00
|
|
|
Type: "Person",
|
2021-06-06 12:39:42 +00:00
|
|
|
ID: a.apIri(b),
|
|
|
|
URL: a.apIri(b),
|
2021-08-05 12:53:22 +00:00
|
|
|
Name: a.renderMdTitle(b.Title),
|
2020-10-26 16:37:31 +00:00
|
|
|
Summary: b.Description,
|
|
|
|
PreferredUsername: blog,
|
2021-06-10 20:09:50 +00:00
|
|
|
Inbox: a.getFullAddress("/activitypub/inbox/" + blog),
|
2020-10-26 16:37:31 +00:00
|
|
|
PublicKey: &asPublicKey{
|
2021-06-06 12:39:42 +00:00
|
|
|
Owner: a.apIri(b),
|
|
|
|
ID: a.apIri(b) + "#main-key",
|
2020-10-26 16:37:31 +00:00
|
|
|
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
|
|
|
|
Type: "PUBLIC KEY",
|
|
|
|
Headers: nil,
|
2022-04-21 16:18:39 +00:00
|
|
|
Bytes: a.apPubKeyBytes,
|
2020-10-26 16:37:31 +00:00
|
|
|
})),
|
|
|
|
},
|
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
if a.cfg.User.Picture != "" {
|
2020-10-26 16:37:31 +00:00
|
|
|
asBlog.Icon = &asAttachment{
|
|
|
|
Type: "Image",
|
2021-06-06 12:39:42 +00:00
|
|
|
URL: a.cfg.User.Picture,
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-07 10:46:49 +00:00
|
|
|
return asBlog
|
2022-04-21 16:18:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
|
2022-08-07 10:46:49 +00:00
|
|
|
person := a.toAsPerson(blog)
|
2022-04-10 09:46:35 +00:00
|
|
|
// Encode
|
|
|
|
buf := bufferpool.Get()
|
|
|
|
defer bufferpool.Put(buf)
|
2022-04-21 16:18:39 +00:00
|
|
|
if err := json.NewEncoder(buf).Encode(person); err != nil {
|
2022-04-10 09:46:35 +00:00
|
|
|
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2021-06-18 12:32:03 +00:00
|
|
|
w.Header().Set(contentType, contenttype.ASUTF8)
|
2022-04-12 06:48:09 +00:00
|
|
|
_ = a.min.Get().Minify(contenttype.AS, w, buf)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|