mirror of https://github.com/jlelse/GoBlog
Send ActivityPub profile updates
This commit is contained in:
parent
607f8e10f0
commit
0159a04a43
|
@ -27,12 +27,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *goBlog) initActivityPub() error {
|
func (a *goBlog) initActivityPub() error {
|
||||||
if a.isPrivate() {
|
if !a.apEnabled() {
|
||||||
// Private mode, no AP
|
// ActivityPub disabled
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if apc := a.cfg.ActivityPub; apc == nil || !apc.Enabled {
|
|
||||||
// Disabled
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Add hooks
|
// Add hooks
|
||||||
|
@ -80,9 +76,28 @@ func (a *goBlog) initActivityPub() error {
|
||||||
}
|
}
|
||||||
// Init send queue
|
// Init send queue
|
||||||
a.initAPSendQueue()
|
a.initAPSendQueue()
|
||||||
|
// Send profile updates
|
||||||
|
go func() {
|
||||||
|
// First wait a bit
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
// Then send profile update
|
||||||
|
a.apSendProfileUpdates()
|
||||||
|
}()
|
||||||
return nil
|
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) {
|
func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
||||||
blog, ok := a.webfingerResources[r.URL.Query().Get("resource")]
|
blog, ok := a.webfingerResources[r.URL.Query().Get("resource")]
|
||||||
if !ok {
|
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)
|
_ = 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) {
|
func (a *goBlog) apSendToAllFollowers(blog string, activity any) {
|
||||||
inboxes, err := a.db.apGetAllInboxes(blog)
|
inboxes, err := a.db.apGetAllInboxes(blog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -426,18 +458,29 @@ func (a *goBlog) loadActivityPubPrivateKey() error {
|
||||||
// continue
|
// continue
|
||||||
} else {
|
} else {
|
||||||
key, err := x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
key, err := x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
||||||
if err == nil && key != nil {
|
|
||||||
a.apPrivateKey = key
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Generate and cache key
|
|
||||||
var err error
|
|
||||||
a.apPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
||||||
|
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(
|
return a.db.cachePersistently(
|
||||||
"activitypub_key",
|
"activitypub_key",
|
||||||
pem.EncodeToMemory(&pem.Block{
|
pem.EncodeToMemory(&pem.Block{
|
||||||
|
|
|
@ -29,6 +29,7 @@ func Test_loadActivityPubPrivateKey(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotNil(t, app.apPrivateKey)
|
assert.NotNil(t, app.apPrivateKey)
|
||||||
|
assert.NotEmpty(t, app.apPubKeyBytes)
|
||||||
|
|
||||||
oldEncodedKey := x509.MarshalPKCS1PrivateKey(app.apPrivateKey)
|
oldEncodedKey := x509.MarshalPKCS1PrivateKey(app.apPrivateKey)
|
||||||
oldPemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: oldEncodedKey})
|
oldPemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: oldEncodedKey})
|
||||||
|
@ -38,6 +39,7 @@ func Test_loadActivityPubPrivateKey(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotNil(t, app.apPrivateKey)
|
assert.NotNil(t, app.apPrivateKey)
|
||||||
|
assert.NotEmpty(t, app.apPubKeyBytes)
|
||||||
|
|
||||||
newEncodedKey := x509.MarshalPKCS1PrivateKey(app.apPrivateKey)
|
newEncodedKey := x509.MarshalPKCS1PrivateKey(app.apPrivateKey)
|
||||||
newPemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: newEncodedKey})
|
newPemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: newEncodedKey})
|
||||||
|
|
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -168,13 +167,8 @@ func (a *goBlog) activityPubId(p *post) string {
|
||||||
return fu
|
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]
|
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{
|
asBlog := &asPerson{
|
||||||
Context: []string{asContext},
|
Context: []string{asContext},
|
||||||
Type: "Person",
|
Type: "Person",
|
||||||
|
@ -190,21 +184,29 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt
|
||||||
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
|
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
|
||||||
Type: "PUBLIC KEY",
|
Type: "PUBLIC KEY",
|
||||||
Headers: nil,
|
Headers: nil,
|
||||||
Bytes: publicKeyDer,
|
Bytes: a.apPubKeyBytes,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Add profile picture
|
|
||||||
if a.cfg.User.Picture != "" {
|
if a.cfg.User.Picture != "" {
|
||||||
asBlog.Icon = &asAttachment{
|
asBlog.Icon = &asAttachment{
|
||||||
Type: "Image",
|
Type: "Image",
|
||||||
URL: a.cfg.User.Picture,
|
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
|
// Encode
|
||||||
buf := bufferpool.Get()
|
buf := bufferpool.Get()
|
||||||
defer bufferpool.Put(buf)
|
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)
|
a.serveError(w, r, "Encoding failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
1
app.go
1
app.go
|
@ -22,6 +22,7 @@ import (
|
||||||
type goBlog struct {
|
type goBlog struct {
|
||||||
// ActivityPub
|
// ActivityPub
|
||||||
apPrivateKey *rsa.PrivateKey
|
apPrivateKey *rsa.PrivateKey
|
||||||
|
apPubKeyBytes []byte
|
||||||
apPostSigner httpsig.Signer
|
apPostSigner httpsig.Signer
|
||||||
apPostSignMutex sync.Mutex
|
apPostSignMutex sync.Mutex
|
||||||
webfingerResources map[string]*configBlog
|
webfingerResources map[string]*configBlog
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -43,7 +43,7 @@ require (
|
||||||
github.com/paulmach/go.geojson v1.4.0
|
github.com/paulmach/go.geojson v1.4.0
|
||||||
github.com/posener/wstest v1.2.0
|
github.com/posener/wstest v1.2.0
|
||||||
github.com/pquerna/otp v1.3.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/schollz/sqlite3dump v1.3.1
|
||||||
github.com/snabb/sitemap v1.0.0
|
github.com/snabb/sitemap v1.0.0
|
||||||
github.com/spf13/cast v1.4.1
|
github.com/spf13/cast v1.4.1
|
||||||
|
@ -58,7 +58,7 @@ require (
|
||||||
// master
|
// master
|
||||||
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
|
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
|
||||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
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/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
golang.org/x/text v0.3.7
|
golang.org/x/text v0.3.7
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -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.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w=
|
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/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.14.0 h1:WpQM8MI6C7K3YNEhAzhOSm84QBV70Bme64Up6zhA1jQ=
|
||||||
github.com/samber/lo v1.13.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
|
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 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
|
||||||
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
|
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=
|
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-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-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-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-20220420153159-1850ba15e1be h1:yx80W7nvY5ySWpaU8UWaj5o9e23YgO9BRhQol7Lc+JI=
|
||||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
|
Loading…
Reference in New Issue