commit d3dfb6729126922affd3865165ef240d239837e4 from: Oliver Lowe date: Mon Mar 18 03:10:22 2024 UTC apub: handle collection recipients mroe reliably When we receive activity that has a collection, we see if any of the other actor recipients have the collection set as their followers. Then we can encode it with plus addressing. If we can't do that we drop it. Not ideal, but it covers most use cases. Next up we can encode arbitrary collections like "some/path/to/thing@example.com". commit - ed0db64ab00968903e807add075cfd6dcbf1dafb commit + d3dfb6729126922affd3865165ef240d239837e4 blob - bfcb8e5d35c75965c081cafe858dbf41b98e644d blob + 32791cec840b2b12d1e1e8d0b65ff70e891168eb --- client.go +++ client.go @@ -4,21 +4,24 @@ import ( "bytes" "crypto/rsa" "encoding/json" + "errors" "fmt" "io" "net/http" + "net/url" "os" + "path" "strings" ) -var defaultClient Client = Client{Client: http.DefaultClient} +var DefaultClient Client = Client{Client: http.DefaultClient} func Lookup(id string) (*Activity, error) { - return defaultClient.Lookup(id) + return DefaultClient.Lookup(id) } func LookupActor(id string) (*Actor, error) { - return defaultClient.LookupActor(id) + return DefaultClient.LookupActor(id) } type Client struct { @@ -38,16 +41,10 @@ func (c *Client) Lookup(id string) (*Activity, error) c.Client = http.DefaultClient } - req, err := http.NewRequest(http.MethodGet, id, nil) + req, err := newRequest(http.MethodGet, id, nil, c.Key, c.PubKeyID) if err != nil { - return nil, err + return nil, fmt.Errorf("new request: %w", err) } - req.Header.Set("Accept", ContentType) - if c.Key != nil && c.PubKeyID != "" { - if err := Sign(req, c.Key, c.PubKeyID); err != nil { - return nil, fmt.Errorf("sign http request: %w", err) - } - } resp, err := c.Do(req) if err != nil { return nil, err @@ -66,7 +63,14 @@ func (c *Client) LookupActor(id string) (*Actor, error if err != nil { return nil, err } - return activityToActor(activity), nil + switch activity.Type { + case "Application", "Group", "Organization", "Person", "Service": + return activityToActor(activity), nil + case "Collection", "OrderedCollection": + // probably followers. let caller work out what it wants to do + return activityToActor(activity), nil + } + return nil, fmt.Errorf("bad object Type %s", activity.Type) } func activityToActor(activity *Activity) *Actor { @@ -93,14 +97,10 @@ func (c *Client) Send(inbox string, activity *Activity if err != nil { return nil, fmt.Errorf("encode outgoing activity: %w", err) } - req, err := http.NewRequest(http.MethodPost, inbox, bytes.NewReader(b)) + req, err := newRequest(http.MethodPost, inbox, bytes.NewReader(b), c.Key, c.PubKeyID) if err != nil { return nil, err } - req.Header.Set("Content-Type", ContentType) - if err := Sign(req, c.Key, c.PubKeyID); err != nil { - return nil, fmt.Errorf("sign outgoing request: %w", err) - } resp, err := c.Do(req) if err != nil { return nil, err @@ -116,3 +116,20 @@ func (c *Client) Send(inbox string, activity *Activity return nil, fmt.Errorf("non-ok response status %s", resp.Status) } } + +func newRequest(method, url string, body io.Reader, key *rsa.PrivateKey, pubkeyURL string) (*http.Request, error) { + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + req.Header.Set("Accept", ContentType) + if body != nil { + req.Header.Set("Content-Type", ContentType) + } + if key != nil { + if err := Sign(req, key, pubkeyURL); err != nil { + return nil, fmt.Errorf("sign request: %w", err) + } + } + return req, nil +} blob - 26753957c01f35d219f7733828b2e4686ffb55a8 blob + afb03db54ceccd4fdbf4ac30487d4c608a682dc6 --- cmd/apget/apget.go +++ cmd/apget/apget.go @@ -23,13 +23,17 @@ import ( "flag" "log" "os" + "os/user" "olowe.co/apub" + "olowe.co/apub/internal/sys" ) var jflag bool func init() { + log.SetFlags(0) + log.SetPrefix("apsend: ") flag.BoolVar(&jflag, "j", false, "format as json") flag.Parse() } @@ -40,8 +44,18 @@ func main() { if len(flag.Args()) != 1 { log.Fatalln("usage:", usage) } - activity, err := apub.Lookup(flag.Args()[0]) + current, err := user.Current() if err != nil { + log.Fatal(err) + } + client, err := sys.ClientFor(current.Username, "apubtest2.srcbeat.com") + if err != nil { + log.Println("create activitypub client for %s: %v", current.Username, err) + log.Println("requests will not be signed") + client = &apub.DefaultClient + } + activity, err := client.Lookup(flag.Args()[0]) + if err != nil { log.Fatalf("lookup %s: %v", flag.Args()[0], err) } if jflag { @@ -52,7 +66,7 @@ func main() { } return } - msg, err := apub.MarshalMail(activity) + msg, err := apub.MarshalMail(activity, client) if err != nil { log.Println("marshal to mail:", err) } blob - a5fbda6ffee7f954b540793f4249cf258a9e5034 blob + 8eab875d3be01c461603b264459d99ef544b1609 --- cmd/apsend/apsend.go +++ cmd/apsend/apsend.go @@ -73,11 +73,18 @@ func main() { os.Exit(1) } + current, err := user.Current() + if err != nil { + log.Fatal(err) + } + client, err := sys.ClientFor(current.Username, sysName) + if err != nil { + log.Fatalf("apub cilent for %s: %v", current.Username, err) + } + var activity *apub.Activity var bmsg []byte - var err error if jflag { - var err error activity, err = apub.Decode(os.Stdin) if err != nil { log.Fatalln("decode activity:", err) @@ -91,42 +98,43 @@ func main() { if err != nil { log.Fatal(err) } - activity, err = apub.UnmarshalMail(msg) + activity, err = apub.UnmarshalMail(msg, client) if err != nil { log.Fatalln("unmarshal activity from message:", err) } } var remote []string + var gotErr bool for _, rcpt := range flag.Args() { if !strings.Contains(rcpt, "@") { if err := deliverLocal(rcpt, bmsg); err != nil { + gotErr = true log.Printf("local delivery to %s: %v", rcpt, err) } continue } remote = append(remote, rcpt) } - - var gotErr bool if len(remote) > 0 { if !strings.HasPrefix(activity.AttributedTo, "https://"+sysName) { log.Fatalln("cannot send activity from non-local actor", activity.AttributedTo) } - - from, err := apub.LookupActor(activity.AttributedTo) + from, err := client.LookupActor(activity.AttributedTo) if err != nil { log.Fatalf("lookup actor %s: %v", activity.AttributedTo, err) } - client, err := sys.ClientFor(from.Username, sysName) + // everything we do from here onwards is on behalf of the sender, + // so outbound requests must be signed with the sender's key. + client, err = sys.ClientFor(from.Username, sysName) if err != nil { - log.Fatalf("apub cilent for %s: %v", from.Username, err) + log.Fatalf("activitypub client for %s: %v", from.Username, err) } // overwrite auto generated ID from mail clients if !strings.HasPrefix(activity.ID, "https://") { activity.ID = from.Outbox + "/" + strconv.Itoa(int(activity.Published.Unix())) - bmsg, err = apub.MarshalMail(activity) + bmsg, err = apub.MarshalMail(activity, client) if err != nil { log.Fatalf("remarshal %s activity to mail: %v", activity.Type, err) } @@ -141,25 +149,15 @@ func main() { } // append outbound activities to the user's outbox so others can fetch it. - sysuser, err := user.Lookup(from.Username) - if err != nil { - log.Fatalf("lookup system user from %s: %v", activity.ID, err) + if err := sys.AppendToOutbox(from.Username, activity, create); err != nil { + log.Fatalf("append activities to outbox: %v", err) } - outbox := path.Join(sys.UserDataDir(sysuser), "outbox") - for _, a := range []*apub.Activity{activity, create} { - b, err := json.Marshal(a) - if err != nil { - log.Fatalf("encode %s: %v", activity.ID, err) - } - fname := path.Base(a.ID) - fname = path.Join(outbox, fname) - if err := os.WriteFile(fname, b, 0644); err != nil { - log.Fatalf("write activity to outbox: %v", err) - } - } for _, rcpt := range remote { - ra, err := apub.Finger(rcpt) + if strings.Contains(rcpt, "+followers") { + rcpt = strings.Replace(rcpt, "+followers", "", 1) + } + ra, err := client.Finger(rcpt) if err != nil { log.Printf("webfinger %s: %v", rcpt, err) gotErr = true blob - 1f9a3ecfcdd21a6e6e0ad3bf0e48aa012eed1b50 blob + 9e65ce624b9326cd7de5e88ad494c0121aa5dc97 --- cmd/apserve/listen.go +++ cmd/apserve/listen.go @@ -14,6 +14,7 @@ import ( "strings" "olowe.co/apub" + "olowe.co/apub/internal/sys" ) type server struct { @@ -55,8 +56,12 @@ func (srv *server) relay(username string, activity *ap } cmd := exec.Command("apsend", username) - msg, err := apub.MarshalMail(activity) + client, err := sys.ClientFor(username, domain) if err != nil { + log.Printf("activitypub client for %s: %v", username, err) + } + msg, err := apub.MarshalMail(activity, client) + if err != nil { log.Printf("marshal %s %s to mail message: %v", activity.Type, activity.ID, err) return } blob - 075f3ce03072e1d27562a1b4a05b35535bcdbcbb blob + 1b28603422eb76b391a73b6fafc538ae10c496ba --- cmd/apsubmit/server.go +++ cmd/apsubmit/server.go @@ -9,9 +9,10 @@ import ( "os" "os/exec" "os/user" + "strings" "github.com/emersion/go-smtp" - "olowe.co/apub" + "webfinger.net/go/webfinger" ) type Backend struct { @@ -58,7 +59,11 @@ func (s *Session) Rcpt(to string, opts *smtp.RcptOptio if err != nil { return err } - if _, err = apub.Finger(addr.Address); err != nil { + q := addr.Address + if strings.Contains(addr.Address, "+followers") { + q = strings.Replace(addr.Address, "+followers", "", 1) + } + if _, err := webfinger.Lookup(q, nil); err != nil { return err } s.recipients = append(s.recipients, to) blob - b1a14b6385df6197b110d7a1955b29bae60507b5 blob + b4b935cad9a1f8b701f55800c9c316e15e7c7059 --- internal/sys/user.go +++ internal/sys/user.go @@ -3,6 +3,7 @@ package sys import ( "crypto/rsa" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" "net/http" @@ -118,3 +119,24 @@ func JRDFor(username, domain string) (*webfinger.JRD, }, }, nil } + +func AppendToOutbox(username string, activities ...*apub.Activity) error { + u, err := user.Lookup(username) + if err != nil { + return fmt.Errorf("lookup user: %w", err) + } + outbox := path.Join(UserDataDir(u), "outbox") + for _, a := range activities { + fname := path.Base(a.ID) + fname = path.Join(outbox, fname) + f, err := os.Create(fname) + if err != nil { + return fmt.Errorf("create file for %s: %w", a.ID, err) + } + if err := json.NewEncoder(f).Encode(a); err != nil { + return fmt.Errorf("encode %s: %w", a.ID, err) + } + f.Close() + } + return nil +} blob - 1e5c0bd4dd580bc4c32e1c2d03f777ca0e989d5e blob + b0629b9bdee6b6ad3ae1f7765b27869f29cfac28 --- mail.go +++ mail.go @@ -10,72 +10,111 @@ import ( "time" ) -func MarshalMail(activity *Activity) ([]byte, error) { - buf := &bytes.Buffer{} +func MarshalMail(activity *Activity, client *Client) ([]byte, error) { + msg, err := marshalMail(activity, client) + if err != nil { + return nil, err + } + return encodeMsg(msg), nil +} - from, err := LookupActor(activity.AttributedTo) +func marshalMail(activity *Activity, client *Client) (*mail.Message, error) { + if client == nil { + client = &DefaultClient + } + + msg := new(mail.Message) + msg.Header = make(mail.Header) + var actors []Actor + from, err := client.LookupActor(activity.AttributedTo) if err != nil { - return nil, fmt.Errorf("lookup actor %s: %w", activity.AttributedTo, err) + return nil, fmt.Errorf("build From: lookup actor %s: %w", activity.AttributedTo, err) } - fmt.Fprintf(buf, "From: %s\n", from.Address()) + actors = append(actors, *from) + msg.Header["From"] = []string{from.Address().String()} - var rcpt []string - for _, u := range activity.To { - if u == PublicCollection { + var addrs, collections []string + for _, id := range activity.To { + if id == PublicCollection { continue } - actor, err := LookupActor(u) + + a, err := client.LookupActor(id) if err != nil { - return nil, fmt.Errorf("lookup actor %s: %w", u, err) + return nil, fmt.Errorf("build To: lookup actor %s: %w", id, err) } - rcpt = append(rcpt, actor.Address().String()) + if a.Type == "Collection" || a.Type == "OrderedCollection" { + collections = append(collections, a.ID) + } else { + addrs = append(addrs, a.Address().String()) + actors = append(actors, *a) + } } - fmt.Fprintln(buf, "To:", strings.Join(rcpt, ", ")) + for _, id := range collections { + if i := indexFollowers(actors, id); i >= 0 { + addrs = append(addrs, actors[i].FollowersAddress().String()) + } + } + msg.Header["To"] = addrs - var rcptcc []string - if activity.CC != nil { - for _, u := range activity.CC { - if u == PublicCollection { - continue - } else if u == from.Followers { - rcptcc = append(rcptcc, from.FollowersAddress().String()) - continue - } - actor, err := LookupActor(u) - if err != nil { - return nil, fmt.Errorf("lookup actor %s: %w", u, err) - } - rcptcc = append(rcptcc, actor.Address().String()) + addrs, collections = []string{}, []string{} + for _, id := range activity.CC { + if id == PublicCollection { + continue } - fmt.Fprintln(buf, "CC:", strings.Join(rcptcc, ", ")) + + a, err := client.LookupActor(id) + if err != nil { + return nil, fmt.Errorf("build CC: lookup actor %s: %w", id, err) + } + if a.Type == "Collection" || a.Type == "OrderedCollection" { + collections = append(collections, a.ID) + continue + } + addrs = append(addrs, a.Address().String()) + actors = append(actors, *a) } + for _, id := range collections { + if i := indexFollowers(actors, id); i >= 0 { + addrs = append(addrs, actors[i].FollowersAddress().String()) + } + } + msg.Header["CC"] = addrs - fmt.Fprintf(buf, "Date: %s\n", activity.Published.Format(time.RFC822)) - fmt.Fprintf(buf, "Message-ID: <%s>\n", activity.ID) + msg.Header["Date"] = []string{activity.Published.Format(time.RFC822)} + msg.Header["Message-ID"] = []string{"<" + activity.ID + ">"} + msg.Header["Subject"] = []string{activity.Name} if activity.Audience != "" { - fmt.Fprintf(buf, "List-ID: <%s>\n", activity.Audience) + msg.Header["List-ID"] = []string{"<" + activity.Audience + ">"} } if activity.InReplyTo != "" { - fmt.Fprintf(buf, "References: <%s>\n", activity.InReplyTo) + msg.Header["In-Reply-To"] = []string{"<" + activity.InReplyTo + ">"} } - body := &activity.Content + msg.Body = strings.NewReader(activity.Content) + msg.Header["Content-Type"] = []string{"text/html; charset=utf-8"} if activity.Source.Content != "" && activity.Source.MediaType == "text/markdown" { - body = &activity.Source.Content - fmt.Fprintln(buf, "Content-Type: text/plain; charset=utf-8") + msg.Body = strings.NewReader(activity.Source.Content) + msg.Header["Content-Type"] = []string{"text/plain; charset=utf-8"} } else if activity.MediaType == "text/markdown" { - fmt.Fprintln(buf, "Content-Type: text/plain; charset=utf-8") - } else { - fmt.Fprintln(buf, "Content-Type:", "text/html; charset=utf-8") + msg.Header["Content-Type"] = []string{"text/plain; charset=utf-8"} } - fmt.Fprintln(buf, "Subject:", activity.Name) - fmt.Fprintln(buf) - fmt.Fprintln(buf, *body) - _, err = mail.ReadMessage(bytes.NewReader(buf.Bytes())) - return buf.Bytes(), err + return msg, nil } -func UnmarshalMail(msg *mail.Message) (*Activity, error) { +func indexFollowers(actors []Actor, id string) int { + for i := range actors { + if actors[i].Followers == id { + return i + } + } + return -1 +} + +func UnmarshalMail(msg *mail.Message, client *Client) (*Activity, error) { + if client == nil { + client = &DefaultClient + } ct := msg.Header.Get("Content-Type") if strings.HasPrefix(ct, "multipart") { return nil, fmt.Errorf("cannot unmarshal from multipart message") @@ -93,7 +132,7 @@ func UnmarshalMail(msg *mail.Message) (*Activity, erro if err != nil { return nil, fmt.Errorf("parse From: %w", err) } - wfrom, err := Finger(from[0].Address) + wfrom, err := client.Finger(from[0].Address) if err != nil { return nil, fmt.Errorf("webfinger From: %w", err) } @@ -107,7 +146,7 @@ func UnmarshalMail(msg *mail.Message) (*Activity, erro if err != nil { return nil, fmt.Errorf("parse To address list: %w", err) } - actors, err := fingerAll(to) + actors, err := client.fingerAll(to) if err != nil { return nil, fmt.Errorf("webfinger To addresses: %w", err) } @@ -127,7 +166,7 @@ func UnmarshalMail(msg *mail.Message) (*Activity, erro if err != nil { return nil, fmt.Errorf("parse CC address list: %w", err) } - actors, err := fingerAll(cc) + actors, err := client.fingerAll(cc) if err != nil { return nil, fmt.Errorf("webfinger CC addresses: %w", err) } @@ -145,6 +184,7 @@ func UnmarshalMail(msg *mail.Message) (*Activity, erro if _, err := io.Copy(buf, msg.Body); err != nil { return nil, fmt.Errorf("read message body: %v", err) } + content := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", "")) return &Activity{ AtContext: NormContext, @@ -154,7 +194,7 @@ func UnmarshalMail(msg *mail.Message) (*Activity, erro CC: wcc, MediaType: "text/markdown", Name: strings.TrimSpace(msg.Header.Get("Subject")), - Content: strings.TrimSpace(buf.String()), + Content: content, InReplyTo: strings.Trim(msg.Header.Get("In-Reply-To"), "<>"), Published: &date, Tag: tags, @@ -162,9 +202,27 @@ func UnmarshalMail(msg *mail.Message) (*Activity, erro } func SendMail(addr string, auth smtp.Auth, from string, to []string, activity *Activity) error { - msg, err := MarshalMail(activity) + msg, err := MarshalMail(activity, nil) if err != nil { return fmt.Errorf("marshal to mail message: %w", err) } return smtp.SendMail(addr, auth, from, to, msg) } + +func encodeMsg(msg *mail.Message) []byte { + buf := &bytes.Buffer{} + // Lead with "From", end with "Subject" to make some mail clients happy. + fmt.Fprintln(buf, "From:", msg.Header.Get("From")) + for k, v := range msg.Header { + switch k { + case "Subject", "From": + continue + default: + fmt.Fprintf(buf, "%s: %s\n", k, strings.Join(v, ", ")) + } + } + fmt.Fprintln(buf, "Subject:", msg.Header.Get("Subject")) + fmt.Fprintln(buf) + io.Copy(buf, msg.Body) + return buf.Bytes() +} blob - fe6864206e60fa17becbdec36ffe8bd64e15b13f blob + 23e4be152cca0e333253127592f31aa76f8346bb --- mail_test.go +++ mail_test.go @@ -2,8 +2,12 @@ package apub import ( "bytes" + "errors" "net/mail" "os" + "reflect" + "sort" + "strings" "testing" ) @@ -52,14 +56,41 @@ func TestMailAddress(t *testing.T) { } func TestMarshalMail(t *testing.T) { - var notes []string = []string{ - "testdata/note/akkoma.json", - "testdata/note/lemmy.json", - "testdata/note/mastodon.json", - "testdata/page.json", + tests := []struct { + name string + recipients []string + }{ + { + "testdata/note/akkoma.json", + []string{ + "kariboka+followers@social.harpia.red", + "otl@apubtest2.srcbeat.com", + }, + }, + { + "testdata/note/lemmy.json", + []string{ + "Feathercrown@lemmy.world", + "technology@lemmy.world", + }, + }, + { + "testdata/note/mastodon.json", + []string{ + "otl+followers@hachyderm.io", + "selfhosted+followers@lemmy.world", + "selfhosted@lemmy.world", + }, + }, + { + "testdata/page.json", + []string{ + "technology@lemmy.world", + }, + }, } - for _, name := range notes { - f, err := os.Open(name) + for _, tt := range tests { + f, err := os.Open(tt.name) if err != nil { t.Error(err) continue @@ -67,31 +98,53 @@ func TestMarshalMail(t *testing.T) { defer f.Close() a, err := Decode(f) if err != nil { - t.Errorf("%s: decode activity: %v", name, err) + t.Errorf("%s: decode activity: %v", tt.name, err) continue } - b, err := MarshalMail(a) + b, err := MarshalMail(a, nil) if err != nil { - t.Errorf("%s: marshal to mail message: %v", name, err) + t.Errorf("%s: marshal to mail message: %v", tt.name, err) continue } msg, err := mail.ReadMessage(bytes.NewReader(b)) if err != nil { - t.Errorf("%s: read back message from marshalled activity: %v", name, err) + t.Errorf("%s: read back message from marshalled activity: %v", tt.name, err) continue } - p := make([]byte, 8) - n, err := msg.Body.Read(p) - if err != nil { - t.Errorf("%s: read message body: %v", name, err) + rcptto, err := msg.Header.AddressList("To") + if errors.Is(err, mail.ErrHeaderNotPresent) { + // whatever; sometimes the Activity has an empty slice. + } else if err != nil { + t.Errorf("%s: parse To addresses: %v", tt.name, err) + t.Log("raw value:", msg.Header.Get("To")) + continue } - if n != len(p) { + rcptcc, err := msg.Header.AddressList("CC") + if errors.Is(err, mail.ErrHeaderNotPresent) { + // whatever; sometimes the Activity has an empty slice. + } else if err != nil { + t.Errorf("%s: parse CC addresses: %v", tt.name, err) + t.Log("raw value:", msg.Header.Get("CC")) + continue + } + t.Log(rcptto) + t.Log(rcptcc) + rcpts := make([]string, len(rcptto)+len(rcptcc)) + for i, rcpt := range append(rcptto, rcptcc...) { + rcpts[i] = rcpt.Address + } + sort.Strings(rcpts) + if !reflect.DeepEqual(rcpts, tt.recipients) { + t.Errorf("%s: unexpected recipients, want %s got %s", tt.name, tt.recipients, rcpts) + } + + p := make([]byte, 8) + if _, err := msg.Body.Read(p); err != nil { + // Pages have no content, so skip this case if a.Type == "Page" { - // Pages have no content, so skip this case continue } - t.Errorf("%s: short read from body", name) - t.Log(string(p)) + t.Errorf("%s: read message body: %v", tt.name, err) } } } @@ -109,7 +162,7 @@ func TestUnmarshalMail(t *testing.T) { if testing.Short() { t.Skip("skipping network calls to unmarshal mail message to Activity") } - a, err := UnmarshalMail(msg) + a, err := UnmarshalMail(msg, nil) if err != nil { t.Fatal(err) } @@ -120,4 +173,19 @@ func TestUnmarshalMail(t *testing.T) { if a.Tag[0].Name != want { t.Errorf("wanted tag name %s, got %s", want, a.Tag[0].Name) } + if a.MediaType != "text/markdown" { + t.Errorf("wrong media type: wanted %s, got %s", "text/markdown", a.MediaType) + } + wantCC := []string{ + "https://programming.dev/c/programming", + "https://programming.dev/u/starman", + "https://hachyderm.io/users/otl/followers", + } + if !reflect.DeepEqual(wantCC, a.CC) { + t.Errorf("wanted recipients %s, got %s", wantCC, a.CC) + } + if strings.Contains(a.Content, "\r") { + t.Errorf("activity content contains carriage returns") + } + t.Log(a) } blob - f872625d946401146d5e4c6104e37b8065d32476 blob + e14e2d1d30a341cb11582c2c92d946dcb6391959 --- webfinger.go +++ webfinger.go @@ -10,7 +10,7 @@ import ( // Finger wraps defaultClient.Finger. func Finger(address string) (*Actor, error) { - return defaultClient.Finger(address) + return DefaultClient.Finger(address) } // Finger is convenience method returning the corresponding Actor, @@ -29,7 +29,7 @@ func (c *Client) Finger(address string) (*Actor, error return nil, ErrNotExist } -func fingerAll(alist []*mail.Address) ([]Actor, error) { +func (c *Client) fingerAll(alist []*mail.Address) ([]Actor, error) { actors := make([]Actor, len(alist)) for i, addr := range alist { q := addr.Address @@ -37,7 +37,7 @@ func fingerAll(alist []*mail.Address) ([]Actor, error) // strip "+followers" to get the regular address that can be fingered. q = strings.Replace(addr.Address, "+followers", "", 1) } - actor, err := Finger(q) + actor, err := c.Finger(q) if err != nil { return actors, fmt.Errorf("finger %s: %w", q, err) }