This renders all requests for that user's posts, collections and related ActivityPub endpoints with 404 responses. While suspended, users may not create or edit posts or collections. User status is listed in the admin user page Admin view of user details shows status and now has a button to activate or suspend a user.
745 lines
19 KiB
Go
745 lines
19 KiB
Go
/*
|
|
* Copyright © 2018-2019 A Bunch Tell LLC.
|
|
*
|
|
* This file is part of WriteFreely.
|
|
*
|
|
* WriteFreely is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License, included
|
|
* in the LICENSE file in this source code package.
|
|
*/
|
|
|
|
package writefreely
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/writeas/activity/streams"
|
|
"github.com/writeas/httpsig"
|
|
"github.com/writeas/impart"
|
|
"github.com/writeas/nerds/store"
|
|
"github.com/writeas/web-core/activitypub"
|
|
"github.com/writeas/web-core/activitystreams"
|
|
"github.com/writeas/web-core/log"
|
|
)
|
|
|
|
const (
|
|
// TODO: delete. don't use this!
|
|
apCustomHandleDefault = "blog"
|
|
)
|
|
|
|
type RemoteUser struct {
|
|
ID int64
|
|
ActorID string
|
|
Inbox string
|
|
SharedInbox string
|
|
}
|
|
|
|
func (ru *RemoteUser) AsPerson() *activitystreams.Person {
|
|
return &activitystreams.Person{
|
|
BaseObject: activitystreams.BaseObject{
|
|
Type: "Person",
|
|
Context: []interface{}{
|
|
activitystreams.Namespace,
|
|
},
|
|
ID: ru.ActorID,
|
|
},
|
|
Inbox: ru.Inbox,
|
|
Endpoints: activitystreams.Endpoints{
|
|
SharedInbox: ru.SharedInbox,
|
|
},
|
|
}
|
|
}
|
|
|
|
func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Request) error {
|
|
w.Header().Set("Server", serverSoftware)
|
|
|
|
vars := mux.Vars(r)
|
|
alias := vars["alias"]
|
|
|
|
// TODO: enforce visibility
|
|
// Get base Collection data
|
|
var c *Collection
|
|
var err error
|
|
if app.cfg.App.SingleUser {
|
|
c, err = app.db.GetCollectionByID(1)
|
|
} else {
|
|
c, err = app.db.GetCollection(alias)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
|
if err != nil {
|
|
log.Error("fetch collection inbox: get owner: %v", err)
|
|
return ErrInternalGeneral
|
|
}
|
|
if suspended {
|
|
return ErrCollectionNotFound
|
|
}
|
|
c.hostName = app.cfg.App.Host
|
|
|
|
p := c.PersonObject()
|
|
|
|
return impart.RenderActivityJSON(w, p, http.StatusOK)
|
|
}
|
|
|
|
func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Request) error {
|
|
w.Header().Set("Server", serverSoftware)
|
|
|
|
vars := mux.Vars(r)
|
|
alias := vars["alias"]
|
|
|
|
// TODO: enforce visibility
|
|
// Get base Collection data
|
|
var c *Collection
|
|
var err error
|
|
if app.cfg.App.SingleUser {
|
|
c, err = app.db.GetCollectionByID(1)
|
|
} else {
|
|
c, err = app.db.GetCollection(alias)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
|
if err != nil {
|
|
log.Error("fetch collection inbox: get owner: %v", err)
|
|
return ErrInternalGeneral
|
|
}
|
|
if suspended {
|
|
return ErrCollectionNotFound
|
|
}
|
|
c.hostName = app.cfg.App.Host
|
|
|
|
if app.cfg.App.SingleUser {
|
|
if alias != c.Alias {
|
|
return ErrCollectionNotFound
|
|
}
|
|
}
|
|
|
|
res := &CollectionObj{Collection: *c}
|
|
app.db.GetPostsCount(res, false)
|
|
accountRoot := c.FederatedAccount()
|
|
|
|
page := r.FormValue("page")
|
|
p, err := strconv.Atoi(page)
|
|
if err != nil || p < 1 {
|
|
// Return outbox
|
|
oc := activitystreams.NewOrderedCollection(accountRoot, "outbox", res.TotalPosts)
|
|
return impart.RenderActivityJSON(w, oc, http.StatusOK)
|
|
}
|
|
|
|
// Return outbox page
|
|
ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p)
|
|
ocp.OrderedItems = []interface{}{}
|
|
|
|
posts, err := app.db.GetPosts(c, p, false, true, false)
|
|
for _, pp := range *posts {
|
|
pp.Collection = res
|
|
o := pp.ActivityObject()
|
|
a := activitystreams.NewCreateActivity(o)
|
|
ocp.OrderedItems = append(ocp.OrderedItems, *a)
|
|
}
|
|
|
|
return impart.RenderActivityJSON(w, ocp, http.StatusOK)
|
|
}
|
|
|
|
func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Request) error {
|
|
w.Header().Set("Server", serverSoftware)
|
|
|
|
vars := mux.Vars(r)
|
|
alias := vars["alias"]
|
|
|
|
// TODO: enforce visibility
|
|
// Get base Collection data
|
|
var c *Collection
|
|
var err error
|
|
if app.cfg.App.SingleUser {
|
|
c, err = app.db.GetCollectionByID(1)
|
|
} else {
|
|
c, err = app.db.GetCollection(alias)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
|
if err != nil {
|
|
log.Error("fetch collection inbox: get owner: %v", err)
|
|
return ErrInternalGeneral
|
|
}
|
|
if suspended {
|
|
return ErrCollectionNotFound
|
|
}
|
|
c.hostName = app.cfg.App.Host
|
|
|
|
accountRoot := c.FederatedAccount()
|
|
|
|
folls, err := app.db.GetAPFollowers(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
page := r.FormValue("page")
|
|
p, err := strconv.Atoi(page)
|
|
if err != nil || p < 1 {
|
|
// Return outbox
|
|
oc := activitystreams.NewOrderedCollection(accountRoot, "followers", len(*folls))
|
|
return impart.RenderActivityJSON(w, oc, http.StatusOK)
|
|
}
|
|
|
|
// Return outbox page
|
|
ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "followers", len(*folls), p)
|
|
ocp.OrderedItems = []interface{}{}
|
|
/*
|
|
for _, f := range *folls {
|
|
ocp.OrderedItems = append(ocp.OrderedItems, f.ActorID)
|
|
}
|
|
*/
|
|
return impart.RenderActivityJSON(w, ocp, http.StatusOK)
|
|
}
|
|
|
|
func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Request) error {
|
|
w.Header().Set("Server", serverSoftware)
|
|
|
|
vars := mux.Vars(r)
|
|
alias := vars["alias"]
|
|
|
|
// TODO: enforce visibility
|
|
// Get base Collection data
|
|
var c *Collection
|
|
var err error
|
|
if app.cfg.App.SingleUser {
|
|
c, err = app.db.GetCollectionByID(1)
|
|
} else {
|
|
c, err = app.db.GetCollection(alias)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
|
if err != nil {
|
|
log.Error("fetch collection inbox: get owner: %v", err)
|
|
return ErrInternalGeneral
|
|
}
|
|
if suspended {
|
|
return ErrCollectionNotFound
|
|
}
|
|
c.hostName = app.cfg.App.Host
|
|
|
|
accountRoot := c.FederatedAccount()
|
|
|
|
page := r.FormValue("page")
|
|
p, err := strconv.Atoi(page)
|
|
if err != nil || p < 1 {
|
|
// Return outbox
|
|
oc := activitystreams.NewOrderedCollection(accountRoot, "following", 0)
|
|
return impart.RenderActivityJSON(w, oc, http.StatusOK)
|
|
}
|
|
|
|
// Return outbox page
|
|
ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "following", 0, p)
|
|
ocp.OrderedItems = []interface{}{}
|
|
return impart.RenderActivityJSON(w, ocp, http.StatusOK)
|
|
}
|
|
|
|
func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request) error {
|
|
w.Header().Set("Server", serverSoftware)
|
|
|
|
vars := mux.Vars(r)
|
|
alias := vars["alias"]
|
|
var c *Collection
|
|
var err error
|
|
if app.cfg.App.SingleUser {
|
|
c, err = app.db.GetCollectionByID(1)
|
|
} else {
|
|
c, err = app.db.GetCollection(alias)
|
|
}
|
|
if err != nil {
|
|
// TODO: return Reject?
|
|
return err
|
|
}
|
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
|
if err != nil {
|
|
log.Error("fetch collection inbox: get owner: %v", err)
|
|
return ErrInternalGeneral
|
|
}
|
|
if suspended {
|
|
return ErrCollectionNotFound
|
|
}
|
|
c.hostName = app.cfg.App.Host
|
|
|
|
if debugging {
|
|
dump, err := httputil.DumpRequest(r, true)
|
|
if err != nil {
|
|
log.Error("Can't dump: %v", err)
|
|
} else {
|
|
log.Info("Rec'd! %q", dump)
|
|
}
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
|
|
return err
|
|
}
|
|
|
|
a := streams.NewAccept()
|
|
p := c.PersonObject()
|
|
var to *url.URL
|
|
var isFollow, isUnfollow bool
|
|
fullActor := &activitystreams.Person{}
|
|
var remoteUser *RemoteUser
|
|
|
|
res := &streams.Resolver{
|
|
FollowCallback: func(f *streams.Follow) error {
|
|
isFollow = true
|
|
|
|
// 1) Use the Follow concrete type here
|
|
// 2) Errors are propagated to res.Deserialize call below
|
|
m["@context"] = []string{activitystreams.Namespace}
|
|
b, _ := json.Marshal(m)
|
|
if debugging {
|
|
log.Info("Follow: %s", b)
|
|
}
|
|
|
|
_, followID := f.GetId()
|
|
if followID == nil {
|
|
log.Error("Didn't resolve follow ID")
|
|
} else {
|
|
aID := c.FederatedAccount() + "#accept-" + store.GenerateFriendlyRandomString(20)
|
|
acceptID, err := url.Parse(aID)
|
|
if err != nil {
|
|
log.Error("Couldn't parse generated Accept URL '%s': %v", aID, err)
|
|
}
|
|
a.SetId(acceptID)
|
|
}
|
|
a.AppendObject(f.Raw())
|
|
_, to = f.GetActor(0)
|
|
obj := f.Raw().GetObjectIRI(0)
|
|
a.AppendActor(obj)
|
|
|
|
// First get actor information
|
|
if to == nil {
|
|
return fmt.Errorf("No valid `to` string")
|
|
}
|
|
fullActor, remoteUser, err = getActor(app, to.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return impart.RenderActivityJSON(w, m, http.StatusOK)
|
|
},
|
|
UndoCallback: func(u *streams.Undo) error {
|
|
isUnfollow = true
|
|
|
|
m["@context"] = []string{activitystreams.Namespace}
|
|
b, _ := json.Marshal(m)
|
|
if debugging {
|
|
log.Info("Undo: %s", b)
|
|
}
|
|
|
|
a.AppendObject(u.Raw())
|
|
_, to = u.GetActor(0)
|
|
// TODO: get actor from object.object, not object
|
|
obj := u.Raw().GetObjectIRI(0)
|
|
a.AppendActor(obj)
|
|
if to != nil {
|
|
// Populate fullActor from DB?
|
|
remoteUser, err = getRemoteUser(app, to.String())
|
|
if err != nil {
|
|
if iErr, ok := err.(*impart.HTTPError); ok {
|
|
if iErr.Status == http.StatusNotFound {
|
|
log.Error("No remoteuser info for Undo event!")
|
|
}
|
|
}
|
|
return err
|
|
} else {
|
|
fullActor = remoteUser.AsPerson()
|
|
}
|
|
} else {
|
|
log.Error("No to on Undo!")
|
|
}
|
|
return impart.RenderActivityJSON(w, m, http.StatusOK)
|
|
},
|
|
}
|
|
if err := res.Deserialize(m); err != nil {
|
|
// 3) Any errors from #2 can be handled, or the payload is an unknown type.
|
|
log.Error("Unable to resolve Follow: %v", err)
|
|
if debugging {
|
|
log.Error("Map: %s", m)
|
|
}
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
time.Sleep(2 * time.Second)
|
|
am, err := a.Serialize()
|
|
if err != nil {
|
|
log.Error("Unable to serialize Accept: %v", err)
|
|
return
|
|
}
|
|
am["@context"] = []string{activitystreams.Namespace}
|
|
|
|
if to == nil {
|
|
log.Error("No to! %v", err)
|
|
return
|
|
}
|
|
err = makeActivityPost(app.cfg.App.Host, p, fullActor.Inbox, am)
|
|
if err != nil {
|
|
log.Error("Unable to make activity POST: %v", err)
|
|
return
|
|
}
|
|
|
|
if isFollow {
|
|
t, err := app.db.Begin()
|
|
if err != nil {
|
|
log.Error("Unable to start transaction: %v", err)
|
|
return
|
|
}
|
|
|
|
var followerID int64
|
|
|
|
if remoteUser != nil {
|
|
followerID = remoteUser.ID
|
|
} else {
|
|
// Add follower locally, since it wasn't found before
|
|
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
|
|
if err != nil {
|
|
if !app.db.isDuplicateKeyErr(err) {
|
|
t.Rollback()
|
|
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
followerID, err = res.LastInsertId()
|
|
if err != nil {
|
|
t.Rollback()
|
|
log.Error("no lastinsertid for followers, rolling back: %v", err)
|
|
return
|
|
}
|
|
|
|
// Add in key
|
|
_, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM)
|
|
if err != nil {
|
|
if !app.db.isDuplicateKeyErr(err) {
|
|
t.Rollback()
|
|
log.Error("Couldn't add follower keys in DB: %v\n", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add follow
|
|
_, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", c.ID, followerID)
|
|
if err != nil {
|
|
if !app.db.isDuplicateKeyErr(err) {
|
|
t.Rollback()
|
|
log.Error("Couldn't add follower in DB: %v\n", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
err = t.Commit()
|
|
if err != nil {
|
|
t.Rollback()
|
|
log.Error("Rolling back after Commit(): %v\n", err)
|
|
return
|
|
}
|
|
} else if isUnfollow {
|
|
// Remove follower locally
|
|
_, err = app.db.Exec("DELETE FROM remotefollows WHERE collection_id = ? AND remote_user_id = (SELECT id FROM remoteusers WHERE actor_id = ?)", c.ID, to.String())
|
|
if err != nil {
|
|
log.Error("Couldn't remove follower from DB: %v\n", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func makeActivityPost(hostName string, p *activitystreams.Person, url string, m interface{}) error {
|
|
log.Info("POST %s", url)
|
|
b, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, _ := http.NewRequest("POST", url, bytes.NewBuffer(b))
|
|
r.Header.Add("Content-Type", "application/activity+json")
|
|
r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
|
|
h := sha256.New()
|
|
h.Write(b)
|
|
r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
|
|
|
// Sign using the 'Signature' header
|
|
privKey, err := activitypub.DecodePrivateKey(p.GetPrivKey())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
signer := httpsig.NewSigner(p.PublicKey.ID, privKey, httpsig.RSASHA256, []string{"(request-target)", "date", "host", "digest"})
|
|
err = signer.SignSigHeader(r)
|
|
if err != nil {
|
|
log.Error("Can't sign: %v", err)
|
|
}
|
|
|
|
if debugging {
|
|
dump, err := httputil.DumpRequestOut(r, true)
|
|
if err != nil {
|
|
log.Error("Can't dump: %v", err)
|
|
} else {
|
|
log.Info("%s", dump)
|
|
}
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp != nil && resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if debugging {
|
|
log.Info("Status : %s", resp.Status)
|
|
log.Info("Response: %s", body)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resolveIRI(hostName, url string) ([]byte, error) {
|
|
log.Info("GET %s", url)
|
|
|
|
r, _ := http.NewRequest("GET", url, nil)
|
|
r.Header.Add("Accept", "application/activity+json")
|
|
r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
|
|
|
|
if debugging {
|
|
dump, err := httputil.DumpRequestOut(r, true)
|
|
if err != nil {
|
|
log.Error("Can't dump: %v", err)
|
|
} else {
|
|
log.Info("%s", dump)
|
|
}
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp != nil && resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if debugging {
|
|
log.Info("Status : %s", resp.Status)
|
|
log.Info("Response: %s", body)
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
|
|
if debugging {
|
|
log.Info("Deleting federated post!")
|
|
}
|
|
p.Collection.hostName = app.cfg.App.Host
|
|
actor := p.Collection.PersonObject(collID)
|
|
na := p.ActivityObject()
|
|
|
|
// Add followers
|
|
p.Collection.ID = collID
|
|
followers, err := app.db.GetAPFollowers(&p.Collection.Collection)
|
|
if err != nil {
|
|
log.Error("Couldn't delete post (get followers)! %v", err)
|
|
return err
|
|
}
|
|
|
|
inboxes := map[string][]string{}
|
|
for _, f := range *followers {
|
|
inbox := f.SharedInbox
|
|
if inbox == "" {
|
|
inbox = f.Inbox
|
|
}
|
|
if _, ok := inboxes[inbox]; ok {
|
|
inboxes[inbox] = append(inboxes[inbox], f.ActorID)
|
|
} else {
|
|
inboxes[inbox] = []string{f.ActorID}
|
|
}
|
|
}
|
|
|
|
for si, instFolls := range inboxes {
|
|
na.CC = []string{}
|
|
for _, f := range instFolls {
|
|
na.CC = append(na.CC, f)
|
|
}
|
|
|
|
err = makeActivityPost(app.cfg.App.Host, actor, si, activitystreams.NewDeleteActivity(na))
|
|
if err != nil {
|
|
log.Error("Couldn't delete post! %v", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
|
if debugging {
|
|
if isUpdate {
|
|
log.Info("Federating updated post!")
|
|
} else {
|
|
log.Info("Federating new post!")
|
|
}
|
|
}
|
|
actor := p.Collection.PersonObject(collID)
|
|
na := p.ActivityObject()
|
|
|
|
// Add followers
|
|
p.Collection.ID = collID
|
|
followers, err := app.db.GetAPFollowers(&p.Collection.Collection)
|
|
if err != nil {
|
|
log.Error("Couldn't post! %v", err)
|
|
return err
|
|
}
|
|
log.Info("Followers for %d: %+v", collID, followers)
|
|
|
|
inboxes := map[string][]string{}
|
|
for _, f := range *followers {
|
|
inbox := f.SharedInbox
|
|
if inbox == "" {
|
|
inbox = f.Inbox
|
|
}
|
|
if _, ok := inboxes[inbox]; ok {
|
|
inboxes[inbox] = append(inboxes[inbox], f.ActorID)
|
|
} else {
|
|
inboxes[inbox] = []string{f.ActorID}
|
|
}
|
|
}
|
|
|
|
for si, instFolls := range inboxes {
|
|
na.CC = []string{}
|
|
for _, f := range instFolls {
|
|
na.CC = append(na.CC, f)
|
|
}
|
|
var activity *activitystreams.Activity
|
|
if isUpdate {
|
|
activity = activitystreams.NewUpdateActivity(na)
|
|
} else {
|
|
activity = activitystreams.NewCreateActivity(na)
|
|
activity.To = na.To
|
|
activity.CC = na.CC
|
|
}
|
|
err = makeActivityPost(app.cfg.App.Host, actor, si, activity)
|
|
if err != nil {
|
|
log.Error("Couldn't post! %v", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
|
u := RemoteUser{ActorID: actorID}
|
|
err := app.db.QueryRow("SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox)
|
|
switch {
|
|
case err == sql.ErrNoRows:
|
|
return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
|
|
case err != nil:
|
|
log.Error("Couldn't get remote user %s: %v", actorID, err)
|
|
return nil, err
|
|
}
|
|
|
|
return &u, nil
|
|
}
|
|
|
|
func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
|
|
log.Info("Fetching actor %s locally", actorIRI)
|
|
actor := &activitystreams.Person{}
|
|
remoteUser, err := getRemoteUser(app, actorIRI)
|
|
if err != nil {
|
|
if iErr, ok := err.(impart.HTTPError); ok {
|
|
if iErr.Status == http.StatusNotFound {
|
|
// Fetch remote actor
|
|
log.Info("Not found; fetching actor %s remotely", actorIRI)
|
|
actorResp, err := resolveIRI(app.cfg.App.Host, actorIRI)
|
|
if err != nil {
|
|
log.Error("Unable to get actor! %v", err)
|
|
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."}
|
|
}
|
|
if err := unmarshalActor(actorResp, actor); err != nil {
|
|
log.Error("Unable to unmarshal actor! %v", err)
|
|
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."}
|
|
}
|
|
} else {
|
|
return nil, nil, err
|
|
}
|
|
} else {
|
|
return nil, nil, err
|
|
}
|
|
} else {
|
|
actor = remoteUser.AsPerson()
|
|
}
|
|
return actor, remoteUser, nil
|
|
}
|
|
|
|
// unmarshal actor normalizes the actor response to conform to
|
|
// the type Person from github.com/writeas/web-core/activitysteams
|
|
//
|
|
// some implementations return different context field types
|
|
// this converts any non-slice contexts into a slice
|
|
func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error {
|
|
// FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string
|
|
|
|
// flexActor overrides the Context field to allow
|
|
// all valid representations during unmarshal
|
|
flexActor := struct {
|
|
activitystreams.Person
|
|
Context json.RawMessage `json:"@context,omitempty"`
|
|
}{}
|
|
if err := json.Unmarshal(actorResp, &flexActor); err != nil {
|
|
return err
|
|
}
|
|
|
|
actor.Endpoints = flexActor.Endpoints
|
|
actor.Followers = flexActor.Followers
|
|
actor.Following = flexActor.Following
|
|
actor.ID = flexActor.ID
|
|
actor.Icon = flexActor.Icon
|
|
actor.Inbox = flexActor.Inbox
|
|
actor.Name = flexActor.Name
|
|
actor.Outbox = flexActor.Outbox
|
|
actor.PreferredUsername = flexActor.PreferredUsername
|
|
actor.PublicKey = flexActor.PublicKey
|
|
actor.Summary = flexActor.Summary
|
|
actor.Type = flexActor.Type
|
|
actor.URL = flexActor.URL
|
|
|
|
func(val interface{}) {
|
|
switch val.(type) {
|
|
case []interface{}:
|
|
// already a slice, do nothing
|
|
actor.Context = val.([]interface{})
|
|
default:
|
|
actor.Context = []interface{}{val}
|
|
}
|
|
}(flexActor.Context)
|
|
|
|
return nil
|
|
}
|