package adapter import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" "forge.lightcrystal.systems/nilix/underbbs/models" _ "github.com/go-fed/httpsig" ) type anonAPAdapter struct { data *chan models.SocketData server string client http.Client protocol string nickname string } type apLink struct { Href string Rel string Type string } type apAttachment struct { MediaType string Type string Name string Summary string Url string } type apTag struct { Href string Name string Type string } type apIcon struct { MediaType string Type string Url string } type apActor struct { Icon apIcon Id string Name string PreferredUsername string Summary string Url string } type apOutbox struct { OrderedItems []interface{} `json:"-"` RawOrderedItems []json.RawMessage `json:"OrderedItems"` } func (self *apOutbox) UnmarshalJSON(b []byte) error { type obInternal apOutbox err := json.Unmarshal(b, (*obInternal)(self)) if err != nil { return err } for _, raw := range self.RawOrderedItems { var i apActivity err := json.Unmarshal(raw, &i) var x interface{} switch i.Type { case "Create": fallthrough case "Update": x = &apCreateUpdateActivity{} case "Announce": x = &apAnnounceActivity{} } err = json.Unmarshal(raw, &x) if err != nil { return err } self.OrderedItems = append(self.OrderedItems, x) } return nil } type apActivity struct { Actor string Id string Type string To string Published string } type apCreateUpdateActivity struct { Object apObject apActivity } type apAnnounceActivity struct { Object string apActivity } type apObject struct { Id string Content string AttributedTo string Context string Conversation string Published string Tag []apTag Attachment []apAttachment To string Url string InReplyTo *string } type webFinger struct { Links []apLink } func (self *anonAPAdapter) Init(data *chan models.SocketData, server, protocol, nickname string) error { self.data = data self.server = server self.nickname = nickname self.protocol = protocol return nil } func getBodyJson(res *http.Response) []byte { l := res.ContentLength // if the length is unknown, we'll allocate 4k // really we should be using Decoders all around if l < 0 { l = 4096 } jsonData := make([]byte, l) res.Body.Read(jsonData) return jsonData } func (self *anonAPAdapter) makeApRequest(method, url string, data io.Reader) (*http.Response, error) { req, err := http.NewRequest(method, url, data) if err != nil { return nil, err } req.Header.Set("Accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") return self.client.Do(req) } func (self *anonAPAdapter) toMsg(object apObject) *models.Message { t, err := time.Parse(time.RFC3339, object.Published) if err != nil { t = time.Now() } vis := strings.Split(object.To, "#") if len(vis) > 1 { object.To = vis[1] } m := &models.Message{ Datagram: models.Datagram{ Id: object.Id, Uri: object.Url, Type: "message", Created: t.UnixMilli(), Updated: nil, Protocol: self.protocol, Adapter: self.nickname, }, Author: object.AttributedTo, Content: object.Content, ReplyTo: object.InReplyTo, Visibility: object.To, } for _, a := range object.Attachment { m.Attachments = append(m.Attachments, models.Attachment{ Src: a.Url, Desc: a.Summary, }) } return m } func (self *anonAPAdapter) send(data models.SocketData) { if self.data != nil { *self.data <- data } else { fmt.Fprintln(os.Stderr, string(data.ToDatagram())) } } func (self *anonAPAdapter) toAuthor(actor apActor) *models.Author { curtime := time.Now().UnixMilli() a := &models.Author{ Datagram: models.Datagram{ Id: actor.Id, Uri: actor.Url, Type: "author", Created: curtime, Updated: &curtime, Protocol: self.protocol, Adapter: self.nickname, }, Name: actor.PreferredUsername, ProfileData: actor.Summary, ProfilePic: actor.Icon.Url, } return a } func (self *anonAPAdapter) getHostForId(id string) string { if strings.HasPrefix(id, "https://") { idNoScheme := strings.Split(id, "https://")[1] serverNoScheme := strings.Split(idNoScheme, "/")[0] return "https://" + serverNoScheme } else { return "https://" + strings.Split(id, "@")[1] } } func (self *anonAPAdapter) normalizeActorId(id string) string { if string([]byte{id[0]}) == "@" { id = id[1:] } if !strings.Contains(id, "@") { // if the id is not a URI, add local server to it if !strings.HasPrefix(id, "https://") { serverNoScheme := strings.Split(self.server, "https://")[1] id = fmt.Sprintf("%s@%s", id, serverNoScheme) } } return id } func (self *anonAPAdapter) Fetch(etype string, ids []string) error { for _, id := range ids { switch etype { case "author": // webfinger lookup on id normalizedId := self.normalizeActorId(id) fmt.Println(normalizedId) reqHost := self.getHostForId(normalizedId) profile := normalizedId fmt.Println(reqHost) if !strings.HasPrefix(normalizedId, "https://") { res, err := http.Get(reqHost + "/.well-known/webfinger?resource=acct:" + normalizedId) if err != nil { return err } data := getBodyJson(res) wf := webFinger{} json.Unmarshal(data, &wf) for _, l := range wf.Links { if l.Rel == "self" { profile = l.Href break } } } res, err := self.makeApRequest("GET", profile, nil) if err != nil { return err } authorData := getBodyJson(res) actor := apActor{} json.Unmarshal(authorData, &actor) author := self.toAuthor(actor) if author != nil { self.send(author) } case "byAuthor": normalizedId := self.normalizeActorId(id) reqHost := self.getHostForId(normalizedId) profile := normalizedId if !strings.HasPrefix(normalizedId, "https://") { res, err := http.Get(reqHost + "/.well-known/webfinger?resource=acct:" + normalizedId) if err != nil { return err } data := getBodyJson(res) wf := webFinger{} json.Unmarshal(data, &wf) for _, l := range wf.Links { if l.Rel == "self" { profile = l.Href break } } } res, err := self.makeApRequest("GET", profile+"/outbox", nil) if err != nil { return err } obData := getBodyJson(res) ob := apOutbox{} ob.UnmarshalJSON(obData) for _, i := range ob.OrderedItems { switch a := i.(type) { case *apCreateUpdateActivity: msg := self.toMsg(a.Object) if msg != nil { self.send(msg) } case *apAnnounceActivity: res, err = self.makeApRequest("GET", a.Object, nil) if err != nil { return err } objectData := getBodyJson(res) object := apObject{} json.Unmarshal(objectData, &object) ogMsg := self.toMsg(object) // if we couldn't fetch the original, skip it if ogMsg != nil && ogMsg.Author != "" { t, err := time.Parse(time.RFC3339, a.Published) if err != nil { t = time.Now() } rt := t.UnixMilli() ogMsg.RenoteId = &a.Id ogMsg.Renoter = &a.Actor ogMsg.RenoteTime = &rt self.send(ogMsg) } } } // for each item in outbox, check if it's a Create/Update or an Announce // Create/Update you can directly deserialize the object // if it's an Announce, try to get the object and deserialize it, build a boost out of it case "message": res, err := self.makeApRequest("GET", id, nil) if err != nil { return err } objectData := getBodyJson(res) object := apObject{} json.Unmarshal(objectData, &object) message := self.toMsg(object) if message != nil { self.send(message) } case "children": case "convoy": default: break } } return nil }