Following, being followed and sending toots works
parent
86eda3a0e9
commit
47dfecbeb3
6
TODO
6
TODO
|
@ -39,8 +39,8 @@
|
||||||
[ ] Create debug flag
|
[ ] Create debug flag
|
||||||
[ ] Write to following only upon accept
|
[ ] Write to following only upon accept
|
||||||
(waiting to actually get an accept so that I can test this)
|
(waiting to actually get an accept so that I can test this)
|
||||||
[ ] Implement webfinger
|
[✔] Implement webfinger
|
||||||
[ ] Make sure masto finds signature
|
[✔] Make sure masto finds signature
|
||||||
[ ] Implement Unfollow
|
[ ] Implement Unfollow
|
||||||
[ ] Implement accept (accept when other follow us)
|
[ ] Implement accept (accept when other follow us)
|
||||||
(done but can't test it pending http signatures)
|
(done but can't test it pending http signatures)
|
||||||
|
@ -66,5 +66,3 @@
|
||||||
[ ] Handle followers and following uri's
|
[ ] Handle followers and following uri's
|
||||||
[ ] Do I care about the inbox?
|
[ ] Do I care about the inbox?
|
||||||
[ ] Maybe look at implementing lock files?
|
[ ] Maybe look at implementing lock files?
|
||||||
[ ] Check if it's worth it to reuse pubActor instead of creating
|
|
||||||
a new one every time
|
|
|
@ -33,7 +33,7 @@ type Actor struct {
|
||||||
name, summary, actorType, iri string
|
name, summary, actorType, iri string
|
||||||
followersIRI string
|
followersIRI string
|
||||||
nuIri *url.URL
|
nuIri *url.URL
|
||||||
followers, following map[string]interface{}
|
followers, following, rejected map[string]interface{}
|
||||||
posts map[int]map[string]interface{}
|
posts map[int]map[string]interface{}
|
||||||
publicKey crypto.PublicKey
|
publicKey crypto.PublicKey
|
||||||
privateKey crypto.PrivateKey
|
privateKey crypto.PrivateKey
|
||||||
|
@ -48,7 +48,7 @@ type Actor struct {
|
||||||
// see https://stackoverflow.com/questions/26327391/json-marshalstruct-returns
|
// see https://stackoverflow.com/questions/26327391/json-marshalstruct-returns
|
||||||
type ActorToSave struct {
|
type ActorToSave struct {
|
||||||
Name, Summary, ActorType, IRI, PublicKey, PrivateKey string
|
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
|
// MakeActor returns a new local actor we can act
|
||||||
|
@ -56,6 +56,7 @@ type ActorToSave struct {
|
||||||
func MakeActor(name, summary, actorType string) (Actor, error) {
|
func MakeActor(name, summary, actorType string) (Actor, error) {
|
||||||
followers := make(map[string]interface{})
|
followers := make(map[string]interface{})
|
||||||
following := make(map[string]interface{})
|
following := make(map[string]interface{})
|
||||||
|
rejected := make(map[string]interface{})
|
||||||
followersIRI := baseURL + name + "/followers"
|
followersIRI := baseURL + name + "/followers"
|
||||||
publicKeyID := baseURL + name + "#main-key"
|
publicKeyID := baseURL + name + "#main-key"
|
||||||
iri := baseURL + name
|
iri := baseURL + name
|
||||||
|
@ -72,6 +73,7 @@ func MakeActor(name, summary, actorType string) (Actor, error) {
|
||||||
nuIri: nuIri,
|
nuIri: nuIri,
|
||||||
followers: followers,
|
followers: followers,
|
||||||
following: following,
|
following: following,
|
||||||
|
rejected: rejected,
|
||||||
followersIRI: followersIRI,
|
followersIRI: followersIRI,
|
||||||
publicKeyID: publicKeyID,
|
publicKeyID: publicKeyID,
|
||||||
}
|
}
|
||||||
|
@ -183,6 +185,7 @@ func LoadActor(name string) (Actor, error) {
|
||||||
nuIri: nuIri,
|
nuIri: nuIri,
|
||||||
followers: jsonData["Followers"].(map[string]interface{}),
|
followers: jsonData["Followers"].(map[string]interface{}),
|
||||||
following: jsonData["Following"].(map[string]interface{}),
|
following: jsonData["Following"].(map[string]interface{}),
|
||||||
|
rejected: jsonData["Rejected"].(map[string]interface{}),
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
publicKeyPem: jsonData["PublicKey"].(string),
|
publicKeyPem: jsonData["PublicKey"].(string),
|
||||||
|
@ -215,6 +218,7 @@ func (a *Actor) save() error {
|
||||||
IRI: a.iri,
|
IRI: a.iri,
|
||||||
Followers: a.followers,
|
Followers: a.followers,
|
||||||
Following: a.following,
|
Following: a.following,
|
||||||
|
Rejected: a.rejected,
|
||||||
PublicKey: a.publicKeyPem,
|
PublicKey: a.publicKeyPem,
|
||||||
PrivateKey: a.privateKeyPem,
|
PrivateKey: a.privateKeyPem,
|
||||||
}
|
}
|
||||||
|
@ -263,6 +267,8 @@ func (a *Actor) newIDurl() string {
|
||||||
return baseURL + a.name + "/" + a.newIDhash()
|
return baseURL + a.name + "/" + a.newIDhash()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Reply(content string, inReplyTo string)
|
||||||
|
|
||||||
// CreateNote posts an activityPub note to our followers
|
// CreateNote posts an activityPub note to our followers
|
||||||
func (a *Actor) CreateNote(content string) {
|
func (a *Actor) CreateNote(content string) {
|
||||||
// for now I will just write this to the outbox
|
// 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["attributedTo"] = baseURL + a.name
|
||||||
note["cc"] = a.followersIRI
|
note["cc"] = a.followersIRI
|
||||||
note["content"] = content
|
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["id"] = baseURL + a.name + "/note/" + id
|
||||||
note["published"] = time.Now().Format(time.RFC3339)
|
note["published"] = time.Now().Format(time.RFC3339)
|
||||||
note["url"] = create["id"]
|
note["url"] = create["id"]
|
||||||
|
@ -286,16 +292,24 @@ func (a *Actor) CreateNote(content string) {
|
||||||
note["to"] = "https://www.w3.org/ns/activitystreams#Public"
|
note["to"] = "https://www.w3.org/ns/activitystreams#Public"
|
||||||
create["published"] = note["published"]
|
create["published"] = note["published"]
|
||||||
create["type"] = "Create"
|
create["type"] = "Create"
|
||||||
to, _ := url.Parse("https://cybre.space/inbox")
|
go a.sendToFollowers(create)
|
||||||
go a.send(create, to)
|
err := a.saveItem(id, create)
|
||||||
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")
|
JSON, _ := json.MarshalIndent(content, "", "\t")
|
||||||
|
|
||||||
dir := storage + slash + "actors" + slash + a.name + slash + "items"
|
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 {
|
if err != nil {
|
||||||
log.Printf("WriteFileJson ERROR: %+v", err)
|
log.Printf("WriteFileJson ERROR: %+v", err)
|
||||||
return err
|
return err
|
||||||
|
@ -303,6 +317,24 @@ func (a *Actor) saveItem(id string, content map[string]interface{}) error {
|
||||||
return nil
|
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
|
// send is here for backward compatibility and maybe extra pre-processing
|
||||||
// not always required
|
// not always required
|
||||||
func (a *Actor) send(content map[string]interface{}, to *url.URL) (err error) {
|
func (a *Actor) send(content map[string]interface{}, to *url.URL) (err error) {
|
||||||
|
@ -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("Accept-Charset", "utf-8")
|
||||||
req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
|
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("Host", iri.Host)
|
||||||
req.Header.Add("Accept", "application/activity+json")
|
req.Header.Add("Accept", "application/activity+json")
|
||||||
sum := sha256.Sum256(b)
|
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
|
// NewFollower records a new follower to the actor file
|
||||||
func (a *Actor) NewFollower(iri string) error {
|
func (a *Actor) NewFollower(iri string, inbox string) error {
|
||||||
a.followers[iri] = struct{}{}
|
a.followers[iri] = inbox
|
||||||
return a.save()
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -176,12 +176,14 @@ func Serve() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
follower, err := NewRemoteActor(activity["actor"].(string))
|
||||||
|
|
||||||
// check if this user is already following us
|
// check if this user is already following us
|
||||||
if _, ok := actor.followers[newFollower]; ok {
|
if _, ok := actor.followers[newFollower]; ok {
|
||||||
log.Info("You're already following us, yay!")
|
log.Info("You're already following us, yay!")
|
||||||
// do nothing, they're already following us
|
// do nothing, they're already following us
|
||||||
} else {
|
} else {
|
||||||
actor.NewFollower(newFollower)
|
actor.NewFollower(newFollower, follower.inbox)
|
||||||
}
|
}
|
||||||
// send accept anyway even if they are following us already
|
// send accept anyway even if they are following us already
|
||||||
// this is very verbose. I would prefer creating a map by hand
|
// this is very verbose. I would prefer creating a map by hand
|
||||||
|
@ -198,7 +200,6 @@ func Serve() {
|
||||||
accept["object"] = activity
|
accept["object"] = activity
|
||||||
accept["type"] = "Accept"
|
accept["type"] = "Accept"
|
||||||
|
|
||||||
follower, err := NewRemoteActor(activity["actor"].(string))
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info("Couldn't retrieve remote actor info, maybe server is down?")
|
log.Info("Couldn't retrieve remote actor info, maybe server is down?")
|
||||||
|
@ -207,6 +208,44 @@ func Serve() {
|
||||||
|
|
||||||
go actor.signedHTTPPost(accept, follower.inbox)
|
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:
|
default:
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
4
main.go
4
main.go
|
@ -53,9 +53,11 @@ func main() {
|
||||||
|
|
||||||
// actor, _ := activityserve.MakeActor("activityserve_test_actor_2", "This is an activityserve test actor", "Service")
|
// actor, _ := activityserve.MakeActor("activityserve_test_actor_2", "This is an activityserve test actor", "Service")
|
||||||
// actor, _ := activityserve.LoadActor("activityserve_test_actor_2")
|
// actor, _ := activityserve.LoadActor("activityserve_test_actor_2")
|
||||||
|
// actor.Follow("https://cybre.space/users/tzo")
|
||||||
// actor.CreateNote("Hello World!")
|
// actor.CreateNote("Hello World!")
|
||||||
|
|
||||||
activityserve.LoadActor("activityserve_test_actor_2")
|
actor, _ := activityserve.LoadActor("activityserve_test_actor_2")
|
||||||
|
actor.CreateNote("Hello World, again!")
|
||||||
|
|
||||||
activityserve.Serve()
|
activityserve.Serve()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue