Add /following endpoint, move followers and following

endpoints to /peers/{follow*}, move posts to /items/{hash},
implement unfollow
This commit is contained in:
Michael Demetriou 2019-09-14 11:12:15 +03:00
parent f1bfedb5a1
commit 0979f847e9
3 changed files with 158 additions and 46 deletions

10
TODO
View file

@ -1,10 +1,10 @@
[✔] Follow users [✔] Follow users
[ ] Announcements [] Announcements
[✔] Federate the post to our followers (hardcoded for now) [✔] Federate the post to our followers (hardcoded for now)
[✔] Handle more than one local actors [✔] Handle more than one local actors
[✔] Handle the /actor endpoint [✔] Handle the /actor endpoint
[✔] Create configuration file [✔] Create configuration file
[ ] Implement database backend [] Implement database backend
[✔] Create a file with the actors we have, their following [✔] Create a file with the actors we have, their following
and their followers. and their followers.
[✔] `MakeActor` should create a file with that actor. [✔] `MakeActor` should create a file with that actor.
@ -12,7 +12,7 @@
[✔] `actor.Follow` should write the new following to file [✔] `actor.Follow` should write the new following to file
[✔] Handle being followed [✔] Handle being followed
[✔] When followed, the handler should write the new follower to file [✔] When followed, the handler should write the new follower to file
[ ] Make sure we send our boosts to all our followers [] Make sure we send our boosts to all our followers
[ ] Write incoming activities to disk (do we have to?) [ ] Write incoming activities to disk (do we have to?)
[ ] Write all the announcements (boosts) to the database to [ ] Write all the announcements (boosts) to the database to
their correct actors their correct actors
@ -33,7 +33,7 @@
(waiting to actually get an accept so that I can test this) (waiting to actually get an accept so that I can test this)
[✔] Implement webfinger [✔] Implement webfinger
[✔] Make sure masto finds signature [✔] Make sure masto finds signature
[ ] Implement Unfollow [] Implement Unfollow
[✔] Implement accept (accept when other follow us) [✔] Implement accept (accept when other follow us)
(done but can't test it pending http signatures) (done but can't test it pending http signatures)
Works in pleroma/pixelfed not working on masto Works in pleroma/pixelfed not working on masto
@ -50,5 +50,5 @@
[ ] Remove them? [ ] Remove them?
[ ] Leave them read-only? [ ] Leave them read-only?
[ ] Leave them as is? [ ] Leave them as is?
[ ] Handle followers and following uri's [] Handle followers and following uri's
[ ] Do I care about the inbox? [ ] Do I care about the inbox?

View file

@ -80,7 +80,7 @@ func MakeActor(name, summary, actorType string) (Actor, error) {
} }
// set auto accept by default (this could be a configuration value) // set auto accept by default (this could be a configuration value)
actor.OnFollow = func(activity map[string]interface{}) {actor.Accept(activity)} actor.OnFollow = func(activity map[string]interface{}) { actor.Accept(activity) }
// create actor's keypair // create actor's keypair
rng := rand.Reader rng := rand.Reader
@ -252,9 +252,8 @@ func (a *Actor) whoAmI() string {
"summary": "` + a.summary + `", "summary": "` + a.summary + `",
"inbox": "` + baseURL + a.name + `/inbox", "inbox": "` + baseURL + a.name + `/inbox",
"outbox": "` + baseURL + a.name + `/outbox", "outbox": "` + baseURL + a.name + `/outbox",
"followers": "` + baseURL + a.name + `/followers", "followers": "` + baseURL + a.name + `/peers/followers",
"following": "` + baseURL + a.name + `/following", "following": "` + baseURL + a.name + `/peers/following",
"liked": "` + baseURL + a.name + `/liked",
"publicKey": { "publicKey": {
"id": "` + baseURL + a.name + `#main-key", "id": "` + baseURL + a.name + `#main-key",
"owner": "` + baseURL + a.name + `", "owner": "` + baseURL + a.name + `",
@ -263,33 +262,39 @@ func (a *Actor) whoAmI() string {
}` }`
} }
func (a *Actor) newIDhash() string { func (a *Actor) newID() (hash string, url string) {
return uniuri.New() hash = uniuri.New()
} return hash, baseURL + a.name + "/item/" + hash
func (a *Actor) newIDurl() string {
return baseURL + a.name + "/" + a.newIDhash()
} }
// TODO Reply(content string, inReplyTo string) // TODO Reply(content string, inReplyTo string)
// CreateNote posts an activityPub note to our followers // ReplyNote sends a note to a specific actor in reply to
func (a *Actor) CreateNote(content string) { // a post
// for now I will just write this to the outbox //TODO
hash := a.newIDhash() // DM sends a direct message to a user
// TODO
// CreateNote posts an activityPub note to our followers
//
func (a *Actor) CreateNote(content, inReplyTo string) {
// for now I will just write this to the outbox
hash, id := a.newID()
create := make(map[string]interface{}) create := make(map[string]interface{})
note := make(map[string]interface{}) note := make(map[string]interface{})
create["@context"] = context() create["@context"] = context()
create["actor"] = baseURL + a.name create["actor"] = baseURL + a.name
create["cc"] = a.followersIRI create["cc"] = a.followersIRI
create["id"] = baseURL + a.name + "/" + hash create["id"] = id
create["object"] = note create["object"] = note
note["attributedTo"] = baseURL + a.name note["attributedTo"] = baseURL + a.name
note["cc"] = a.followersIRI note["cc"] = a.followersIRI
note["content"] = content note["content"] = content
// note["inReplyTo"] = "https://cybre.space/@qwazix/102688373602724023" if inReplyTo != "" {
note["id"] = baseURL + a.name + "/note/" + hash note["inReplyTo"] = inReplyTo
}
note["id"] = id
note["published"] = time.Now().Format(time.RFC3339) note["published"] = time.Now().Format(time.RFC3339)
note["url"] = create["id"] note["url"] = create["id"]
note["type"] = "Note" note["type"] = "Note"
@ -301,7 +306,7 @@ func (a *Actor) CreateNote(content string) {
if err != nil { if err != nil {
log.Info("Could not save note to disk") log.Info("Could not save note to disk")
} }
err = a.appendToOutbox(baseURL + a.name + "/" + hash) err = a.appendToOutbox(id)
if err != nil { if err != nil {
log.Info("Could not append Note to outbox.txt") log.Info("Could not append Note to outbox.txt")
} }
@ -345,33 +350,52 @@ func (a *Actor) send(content map[string]interface{}, to *url.URL) (err error) {
return a.signedHTTPPost(content, to.String()) return a.signedHTTPPost(content, to.String())
} }
// GetFollowers returns a list of people that follow us // getPeers gets followers or following depending on `who`
func (a *Actor) GetFollowers(page int) (response []byte, err error) { func (a *Actor) getPeers(page int, who string) (response []byte, err error) {
// if there's no page parameter mastodon displays an // if there's no page parameter mastodon displays an
// OrderedCollection with info of where to find orderedCollectionPages // OrderedCollection with info of where to find orderedCollectionPages
// with the actual information. We are mirroring that behavior // with the actual information. We are mirroring that behavior
var collection map[string]interface{}
if who == "followers" {
collection = a.followers
} else if who == "following" {
collection = a.following
} else {
return nil, errors.New("cannot find collection" + who)
}
themap := make(map[string]interface{}) themap := make(map[string]interface{})
themap["@context"] = "https://www.w3.org/ns/activitystreams" themap["@context"] = "https://www.w3.org/ns/activitystreams"
if page == 0 { if page == 0 {
themap["first"] = baseURL + a.name + "/followers?page=1" themap["first"] = baseURL + a.name + "/" + who + "?page=1"
themap["id"] = baseURL + a.name + "/followers" themap["id"] = baseURL + a.name + "/" + who
themap["totalItems"] = strconv.Itoa(len(a.followers)) themap["totalItems"] = strconv.Itoa(len(collection))
themap["type"] = "OrderedCollection" themap["type"] = "OrderedCollection"
} else if page == 1 { // implement pagination } else if page == 1 { // implement pagination
themap["id"] = baseURL + a.name + "followers?page=" + strconv.Itoa(page) themap["id"] = baseURL + a.name + who + "?page=" + strconv.Itoa(page)
items := make([]string, 0, len(a.followers)) items := make([]string, 0, len(collection))
for k := range a.followers { for k := range collection {
items = append(items, k) items = append(items, k)
} }
themap["orderedItems"] = items themap["orderedItems"] = items
themap["partOf"] = baseURL + a.name + "/followers" themap["partOf"] = baseURL + a.name + "/" + who
themap["totalItems"] = len(a.followers) themap["totalItems"] = len(collection)
themap["type"] = "OrderedCollectionPage" themap["type"] = "OrderedCollectionPage"
} }
response, _ = json.Marshal(themap) response, _ = json.Marshal(themap)
return return
} }
// GetFollowers returns a list of people that follow us
func (a *Actor) GetFollowers(page int) (response []byte, err error) {
return a.getPeers(page, "followers")
}
// GetFollowing returns a list of people that we follow
func (a *Actor) GetFollowing(page int) (response []byte, err error) {
return a.getPeers(page, "following")
}
func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err error) { func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err error) {
b, err := json.Marshal(content) b, err := json.Marshal(content)
if err != nil { if err != nil {
@ -531,11 +555,11 @@ func (a *Actor) Follow(user string) (err error) {
} }
follow := make(map[string]interface{}) follow := make(map[string]interface{})
id := a.newIDhash() _, id := a.newID()
follow["@context"] = context() follow["@context"] = context()
follow["actor"] = a.iri follow["actor"] = a.iri
follow["id"] = baseURL + a.name + "/" + id follow["id"] = id
follow["object"] = user follow["object"] = user
follow["type"] = "Follow" follow["type"] = "Follow"
@ -561,13 +585,69 @@ func (a *Actor) Follow(user string) (err error) {
return nil return nil
} }
// Unfollow the user declared by the iri in `user`
// this recreates the original follow activity
// , wraps it in an Undo activity, sets it's
// id to the id of the original Follow activity that
// was accepted when initially following that user
// (this is read from the `actor.following` map
func (a *Actor) Unfollow(user string){
log.Info("Unfollowing " + user)
// create an undo activiy
undo := make(map[string]interface{})
undo["@context"] = context()
undo["actor"] = a.iri
// find the id of the original follow
hash := a.following[user].(string)
follow := make(map[string]interface{})
follow["@context"] = context()
follow["actor"] = a.iri
follow["id"] = baseURL + "/item/" + hash
follow["object"] = user
follow["type"] = "Follow"
// add the properties to the undo activity
undo["object"] = follow
// get the remote user's inbox
remoteUser, err := NewRemoteActor(user)
if err != nil {
log.Info("Failed to contact remote actor")
return
}
// only if we're already following them
if _, ok := a.following[user]; ok {
PrettyPrint(undo)
go func() {
err := a.signedHTTPPost(remoteUser.inbox, undo)
if err != nil {
log.Info("Couldn't unfollow " + user)
log.Info(err)
return
}
// if there was no error then delete the follow
// from the list
delete(a.following, user)
a.save()
}()
}
}
// Announce this activity to our followers // Announce this activity to our followers
func (a *Actor) Announce(url string) { func (a *Actor) Announce(url string) {
// our announcements are public. Public stuff have a "To" to the url below // our announcements are public. Public stuff have a "To" to the url below
toURL := "https://www.w3.org/ns/activitystreams#Public" toURL := "https://www.w3.org/ns/activitystreams#Public"
id, hash := a.newID()
announce := make(map[string]interface{}) announce := make(map[string]interface{})
announce["@context"] = context() announce["@context"] = context()
announce["id"] = id
announce["object"] = url announce["object"] = url
announce["actor"] = a.name announce["actor"] = a.name
announce["to"] = toURL announce["to"] = toURL
@ -583,6 +663,8 @@ func (a *Actor) Announce(url string) {
// add a timestamp // add a timestamp
announce["published"] = time.Now().Format(time.RFC3339) announce["published"] = time.Now().Format(time.RFC3339)
a.appendToOutbox(announce["id"].(string))
a.saveItem(hash, announce)
a.sendToFollowers(announce) a.sendToFollowers(announce)
} }
@ -595,7 +677,7 @@ func (a *Actor) followersSlice() []string {
} }
// Accept a follow request // Accept a follow request
func (a *Actor) Accept(follow map[string]interface{}){ func (a *Actor) Accept(follow map[string]interface{}) {
// it's a follow, write it down // it's a follow, write it down
newFollower := follow["actor"].(string) newFollower := follow["actor"].(string)
// check we aren't following ourselves // check we aren't following ourselves
@ -623,7 +705,7 @@ func (a *Actor) Accept(follow map[string]interface{}){
accept["@context"] = "https://www.w3.org/ns/activitystreams" accept["@context"] = "https://www.w3.org/ns/activitystreams"
accept["to"] = follow["actor"] accept["to"] = follow["actor"]
accept["id"] = a.newIDurl() accept["id"], _ = a.newID()
accept["actor"] = a.iri accept["actor"] = a.iri
accept["object"] = follow accept["object"] = follow
accept["type"] = "Accept" accept["type"] = "Accept"
@ -633,6 +715,7 @@ func (a *Actor) Accept(follow map[string]interface{}){
log.Info(err) log.Info(err)
} }
// Maybe we need to save this accept?
go a.signedHTTPPost(accept, follower.inbox) go a.signedHTTPPost(accept, follower.inbox)
} }

View file

@ -237,12 +237,17 @@ func Serve() {
default: default:
} }
} }
var followersHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { var peersHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/activity+json; charset=utf-8") w.Header().Set("content-type", "application/activity+json; charset=utf-8")
username := mux.Vars(r)["actor"] username := mux.Vars(r)["actor"]
collection := mux.Vars(r)["peers"]
if collection != "followers" && collection != "following" {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 - No such collection"))
return
}
actor, err := LoadActor(username) actor, err := LoadActor(username)
// error out if this actor does not exist // error out if this actor does not exist
if err != nil { if err != nil {
@ -256,21 +261,45 @@ func Serve() {
} else { } else {
page, _ = strconv.Atoi(pageS) page, _ = strconv.Atoi(pageS)
} }
response, _ := actor.GetFollowers(page) response, _ := actor.getPeers(page, collection)
w.Write(response) w.Write(response)
} }
var postHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/activity+json; charset=utf-8")
username := mux.Vars(r)["actor"]
hash := mux.Vars(r)["hash"]
actor, err := LoadActor(username)
// error out if this actor does not exist
if err != nil {
log.Info("Can't create local actor")
return
}
post, err := actor.loadItem(hash)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "404 - post not found")
return
}
postJSON, err := json.Marshal(post)
if err!= nil{
log.Info("failed to marshal json from item " + hash + " text")
return
}
w.Write(postJSON)
}
// Add the handlers to a HTTP server // Add the handlers to a HTTP server
gorilla := mux.NewRouter() gorilla := mux.NewRouter()
gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler) gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler)
gorilla.HandleFunc("/{actor}/followers", followersHandler) gorilla.HandleFunc("/{actor}/peers/{peers}", peersHandler)
gorilla.HandleFunc("/{actor}/outbox", outboxHandler) gorilla.HandleFunc("/{actor}/outbox", outboxHandler)
gorilla.HandleFunc("/{actor}/outbox/", outboxHandler) gorilla.HandleFunc("/{actor}/outbox/", outboxHandler)
gorilla.HandleFunc("/{actor}/inbox", inboxHandler) gorilla.HandleFunc("/{actor}/inbox", inboxHandler)
gorilla.HandleFunc("/{actor}/inbox/", inboxHandler) gorilla.HandleFunc("/{actor}/inbox/", inboxHandler)
gorilla.HandleFunc("/{actor}/", actorHandler) gorilla.HandleFunc("/{actor}/", actorHandler)
gorilla.HandleFunc("/{actor}", actorHandler) gorilla.HandleFunc("/{actor}", actorHandler)
// gorilla.HandleFunc("/{actor}/post/{hash}", postHandler) gorilla.HandleFunc("/{actor}/item/{hash}", postHandler)
http.Handle("/", gorilla) http.Handle("/", gorilla)
log.Fatal(http.ListenAndServe(":8081", nil)) log.Fatal(http.ListenAndServe(":8081", nil))