Following, being followed and sending toots works

master
Michael Demetriou 2019-09-11 12:21:38 +03:00
parent 86eda3a0e9
commit 47dfecbeb3
4 changed files with 212 additions and 29 deletions

8
TODO
View File

@ -39,8 +39,8 @@
[ ] Create debug flag
[ ] Write to following only upon accept
(waiting to actually get an accept so that I can test this)
[ ] Implement webfinger
[ ] Make sure masto finds signature
[] Implement webfinger
[] Make sure masto finds signature
[ ] Implement Unfollow
[ ] Implement accept (accept when other follow us)
(done but can't test it pending http signatures)
@ -65,6 +65,4 @@
[ ] Check if an early failure in announcing posts causes a problem to the following ones
[ ] Handle followers and following uri's
[ ] Do I care about the inbox?
[ ] Maybe look at implementing lock files?
[ ] Check if it's worth it to reuse pubActor instead of creating
a new one every time
[ ] Maybe look at implementing lock files?

View File

@ -30,16 +30,16 @@ import (
// Actor represents a local actor we can act on
// behalf of.
type Actor struct {
name, summary, actorType, iri string
followersIRI string
nuIri *url.URL
followers, following map[string]interface{}
posts map[int]map[string]interface{}
publicKey crypto.PublicKey
privateKey crypto.PrivateKey
publicKeyPem string
privateKeyPem string
publicKeyID string
name, summary, actorType, iri string
followersIRI string
nuIri *url.URL
followers, following, rejected map[string]interface{}
posts map[int]map[string]interface{}
publicKey crypto.PublicKey
privateKey crypto.PrivateKey
publicKeyPem string
privateKeyPem string
publicKeyID string
}
// ActorToSave is a stripped down actor representation
@ -48,7 +48,7 @@ type Actor struct {
// see https://stackoverflow.com/questions/26327391/json-marshalstruct-returns
type ActorToSave struct {
Name, Summary, ActorType, IRI, PublicKey, PrivateKey string
Followers, Following map[string]interface{}
Followers, Following, Rejected map[string]interface{}
}
// MakeActor returns a new local actor we can act
@ -56,6 +56,7 @@ type ActorToSave struct {
func MakeActor(name, summary, actorType string) (Actor, error) {
followers := make(map[string]interface{})
following := make(map[string]interface{})
rejected := make(map[string]interface{})
followersIRI := baseURL + name + "/followers"
publicKeyID := baseURL + name + "#main-key"
iri := baseURL + name
@ -72,6 +73,7 @@ func MakeActor(name, summary, actorType string) (Actor, error) {
nuIri: nuIri,
followers: followers,
following: following,
rejected: rejected,
followersIRI: followersIRI,
publicKeyID: publicKeyID,
}
@ -183,6 +185,7 @@ func LoadActor(name string) (Actor, error) {
nuIri: nuIri,
followers: jsonData["Followers"].(map[string]interface{}),
following: jsonData["Following"].(map[string]interface{}),
rejected: jsonData["Rejected"].(map[string]interface{}),
publicKey: publicKey,
privateKey: privateKey,
publicKeyPem: jsonData["PublicKey"].(string),
@ -215,6 +218,7 @@ func (a *Actor) save() error {
IRI: a.iri,
Followers: a.followers,
Following: a.following,
Rejected: a.rejected,
PublicKey: a.publicKeyPem,
PrivateKey: a.privateKeyPem,
}
@ -263,6 +267,8 @@ func (a *Actor) newIDurl() string {
return baseURL + a.name + "/" + a.newIDhash()
}
// TODO Reply(content string, inReplyTo string)
// CreateNote posts an activityPub note to our followers
func (a *Actor) CreateNote(content string) {
// for now I will just write this to the outbox
@ -278,7 +284,7 @@ func (a *Actor) CreateNote(content string) {
note["attributedTo"] = baseURL + a.name
note["cc"] = a.followersIRI
note["content"] = content
note["inReplyTo"] = "https://cybre.space/@qwazix/102688373602724023"
// note["inReplyTo"] = "https://cybre.space/@qwazix/102688373602724023"
note["id"] = baseURL + a.name + "/note/" + id
note["published"] = time.Now().Format(time.RFC3339)
note["url"] = create["id"]
@ -286,16 +292,24 @@ func (a *Actor) CreateNote(content string) {
note["to"] = "https://www.w3.org/ns/activitystreams#Public"
create["published"] = note["published"]
create["type"] = "Create"
to, _ := url.Parse("https://cybre.space/inbox")
go a.send(create, to)
a.saveItem(id, create)
go a.sendToFollowers(create)
err := a.saveItem(id, create)
if err != nil {
log.Info("Could not save note to disk")
}
err = a.appendToOutbox(id)
if err != nil {
log.Info("Could not append Note to outbox.txt")
}
}
func (a *Actor) saveItem(id string, content map[string]interface{}) error {
// saveItem saves an activity to disk under the actor and with the id as
// filename
func (a *Actor) saveItem(hash string, content map[string]interface{}) error {
JSON, _ := json.MarshalIndent(content, "", "\t")
dir := storage + slash + "actors" + slash + a.name + slash + "items"
err := ioutil.WriteFile(dir+slash+id+".json", JSON, 0644)
err := ioutil.WriteFile(dir+slash+hash+".json", JSON, 0644)
if err != nil {
log.Printf("WriteFileJson ERROR: %+v", err)
return err
@ -303,6 +317,24 @@ func (a *Actor) saveItem(id string, content map[string]interface{}) error {
return nil
}
func (a *Actor) loadItem(hash string) (item map[string]interface{}, err error) {
dir := storage + slash + "actors" + slash + a.name + slash + "items"
jsonFile := dir + slash + hash + ".json"
fileHandle, err := os.Open(jsonFile)
if os.IsNotExist(err) {
log.Info("We don't have this item stored")
return
}
byteValue, err := ioutil.ReadAll(fileHandle)
if err != nil {
log.Info("Error reading item file")
return
}
json.Unmarshal(byteValue, &item)
return
}
// send is here for backward compatibility and maybe extra pre-processing
// not always required
func (a *Actor) send(content map[string]interface{}, to *url.URL) (err error) {
@ -328,7 +360,7 @@ func (a *Actor) GetFollowers(page int) (response []byte, err error) {
items = append(items, k)
}
themap["orderedItems"] = items
themap["partOf"] = baseURL + a.name + "/followers"
themap["partOf"] = baseURL + a.name + "/followers"
themap["totalItems"] = len(a.followers)
themap["type"] = "OrderedCollectionPage"
}
@ -363,7 +395,7 @@ func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err e
}
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", userAgent + " " + version)
req.Header.Add("User-Agent", userAgent+" "+version)
req.Header.Add("Host", iri.Host)
req.Header.Add("Accept", "application/activity+json")
sum := sha256.Sum256(b)
@ -441,7 +473,119 @@ func (a *Actor) signedHTTPGet(address string) (string, error) {
}
// NewFollower records a new follower to the actor file
func (a *Actor) NewFollower(iri string) error {
a.followers[iri] = struct{}{}
func (a *Actor) NewFollower(iri string, inbox string) error {
a.followers[iri] = inbox
return a.save()
}
func (a *Actor) appendToOutbox(iri string) (err error) {
// create outbox file if it doesn't exist
var outbox *os.File
outboxFilePath := storage + slash + "actors" + slash + a.name + slash + "outbox"
outbox, err = os.OpenFile(outboxFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Info("Cannot create or open outbox file")
log.Info(err)
return err
}
defer outbox.Close()
outbox.Write([]byte(iri))
return nil
}
func (a *Actor) batchSend(activity map[string]interface{}, recipients []string) (err error) {
for _, v := range recipients {
err := a.signedHTTPPost(activity, v)
if err != nil {
log.Info("Failed to deliver message to " + v)
}
}
return
}
func (a *Actor) sendToFollowers(activity map[string]interface{}) (err error) {
recipients := make([]string, len(a.followers))
i := 0
for _, inbox := range a.followers {
recipients[i] = inbox.(string)
i++
}
a.batchSend(activity, recipients)
return
}
// Follow a remote user by their iri
func (a *Actor) Follow(user string) (err error) {
remote, err := NewRemoteActor(user)
if err != nil {
log.Info("Can't contact " + user + " to get their inbox")
return
}
follow := make(map[string]interface{})
id := a.newIDhash()
follow["@context"] = context()
follow["actor"] = a.iri
follow["id"] = baseURL + a.name + "/" + id
follow["object"] = user
follow["type"] = "Follow"
// if we are not already following them
if _, ok := a.following[user]; !ok {
// if we have not been rejected previously
if _, ok := a.rejected[user]; !ok {
go func() {
err := a.signedHTTPPost(follow, remote.inbox)
if err != nil {
log.Info("Couldn't follow " + user)
log.Info(err)
return
}
// save the activity
a.saveItem(id, follow)
// we are going to save only on accept so look at
// the http handler for the accept code
}()
}
}
return nil
}
// Announce this activity to our followers
func (a *Actor) Announce(url string) {
// our announcements are public. Public stuff have a "To" to the url below
toURL := "https://www.w3.org/ns/activitystreams#Public"
announce := make(map[string]interface{})
announce["@context"] = context()
announce["object"] = url
announce["actor"] = a.name
announce["to"] = toURL
// cc this to all our followers one by one
// I've seen activities to just include the url of the
// collection but for now this works.
// It seems that sharedInbox will be deprecated
// so this is probably a better idea anyway (#APConf)
announce["cc"] = a.followersSlice()
// add a timestamp
announce["published"] = time.Now().Format(time.RFC3339)
a.sendToFollowers(announce)
}
func (a *Actor) followersSlice() []string {
followersSlice := make([]string, len(a.followers))
for k := range a.followers {
followersSlice = append(followersSlice, k)
}
return followersSlice
}

View File

@ -176,12 +176,14 @@ func Serve() {
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)
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
@ -198,7 +200,6 @@ func Serve() {
accept["object"] = activity
accept["type"] = "Accept"
follower, err := NewRemoteActor(activity["actor"].(string))
if err != nil {
log.Info("Couldn't retrieve remote actor info, maybe server is down?")
@ -207,6 +208,44 @@ func Serve() {
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:
}

View File

@ -53,9 +53,11 @@ func main() {
// actor, _ := activityserve.MakeActor("activityserve_test_actor_2", "This is an activityserve test actor", "Service")
// actor, _ := activityserve.LoadActor("activityserve_test_actor_2")
// actor.Follow("https://cybre.space/users/tzo")
// actor.CreateNote("Hello World!")
activityserve.LoadActor("activityserve_test_actor_2")
actor, _ := activityserve.LoadActor("activityserve_test_actor_2")
actor.CreateNote("Hello World, again!")
activityserve.Serve()
}