diff --git a/TODO b/TODO index 34fb93e..dff835e 100644 --- a/TODO +++ b/TODO @@ -13,7 +13,7 @@ [✔] Handle being followed [✔] When followed, the handler should write the new follower to file [✔] Make sure we send our boosts to all our followers -[ ] Write incoming activities to disk (do we have to?) +[x] Write incoming activities to disk (do we have to?) [✔] Write all the announcements (boosts) to the database to their correct actors [✔] Check if we are already following users @@ -22,12 +22,13 @@ [✔] Make OS-independent (mosty directory separators) [✔] Create outbox.json programmatically [✔] Make storage configurable (search for "storage" in project) -[ ] Check if we're boosting only stuff from actors we follow, not whatever comes +[✔] Check if we're boosting only stuff from actors we follow, not whatever comes through in our inbox [✔] Boost not only articles but other things too [✔] Sanitize input, never allow slashes or dots [✔] Add summary to actors.json -[ ] Check local actor names for characters illegal for filenames and ban them +[✔] Check local actor names for characters illegal for filenames and ban them + (Done in pherephone, not activityserve) [✔] Create debug flag [✔] Write to following only upon accept (waiting to actually get an accept so that I can test this) @@ -41,16 +42,16 @@ [ ] Implement nodeinfo and statistics [✔] Accept even if already follows us [✔] Handle paging -[ ] Test paging +[✔] Test paging [✔] Handle http signatures [ ] Verify http signatures -[ ] Refactor, comment and clean up -[ ] Split to pherephone and activityServe +[✔] Refactor, comment and clean up +[✔] Split to pherephone and activityServe [ ] Decide what's to be done with actors removed from `actors.json`. [ ] Remove them? [ ] Leave them read-only? - [ ] Leave them as is? + [✔] Leave them as is? [✔] Handle followers and following uri's [ ] Do I care about the inbox? -[ ] Expose configuration to apps -[ ] Do not boost replies (configurable) \ No newline at end of file +[✔] Expose configuration to apps +[✔] Do not boost replies (configurable) \ No newline at end of file diff --git a/actor.go b/actor.go index 7026215..52c6b8a 100644 --- a/actor.go +++ b/actor.go @@ -54,8 +54,8 @@ type ActorToSave struct { Followers, Following, Rejected, Requested map[string]interface{} } -// MakeActor returns a new local actor we can act -// on behalf of +// MakeActor creates and returns a new local actor we can act +// on behalf of. It also creates its files on disk func MakeActor(name, summary, actorType string) (Actor, error) { followers := make(map[string]interface{}) following := make(map[string]interface{}) @@ -125,7 +125,7 @@ func MakeActor(name, summary, actorType string) (Actor, error) { return actor, nil } -// GetOutboxIRI returns the outbox iri in net/url +// GetOutboxIRI returns the outbox iri in net/url format func (a *Actor) GetOutboxIRI() *url.URL { iri := a.iri + "/outbox" nuiri, _ := url.Parse(iri) @@ -133,7 +133,8 @@ func (a *Actor) GetOutboxIRI() *url.URL { } // LoadActor searches the filesystem and creates an Actor -// from the data in name.json +// from the data in .json +// This does not preserve events so use with caution func LoadActor(name string) (Actor, error) { // make sure our users can't read our hard drive if strings.ContainsAny(name, "./ ") { @@ -161,9 +162,6 @@ func LoadActor(name string) (Actor, error) { return Actor{}, err } - // publicKeyNewLines := strings.ReplaceAll(jsonData["PublicKey"].(string), "\\n", "\n") - // privateKeyNewLines := strings.ReplaceAll(jsonData["PrivateKey"].(string), "\\n", "\n") - publicKeyDecoded, rest := pem.Decode([]byte(jsonData["PublicKey"].(string))) if publicKeyDecoded == nil { log.Info(rest) @@ -245,7 +243,7 @@ func GetActor(name, summary, actorType string) (Actor, error) { } // func LoadActorFromIRI(iri string) a Actor{ - +// TODO, this should parse the iri and load the right actor // } // save the actor to file @@ -445,6 +443,9 @@ func (a *Actor) GetFollowing(page int) (response []byte, err error) { return a.getPeers(page, "following") } +// signedHTTPPost performs an HTTP post on behalf of Actor with the +// request-target, date, host and digest headers signed +// with the actor's private key. func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err error) { b, err := json.Marshal(content) if err != nil { @@ -498,7 +499,7 @@ func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err e return } responseData, _ := ioutil.ReadAll(resp.Body) - fmt.Printf("POST request to %s succeeded (%d): %s \nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, FormatJSON(responseData), FormatJSON(byteCopy), FormatHeaders(req.Header)) + log.Errorf("POST request to %s succeeded (%d): %s \nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, FormatJSON(responseData), FormatJSON(byteCopy), FormatHeaders(req.Header)) return } @@ -556,6 +557,8 @@ func (a *Actor) NewFollower(iri string, inbox string) error { return a.save() } +// appendToOutbox adds a new line with the id of the activity +// to outbox.txt func (a *Actor) appendToOutbox(iri string) (err error) { // create outbox file if it doesn't exist var outbox *os.File @@ -574,6 +577,7 @@ func (a *Actor) appendToOutbox(iri string) (err error) { return nil } +// batchSend sends a batch of http posts to a list of recipients func (a *Actor) batchSend(activity map[string]interface{}, recipients []string) (err error) { for _, v := range recipients { err := a.signedHTTPPost(activity, v) @@ -584,6 +588,7 @@ func (a *Actor) batchSend(activity map[string]interface{}, recipients []string) return } +// send to followers sends a batch of http posts to each one of the followers func (a *Actor) sendToFollowers(activity map[string]interface{}) (err error) { recipients := make([]string, len(a.followers)) @@ -645,15 +650,34 @@ func (a *Actor) Follow(user string) (err error) { // was accepted when initially following that user // (this is read from the `actor.following` map func (a *Actor) Unfollow(user string) { + // if we have a request to follow this user cancel it + cancelRequest := false + if _, ok := a.requested[user]; ok { + log.Info("Cancelling follow request") + cancelRequest = true + // then continue to send the unfollow to the receipient + // to inform them that the request is cancelled. + } else if _, ok := a.following[user]; !ok { + log.Info("We are not following this user, ignoring...") + return + } + log.Info("Unfollowing " + user) + var hash string + // find the id of the original follow + if cancelRequest { + hash = a.requested[user].(string) + } else { + hash = a.following[user].(string) + } + // 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) + undo["id"] = baseURL + "/item/" + hash + "/undo" + undo["type"] = "Undo" follow := make(map[string]interface{}) @@ -673,22 +697,23 @@ func (a *Actor) Unfollow(user string) { return } - // only if we're already following them - if _, ok := a.following[user]; ok { - PrettyPrint(undo) - go func() { - err := a.signedHTTPPost(undo, remoteUser.inbox) - 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 + PrettyPrint(undo) + go func() { + err := a.signedHTTPPost(undo, remoteUser.inbox) + 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 + if cancelRequest { + delete(a.requested, user) + } else { delete(a.following, user) - a.save() - }() - } + } + a.save() + }() } // Announce this activity to our followers diff --git a/http.go b/http.go index 8defa01..061a67a 100644 --- a/http.go +++ b/http.go @@ -251,6 +251,7 @@ func Serve(actors map[string]Actor) { // return // } actor.following[acceptor] = hash + PrettyPrint(activity) delete(actor.requested, acceptor) actor.save() case "Reject": diff --git a/readme.md b/readme.md index 4802c8c..a7e2e4f 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ ## A very light ActivityPub library in go -This library was built to support the very little functions that [pherephone](https://github.com/writeas/pherephone) requires. It might never be feature-complete but it's a very good point to start your activityPub journey. Take a look at [activityserve-example] for a simple main file that uses **activityserve** to post a "Hello, world" message. +This library was built to support the very little functions that [pherephone](https://github.com/writeas/pherephone) requires. It might never be feature-complete but it's a very good point to start your activityPub journey. Take a look at [activityserve-example](https://github.com/writeas/activityserve-example) for a simple main file that uses **activityserve** to post a "Hello, world" message. For now it supports following and unfollowing users, accepting follows, announcing (boosting) other posts and this is pretty much it.