2019-09-04 12:33:32 +03:00
|
|
|
package activityserve
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2019-09-10 11:21:39 +03:00
|
|
|
"io/ioutil"
|
2019-09-04 12:33:32 +03:00
|
|
|
"net/http"
|
2019-09-10 11:21:39 +03:00
|
|
|
"strconv"
|
2019-09-04 12:33:32 +03:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/gologme/log"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
|
|
|
|
"encoding/json"
|
|
|
|
)
|
|
|
|
|
2019-09-10 11:21:39 +03:00
|
|
|
// Serve starts an http server with all the required handlers
|
2019-09-04 12:33:32 +03:00
|
|
|
func Serve() {
|
|
|
|
|
|
|
|
var webfingerHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Set("content-type", "application/jrd+json; charset=utf-8")
|
|
|
|
account := r.URL.Query().Get("resource") // should be something like acct:user@example.com
|
|
|
|
account = strings.Replace(account, "acct:", "", 1) // remove acct:
|
|
|
|
server := strings.Split(baseURL, "://")[1] // remove protocol from baseURL. Should get example.com
|
|
|
|
server = strings.TrimSuffix(server, "/") // remove protocol from baseURL. Should get example.com
|
|
|
|
account = strings.Replace(account, "@"+server, "", 1) // remove server from handle. Should get user
|
|
|
|
actor, err := LoadActor(account)
|
|
|
|
// error out if this actor does not exist
|
|
|
|
if err != nil {
|
|
|
|
log.Info("No such actor")
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
fmt.Fprintf(w, "404 - actor not found")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// response := `{"subject":"acct:` + actor.name + `@` + server + `","aliases":["` + baseURL + actor.name + `","` + baseURL + actor.name + `"],"links":[{"href":"` + baseURL + `","type":"text/html","rel":"https://webfinger.net/rel/profile-page"},{"href":"` + baseURL + actor.name + `","type":"application/activity+json","rel":"self"}]}`
|
|
|
|
|
|
|
|
responseMap := make(map[string]interface{})
|
|
|
|
|
|
|
|
responseMap["subject"] = "acct:" + actor.name + "@" + server
|
2019-09-10 11:21:39 +03:00
|
|
|
// links is a json array with a single element
|
|
|
|
var links [1]map[string]string
|
|
|
|
link1 := make(map[string]string)
|
|
|
|
link1["rel"] = "self"
|
|
|
|
link1["type"] = "application/activity+json"
|
|
|
|
link1["href"] = baseURL + actor.name
|
|
|
|
links[0] = link1
|
2019-09-04 12:33:32 +03:00
|
|
|
responseMap["links"] = links
|
|
|
|
|
|
|
|
response, err := json.Marshal(responseMap)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("problem creating the webfinger response json")
|
|
|
|
}
|
2019-09-10 11:21:39 +03:00
|
|
|
PrettyPrintJSON(response)
|
2019-09-04 12:33:32 +03:00
|
|
|
w.Write([]byte(response))
|
|
|
|
}
|
|
|
|
|
|
|
|
var actorHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Set("content-type", "application/activity+json; charset=utf-8")
|
2019-09-10 11:21:39 +03:00
|
|
|
log.Info("Remote server " + r.RemoteAddr + " just fetched our /actor endpoint")
|
2019-09-04 12:33:32 +03:00
|
|
|
username := mux.Vars(r)["actor"]
|
|
|
|
log.Info(username)
|
|
|
|
if username == ".well-known" || username == "favicon.ico" {
|
|
|
|
log.Info("well-known, skipping...")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
actor, err := LoadActor(username)
|
|
|
|
// error out if this actor does not exist (or there are dots or slashes in his name)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
fmt.Fprintf(w, "404 - page not found")
|
|
|
|
log.Info("Can't create local actor")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
fmt.Fprintf(w, actor.whoAmI())
|
2019-09-10 11:21:39 +03:00
|
|
|
|
|
|
|
// Show some debugging information
|
|
|
|
printer.Info("")
|
|
|
|
body, _ := ioutil.ReadAll(r.Body)
|
|
|
|
PrettyPrintJSON(body)
|
|
|
|
log.Info(FormatHeaders(r.Header))
|
|
|
|
printer.Info("")
|
2019-09-04 12:33:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
var outboxHandler 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"] // get the needed actor from the muxer (url variable {actor} below)
|
|
|
|
actor, err := LoadActor(username) // load the actor from disk
|
|
|
|
if err != nil { // either actor requested has illegal characters or
|
|
|
|
log.Info("Can't load local actor") // we don't have such actor
|
|
|
|
fmt.Fprintf(w, "404 - page not found")
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var response string
|
|
|
|
if r.URL.Query().Get("page") == "" {
|
|
|
|
//TODO fix total items
|
|
|
|
response = `{
|
|
|
|
"@context" : "https://www.w3.org/ns/activitystreams",
|
|
|
|
"first" : "` + baseURL + actor.name + `/outbox?page=true",
|
|
|
|
"id" : "` + baseURL + actor.name + `/outbox",
|
|
|
|
"last" : "` + baseURL + actor.name + `/outbox?min_id=0&page=true",
|
|
|
|
"totalItems" : 10,
|
|
|
|
"type" : "OrderedCollection"
|
|
|
|
}`
|
|
|
|
} else {
|
|
|
|
content := "Hello, World!"
|
|
|
|
id := "asfdasdf"
|
|
|
|
response = `
|
|
|
|
{
|
|
|
|
"@context" : "https://www.w3.org/ns/activitystreams",
|
|
|
|
"id" : "` + baseURL + actor.name + `/outbox?min_id=0&page=true",
|
|
|
|
"next" : "` + baseURL + actor.name + `/outbox?max_id=99524642494530460&page=true",
|
|
|
|
"orderedItems" :[
|
|
|
|
{
|
|
|
|
"actor" : "https://` + baseURL + actor.name + `",
|
|
|
|
"cc" : [
|
|
|
|
"https://` + baseURL + actor.name + `/followers"
|
|
|
|
],
|
|
|
|
"id" : "https://` + baseURL + actor.name + `/` + id + `",
|
|
|
|
"object" : {
|
|
|
|
"attributedTo" : "https://` + baseURL + actor.name + `",
|
|
|
|
"cc" : [
|
|
|
|
"https://` + baseURL + actor.name + `/followers"
|
|
|
|
],
|
|
|
|
"content" : "` + content + `",
|
|
|
|
"id" : "https://` + baseURL + actor.name + `/` + id + `",
|
|
|
|
"inReplyTo" : null,
|
|
|
|
"published" : "2019-08-26T16:25:26Z",
|
|
|
|
"to" : [
|
|
|
|
"https://www.w3.org/ns/activitystreams#Public"
|
|
|
|
],
|
|
|
|
"type" : "Note",
|
|
|
|
"url" : "https://` + baseURL + actor.name + `/` + id + `"
|
|
|
|
},
|
|
|
|
"published" : "2019-08-26T16:25:26Z",
|
|
|
|
"to" : [
|
|
|
|
"https://www.w3.org/ns/activitystreams#Public"
|
|
|
|
],
|
|
|
|
"type" : "Create"
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"partOf" : "` + baseURL + actor.name + `/outbox",
|
|
|
|
"prev" : "` + baseURL + actor.name + `/outbox?min_id=99982453036184436&page=true",
|
|
|
|
"type" : "OrderedCollectionPage"
|
|
|
|
}`
|
|
|
|
}
|
|
|
|
w.Write([]byte(response))
|
|
|
|
}
|
|
|
|
|
2019-09-10 11:21:39 +03:00
|
|
|
var inboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
b, err := ioutil.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
activity := make(map[string]interface{})
|
|
|
|
err = json.Unmarshal(b, &activity)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Probably this request didn't have (valid) JSON inside it")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// TODO check if it's actually an activity
|
|
|
|
|
|
|
|
// check if case is going to be an issue
|
|
|
|
switch activity["type"] {
|
|
|
|
case "Follow":
|
|
|
|
// it's a follow, write it down
|
|
|
|
newFollower := activity["actor"].(string)
|
|
|
|
// check we aren't following ourselves
|
|
|
|
if newFollower == activity["object"] {
|
|
|
|
log.Info("You can't follow yourself")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// load the object as actor
|
|
|
|
actor, err := LoadActor(mux.Vars(r)["actor"]) // load the actor from disk
|
|
|
|
if err != nil {
|
|
|
|
log.Error("No such actor")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-11 12:21:38 +03:00
|
|
|
follower, err := NewRemoteActor(activity["actor"].(string))
|
|
|
|
|
2019-09-10 11:21:39 +03:00
|
|
|
// check if this user is already following us
|
|
|
|
if _, ok := actor.followers[newFollower]; ok {
|
|
|
|
log.Info("You're already following us, yay!")
|
|
|
|
// do nothing, they're already following us
|
|
|
|
} else {
|
2019-09-11 12:21:38 +03:00
|
|
|
actor.NewFollower(newFollower, follower.inbox)
|
2019-09-10 11:21:39 +03:00
|
|
|
}
|
|
|
|
// send accept anyway even if they are following us already
|
|
|
|
// this is very verbose. I would prefer creating a map by hand
|
|
|
|
|
|
|
|
// remove @context from the inner activity
|
|
|
|
delete(activity, "@context")
|
|
|
|
|
|
|
|
accept := make(map[string]interface{})
|
|
|
|
|
|
|
|
accept["@context"] = "https://www.w3.org/ns/activitystreams"
|
|
|
|
accept["to"] = activity["actor"]
|
|
|
|
accept["id"] = actor.newIDurl()
|
|
|
|
accept["actor"] = actor.iri
|
|
|
|
accept["object"] = activity
|
|
|
|
accept["type"] = "Accept"
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Info("Couldn't retrieve remote actor info, maybe server is down?")
|
|
|
|
log.Info(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
go actor.signedHTTPPost(accept, follower.inbox)
|
|
|
|
|
2019-09-11 12:21:38 +03:00
|
|
|
case "Accept":
|
|
|
|
acceptor := activity["actor"].(string)
|
|
|
|
actor, err := LoadActor(mux.Vars(r)["actor"]) // load the actor from disk
|
|
|
|
if err != nil {
|
|
|
|
log.Error("No such actor")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
follow := activity["object"].(map[string]interface{})
|
|
|
|
id := follow["id"].(string)
|
|
|
|
|
|
|
|
// check if the object of the follow is us
|
|
|
|
if follow["actor"].(string) != baseURL+actor.name {
|
|
|
|
log.Info("This is not for us, ignoring")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// try to get the hash only
|
|
|
|
hash := strings.Replace(id, baseURL+actor.name+"/", "", 1)
|
|
|
|
// if there are still slashes in the result this means the
|
|
|
|
// above didn't work
|
|
|
|
if strings.ContainsAny(hash, "/") {
|
|
|
|
log.Info("The id of this follow is probably wrong")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Have we already requested this follow or are we following anybody that
|
|
|
|
// sprays accepts?
|
|
|
|
savedFollowRequest, err := actor.loadItem(hash)
|
|
|
|
if err != nil {
|
|
|
|
log.Info("We never requested this follow, ignoring the Accept")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if savedFollowRequest["id"] != id {
|
|
|
|
log.Info("Id mismatch between Follow request and Accept")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
actor.following[acceptor] = hash
|
|
|
|
actor.save()
|
2019-09-10 11:21:39 +03:00
|
|
|
default:
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
var followersHandler 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"]
|
|
|
|
actor, err := LoadActor(username)
|
|
|
|
// error out if this actor does not exist
|
|
|
|
if err != nil {
|
|
|
|
log.Info("Can't create local actor")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var page int
|
|
|
|
pageS := r.URL.Query().Get("page")
|
|
|
|
if pageS == "" {
|
|
|
|
page = 0
|
|
|
|
} else {
|
|
|
|
page, _ = strconv.Atoi(pageS)
|
|
|
|
}
|
|
|
|
response, _ := actor.GetFollowers(page)
|
|
|
|
w.Write(response)
|
|
|
|
}
|
|
|
|
|
2019-09-04 12:33:32 +03:00
|
|
|
// Add the handlers to a HTTP server
|
|
|
|
gorilla := mux.NewRouter()
|
|
|
|
gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler)
|
2019-09-10 11:21:39 +03:00
|
|
|
gorilla.HandleFunc("/{actor}/followers", followersHandler)
|
2019-09-04 12:33:32 +03:00
|
|
|
gorilla.HandleFunc("/{actor}/outbox", outboxHandler)
|
|
|
|
gorilla.HandleFunc("/{actor}/outbox/", outboxHandler)
|
2019-09-10 11:21:39 +03:00
|
|
|
gorilla.HandleFunc("/{actor}/inbox", inboxHandler)
|
|
|
|
gorilla.HandleFunc("/{actor}/inbox/", inboxHandler)
|
2019-09-04 12:33:32 +03:00
|
|
|
gorilla.HandleFunc("/{actor}/", actorHandler)
|
|
|
|
gorilla.HandleFunc("/{actor}", actorHandler)
|
|
|
|
// gorilla.HandleFunc("/{actor}/post/{hash}", postHandler)
|
|
|
|
http.Handle("/", gorilla)
|
|
|
|
|
|
|
|
log.Fatal(http.ListenAndServe(":8081", nil))
|
|
|
|
}
|