package activityserve import ( "fmt" "io/ioutil" "net/http" "strconv" "strings" "github.com/gologme/log" "github.com/gorilla/mux" "encoding/json" ) // Serve starts an http server with all the required handlers 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 // 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 responseMap["links"] = links response, err := json.Marshal(responseMap) if err != nil { log.Error("problem creating the webfinger response json") } PrettyPrintJSON(response) 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") log.Info("Remote server " + r.RemoteAddr + " just fetched our /actor endpoint") 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()) // Show some debugging information printer.Info("") body, _ := ioutil.ReadAll(r.Body) PrettyPrintJSON(body) log.Info(FormatHeaders(r.Header)) printer.Info("") } var outboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/activity+json; charset=utf-8") pageStr := r.URL.Query().Get("page") // get the page from the query string as string 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 []byte if pageStr == "" { //TODO fix total items response = []byte(`{ "@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=1", "totalItems" : 10, "type" : "OrderedCollection" }`) } else { page, err := strconv.Atoi(pageStr) // get page number from query string if err != nil { log.Info("Page number not a number, assuming 1") page = 1 } postsPerPage := 100 filename := storage + slash + "actors" + slash + actor.name + slash + "outbox.txt" lines, err := ReadLines(filename, (page-1)*postsPerPage, page*(postsPerPage+1)-1) if err != nil { log.Info("Can't read outbox file") log.Info(err) return } responseMap := make(map[string]interface{}) responseMap["@context"] = context() responseMap["id"] = baseURL + actor.name + "/outbox?page=" + pageStr totalLines, err := lineCounter(filename) if err != nil { log.Info("The file was deleted? It was okay a few lines above. Wtf?") log.Info(err) return } if page*postsPerPage < totalLines { responseMap["next"] = baseURL + actor.name + "/outbox?page=" + strconv.Itoa(page+1) } if page > 1 { responseMap["prev"] = baseURL + actor.name + "/outbox?page=" + strconv.Itoa(page-1) } responseMap["partOf"] = baseURL + actor.name + "/outbox" responseMap["type"] = "OrderedCollectionPage" orderedItems := make(map[string]interface{}) for item := range lines { // read the line // parse the hash // build the filename // open the file // unmarshal // append to orderedItems } responseMap["orderedItems"] = orderedItems response, err = json.Marshal(responseMap) if err != nil { log.Info("can't marshal map to json") log.Info(err) return } } w.Write(response) } 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 } follower, err := NewRemoteActor(activity["actor"].(string)) // 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 { actor.NewFollower(newFollower, follower.inbox) } // 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) 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() 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) } // Add the handlers to a HTTP server gorilla := mux.NewRouter() gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler) gorilla.HandleFunc("/{actor}/followers", followersHandler) 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) http.Handle("/", gorilla) log.Fatal(http.ListenAndServe(":8081", nil)) }