diff --git a/activityserve/actor.go b/activityserve/actor.go index dc5243d..e6740ed 100644 --- a/activityserve/actor.go +++ b/activityserve/actor.go @@ -40,6 +40,7 @@ type Actor struct { publicKeyPem string privateKeyPem string publicKeyID string + OnFollow func(map[string]interface{}) } // ActorToSave is a stripped down actor representation @@ -77,6 +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)} // create actor's keypair rng := rand.Reader @@ -273,19 +277,19 @@ func (a *Actor) newIDurl() string { func (a *Actor) CreateNote(content string) { // for now I will just write this to the outbox - id := a.newIDurl() + hash := a.newIDhash() 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 + "/" + id + create["id"] = baseURL + a.name + "/" + hash 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/" + id + note["id"] = baseURL + a.name + "/note/" + hash note["published"] = time.Now().Format(time.RFC3339) note["url"] = create["id"] note["type"] = "Note" @@ -293,11 +297,11 @@ func (a *Actor) CreateNote(content string) { create["published"] = note["published"] create["type"] = "Create" go a.sendToFollowers(create) - err := a.saveItem(id, create) + err := a.saveItem(hash, create) if err != nil { log.Info("Could not save note to disk") } - err = a.appendToOutbox(id) + err = a.appendToOutbox(baseURL + a.name + "/" + hash) if err != nil { log.Info("Could not append Note to outbox.txt") } @@ -589,3 +593,46 @@ func (a *Actor) followersSlice() []string { } return followersSlice } + +// Accept a follow request +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 + if newFollower == follow["object"] { + log.Info("You can't follow yourself") + return + } + + follower, err := NewRemoteActor(follow["actor"].(string)) + + // check if this user is already following us + if _, ok := a.followers[newFollower]; ok { + log.Info("You're already following us, yay!") + // do nothing, they're already following us + } else { + a.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(follow, "@context") + + accept := make(map[string]interface{}) + + accept["@context"] = "https://www.w3.org/ns/activitystreams" + accept["to"] = follow["actor"] + accept["id"] = a.newIDurl() + accept["actor"] = a.iri + accept["object"] = follow + accept["type"] = "Accept" + + if err != nil { + log.Info("Couldn't retrieve remote actor info, maybe server is down?") + log.Info(err) + } + + go a.signedHTTPPost(accept, follower.inbox) + +} \ No newline at end of file diff --git a/activityserve/http.go b/activityserve/http.go index 8c9141f..8379dd9 100644 --- a/activityserve/http.go +++ b/activityserve/http.go @@ -91,14 +91,22 @@ func Serve() { w.WriteHeader(http.StatusNotFound) return } + postsPerPage := 100 var response []byte + filename := storage + slash + "actors" + slash + actor.name + slash + "outbox.txt" + totalLines, err := lineCounter(filename) + if err != nil { + log.Info("Can't read outbox.txt") + log.Info(err) + return + } if pageStr == "" { //TODO fix total items response = []byte(`{ "@context" : "https://www.w3.org/ns/activitystreams", - "first" : "` + baseURL + actor.name + `/outbox?page=true", + "first" : "` + baseURL + actor.name + `/outbox?page=1", "id" : "` + baseURL + actor.name + `/outbox", - "last" : "` + baseURL + actor.name + `/outbox?min_id=0&page=1", + "last" : "` + baseURL + actor.name + `/outbox?page=` + strconv.Itoa(totalLines/postsPerPage+1) + `", "totalItems" : 10, "type" : "OrderedCollection" }`) @@ -108,8 +116,6 @@ func Serve() { 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") @@ -119,12 +125,7 @@ func Serve() { 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) } @@ -134,15 +135,28 @@ func Serve() { responseMap["partOf"] = baseURL + actor.name + "/outbox" responseMap["type"] = "OrderedCollectionPage" - orderedItems := make(map[string]interface{}) + orderedItems := make([]interface{}, 0, postsPerPage) - for item := range lines { - // read the line - // parse the hash + for _, item := range lines { + // split the line + parts := strings.Split(item, "/") + + // keep the hash + hash := parts[len(parts)-1] // build the filename + filename := storage + slash + "actors" + slash + actor.name + slash + "items" + slash + hash + ".json" // open the file - // unmarshal + activityJSON, err := ioutil.ReadFile(filename) + if err != nil { + log.Error("can't read activity") + log.Info(filename) + return + } + var temp map[string]interface{} + // put it into a map + json.Unmarshal(activityJSON, &temp) // append to orderedItems + orderedItems = append(orderedItems, temp) } responseMap["orderedItems"] = orderedItems @@ -173,51 +187,13 @@ func Serve() { // 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) - + actor.OnFollow(activity) case "Accept": acceptor := activity["actor"].(string) actor, err := LoadActor(mux.Vars(r)["actor"]) // load the actor from disk @@ -226,6 +202,8 @@ func Serve() { return } + // From here down this could be moved to Actor (TBD) + follow := activity["object"].(map[string]interface{}) id := follow["id"].(string) diff --git a/activityserve/util.go b/activityserve/util.go index 28a27c6..3cebb82 100644 --- a/activityserve/util.go +++ b/activityserve/util.go @@ -58,7 +58,7 @@ func context() [1]string { // ReadLines reads specific lines from a file and returns them as // an array of strings func ReadLines(filename string, from, to int) (lines []string, err error) { - lines = make([]string, to-from) + lines = make([]string, 0, to-from) reader, err := os.Open(filename) if err != nil { log.Info("could not read file") diff --git a/main.go b/main.go index 638d0ea..9580a62 100644 --- a/main.go +++ b/main.go @@ -47,6 +47,7 @@ func main() { if *debugFlag == true { log.EnableLevel("info") + log.EnableLevel("error") } activityserve.Setup("config.ini", *debugFlag) @@ -56,8 +57,9 @@ func main() { // actor.Follow("https://cybre.space/users/tzo") // actor.CreateNote("Hello World!") - actor, _ := activityserve.LoadActor("activityserve_test_actor_2") - actor.CreateNote("Hello World, again!") + actor, _ := + activityserve.LoadActor("activityserve_test_actor_2") + actor.CreateNote("I'm building #ActivityPub stuff") activityserve.Serve() }