commit - d32587396427e2e5dba703667d4bb7cd5bb4ad59
commit + 8403ab16d82c2829a3ee4c45b8147aae113b4d66
blob - b345580cbf145ad67518ffb16831a5f1f54ef4c3
blob + 4e0cb1a0f3ef7b5519d3283455aa3ca9a00fef29
--- apub.go
+++ apub.go
Audience string `json:"audience,omitempty"`
Href string `json:"href,omitempty"`
Tag []Activity `json:"tag,omitempty"`
+ Endpoints Endpoints `json:"endpoints,omitempty"`
// Contains a JSON-encoded Activity, or a URL as a JSON string
// pointing to an Activity. Use Activity.Unwrap() to access
// the enclosed, decoded value.
Inbox string `json:"inbox"`
Outbox string `json:"outbox"`
Followers string `json:"followers"`
+ Endpoints Endpoints `json:"endpoints,omitempty"`
Published *time.Time `json:"published,omitempty"`
PublicKey PublicKey `json:"publicKey"`
}
}
return addr
}
+
+type Endpoints struct {
+ SharedInbox string `json:"sharedInbox,omitempty"`
+}
+
+// Inboxes returns a deduplicated slice of inbox endpoints ActivityPub clients should send to.
+// Shared inboxes, if present, are selected over an Actor's personal inbox.
+// See W3C Recommendation ActivityPub Section 7.1.3 Shared Inbox Delivery.
+func Inboxes(actors []Actor) []string {
+ var inboxes []string
+ for _, a := range actors {
+ box := a.Inbox
+ if a.Endpoints.SharedInbox != "" {
+ box = a.Endpoints.SharedInbox
+ }
+ var match bool
+ for i := range inboxes {
+ if inboxes[i] == box {
+ match = true
+ break
+ }
+ }
+ if match {
+ continue
+ }
+ inboxes = append(inboxes, box)
+ }
+ return inboxes
+}
blob - 4117b5de1e745b4944071681904bb67c6bdb4738
blob + ea725fd9a80255945d4fbc8e58f4bc86f1b9d538
--- apub_test.go
+++ apub_test.go
import (
"os"
+ "path"
+ "reflect"
+ "sort"
"testing"
)
}
}
}
+
+func TestInboxes(t *testing.T) {
+ want := []string{
+ "https://apubtest2.srcbeat.com/otl/inbox",
+ "https://hachyderm.io/inbox",
+ "https://lemmy.world/inbox",
+ "https://social.harpia.red/inbox",
+ }
+
+ root := "testdata/actor"
+ dirent, err := os.ReadDir("testdata/actor")
+ if err != nil {
+ t.Fatal(err)
+ }
+ actors := make([]Actor, len(dirent))
+ for i, ent := range dirent {
+ f, err := os.Open(path.Join(root, ent.Name()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+ a, err := DecodeActor(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ actors[i] = *a
+ }
+ got := Inboxes(actors)
+ sort.Strings(got)
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("unexpected inbox slice of multiple actors, want %s got %s", want, got)
+ }
+}
blob - 32791cec840b2b12d1e1e8d0b65ff70e891168eb
blob + ebec2e71f4656e3901d092e4341455bc24af2c11
--- client.go
+++ client.go
"bytes"
"crypto/rsa"
"encoding/json"
- "errors"
"fmt"
"io"
"net/http"
- "net/url"
"os"
- "path"
"strings"
)
Followers: activity.Followers,
Published: activity.Published,
Summary: activity.Summary,
+ Endpoints: activity.Endpoints,
}
if activity.PublicKey != nil {
actor.PublicKey = *activity.PublicKey
blob - 8eab875d3be01c461603b264459d99ef544b1609
blob + a7bbc2b8a8d81d50b7811433945040cbddfd9cb4
--- cmd/apsend/apsend.go
+++ cmd/apsend/apsend.go
log.Fatalf("append activities to outbox: %v", err)
}
+ var actors []apub.Actor
for _, rcpt := range remote {
if strings.Contains(rcpt, "+followers") {
rcpt = strings.Replace(rcpt, "+followers", "", 1)
}
- ra, err := client.Finger(rcpt)
+ a, err := client.Finger(rcpt)
if err != nil {
log.Printf("webfinger %s: %v", rcpt, err)
gotErr = true
continue
}
- if _, err = client.Send(ra.Inbox, create); err != nil {
- log.Printf("send %s %s to %s: %v", activity.Type, activity.ID, rcpt, err)
+ actors = append(actors, *a)
+ }
+ for _, inbox := range apub.Inboxes(actors) {
+ if _, err = client.Send(inbox, create); err != nil {
+ log.Printf("send %s %s to %s: %v", activity.Type, activity.ID, inbox, err)
gotErr = true
}
}
blob - /dev/null
blob + 9f50d2671b2ef00c52a2aafeacfbfdfc692f8bb6 (mode 644)
--- /dev/null
+++ testdata/actor/apas.json
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1"
+ ],
+ "id": "https://apubtest2.srcbeat.com/otl/actor.json",
+ "type": "Person",
+ "inbox": "https://apubtest2.srcbeat.com/otl/inbox",
+ "outbox": "https://apubtest2.srcbeat.com/otl/outbox",
+ "name": "Oliver Lowe",
+ "preferredUsername": "otl",
+ "publicKey": {
+ "id": "https://apubtest2.srcbeat.com/otl/actor.json#main-key",
+ "owner": "https://apubtest2.srcbeat.com/otl/actor.json",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAup/3R3YKTRAdhz/xX908\ns4nZKX9IwJfo8xKANbDrFkuBHHYggXV6bB3eVBGh6vDoGDVqmZ79ujXl8LMSo1dC\nVTo4GdBhCy3/BEtl3xoM80LEqmnamq72+tleCYe3a22YOkSLeTXTIREB4VfTataS\n6fVntVs1xDoR6YC+yR6PKqeFsEhQVcnFQ1Fk93nhPoC03RLqlmWkOWuhw4AhELCK\na6ydVEH8raevAbujJyRpZ7JoMtZy9NfuKI4bxorNga2LGnf3qyqS/gAFmGO/ncRu\nwqyhE1VWbtK7MRl3H+ltL/gwhaQMrHImCnWR1EvYw0nabp0519QaPoQoFWvdJbHv\nPwIDAQAB\n-----END PUBLIC KEY-----"
+ }
+}
+
blob - /dev/null
blob + 8bed4314314bf0e7ea7960053c42e785eda5eb79 (mode 644)
--- /dev/null
+++ testdata/actor/lemmy2.json
+{
+ "@context": [
+ "https://join-lemmy.org/context.json",
+ "https://www.w3.org/ns/activitystreams"
+ ],
+ "type": "Person",
+ "id": "https://lemmy.world/u/Ghostalmedia",
+ "preferredUsername": "Ghostalmedia",
+ "inbox": "https://lemmy.world/u/Ghostalmedia/inbox",
+ "outbox": "https://lemmy.world/u/Ghostalmedia/outbox",
+ "publicKey": {
+ "id": "https://lemmy.world/u/Ghostalmedia#main-key",
+ "owner": "https://lemmy.world/u/Ghostalmedia",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu04IUDC0EqVJ+laJ3TQc\ng70fuBRhjdyT4rVnAR0oNRvOedNEW7aOgNWmC/q1Kgzmya83HKLq7ZvB6Z2kz/tS\nTObQ6P7+sDlDv9l65mmC+oi++ji/2X37HvPAJ71iKs+ky4IaGZ67ygoAwcEvfJnv\nBGELomuzwR0/CqILeSDeceAZYZVUploXKwzHdX4efQGrNWNzglZZNKkPuv4/s5Mi\nwEOUOmTVp7yjSMOGB6ImuSv81hDszIAxptUXXjDd5oEAcQR6gWHlwMG0V7Ih7jyM\nZeyJXWZ7kS3+4/mcpr1MAeupQBjHSjnFSOiE+1bG8OkKg7B6rhj2JKRcQDdSaHD8\nrwIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "name": "Ghostalmedia",
+ "summary": "<p>I’m from space!</p>\n",
+ "source": {
+ "content": "I'm from space!",
+ "mediaType": "text/markdown"
+ },
+ "icon": {
+ "type": "Image",
+ "url": "https://lemmy.world/pictrs/image/e78bbe19-dc73-490e-b9d9-c1ced432f002.jpeg"
+ },
+ "image": {
+ "type": "Image",
+ "url": "https://lemmy.world/pictrs/image/da47b488-57a9-49fc-9d58-4ad4d471e0cc.jpeg"
+ },
+ "endpoints": {
+ "sharedInbox": "https://lemmy.world/inbox"
+ },
+ "published": "2023-06-10T18:40:35.038963Z"
+}
\ No newline at end of file