commit - ed0db64ab00968903e807add075cfd6dcbf1dafb
commit + d3dfb6729126922affd3865165ef240d239837e4
blob - bfcb8e5d35c75965c081cafe858dbf41b98e644d
blob + 32791cec840b2b12d1e1e8d0b65ff70e891168eb
--- client.go
+++ client.go
"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 {
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
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 {
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
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
"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()
}
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 {
}
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
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)
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)
}
}
// 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
"strings"
"olowe.co/apub"
+ "olowe.co/apub/internal/sys"
)
type server struct {
}
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
"os"
"os/exec"
"os/user"
+ "strings"
"github.com/emersion/go-smtp"
- "olowe.co/apub"
+ "webfinger.net/go/webfinger"
)
type Backend struct {
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
import (
"crypto/rsa"
"crypto/x509"
+ "encoding/json"
"encoding/pem"
"fmt"
"net/http"
},
}, 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
"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")
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)
}
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)
}
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)
}
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,
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,
}
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
import (
"bytes"
+ "errors"
"net/mail"
"os"
+ "reflect"
+ "sort"
+ "strings"
"testing"
)
}
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
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)
}
}
}
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)
}
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
// 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,
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
// 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)
}