Add /following endpoint, move followers and following

endpoints to /peers/{follow*}, move posts to /items/{hash},
implement unfollow
pull/15/head
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
[ ] Announcements
[] Announcements
[✔] Federate the post to our followers (hardcoded for now)
[✔] Handle more than one local actors
[✔] Handle the /actor endpoint
[✔] Create configuration file
[ ] Implement database backend
[] Implement database backend
[✔] Create a file with the actors we have, their following
and their followers.
[✔] `MakeActor` should create a file with that actor.
@ -12,7 +12,7 @@
[✔] `actor.Follow` should write the new following to file
[✔] Handle being followed
[✔] 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 all the announcements (boosts) to the database to
their correct actors
@ -33,7 +33,7 @@
(waiting to actually get an accept so that I can test this)
[✔] Implement webfinger
[✔] Make sure masto finds signature
[ ] Implement Unfollow
[] Implement Unfollow
[✔] Implement accept (accept when other follow us)
(done but can't test it pending http signatures)
Works in pleroma/pixelfed not working on masto
@ -50,5 +50,5 @@
[ ] Remove them?
[ ] Leave them read-only?
[ ] Leave them as is?
[ ] Handle followers and following uri's
[] Handle followers and following uri's
[ ] Do I care about the inbox?

View File

@ -40,7 +40,7 @@ type Actor struct {
publicKeyPem string
privateKeyPem string
publicKeyID string
OnFollow func(map[string]interface{})
OnFollow func(map[string]interface{})
}
// ActorToSave is a stripped down actor representation
@ -78,9 +78,9 @@ func MakeActor(name, summary, actorType string) (Actor, error) {
followersIRI: followersIRI,
publicKeyID: publicKeyID,
}
// 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
rng := rand.Reader
@ -252,9 +252,8 @@ func (a *Actor) whoAmI() string {
"summary": "` + a.summary + `",
"inbox": "` + baseURL + a.name + `/inbox",
"outbox": "` + baseURL + a.name + `/outbox",
"followers": "` + baseURL + a.name + `/followers",
"following": "` + baseURL + a.name + `/following",
"liked": "` + baseURL + a.name + `/liked",
"followers": "` + baseURL + a.name + `/peers/followers",
"following": "` + baseURL + a.name + `/peers/following",
"publicKey": {
"id": "` + baseURL + a.name + `#main-key",
"owner": "` + baseURL + a.name + `",
@ -263,33 +262,39 @@ func (a *Actor) whoAmI() string {
}`
}
func (a *Actor) newIDhash() string {
return uniuri.New()
}
func (a *Actor) newIDurl() string {
return baseURL + a.name + "/" + a.newIDhash()
func (a *Actor) newID() (hash string, url string) {
hash = uniuri.New()
return hash, baseURL + a.name + "/item/" + hash
}
// TODO Reply(content string, inReplyTo string)
// CreateNote posts an activityPub note to our followers
func (a *Actor) CreateNote(content string) {
// for now I will just write this to the outbox
// ReplyNote sends a note to a specific actor in reply to
// a post
//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{})
note := make(map[string]interface{})
create["@context"] = context()
create["actor"] = baseURL + a.name
create["cc"] = a.followersIRI
create["id"] = baseURL + a.name + "/" + hash
create["id"] = id
create["object"] = note
note["attributedTo"] = baseURL + a.name
note["cc"] = a.followersIRI
note["content"] = content
// note["inReplyTo"] = "https://cybre.space/@qwazix/102688373602724023"
note["id"] = baseURL + a.name + "/note/" + hash
if inReplyTo != "" {
note["inReplyTo"] = inReplyTo
}
note["id"] = id
note["published"] = time.Now().Format(time.RFC3339)
note["url"] = create["id"]
note["type"] = "Note"
@ -301,7 +306,7 @@ func (a *Actor) CreateNote(content string) {
if err != nil {
log.Info("Could not save note to disk")
}
err = a.appendToOutbox(baseURL + a.name + "/" + hash)
err = a.appendToOutbox(id)
if err != nil {
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())
}
// GetFollowers returns a list of people that follow us
func (a *Actor) GetFollowers(page int) (response []byte, err error) {
// getPeers gets followers or following depending on `who`
func (a *Actor) getPeers(page int, who string) (response []byte, err error) {
// if there's no page parameter mastodon displays an
// OrderedCollection with info of where to find orderedCollectionPages
// 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["@context"] = "https://www.w3.org/ns/activitystreams"
if page == 0 {
themap["first"] = baseURL + a.name + "/followers?page=1"
themap["id"] = baseURL + a.name + "/followers"
themap["totalItems"] = strconv.Itoa(len(a.followers))
themap["first"] = baseURL + a.name + "/" + who + "?page=1"
themap["id"] = baseURL + a.name + "/" + who
themap["totalItems"] = strconv.Itoa(len(collection))
themap["type"] = "OrderedCollection"
} else if page == 1 { // implement pagination
themap["id"] = baseURL + a.name + "followers?page=" + strconv.Itoa(page)
items := make([]string, 0, len(a.followers))
for k := range a.followers {
themap["id"] = baseURL + a.name + who + "?page=" + strconv.Itoa(page)
items := make([]string, 0, len(collection))
for k := range collection {
items = append(items, k)
}
themap["orderedItems"] = items
themap["partOf"] = baseURL + a.name + "/followers"
themap["totalItems"] = len(a.followers)
themap["partOf"] = baseURL + a.name + "/" + who
themap["totalItems"] = len(collection)
themap["type"] = "OrderedCollectionPage"
}
response, _ = json.Marshal(themap)
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) {
b, err := json.Marshal(content)
if err != nil {
@ -531,11 +555,11 @@ func (a *Actor) Follow(user string) (err error) {
}
follow := make(map[string]interface{})
id := a.newIDhash()
_, id := a.newID()
follow["@context"] = context()
follow["actor"] = a.iri
follow["id"] = baseURL + a.name + "/" + id
follow["id"] = id
follow["object"] = user
follow["type"] = "Follow"
@ -561,13 +585,69 @@ func (a *Actor) Follow(user string) (err error) {
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
func (a *Actor) Announce(url string) {
// our announcements are public. Public stuff have a "To" to the url below
toURL := "https://www.w3.org/ns/activitystreams#Public"
id, hash := a.newID()
announce := make(map[string]interface{})
announce["@context"] = context()
announce["id"] = id
announce["object"] = url
announce["actor"] = a.name
announce["to"] = toURL
@ -583,6 +663,8 @@ func (a *Actor) Announce(url string) {
// add a timestamp
announce["published"] = time.Now().Format(time.RFC3339)
a.appendToOutbox(announce["id"].(string))
a.saveItem(hash, announce)
a.sendToFollowers(announce)
}
@ -595,7 +677,7 @@ func (a *Actor) followersSlice() []string {
}
// 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
newFollower := follow["actor"].(string)
// 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["to"] = follow["actor"]
accept["id"] = a.newIDurl()
accept["id"], _ = a.newID()
accept["actor"] = a.iri
accept["object"] = follow
accept["type"] = "Accept"
@ -633,6 +715,7 @@ func (a *Actor) Accept(follow map[string]interface{}){
log.Info(err)
}
// Maybe we need to save this accept?
go a.signedHTTPPost(accept, follower.inbox)
}
}

View File

@ -237,12 +237,17 @@ func Serve() {
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")
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)
// error out if this actor does not exist
if err != nil {
@ -256,21 +261,45 @@ func Serve() {
} else {
page, _ = strconv.Atoi(pageS)
}
response, _ := actor.GetFollowers(page)
response, _ := actor.getPeers(page, collection)
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
gorilla := mux.NewRouter()
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}/inbox", inboxHandler)
gorilla.HandleFunc("/{actor}/inbox/", inboxHandler)
gorilla.HandleFunc("/{actor}/", actorHandler)
gorilla.HandleFunc("/{actor}", actorHandler)
// gorilla.HandleFunc("/{actor}/post/{hash}", postHandler)
gorilla.HandleFunc("/{actor}/item/{hash}", postHandler)
http.Handle("/", gorilla)
log.Fatal(http.ListenAndServe(":8081", nil))