Blob


1 package apub
3 import (
4 "bytes"
5 "crypto/rsa"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "os"
11 "strings"
12 )
14 var defaultClient Client = Client{Client: http.DefaultClient}
16 func Lookup(id string) (*Activity, error) {
17 return defaultClient.Lookup(id)
18 }
20 func LookupActor(id string) (*Actor, error) {
21 return defaultClient.LookupActor(id)
22 }
24 type Client struct {
25 *http.Client
26 // Key is a RSA private key which will be used to sign requests.
27 Key *rsa.PrivateKey
28 // PubKeyID is a URL where the corresponding public key of Key
29 // may be accessed. This must be set if Key is also set.
30 PubKeyID string // actor.PublicKey.ID
31 }
33 func (c *Client) Lookup(id string) (*Activity, error) {
34 if !strings.HasPrefix(id, "http") {
35 return nil, fmt.Errorf("id is not a HTTP URL")
36 }
37 if c.Client == nil {
38 c.Client = http.DefaultClient
39 }
41 req, err := http.NewRequest(http.MethodGet, id, nil)
42 if err != nil {
43 return nil, err
44 }
45 req.Header.Set("Accept", ContentType)
46 if c.Key != nil && c.PubKeyID != "" {
47 if err := Sign(req, c.Key, c.PubKeyID); err != nil {
48 return nil, fmt.Errorf("sign http request: %w", err)
49 }
50 }
51 resp, err := c.Do(req)
52 if err != nil {
53 return nil, err
54 }
55 defer resp.Body.Close()
56 if resp.StatusCode == http.StatusNotFound {
57 return nil, ErrNotExist
58 } else if resp.StatusCode >= 400 {
59 return nil, fmt.Errorf("non-ok response status %s", resp.Status)
60 }
61 return Decode(resp.Body)
62 }
64 func (c *Client) LookupActor(id string) (*Actor, error) {
65 activity, err := c.Lookup(id)
66 if err != nil {
67 return nil, err
68 }
69 return activityToActor(activity), nil
70 }
72 func activityToActor(activity *Activity) *Actor {
73 actor := &Actor{
74 AtContext: activity.AtContext,
75 ID: activity.ID,
76 Type: activity.Type,
77 Name: activity.Name,
78 Username: activity.Username,
79 Inbox: activity.Inbox,
80 Outbox: activity.Outbox,
81 Followers: activity.Followers,
82 Published: activity.Published,
83 Summary: activity.Summary,
84 }
85 if activity.PublicKey != nil {
86 actor.PublicKey = *activity.PublicKey
87 }
88 return actor
89 }
91 func (c *Client) Send(inbox string, activity *Activity) (*Activity, error) {
92 b, err := json.Marshal(activity)
93 if err != nil {
94 return nil, fmt.Errorf("encode outgoing activity: %w", err)
95 }
96 req, err := http.NewRequest(http.MethodPost, inbox, bytes.NewReader(b))
97 if err != nil {
98 return nil, err
99 }
100 req.Header.Set("Content-Type", ContentType)
101 if err := Sign(req, c.Key, c.PubKeyID); err != nil {
102 return nil, fmt.Errorf("sign outgoing request: %w", err)
104 resp, err := c.Do(req)
105 if err != nil {
106 return nil, err
108 switch resp.StatusCode {
109 case http.StatusOK, http.StatusAccepted, http.StatusNoContent:
110 return nil, nil
111 case http.StatusNotFound:
112 return nil, fmt.Errorf("no such inbox %s", inbox)
113 default:
114 io.Copy(os.Stderr, resp.Body)
115 resp.Body.Close()
116 return nil, fmt.Errorf("non-ok response status %s", resp.Status)