Blob


1 // apub is an implementation of the ActivityPub protocol.
2 //
3 // https://www.w3.org/TR/activitypub/
4 // https://www.w3.org/TR/activitystreams-core/
5 // https://www.w3.org/TR/activitystreams-vocabulary/
6 package apub
8 import (
9 "bytes"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "io"
14 "net/mail"
15 "strings"
16 "time"
17 )
19 // @context
20 const AtContext string = "https://www.w3.org/ns/activitystreams"
22 const ContentType string = "application/activity+json"
24 const AcceptMediaType string = `application/activity+json; profile="https://www.w3.org/ns/activitystreams"`
26 // Activities addressed to this collection indicates the activity
27 // is available to all users, authenticated or not.
28 // See W3C Recommendation ActivityPub Section 5.6.
29 const PublicCollection string = "https://www.w3.org/ns/activitystreams#Public"
31 var ErrNotExist = errors.New("no such activity")
33 type Activity struct {
34 AtContext string `json:"@context"`
35 ID string `json:"id"`
36 Type string `json:"type"`
37 Name string `json:"name,omitempty"`
38 Actor string `json:"actor,omitempty"`
39 Username string `json:"preferredUsername,omitempty"`
40 Summary string `json:"summary,omitempty"`
41 Inbox string `json:"inbox,omitempty"`
42 Outbox string `json:"outbox,omitempty"`
43 To []string `json:"to,omitempty"`
44 CC []string `json:"cc,omitempty"`
45 Followers string `json:"followers,omitempty"`
46 InReplyTo string `json:"inReplyTo,omitempty"`
47 Published *time.Time `json:"published,omitempty"`
48 AttributedTo string `json:"attributedTo,omitempty"`
49 Content string `json:"content,omitempty"`
50 MediaType string `json:"mediaType,omitempty"`
51 Source struct {
52 Content string `json:"content,omitempty"`
53 MediaType string `json:"mediaType,omitempty"`
54 } `json:"source,omitempty"`
55 PublicKey *PublicKey `json:"publicKey,omitempty"`
56 Audience string `json:"audience,omitempty"`
57 Object json.RawMessage `json:"object,omitempty"`
58 }
60 func (act *Activity) UnmarshalJSON(b []byte) error {
61 type Alias Activity
62 aux := &struct {
63 AtContext interface{} `json:"@context"`
64 Object interface{}
65 *Alias
66 }{
67 Alias: (*Alias)(act),
68 }
69 if err := json.Unmarshal(b, &aux); err != nil {
70 return err
71 }
72 switch v := aux.AtContext.(type) {
73 case string:
74 act.AtContext = v
75 case []interface{}:
76 if vv, ok := v[0].(string); ok {
77 act.AtContext = vv
78 }
79 }
80 return nil
81 }
83 func (act *Activity) Unwrap(client *Client) (*Activity, error) {
84 if act.Object == nil {
85 return nil, errors.New("no wrapped activity")
86 }
88 var buf io.Reader
89 buf = bytes.NewReader(act.Object)
90 if strings.HasPrefix(string(act.Object), "https") {
91 if client == nil {
92 return Lookup(string(act.Object))
93 }
94 return client.Lookup(string(act.Object))
95 }
96 return Decode(buf)
97 }
99 func Decode(r io.Reader) (*Activity, error) {
100 var a Activity
101 if err := json.NewDecoder(r).Decode(&a); err != nil {
102 return nil, fmt.Errorf("decode activity: %w", err)
104 return &a, nil
107 func DecodeActor(r io.Reader) (*Actor, error) {
108 a, err := Decode(r)
109 if err != nil {
110 return nil, err
112 return activityToActor(a), nil
115 type Actor struct {
116 AtContext string `json:"@context"`
117 ID string `json:"id"`
118 Type string `json:"type"`
119 Name string `json:"name"`
120 Username string `json:"preferredUsername"`
121 Summary string `json:"summary,omitempty"`
122 Inbox string `json:"inbox"`
123 Outbox string `json:"outbox"`
124 Followers string `json:"followers"`
125 Published *time.Time `json:"published,omitempty"`
126 PublicKey PublicKey `json:"publicKey"`
129 type PublicKey struct {
130 ID string `json:"id"`
131 Owner string `json:"owner"`
132 PublicKeyPEM string `json:"publicKeyPem"`
135 func (a *Actor) Address() *mail.Address {
136 if a.Username == "" && a.Name == "" {
137 return &mail.Address{"", a.ID}
139 trimmed := strings.TrimPrefix(a.ID, "https://")
140 host, _, _ := strings.Cut(trimmed, "/")
141 addr := fmt.Sprintf("%s@%s", a.Username, host)
142 return &mail.Address{a.Name, addr}
145 func (a *Actor) FollowersAddress() *mail.Address {
146 if a.Followers == "" {
147 return &mail.Address{"", ""}
149 addr := a.Address()
150 user, domain, found := strings.Cut(addr.Address, "@")
151 if !found {
152 return &mail.Address{"", ""}
154 addr.Address = fmt.Sprintf("%s+followers@%s", user, domain)
155 if addr.Name != "" {
156 addr.Name += " (followers)"
158 return addr