Add /following endpoint, move followers and following
endpoints to /peers/{follow*}, move posts to /items/{hash}, implement unfollow
This commit is contained in:
parent
f1bfedb5a1
commit
0979f847e9
3 changed files with 158 additions and 46 deletions
10
TODO
10
TODO
|
@ -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?
|
|
@ -40,7 +40,7 @@ type Actor struct {
|
||||||
publicKeyPem string
|
publicKeyPem string
|
||||||
privateKeyPem string
|
privateKeyPem string
|
||||||
publicKeyID string
|
publicKeyID string
|
||||||
OnFollow func(map[string]interface{})
|
OnFollow func(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActorToSave is a stripped down actor representation
|
// ActorToSave is a stripped down actor representation
|
||||||
|
@ -78,9 +78,9 @@ func MakeActor(name, summary, actorType string) (Actor, error) {
|
||||||
followersIRI: followersIRI,
|
followersIRI: followersIRI,
|
||||||
publicKeyID: publicKeyID,
|
publicKeyID: publicKeyID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in a new issue