Send ActivityPub profile updates

This commit is contained in:
Jan-Lukas Else 2022-04-21 18:18:39 +02:00
parent 607f8e10f0
commit 0159a04a43
6 changed files with 75 additions and 27 deletions

View File

@ -27,12 +27,8 @@ import (
)
func (a *goBlog) initActivityPub() error {
if a.isPrivate() {
// Private mode, no AP
return nil
}
if apc := a.cfg.ActivityPub; apc == nil || !apc.Enabled {
// Disabled
if !a.apEnabled() {
// ActivityPub disabled
return nil
}
// Add hooks
@ -80,9 +76,28 @@ func (a *goBlog) initActivityPub() error {
}
// Init send queue
a.initAPSendQueue()
// Send profile updates
go func() {
// First wait a bit
time.Sleep(time.Second * 10)
// Then send profile update
a.apSendProfileUpdates()
}()
return nil
}
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
}
func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
blog, ok := a.webfingerResources[r.URL.Query().Get("resource")]
if !ok {
@ -383,6 +398,23 @@ func (a *goBlog) apAccept(blogName string, blog *configBlog, follow map[string]a
_ = a.apQueueSendSigned(a.apIri(blog), inbox, accept)
}
func (a *goBlog) apSendProfileUpdates() {
for blog, config := range a.cfg.Blogs {
person, err := a.toAsPerson(blog)
if err != nil {
log.Println("Failed to create Person object:", err)
continue
}
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,
})
}
}
func (a *goBlog) apSendToAllFollowers(blog string, activity any) {
inboxes, err := a.db.apGetAllInboxes(blog)
if err != nil {
@ -426,18 +458,29 @@ func (a *goBlog) loadActivityPubPrivateKey() error {
// continue
} else {
key, err := x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
if err == nil && key != nil {
a.apPrivateKey = key
return nil
if err != nil {
return err
}
pubKeyBytes, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return err
}
a.apPrivateKey = key
a.apPubKeyBytes = pubKeyBytes
return nil
}
}
// Generate and cache key
var err error
a.apPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048)
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
pubKeyBytes, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return err
}
a.apPrivateKey = key
a.apPubKeyBytes = pubKeyBytes
return a.db.cachePersistently(
"activitypub_key",
pem.EncodeToMemory(&pem.Block{

View File

@ -29,6 +29,7 @@ func Test_loadActivityPubPrivateKey(t *testing.T) {
require.NoError(t, err)
assert.NotNil(t, app.apPrivateKey)
assert.NotEmpty(t, app.apPubKeyBytes)
oldEncodedKey := x509.MarshalPKCS1PrivateKey(app.apPrivateKey)
oldPemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: oldEncodedKey})
@ -38,6 +39,7 @@ func Test_loadActivityPubPrivateKey(t *testing.T) {
require.NoError(t, err)
assert.NotNil(t, app.apPrivateKey)
assert.NotEmpty(t, app.apPubKeyBytes)
newEncodedKey := x509.MarshalPKCS1PrivateKey(app.apPrivateKey)
newPemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: newEncodedKey})

View File

@ -2,7 +2,6 @@ package main
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
@ -168,13 +167,8 @@ func (a *goBlog) activityPubId(p *post) string {
return fu
}
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
func (a *goBlog) toAsPerson(blog string) (*asPerson, error) {
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",
@ -190,21 +184,29 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: publicKeyDer,
Bytes: a.apPubKeyBytes,
})),
},
}
// Add profile picture
if a.cfg.User.Picture != "" {
asBlog.Icon = &asAttachment{
Type: "Image",
URL: a.cfg.User.Picture,
}
}
return asBlog, nil
}
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
person, err := a.toAsPerson(blog)
if err != nil {
a.serveError(w, r, "Failed to create ActivityStreams Person", http.StatusInternalServerError)
return
}
// Encode
buf := bufferpool.Get()
defer bufferpool.Put(buf)
if err := json.NewEncoder(buf).Encode(asBlog); err != nil {
if err := json.NewEncoder(buf).Encode(person); err != nil {
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
return
}

1
app.go
View File

@ -22,6 +22,7 @@ import (
type goBlog struct {
// ActivityPub
apPrivateKey *rsa.PrivateKey
apPubKeyBytes []byte
apPostSigner httpsig.Signer
apPostSignMutex sync.Mutex
webfingerResources map[string]*configBlog

4
go.mod
View File

@ -43,7 +43,7 @@ require (
github.com/paulmach/go.geojson v1.4.0
github.com/posener/wstest v1.2.0
github.com/pquerna/otp v1.3.0
github.com/samber/lo v1.13.0
github.com/samber/lo v1.14.0
github.com/schollz/sqlite3dump v1.3.1
github.com/snabb/sitemap v1.0.0
github.com/spf13/cast v1.4.1
@ -58,7 +58,7 @@ require (
// master
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/net v0.0.0-20220412020605-290c469a71a5
golang.org/x/net v0.0.0-20220420153159-1850ba15e1be
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/text v0.3.7
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b

8
go.sum
View File

@ -410,8 +410,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.13.0 h1:82AIN+opOjdaP2pntOu8UxK1Zp0OyjJrOTMFb44YF1o=
github.com/samber/lo v1.13.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
github.com/samber/lo v1.14.0 h1:WpQM8MI6C7K3YNEhAzhOSm84QBV70Bme64Up6zhA1jQ=
github.com/samber/lo v1.14.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
@ -602,8 +602,8 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220420153159-1850ba15e1be h1:yx80W7nvY5ySWpaU8UWaj5o9e23YgO9BRhQol7Lc+JI=
golang.org/x/net v0.0.0-20220420153159-1850ba15e1be/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=