2020-10-26 16:37:31 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-02-22 09:14:48 +00:00
|
|
|
"context"
|
2021-07-29 13:31:49 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
2020-10-26 16:37:31 +00:00
|
|
|
"crypto/x509"
|
2020-11-09 15:40:12 +00:00
|
|
|
"database/sql"
|
2021-01-21 16:59:47 +00:00
|
|
|
"encoding/json"
|
2020-10-26 16:37:31 +00:00
|
|
|
"encoding/pem"
|
2022-02-12 21:29:45 +00:00
|
|
|
"encoding/xml"
|
2020-10-26 16:37:31 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-02-12 21:29:45 +00:00
|
|
|
"io"
|
2020-10-26 16:37:31 +00:00
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2021-03-03 17:19:55 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
2020-10-26 16:37:31 +00:00
|
|
|
"github.com/go-fed/httpsig"
|
2021-11-18 16:21:50 +00:00
|
|
|
"github.com/google/uuid"
|
2021-06-15 20:20:54 +00:00
|
|
|
"github.com/spf13/cast"
|
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-06 12:39:42 +00:00
|
|
|
func (a *goBlog) initActivityPub() error {
|
2022-04-21 16:18:39 +00:00
|
|
|
if !a.apEnabled() {
|
|
|
|
// ActivityPub disabled
|
2020-11-17 21:10:13 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// Add hooks
|
2021-06-06 12:39:42 +00:00
|
|
|
a.pPostHooks = append(a.pPostHooks, func(p *post) {
|
2020-11-17 21:10:13 +00:00
|
|
|
if p.isPublishedSectionPost() {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.apPost(p)
|
2020-11-17 21:10:13 +00:00
|
|
|
}
|
|
|
|
})
|
2021-06-06 12:39:42 +00:00
|
|
|
a.pUpdateHooks = append(a.pUpdateHooks, func(p *post) {
|
2020-11-17 21:10:13 +00:00
|
|
|
if p.isPublishedSectionPost() {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.apUpdate(p)
|
2020-11-17 21:10:13 +00:00
|
|
|
}
|
|
|
|
})
|
2021-06-06 12:39:42 +00:00
|
|
|
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
|
|
|
|
a.apDelete(p)
|
2020-11-17 21:10:13 +00:00
|
|
|
})
|
2022-01-03 12:55:44 +00:00
|
|
|
a.pUndeleteHooks = append(a.pUndeleteHooks, func(p *post) {
|
2022-02-22 15:52:03 +00:00
|
|
|
if p.isPublishedSectionPost() {
|
|
|
|
a.apUndelete(p)
|
|
|
|
}
|
2022-01-03 12:55:44 +00:00
|
|
|
})
|
2021-02-19 13:32:34 +00:00
|
|
|
// Prepare webfinger
|
2022-04-28 21:09:38 +00:00
|
|
|
a.prepareWebfinger()
|
2020-11-17 21:10:13 +00:00
|
|
|
// Read key and prepare signing
|
2021-07-29 13:31:49 +00:00
|
|
|
err := a.loadActivityPubPrivateKey()
|
2020-10-26 16:37:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
a.apPostSigner, _, err = httpsig.NewSigner(
|
2021-03-19 13:26:45 +00:00
|
|
|
[]httpsig.Algorithm{httpsig.RSA_SHA256},
|
|
|
|
httpsig.DigestSha256,
|
|
|
|
[]string{httpsig.RequestTarget, "date", "host", "digest"},
|
|
|
|
httpsig.Signature,
|
|
|
|
0,
|
|
|
|
)
|
2020-10-26 16:37:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-11-22 15:10:59 +00:00
|
|
|
// Init send queue
|
2021-06-06 12:39:42 +00:00
|
|
|
a.initAPSendQueue()
|
2022-04-21 16:18:39 +00:00
|
|
|
// Send profile updates
|
|
|
|
go func() {
|
|
|
|
// First wait a bit
|
|
|
|
time.Sleep(time.Second * 10)
|
|
|
|
// Then send profile update
|
|
|
|
a.apSendProfileUpdates()
|
|
|
|
}()
|
2020-10-26 16:37:31 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-04-21 16:18:39 +00:00
|
|
|
func (a *goBlog) apEnabled() bool {
|
|
|
|
if a.isPrivate() {
|
|
|
|
// Private mode, no AP
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if apc := a.cfg.ActivityPub; apc == nil || !apc.Enabled {
|
|
|
|
// Disabled
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-04-28 21:09:38 +00:00
|
|
|
func (a *goBlog) prepareWebfinger() {
|
|
|
|
a.webfingerResources = map[string]*configBlog{}
|
|
|
|
a.webfingerAccts = map[string]string{}
|
|
|
|
for name, blog := range a.cfg.Blogs {
|
|
|
|
acct := "acct:" + name + "@" + a.cfg.Server.publicHostname
|
|
|
|
a.webfingerResources[acct] = blog
|
|
|
|
a.webfingerResources[a.apIri(blog)] = blog
|
|
|
|
a.webfingerAccts[a.apIri(blog)] = acct
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
|
|
|
blog, ok := a.webfingerResources[r.URL.Query().Get("resource")]
|
2021-02-19 13:32:34 +00:00
|
|
|
if !ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "Resource not found", http.StatusNotFound)
|
2020-10-26 16:37:31 +00:00
|
|
|
return
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
apIri := a.apIri(blog)
|
2022-04-10 09:46:35 +00:00
|
|
|
// Encode
|
|
|
|
buf := bufferpool.Get()
|
|
|
|
defer bufferpool.Put(buf)
|
2022-04-28 21:09:38 +00:00
|
|
|
if err := json.NewEncoder(buf).Encode(map[string]any{
|
2021-06-23 17:20:50 +00:00
|
|
|
"subject": a.webfingerAccts[apIri],
|
2021-02-19 13:32:34 +00:00
|
|
|
"aliases": []string{
|
2021-06-23 17:20:50 +00:00
|
|
|
a.webfingerAccts[apIri],
|
|
|
|
apIri,
|
2021-02-19 13:32:34 +00:00
|
|
|
},
|
2020-10-26 16:37:31 +00:00
|
|
|
"links": []map[string]string{
|
|
|
|
{
|
|
|
|
"rel": "self",
|
2021-06-18 12:32:03 +00:00
|
|
|
"type": contenttype.AS,
|
2021-06-23 17:20:50 +00:00
|
|
|
"href": apIri,
|
2020-10-26 16:37:31 +00:00
|
|
|
},
|
2021-02-19 13:32:34 +00:00
|
|
|
{
|
|
|
|
"rel": "http://webfinger.net/rel/profile-page",
|
|
|
|
"type": "text/html",
|
2021-06-23 17:20:50 +00:00
|
|
|
"href": apIri,
|
2021-02-19 13:32:34 +00:00
|
|
|
},
|
2020-10-26 16:37:31 +00:00
|
|
|
},
|
2022-04-10 09:46:35 +00:00
|
|
|
}); err != nil {
|
|
|
|
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2021-06-18 12:32:03 +00:00
|
|
|
w.Header().Set(contentType, "application/jrd+json"+contenttype.CharsetUtf8Suffix)
|
2022-04-10 09:46:35 +00:00
|
|
|
_ = a.min.Get().Minify(contenttype.JSON, w, buf)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
2020-10-26 16:37:31 +00:00
|
|
|
blogName := chi.URLParam(r, "blog")
|
2021-06-23 17:20:50 +00:00
|
|
|
blog, ok := a.cfg.Blogs[blogName]
|
|
|
|
if !ok || blog == nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "Inbox not found", http.StatusNotFound)
|
2020-10-26 16:37:31 +00:00
|
|
|
return
|
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
blogIri := a.apIri(blog)
|
2020-11-13 14:19:09 +00:00
|
|
|
// Verify request
|
2021-06-19 06:37:16 +00:00
|
|
|
requestActor, requestKey, requestActorStatus, err := a.apVerifySignature(r)
|
2020-11-13 14:19:09 +00:00
|
|
|
if err != nil {
|
2020-11-13 20:29:09 +00:00
|
|
|
// Send 401 because signature could not be verified
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, err.Error(), http.StatusUnauthorized)
|
2020-11-13 14:19:09 +00:00
|
|
|
return
|
|
|
|
}
|
2020-11-13 15:29:22 +00:00
|
|
|
if requestActorStatus != 0 {
|
|
|
|
if requestActorStatus == http.StatusGone || requestActorStatus == http.StatusNotFound {
|
|
|
|
u, err := url.Parse(requestKey)
|
|
|
|
if err == nil {
|
|
|
|
u.Fragment = ""
|
|
|
|
u.RawFragment = ""
|
2021-06-06 12:39:42 +00:00
|
|
|
_ = a.db.apRemoveFollower(blogName, u.String())
|
2020-11-13 20:29:09 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2020-11-13 15:29:22 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "Error when trying to get request actor", http.StatusBadRequest)
|
2020-11-13 15:29:22 +00:00
|
|
|
return
|
|
|
|
}
|
2020-11-13 14:19:09 +00:00
|
|
|
// Parse activity
|
2022-03-16 07:28:03 +00:00
|
|
|
activity := map[string]any{}
|
2020-11-13 14:19:09 +00:00
|
|
|
err = json.NewDecoder(r.Body).Decode(&activity)
|
2020-10-26 16:37:31 +00:00
|
|
|
_ = r.Body.Close()
|
|
|
|
if err != nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "Failed to decode body", http.StatusBadRequest)
|
2020-10-26 16:37:31 +00:00
|
|
|
return
|
|
|
|
}
|
2020-11-13 14:19:09 +00:00
|
|
|
// Get and check activity actor
|
|
|
|
activityActor, ok := activity["actor"].(string)
|
|
|
|
if !ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "actor in activity is no string", http.StatusBadRequest)
|
2020-11-13 14:19:09 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if activityActor != requestActor.ID {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "Request actor isn't activity actor", http.StatusForbidden)
|
2020-11-13 14:19:09 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
// Do
|
2020-10-26 16:37:31 +00:00
|
|
|
switch activity["type"] {
|
|
|
|
case "Follow":
|
2021-06-06 12:39:42 +00:00
|
|
|
a.apAccept(blogName, blog, activity)
|
2020-10-26 16:37:31 +00:00
|
|
|
case "Undo":
|
2022-03-16 07:28:03 +00:00
|
|
|
if object, ok := activity["object"].(map[string]any); ok {
|
2022-02-26 19:38:52 +00:00
|
|
|
ot := cast.ToString(object["type"])
|
|
|
|
actor := cast.ToString(object["actor"])
|
|
|
|
if ot == "Follow" && actor == activityActor {
|
|
|
|
_ = a.db.apRemoveFollower(blogName, activityActor)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
case "Create":
|
2022-03-16 07:28:03 +00:00
|
|
|
if object, ok := activity["object"].(map[string]any); ok {
|
2022-02-26 19:38:52 +00:00
|
|
|
baseUrl := cast.ToString(object["id"])
|
|
|
|
if ou := cast.ToString(object["url"]); ou != "" {
|
|
|
|
baseUrl = ou
|
|
|
|
}
|
|
|
|
if r := cast.ToString(object["inReplyTo"]); r != "" && baseUrl != "" && strings.HasPrefix(r, blogIri) {
|
|
|
|
// It's an ActivityPub reply; save reply as webmention
|
|
|
|
_ = a.createWebmention(baseUrl, r)
|
|
|
|
} else if content := cast.ToString(object["content"]); content != "" && baseUrl != "" {
|
|
|
|
// May be a mention; find links to blog and save them as webmentions
|
|
|
|
if links, err := allLinksFromHTMLString(content, baseUrl); err == nil {
|
|
|
|
for _, link := range links {
|
|
|
|
if strings.HasPrefix(link, a.cfg.Server.PublicAddress) {
|
|
|
|
_ = a.createWebmention(baseUrl, link)
|
2020-11-06 17:45:31 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
case "Delete", "Block":
|
2022-02-26 19:38:52 +00:00
|
|
|
if o := cast.ToString(activity["object"]); o == activityActor {
|
|
|
|
_ = a.db.apRemoveFollower(blogName, activityActor)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
case "Like":
|
2022-02-26 19:38:52 +00:00
|
|
|
if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) {
|
|
|
|
a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, o))
|
2020-11-06 17:45:31 +00:00
|
|
|
}
|
2020-10-26 16:37:31 +00:00
|
|
|
case "Announce":
|
2022-02-26 19:38:52 +00:00
|
|
|
if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) {
|
|
|
|
a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, o))
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
}
|
2020-11-13 20:29:09 +00:00
|
|
|
// Return 200
|
|
|
|
w.WriteHeader(http.StatusOK)
|
2020-11-13 14:19:09 +00:00
|
|
|
}
|
2020-10-26 16:37:31 +00:00
|
|
|
|
2021-06-19 06:37:16 +00:00
|
|
|
func (a *goBlog) apVerifySignature(r *http.Request) (*asPerson, string, int, error) {
|
2020-11-13 14:19:09 +00:00
|
|
|
verifier, err := httpsig.NewVerifier(r)
|
|
|
|
if err != nil {
|
|
|
|
// Error with signature header etc.
|
2020-11-13 15:29:22 +00:00
|
|
|
return nil, "", 0, err
|
2020-11-13 14:19:09 +00:00
|
|
|
}
|
|
|
|
keyID := verifier.KeyId()
|
2021-06-19 06:37:16 +00:00
|
|
|
actor, statusCode, err := a.apGetRemoteActor(keyID)
|
2020-11-13 15:29:22 +00:00
|
|
|
if err != nil || actor == nil || statusCode != 0 {
|
2020-11-13 14:19:09 +00:00
|
|
|
// Actor not found or something else bad
|
2020-11-13 15:29:22 +00:00
|
|
|
return nil, keyID, statusCode, err
|
2020-11-13 14:19:09 +00:00
|
|
|
}
|
2020-11-13 15:29:22 +00:00
|
|
|
if actor.PublicKey == nil || actor.PublicKey.PublicKeyPem == "" {
|
2021-06-23 17:20:50 +00:00
|
|
|
return nil, keyID, 0, errors.New("actor has no public key")
|
2020-11-13 14:19:09 +00:00
|
|
|
}
|
|
|
|
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
|
|
|
|
if block == nil {
|
2021-06-23 17:20:50 +00:00
|
|
|
return nil, keyID, 0, errors.New("public key invalid")
|
2020-11-13 14:19:09 +00:00
|
|
|
}
|
|
|
|
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
|
|
if err != nil {
|
|
|
|
// Unable to parse public key
|
2020-11-13 15:29:22 +00:00
|
|
|
return nil, keyID, 0, err
|
2020-11-13 14:19:09 +00:00
|
|
|
}
|
2020-11-13 15:29:22 +00:00
|
|
|
return actor, keyID, 0, verifier.Verify(pubKey, httpsig.RSA_SHA256)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func handleWellKnownHostMeta(w http.ResponseWriter, r *http.Request) {
|
2021-06-18 12:32:03 +00:00
|
|
|
w.Header().Set(contentType, "application/xrd+xml"+contenttype.CharsetUtf8Suffix)
|
2022-02-12 21:29:45 +00:00
|
|
|
_, _ = io.WriteString(w, xml.Header)
|
|
|
|
_, _ = io.WriteString(w, `<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="https://`+r.Host+`/.well-known/webfinger?resource={uri}"/></XRD>`)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-19 06:37:16 +00:00
|
|
|
func (a *goBlog) apGetRemoteActor(iri string) (*asPerson, int, error) {
|
2022-02-22 09:14:48 +00:00
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, iri, nil)
|
2020-10-26 16:37:31 +00:00
|
|
|
if err != nil {
|
2020-11-13 15:29:22 +00:00
|
|
|
return nil, 0, err
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
2021-06-18 12:32:03 +00:00
|
|
|
req.Header.Set("Accept", contenttype.AS)
|
2020-11-16 17:34:29 +00:00
|
|
|
req.Header.Set(userAgent, appUserAgent)
|
2021-06-19 06:37:16 +00:00
|
|
|
resp, err := a.httpClient.Do(req)
|
2020-10-26 16:37:31 +00:00
|
|
|
if err != nil {
|
2020-11-13 15:29:22 +00:00
|
|
|
return nil, 0, err
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
2021-03-31 07:29:52 +00:00
|
|
|
defer resp.Body.Close()
|
2020-10-26 16:37:31 +00:00
|
|
|
if !apRequestIsSuccess(resp.StatusCode) {
|
2020-11-13 15:29:22 +00:00
|
|
|
return nil, resp.StatusCode, nil
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
actor := &asPerson{}
|
|
|
|
err = json.NewDecoder(resp.Body).Decode(actor)
|
|
|
|
if err != nil {
|
2020-11-13 15:29:22 +00:00
|
|
|
return nil, 0, err
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
2020-11-13 15:29:22 +00:00
|
|
|
return actor, 0, nil
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-07-30 13:43:13 +00:00
|
|
|
func (db *database) apGetAllInboxes(blog string) (inboxes []string, err error) {
|
2021-06-06 12:39:42 +00:00
|
|
|
rows, err := db.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
|
2020-10-26 16:37:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-07-30 13:43:13 +00:00
|
|
|
var inbox string
|
2020-10-26 16:37:31 +00:00
|
|
|
for rows.Next() {
|
2020-11-25 10:29:36 +00:00
|
|
|
err = rows.Scan(&inbox)
|
2020-10-26 16:37:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-11-25 10:29:36 +00:00
|
|
|
inboxes = append(inboxes, inbox)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
2020-11-25 10:29:36 +00:00
|
|
|
return inboxes, nil
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (db *database) apAddFollower(blog, follower, inbox string) error {
|
|
|
|
_, err := db.exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox))
|
2020-11-09 15:40:12 +00:00
|
|
|
return err
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (db *database) apRemoveFollower(blog, follower string) error {
|
|
|
|
_, err := db.exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
|
2020-11-09 15:40:12 +00:00
|
|
|
return err
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (db *database) apRemoveInbox(inbox string) error {
|
|
|
|
_, err := db.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
|
2020-11-25 10:29:36 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) apPost(p *post) {
|
|
|
|
n := a.toASNote(p)
|
2022-03-16 07:28:03 +00:00
|
|
|
a.apSendToAllFollowers(p.Blog, map[string]any{
|
2021-06-18 12:32:03 +00:00
|
|
|
"@context": []string{asContext},
|
2021-06-06 12:39:42 +00:00
|
|
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
2022-03-31 12:55:36 +00:00
|
|
|
"id": a.activityPubId(p),
|
2020-12-08 18:36:19 +00:00
|
|
|
"published": n.Published,
|
|
|
|
"type": "Create",
|
|
|
|
"object": n,
|
|
|
|
})
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) apUpdate(p *post) {
|
2022-03-16 07:28:03 +00:00
|
|
|
a.apSendToAllFollowers(p.Blog, map[string]any{
|
2021-06-18 12:32:03 +00:00
|
|
|
"@context": []string{asContext},
|
2021-06-06 12:39:42 +00:00
|
|
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
2022-03-31 12:55:36 +00:00
|
|
|
"id": a.activityPubId(p),
|
2020-12-08 18:36:19 +00:00
|
|
|
"published": time.Now().Format("2006-01-02T15:04:05-07:00"),
|
|
|
|
"type": "Update",
|
2021-06-06 12:39:42 +00:00
|
|
|
"object": a.toASNote(p),
|
2020-12-08 18:36:19 +00:00
|
|
|
})
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) apDelete(p *post) {
|
2022-03-16 07:28:03 +00:00
|
|
|
a.apSendToAllFollowers(p.Blog, map[string]any{
|
2021-06-18 12:32:03 +00:00
|
|
|
"@context": []string{asContext},
|
2021-06-06 12:39:42 +00:00
|
|
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
2020-12-08 18:36:19 +00:00
|
|
|
"type": "Delete",
|
2022-03-31 12:55:36 +00:00
|
|
|
"object": a.activityPubId(p),
|
2022-01-03 12:55:44 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *goBlog) apUndelete(p *post) {
|
2022-03-31 12:55:36 +00:00
|
|
|
// The optimal way to do this would be to send a "Undo Delete" activity,
|
|
|
|
// but that doesn't work with Mastodon yet.
|
|
|
|
// see:
|
|
|
|
// https://socialhub.activitypub.rocks/t/soft-deletes-and-restoring-deleted-posts/2318
|
|
|
|
// https://github.com/mastodon/mastodon/issues/17553
|
|
|
|
|
|
|
|
// Update "activityPubVersion" parameter to current timestamp in nanoseconds
|
|
|
|
p.Parameters[activityPubVersionParam] = []string{fmt.Sprintf("%d", utcNowNanos())}
|
2022-04-04 11:07:36 +00:00
|
|
|
_ = a.db.replacePostParam(p.Path, activityPubVersionParam, p.Parameters[activityPubVersionParam])
|
2022-03-31 12:55:36 +00:00
|
|
|
// Post as new post
|
|
|
|
a.apPost(p)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2022-03-16 07:28:03 +00:00
|
|
|
func (a *goBlog) apAccept(blogName string, blog *configBlog, follow map[string]any) {
|
2020-10-26 16:37:31 +00:00
|
|
|
// it's a follow, write it down
|
|
|
|
newFollower := follow["actor"].(string)
|
|
|
|
log.Println("New follow request:", newFollower)
|
|
|
|
// check we aren't following ourselves
|
|
|
|
if newFollower == follow["object"] {
|
|
|
|
// actor and object are equal
|
|
|
|
return
|
|
|
|
}
|
2021-06-19 06:37:16 +00:00
|
|
|
follower, status, err := a.apGetRemoteActor(newFollower)
|
2020-11-13 15:29:22 +00:00
|
|
|
if err != nil || status != 0 {
|
2020-10-26 16:37:31 +00:00
|
|
|
// Couldn't retrieve remote actor info
|
|
|
|
log.Println("Failed to retrieve remote actor info:", newFollower)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// Add or update follower
|
2020-11-25 10:29:36 +00:00
|
|
|
inbox := follower.Inbox
|
|
|
|
if endpoints := follower.Endpoints; endpoints != nil && endpoints.SharedInbox != "" {
|
|
|
|
inbox = endpoints.SharedInbox
|
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
if err = a.db.apAddFollower(blogName, follower.ID, inbox); err != nil {
|
2021-02-08 17:51:07 +00:00
|
|
|
return
|
|
|
|
}
|
2021-11-18 16:21:50 +00:00
|
|
|
// Send accept response to the new follower
|
2022-03-16 07:28:03 +00:00
|
|
|
accept := map[string]any{
|
2021-06-18 12:32:03 +00:00
|
|
|
"@context": []string{asContext},
|
2021-11-18 16:21:50 +00:00
|
|
|
"type": "Accept",
|
2020-12-08 18:36:19 +00:00
|
|
|
"to": follow["actor"],
|
2021-06-06 12:39:42 +00:00
|
|
|
"actor": a.apIri(blog),
|
2020-12-08 18:36:19 +00:00
|
|
|
"object": follow,
|
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
_, accept["id"] = a.apNewID(blog)
|
2022-03-31 12:55:36 +00:00
|
|
|
_ = a.apQueueSendSigned(a.apIri(blog), inbox, accept)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2022-04-21 16:18:39 +00:00
|
|
|
func (a *goBlog) apSendProfileUpdates() {
|
|
|
|
for blog, config := range a.cfg.Blogs {
|
2022-08-07 10:46:49 +00:00
|
|
|
person := a.toAsPerson(blog)
|
2022-04-21 16:18:39 +00:00
|
|
|
a.apSendToAllFollowers(blog, map[string]any{
|
|
|
|
"@context": []string{asContext},
|
|
|
|
"actor": a.apIri(config),
|
|
|
|
"published": time.Now().Format("2006-01-02T15:04:05-07:00"),
|
|
|
|
"type": "Update",
|
|
|
|
"object": person,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-16 07:28:03 +00:00
|
|
|
func (a *goBlog) apSendToAllFollowers(blog string, activity any) {
|
2021-06-06 12:39:42 +00:00
|
|
|
inboxes, err := a.db.apGetAllInboxes(blog)
|
2020-10-26 16:37:31 +00:00
|
|
|
if err != nil {
|
2020-11-25 10:29:36 +00:00
|
|
|
log.Println("Failed to retrieve inboxes:", err.Error())
|
2020-10-26 16:37:31 +00:00
|
|
|
return
|
|
|
|
}
|
2022-03-31 12:55:36 +00:00
|
|
|
a.apSendTo(a.apIri(a.cfg.Blogs[blog]), activity, inboxes)
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 12:55:36 +00:00
|
|
|
func (a *goBlog) apSendTo(blogIri string, activity any, inboxes []string) {
|
2020-11-25 10:29:36 +00:00
|
|
|
for _, i := range inboxes {
|
2020-10-26 16:37:31 +00:00
|
|
|
go func(inbox string) {
|
2022-03-31 12:55:36 +00:00
|
|
|
_ = a.apQueueSendSigned(blogIri, inbox, activity)
|
2020-10-26 16:37:31 +00:00
|
|
|
}(i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) apNewID(blog *configBlog) (hash string, url string) {
|
2021-11-18 16:21:50 +00:00
|
|
|
return hash, a.apIri(blog) + "#" + uuid.NewString()
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) apIri(b *configBlog) string {
|
2021-06-11 06:24:41 +00:00
|
|
|
return a.getFullAddress(b.getRelativePath(""))
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func apRequestIsSuccess(code int) bool {
|
|
|
|
return code == http.StatusOK || code == http.StatusCreated || code == http.StatusAccepted || code == http.StatusNoContent
|
|
|
|
}
|
2021-07-29 13:31:49 +00:00
|
|
|
|
|
|
|
// Load or generate key for ActivityPub communication
|
|
|
|
func (a *goBlog) loadActivityPubPrivateKey() error {
|
|
|
|
// Check if already loaded
|
|
|
|
if a.apPrivateKey != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// Check if already generated
|
|
|
|
if keyData, err := a.db.retrievePersistentCache("activitypub_key"); err == nil && keyData != nil {
|
|
|
|
privateKeyDecoded, _ := pem.Decode(keyData)
|
|
|
|
if privateKeyDecoded == nil {
|
|
|
|
log.Println("failed to decode cached private key")
|
|
|
|
// continue
|
|
|
|
} else {
|
|
|
|
key, err := x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
2022-04-21 16:18:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pubKeyBytes, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-07-29 13:31:49 +00:00
|
|
|
}
|
2022-04-21 16:18:39 +00:00
|
|
|
a.apPrivateKey = key
|
|
|
|
a.apPubKeyBytes = pubKeyBytes
|
|
|
|
return nil
|
2021-07-29 13:31:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Generate and cache key
|
2022-04-21 16:18:39 +00:00
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pubKeyBytes, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
2021-07-29 13:31:49 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-21 16:18:39 +00:00
|
|
|
a.apPrivateKey = key
|
|
|
|
a.apPubKeyBytes = pubKeyBytes
|
2022-02-22 15:52:03 +00:00
|
|
|
return a.db.cachePersistently(
|
|
|
|
"activitypub_key",
|
|
|
|
pem.EncodeToMemory(&pem.Block{
|
|
|
|
Type: "PRIVATE KEY",
|
|
|
|
Bytes: x509.MarshalPKCS1PrivateKey(a.apPrivateKey),
|
|
|
|
}),
|
|
|
|
)
|
2021-07-29 13:31:49 +00:00
|
|
|
}
|