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 *rsa.PrivateKey
27 Actor *Actor
28 }
30 func (c *Client) Lookup(id string) (*Activity, error) {
31 if !strings.HasPrefix(id, "http") {
32 return nil, fmt.Errorf("id is not a HTTP URL")
33 }
34 if c.Client == nil {
35 c.Client = http.DefaultClient
36 }
38 req, err := http.NewRequest(http.MethodGet, id, nil)
39 if err != nil {
40 return nil, err
41 }
42 req.Header.Set("Accept", ContentType)
43 if c.Key != nil && c.Actor != nil {
44 if err := Sign(req, c.Key, c.Actor.PublicKey.ID); err != nil {
45 return nil, fmt.Errorf("sign http request: %w", err)
46 }
47 }
48 resp, err := c.Do(req)
49 if err != nil {
50 return nil, err
51 }
52 defer resp.Body.Close()
53 if resp.StatusCode == http.StatusNotFound {
54 return nil, ErrNotExist
55 } else if resp.StatusCode >= 400 {
56 return nil, fmt.Errorf("non-ok response status %s", resp.Status)
57 }
58 return Decode(resp.Body)
59 }
61 func (c *Client) LookupActor(id string) (*Actor, error) {
62 activity, err := c.Lookup(id)
63 if err != nil {
64 return nil, err
65 }
66 return activityToActor(activity), nil
67 }
69 func activityToActor(activity *Activity) *Actor {
70 actor := &Actor{
71 AtContext: activity.AtContext,
72 ID: activity.ID,
73 Type: activity.Type,
74 Name: activity.Name,
75 Username: activity.Username,
76 Inbox: activity.Inbox,
77 Outbox: activity.Outbox,
78 Followers: activity.Followers,
79 Published: activity.Published,
80 Summary: activity.Summary,
81 }
82 if activity.PublicKey != nil {
83 actor.PublicKey = *activity.PublicKey
84 }
85 return actor
86 }
88 func (c *Client) Send(inbox string, activity *Activity) (*Activity, error) {
89 b, err := json.Marshal(activity)
90 if err != nil {
91 return nil, fmt.Errorf("encode outgoing activity: %w", err)
92 }
93 req, err := http.NewRequest(http.MethodPost, inbox, bytes.NewReader(b))
94 if err != nil {
95 return nil, err
96 }
97 req.Header.Set("Content-Type", ContentType)
98 if err := Sign(req, c.Key, c.Actor.PublicKey.ID); err != nil {
99 return nil, fmt.Errorf("sign outgoing request: %w", err)
101 resp, err := c.Do(req)
102 if err != nil {
103 return nil, err
105 switch resp.StatusCode {
106 case http.StatusOK, http.StatusAccepted, http.StatusNoContent:
107 return nil, nil
108 case http.StatusNotFound:
109 return nil, fmt.Errorf("no such inbox %s", inbox)
110 default:
111 io.Copy(os.Stderr, resp.Body)
112 resp.Body.Close()
113 return nil, fmt.Errorf("non-ok response status %s", resp.Status)