Blame


1 1d5ddf5d 2024-02-20 o // apub is an implementation of the ActivityPub protocol.
2 1d5ddf5d 2024-02-20 o //
3 1d5ddf5d 2024-02-20 o // https://www.w3.org/TR/activitypub/
4 1d5ddf5d 2024-02-20 o // https://www.w3.org/TR/activitystreams-core/
5 1d5ddf5d 2024-02-20 o // https://www.w3.org/TR/activitystreams-vocabulary/
6 1d5ddf5d 2024-02-20 o package apub
7 1d5ddf5d 2024-02-20 o
8 1d5ddf5d 2024-02-20 o import (
9 1d5ddf5d 2024-02-20 o "bytes"
10 1d5ddf5d 2024-02-20 o "encoding/json"
11 1d5ddf5d 2024-02-20 o "errors"
12 1d5ddf5d 2024-02-20 o "fmt"
13 1d5ddf5d 2024-02-20 o "io"
14 1d5ddf5d 2024-02-20 o "net/mail"
15 1d5ddf5d 2024-02-20 o "strings"
16 1d5ddf5d 2024-02-20 o "time"
17 1d5ddf5d 2024-02-20 o )
18 1d5ddf5d 2024-02-20 o
19 186ac3bf 2024-03-12 o // NormContext is a URL referencing the
20 186ac3bf 2024-03-12 o // normative Activity Streams 2.0 JSON-LD @context definition.
21 186ac3bf 2024-03-12 o // All [Activity] variables should have their AtContext field set to this value.
22 186ac3bf 2024-03-12 o // See [Activity Streams 2.0] section 2.1.
23 186ac3bf 2024-03-12 o //
24 186ac3bf 2024-03-12 o // [Activity Streams 2.0]: https://www.w3.org/TR/activitystreams-core/
25 186ac3bf 2024-03-12 o const NormContext string = "https://www.w3.org/ns/activitystreams"
26 1d5ddf5d 2024-02-20 o
27 25fed994 2024-03-12 o // ContentType is the MIME media type for ActivityPub.
28 1d5ddf5d 2024-02-20 o const ContentType string = "application/activity+json"
29 1d5ddf5d 2024-02-20 o
30 25fed994 2024-03-12 o // PublicCollection is the ActivityPub ID for the special collection indicating public access.
31 25fed994 2024-03-12 o // Any Activity addressed to this collection is meant to be available to all users,
32 25fed994 2024-03-12 o // authenticated or not.
33 77918b00 2024-03-07 o // See W3C Recommendation ActivityPub Section 5.6.
34 77918b00 2024-03-07 o const PublicCollection string = "https://www.w3.org/ns/activitystreams#Public"
35 1d5ddf5d 2024-02-20 o
36 71191436 2024-02-28 o var ErrNotExist = errors.New("no such activity")
37 71191436 2024-02-28 o
38 186ac3bf 2024-03-12 o // Activity represents the Activity Streams Object core type.
39 186ac3bf 2024-03-12 o // See Activity Streams 2.0, section 4.1.
40 1d5ddf5d 2024-02-20 o type Activity struct {
41 1d5ddf5d 2024-02-20 o AtContext string `json:"@context"`
42 421e5894 2024-03-15 o ID string `json:"id,omitempty"`
43 1d5ddf5d 2024-02-20 o Type string `json:"type"`
44 1d5ddf5d 2024-02-20 o Name string `json:"name,omitempty"`
45 1d5ddf5d 2024-02-20 o Actor string `json:"actor,omitempty"`
46 1d5ddf5d 2024-02-20 o Username string `json:"preferredUsername,omitempty"`
47 77918b00 2024-03-07 o Summary string `json:"summary,omitempty"`
48 1d5ddf5d 2024-02-20 o Inbox string `json:"inbox,omitempty"`
49 1d5ddf5d 2024-02-20 o Outbox string `json:"outbox,omitempty"`
50 1d5ddf5d 2024-02-20 o To []string `json:"to,omitempty"`
51 1d5ddf5d 2024-02-20 o CC []string `json:"cc,omitempty"`
52 77918b00 2024-03-07 o Followers string `json:"followers,omitempty"`
53 1d5ddf5d 2024-02-20 o InReplyTo string `json:"inReplyTo,omitempty"`
54 1d5ddf5d 2024-02-20 o Published *time.Time `json:"published,omitempty"`
55 1d5ddf5d 2024-02-20 o AttributedTo string `json:"attributedTo,omitempty"`
56 1d5ddf5d 2024-02-20 o Content string `json:"content,omitempty"`
57 1d5ddf5d 2024-02-20 o MediaType string `json:"mediaType,omitempty"`
58 1d5ddf5d 2024-02-20 o Source struct {
59 1d5ddf5d 2024-02-20 o Content string `json:"content,omitempty"`
60 1d5ddf5d 2024-02-20 o MediaType string `json:"mediaType,omitempty"`
61 1d5ddf5d 2024-02-20 o } `json:"source,omitempty"`
62 421e5894 2024-03-15 o PublicKey *PublicKey `json:"publicKey,omitempty"`
63 421e5894 2024-03-15 o Audience string `json:"audience,omitempty"`
64 421e5894 2024-03-15 o Href string `json:"href,omitempty"`
65 421e5894 2024-03-15 o Tag []Activity `json:"tag,omitempty"`
66 8403ab16 2024-03-21 o Endpoints Endpoints `json:"endpoints,omitempty"`
67 186ac3bf 2024-03-12 o // Contains a JSON-encoded Activity, or a URL as a JSON string
68 186ac3bf 2024-03-12 o // pointing to an Activity. Use Activity.Unwrap() to access
69 186ac3bf 2024-03-12 o // the enclosed, decoded value.
70 421e5894 2024-03-15 o Object json.RawMessage `json:"object,omitempty"`
71 1d5ddf5d 2024-02-20 o }
72 1d5ddf5d 2024-02-20 o
73 1d5ddf5d 2024-02-20 o func (act *Activity) UnmarshalJSON(b []byte) error {
74 1d5ddf5d 2024-02-20 o type Alias Activity
75 1d5ddf5d 2024-02-20 o aux := &struct {
76 1d5ddf5d 2024-02-20 o AtContext interface{} `json:"@context"`
77 1d5ddf5d 2024-02-20 o Object interface{}
78 1d5ddf5d 2024-02-20 o *Alias
79 1d5ddf5d 2024-02-20 o }{
80 1d5ddf5d 2024-02-20 o Alias: (*Alias)(act),
81 1d5ddf5d 2024-02-20 o }
82 1d5ddf5d 2024-02-20 o if err := json.Unmarshal(b, &aux); err != nil {
83 1d5ddf5d 2024-02-20 o return err
84 1d5ddf5d 2024-02-20 o }
85 1d5ddf5d 2024-02-20 o switch v := aux.AtContext.(type) {
86 1d5ddf5d 2024-02-20 o case string:
87 1d5ddf5d 2024-02-20 o act.AtContext = v
88 1d5ddf5d 2024-02-20 o case []interface{}:
89 1d5ddf5d 2024-02-20 o if vv, ok := v[0].(string); ok {
90 1d5ddf5d 2024-02-20 o act.AtContext = vv
91 1d5ddf5d 2024-02-20 o }
92 1d5ddf5d 2024-02-20 o }
93 1d5ddf5d 2024-02-20 o return nil
94 1d5ddf5d 2024-02-20 o }
95 1d5ddf5d 2024-02-20 o
96 25fed994 2024-03-12 o // Unwrap returns the JSON-encoded Activity, if any, enclosed in act.
97 25fed994 2024-03-12 o // The Activity may be referenced by ID,
98 25fed994 2024-03-12 o // in which case the activity is looked up by client or by
99 25fed994 2024-03-12 o // apub.defaultClient if client is nil.
100 1d5ddf5d 2024-02-20 o func (act *Activity) Unwrap(client *Client) (*Activity, error) {
101 1d5ddf5d 2024-02-20 o if act.Object == nil {
102 1d5ddf5d 2024-02-20 o return nil, errors.New("no wrapped activity")
103 1d5ddf5d 2024-02-20 o }
104 1d5ddf5d 2024-02-20 o
105 1d5ddf5d 2024-02-20 o var buf io.Reader
106 1d5ddf5d 2024-02-20 o buf = bytes.NewReader(act.Object)
107 1d5ddf5d 2024-02-20 o if strings.HasPrefix(string(act.Object), "https") {
108 1d5ddf5d 2024-02-20 o if client == nil {
109 1d5ddf5d 2024-02-20 o return Lookup(string(act.Object))
110 1d5ddf5d 2024-02-20 o }
111 1d5ddf5d 2024-02-20 o return client.Lookup(string(act.Object))
112 1d5ddf5d 2024-02-20 o }
113 1d5ddf5d 2024-02-20 o return Decode(buf)
114 1d5ddf5d 2024-02-20 o }
115 1d5ddf5d 2024-02-20 o
116 1d5ddf5d 2024-02-20 o func Decode(r io.Reader) (*Activity, error) {
117 1d5ddf5d 2024-02-20 o var a Activity
118 1d5ddf5d 2024-02-20 o if err := json.NewDecoder(r).Decode(&a); err != nil {
119 1d5ddf5d 2024-02-20 o return nil, fmt.Errorf("decode activity: %w", err)
120 1d5ddf5d 2024-02-20 o }
121 1d5ddf5d 2024-02-20 o return &a, nil
122 1d5ddf5d 2024-02-20 o }
123 1d5ddf5d 2024-02-20 o
124 71191436 2024-02-28 o func DecodeActor(r io.Reader) (*Actor, error) {
125 71191436 2024-02-28 o a, err := Decode(r)
126 71191436 2024-02-28 o if err != nil {
127 71191436 2024-02-28 o return nil, err
128 71191436 2024-02-28 o }
129 71191436 2024-02-28 o return activityToActor(a), nil
130 71191436 2024-02-28 o }
131 71191436 2024-02-28 o
132 1d5ddf5d 2024-02-20 o type Actor struct {
133 71191436 2024-02-28 o AtContext string `json:"@context"`
134 71191436 2024-02-28 o ID string `json:"id"`
135 71191436 2024-02-28 o Type string `json:"type"`
136 71191436 2024-02-28 o Name string `json:"name"`
137 71191436 2024-02-28 o Username string `json:"preferredUsername"`
138 77918b00 2024-03-07 o Summary string `json:"summary,omitempty"`
139 71191436 2024-02-28 o Inbox string `json:"inbox"`
140 71191436 2024-02-28 o Outbox string `json:"outbox"`
141 77918b00 2024-03-07 o Followers string `json:"followers"`
142 8403ab16 2024-03-21 o Endpoints Endpoints `json:"endpoints,omitempty"`
143 71191436 2024-02-28 o Published *time.Time `json:"published,omitempty"`
144 71191436 2024-02-28 o PublicKey PublicKey `json:"publicKey"`
145 1d5ddf5d 2024-02-20 o }
146 1d5ddf5d 2024-02-20 o
147 1d5ddf5d 2024-02-20 o type PublicKey struct {
148 1d5ddf5d 2024-02-20 o ID string `json:"id"`
149 1d5ddf5d 2024-02-20 o Owner string `json:"owner"`
150 1d5ddf5d 2024-02-20 o PublicKeyPEM string `json:"publicKeyPem"`
151 1d5ddf5d 2024-02-20 o }
152 1d5ddf5d 2024-02-20 o
153 25fed994 2024-03-12 o // Address generates the most likely address of the Actor.
154 25fed994 2024-03-12 o // The Actor's name (not the username) is used as the address' proper name, if present.
155 25fed994 2024-03-12 o // Implementors should verify the address using WebFinger.
156 945bf12e 2024-03-16 o // For example, the address for the Actor ID
157 25fed994 2024-03-12 o // https://hachyderm.io/users/otl is:
158 25fed994 2024-03-12 o //
159 945bf12e 2024-03-16 o // "Oliver Lowe" <otl@hachyderm.io>
160 1d5ddf5d 2024-02-20 o func (a *Actor) Address() *mail.Address {
161 71191436 2024-02-28 o if a.Username == "" && a.Name == "" {
162 71191436 2024-02-28 o return &mail.Address{"", a.ID}
163 71191436 2024-02-28 o }
164 1d5ddf5d 2024-02-20 o trimmed := strings.TrimPrefix(a.ID, "https://")
165 1d5ddf5d 2024-02-20 o host, _, _ := strings.Cut(trimmed, "/")
166 1d5ddf5d 2024-02-20 o addr := fmt.Sprintf("%s@%s", a.Username, host)
167 1d5ddf5d 2024-02-20 o return &mail.Address{a.Name, addr}
168 1d5ddf5d 2024-02-20 o }
169 77918b00 2024-03-07 o
170 25fed994 2024-03-12 o // FollowersAddress generates a non-standard address representing the Actor's followers
171 25fed994 2024-03-12 o // using plus addressing.
172 25fed994 2024-03-12 o // It is the Actor's address username part with a "+followers" suffix.
173 25fed994 2024-03-12 o // The address cannot be resolved using WebFinger.
174 25fed994 2024-03-12 o //
175 25fed994 2024-03-12 o // For example, the followers address for Actor ID
176 25fed994 2024-03-12 o // https://hachyderm.io/users/otl is:
177 25fed994 2024-03-12 o //
178 25fed994 2024-03-12 o // "Oliver Lowe (followers)" <otl+followers@hachyderm.io>
179 77918b00 2024-03-07 o func (a *Actor) FollowersAddress() *mail.Address {
180 77918b00 2024-03-07 o if a.Followers == "" {
181 77918b00 2024-03-07 o return &mail.Address{"", ""}
182 77918b00 2024-03-07 o }
183 77918b00 2024-03-07 o addr := a.Address()
184 77918b00 2024-03-07 o user, domain, found := strings.Cut(addr.Address, "@")
185 77918b00 2024-03-07 o if !found {
186 77918b00 2024-03-07 o return &mail.Address{"", ""}
187 77918b00 2024-03-07 o }
188 77918b00 2024-03-07 o addr.Address = fmt.Sprintf("%s+followers@%s", user, domain)
189 77918b00 2024-03-07 o if addr.Name != "" {
190 77918b00 2024-03-07 o addr.Name += " (followers)"
191 77918b00 2024-03-07 o }
192 77918b00 2024-03-07 o return addr
193 77918b00 2024-03-07 o }
194 8403ab16 2024-03-21 o
195 8403ab16 2024-03-21 o type Endpoints struct {
196 8403ab16 2024-03-21 o SharedInbox string `json:"sharedInbox,omitempty"`
197 8403ab16 2024-03-21 o }
198 8403ab16 2024-03-21 o
199 8403ab16 2024-03-21 o // Inboxes returns a deduplicated slice of inbox endpoints ActivityPub clients should send to.
200 8403ab16 2024-03-21 o // Shared inboxes, if present, are selected over an Actor's personal inbox.
201 8403ab16 2024-03-21 o // See W3C Recommendation ActivityPub Section 7.1.3 Shared Inbox Delivery.
202 8403ab16 2024-03-21 o func Inboxes(actors []Actor) []string {
203 8403ab16 2024-03-21 o var inboxes []string
204 8403ab16 2024-03-21 o for _, a := range actors {
205 8403ab16 2024-03-21 o box := a.Inbox
206 8403ab16 2024-03-21 o if a.Endpoints.SharedInbox != "" {
207 8403ab16 2024-03-21 o box = a.Endpoints.SharedInbox
208 8403ab16 2024-03-21 o }
209 8403ab16 2024-03-21 o var match bool
210 8403ab16 2024-03-21 o for i := range inboxes {
211 8403ab16 2024-03-21 o if inboxes[i] == box {
212 8403ab16 2024-03-21 o match = true
213 8403ab16 2024-03-21 o break
214 8403ab16 2024-03-21 o }
215 8403ab16 2024-03-21 o }
216 8403ab16 2024-03-21 o if match {
217 8403ab16 2024-03-21 o continue
218 8403ab16 2024-03-21 o }
219 8403ab16 2024-03-21 o inboxes = append(inboxes, box)
220 8403ab16 2024-03-21 o }
221 8403ab16 2024-03-21 o return inboxes
222 8403ab16 2024-03-21 o }