Blob


1 package apub
3 import (
4 "bytes"
5 "crypto/rsa"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "os"
12 "strings"
13 )
15 var defaultClient Client = Client{Client: http.DefaultClient}
17 func Lookup(id string) (*Activity, error) {
18 return defaultClient.Lookup(id)
19 }
21 func LookupActor(id string) (*Actor, error) {
22 return defaultClient.LookupActor(id)
23 }
25 type Client struct {
26 *http.Client
27 Key *rsa.PrivateKey
28 Actor *Actor
29 }
31 func (c *Client) Lookup(id string) (*Activity, error) {
32 if !strings.HasPrefix(id, "http") {
33 return nil, fmt.Errorf("id is not a HTTP URL")
34 }
35 if c.Client == nil {
36 c.Client = http.DefaultClient
37 }
39 req, err := http.NewRequest(http.MethodGet, id, nil)
40 if err != nil {
41 return nil, err
42 }
43 req.Header.Set("Accept", ContentType)
44 if c.Key != nil && c.Actor != nil {
45 if err := Sign(req, c.Key, c.Actor.PublicKey.ID); err != nil {
46 return nil, fmt.Errorf("sign http request: %w", err)
47 }
48 }
49 resp, err := c.Do(req)
50 if err != nil {
51 return nil, err
52 }
53 defer resp.Body.Close()
54 if resp.StatusCode == http.StatusNotFound {
55 return nil, ErrNotExist
56 } else if resp.StatusCode >= 400 {
57 return nil, fmt.Errorf("non-ok response status %s", resp.Status)
58 }
59 return Decode(resp.Body)
60 }
62 func (c *Client) LookupActor(id string) (*Actor, error) {
63 activity, err := c.Lookup(id)
64 if err != nil {
65 return nil, err
66 }
67 return activityToActor(activity), nil
68 }
70 func activityToActor(activity *Activity) *Actor {
71 return &Actor{
72 AtContext: activity.AtContext,
73 ID: activity.ID,
74 Type: activity.Type,
75 Name: activity.Name,
76 Username: activity.Username,
77 Inbox: activity.Inbox,
78 Outbox: activity.Outbox,
79 Published: activity.Published,
80 Summary: activity.Summary,
81 }
82 }
84 func (c *Client) Send(inbox string, activity *Activity) (*Activity, error) {
85 b, err := json.Marshal(activity)
86 if err != nil {
87 return nil, fmt.Errorf("encode outgoing activity: %w", err)
88 }
89 req, err := http.NewRequest(http.MethodPost, inbox, bytes.NewReader(b))
90 if err != nil {
91 return nil, err
92 }
93 req.Header.Set("Content-Type", ContentType)
94 if err := Sign(req, c.Key, c.Actor.PublicKey.ID); err != nil {
95 return nil, fmt.Errorf("sign outgoing request: %w", err)
96 }
97 resp, err := c.Do(req)
98 if err != nil {
99 return nil, err
101 switch resp.StatusCode {
102 case http.StatusOK:
103 if resp.ContentLength == 0 {
104 return nil, nil
106 defer resp.Body.Close()
107 activity, err := Decode(resp.Body)
108 if errors.Is(err, io.EOF) {
109 return nil, nil
111 return activity, err
112 case http.StatusAccepted, http.StatusNoContent:
113 return nil, nil
114 case http.StatusNotFound:
115 return nil, fmt.Errorf("no such inbox %s", inbox)
116 default:
117 io.Copy(os.Stderr, resp.Body)
118 resp.Body.Close()
119 return nil, fmt.Errorf("non-ok response status %s", resp.Status)