Implement being followed.

master
Michael Demetriou 2019-09-10 11:21:39 +03:00
parent f67906c96a
commit 86eda3a0e9
8 changed files with 368 additions and 72 deletions

13
TODO
View File

@ -1,8 +1,3 @@
[ ] Load outbox of users and parse latest posts
[ ] Write these posts to local file
(normally we would need to sort by time but this is a
temporary solution until we really follow the actors
and get notifications in timely manner)
[ ] Follow users
[ ] Announcements (read up how boost json looks like)
[ ] Federate the post to our followers (hardcoded for now)
@ -13,14 +8,14 @@
[ ] Handle the /actor endpoint
[ ] Create configuration file
[ ] Implement database backend
[ ] Create a file with the actors we have, their following
[] Create a file with the actors we have, their following
and their followers.
[ ] `MakeActor` should create a file with that actor.
[ ] Implement `LoadActor`
[] `MakeActor` should create a file with that actor.
[] Implement `LoadActor`
[ ] All but `main.go` should run LoadActor instead of MakeActor
(Actually nobody should run LoadActor except GetActor)
[ ] `actor.Follow` should write the new following to file
[ ] Handle being followed
[] Handle being followed
[ ] When followed, the handler should write the new follower to file
[ ] Make sure we send our boosts to all our followers
Code is there but it works sometimes (I hate when this happens)

View File

@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
@ -57,7 +58,7 @@ func MakeActor(name, summary, actorType string) (Actor, error) {
following := make(map[string]interface{})
followersIRI := baseURL + name + "/followers"
publicKeyID := baseURL + name + "#main-key"
iri := baseURL + "/" + name
iri := baseURL + name
nuIri, err := url.Parse(iri)
if err != nil {
log.Info("Something went wrong when parsing the local actor uri into net/url")
@ -193,6 +194,10 @@ func LoadActor(name string) (Actor, error) {
return actor, nil
}
// func LoadActorFromIRI(iri string) a Actor{
// }
// save the actor to file
func (a *Actor) save() error {
@ -250,20 +255,22 @@ func (a *Actor) whoAmI() string {
}`
}
func (a *Actor) newID() string {
func (a *Actor) newIDhash() string {
return uniuri.New()
}
func (a *Actor) newIDurl() string {
return baseURL + a.name + "/" + a.newIDhash()
}
// CreateNote posts an activityPub note to our followers
func (a *Actor) CreateNote(content string) {
// for now I will just write this to the outbox
id := a.newID()
id := a.newIDurl()
create := make(map[string]interface{})
note := make(map[string]interface{})
context := make([]string, 1)
context[0] = "https://www.w3.org/ns/activitystreams"
create["@context"] = context
create["@context"] = context()
create["actor"] = baseURL + a.name
create["cc"] = a.followersIRI
create["id"] = baseURL + a.name + "/" + id
@ -279,34 +286,6 @@ func (a *Actor) CreateNote(content string) {
note["to"] = "https://www.w3.org/ns/activitystreams#Public"
create["published"] = note["published"]
create["type"] = "Create"
// note := `{
// "actor" : "https://` + baseURL + a.name + `",
// "cc" : [
// "https://` + baseURL + a.name + `/followers"
// ],
// "id" : "https://` + baseURL + a.name + `/` + id +`",
// "object" : {
// "attributedTo" : "https://` + baseURL + a.name + `",
// "cc" : [
// "https://` + baseURL + a.name + `/followers"
// ],
// "content" : "`+ content + `",
// "id" : "https://` + baseURL + a.name + `/` + id +`",
// "inReplyTo" : null,
// "published" : "2019-08-26T16:25:26Z",
// "to" : [
// "https://www.w3.org/ns/activitystreams#Public"
// ],
// "type" : "Note",
// "url" : "https://` + baseURL + a.name + `/` + id +`"
// },
// "published" : "2019-08-26T16:25:26Z",
// "to" : [
// "https://www.w3.org/ns/activitystreams#Public"
// ],
// "type" : "Create"
// }`
to, _ := url.Parse("https://cybre.space/inbox")
go a.send(create, to)
a.saveItem(id, create)
@ -330,6 +309,33 @@ func (a *Actor) send(content map[string]interface{}, to *url.URL) (err error) {
return a.signedHTTPPost(content, to.String())
}
// GetFollowers returns a list of people that follow us
func (a *Actor) GetFollowers(page int) (response []byte, err error) {
// if there's no page parameter mastodon displays an
// OrderedCollection with info of where to find orderedCollectionPages
// with the actual information. We are mirroring that behavior
themap := make(map[string]interface{})
themap["@context"] = "https://www.w3.org/ns/activitystreams"
if page == 0 {
themap["first"] = baseURL + a.name + "/followers?page=1"
themap["id"] = baseURL + a.name + "/followers"
themap["totalItems"] = strconv.Itoa(len(a.followers))
themap["type"] = "OrderedCollection"
} else if page == 1 { // implement pagination
themap["id"] = baseURL + a.name + "followers?page=" + strconv.Itoa(page)
items := make([]string, 0, len(a.followers))
for k := range a.followers {
items = append(items, k)
}
themap["orderedItems"] = items
themap["partOf"] = baseURL + a.name + "/followers"
themap["totalItems"] = len(a.followers)
themap["type"] = "OrderedCollectionPage"
}
response, _ = json.Marshal(themap)
return
}
func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err error) {
b, err := json.Marshal(content)
if err != nil {
@ -357,7 +363,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", fmt.Sprintf("activityserve 0.0"))
req.Header.Add("User-Agent", userAgent + " " + version)
req.Header.Add("Host", iri.Host)
req.Header.Add("Accept", "application/activity+json")
sum := sha256.Sum256(b)
@ -377,16 +383,16 @@ func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err e
defer resp.Body.Close()
if !isSuccess(resp.StatusCode) {
responseData, _ := ioutil.ReadAll(resp.Body)
err = fmt.Errorf("POST request to %s failed (%d): %s\nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, formatJSON(responseData), formatJSON(byteCopy), req.Header)
err = fmt.Errorf("POST request to %s failed (%d): %s\nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, FormatJSON(responseData), FormatJSON(byteCopy), FormatHeaders(req.Header))
log.Info(err)
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), req.Header)
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))
return
}
func (a *Actor) signedHTTPGet(address string) (string, error){
func (a *Actor) signedHTTPGet(address string) (string, error) {
req, err := http.NewRequest("GET", address, nil)
if err != nil {
log.Error("cannot create new http.request")
@ -413,7 +419,7 @@ func (a *Actor) signedHTTPGet(address string) (string, error){
log.Error("Can't sign the request")
return "", err
}
resp, err := client.Do(req)
if err != nil {
log.Error("Cannot perform the GET request")
@ -422,14 +428,20 @@ func (a *Actor) signedHTTPGet(address string) (string, error){
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
responseData, _ := ioutil.ReadAll(resp.Body)
return "", fmt.Errorf("GET request to %s failed (%d): %s \n%s", iri.String(), resp.StatusCode, resp.Status, formatJSON(responseData))
return "", fmt.Errorf("GET request to %s failed (%d): %s \n%s", iri.String(), resp.StatusCode, resp.Status, FormatJSON(responseData))
}
responseData, _ := ioutil.ReadAll(resp.Body)
fmt.Println("GET request succeeded:", iri.String(), req.Header, resp.StatusCode, resp.Status, "\n", formatJSON(responseData))
fmt.Println("GET request succeeded:", iri.String(), req.Header, resp.StatusCode, resp.Status, "\n", FormatJSON(responseData))
responseText := string(responseData)
return responseText, nil
}
}
// NewFollower records a new follower to the actor file
func (a *Actor) NewFollower(iri string) error {
a.followers[iri] = struct{}{}
return a.save()
}

View File

@ -2,7 +2,9 @@ package activityserve
import (
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"github.com/gologme/log"
@ -11,7 +13,7 @@ import (
"encoding/json"
)
// SetupHTTP starts an http server with all the required handlers
// Serve starts an http server with all the required handlers
func Serve() {
var webfingerHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
@ -34,23 +36,26 @@ func Serve() {
responseMap := make(map[string]interface{})
responseMap["subject"] = "acct:" + actor.name + "@" + server
links := make(map[string]string)
links["rel"] = "self"
links["type"] = "application/activity+json"
links["href"] = baseURL + actor.name
// 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")
}
log.Info(string(response))
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 just fetched our /actor endpoint")
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" {
@ -66,9 +71,13 @@ func Serve() {
return
}
fmt.Fprintf(w, actor.whoAmI())
log.Info(r.RemoteAddr)
log.Info(r.Body)
log.Info(r.Header)
// 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) {
@ -137,13 +146,101 @@ func Serve() {
w.Write([]byte(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
}
// 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)
}
// 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"
follower, err := NewRemoteActor(activity["actor"].(string))
if err != nil {
log.Info("Couldn't retrieve remote actor info, maybe server is down?")
log.Info(err)
}
go actor.signedHTTPPost(accept, follower.inbox)
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}/inbox", inboxHandler)
gorilla.HandleFunc("/{actor}/inbox/", inboxHandler)
gorilla.HandleFunc("/{actor}/", actorHandler)
gorilla.HandleFunc("/{actor}", actorHandler)
// gorilla.HandleFunc("/{actor}/post/{hash}", postHandler)

View File

@ -0,0 +1,92 @@
package activityserve
import (
"fmt"
"io/ioutil"
"github.com/gologme/log"
// "github.com/go-fed/activity/pub"
// "github.com/go-fed/httpsig"
"net/http"
// "net/url"
"encoding/json"
"bytes"
)
// RemoteActor is a type that holds an actor
// that we want to interact with
type RemoteActor struct {
iri, outbox, inbox string
info map[string]interface{}
}
// NewRemoteActor returns a remoteActor which holds
// all the info required for an actor we want to
// interact with (not essentially sitting in our instance)
func NewRemoteActor(iri string) (RemoteActor, error) {
info, err := get(iri)
if err != nil {
log.Info("Couldn't get remote actor information")
log.Info(err)
return RemoteActor{}, err
}
outbox := info["outbox"].(string)
inbox := info["inbox"].(string)
return RemoteActor{
iri: iri,
outbox: outbox,
inbox: inbox,
}, err
}
func (ra RemoteActor) getLatestPosts(number int) (map[string]interface{}, error) {
return get(ra.outbox)
}
func get(iri string) (info map[string]interface{}, err error) {
buf := new(bytes.Buffer)
req, err := http.NewRequest("GET", iri, buf)
if err != nil {
log.Info(err)
return
}
req.Header.Add("Accept", "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"")
req.Header.Add("User-Agent", userAgent+" "+version)
req.Header.Add("Accept-Charset", "utf-8")
resp, err := client.Do(req)
if err != nil {
log.Info("Cannot perform the request")
log.Info(err)
return
}
responseData, _ := ioutil.ReadAll(resp.Body)
if !isSuccess(resp.StatusCode) {
err = fmt.Errorf("GET request to %s failed (%d): %s\nResponse: %s \nHeaders: %s", iri, resp.StatusCode, resp.Status, FormatJSON(responseData), FormatHeaders(req.Header))
log.Info(err)
return
}
var e interface{}
err = json.Unmarshal(responseData, &e)
if err != nil {
log.Info("something went wrong when unmarshalling the json")
log.Info(err)
}
info = e.(map[string]interface{})
return
}

View File

@ -2,8 +2,8 @@ package activityserve
import (
"fmt"
"os"
"net/http"
"os"
"github.com/gologme/log"
"gopkg.in/ini.v1"
@ -13,6 +13,7 @@ var slash = string(os.PathSeparator)
var baseURL = "http://example.com/"
var storage = "storage"
var userAgent = "activityserve"
var printer *log.Logger
const libName = "activityserve"
const version = "0.99"
@ -61,7 +62,7 @@ func Setup(configurationFile string, debug bool) {
log.EnableLevel("warn")
// create a logger with levels but without prefixes for easier to read
// debug output
printer := log.New(os.Stdout, " ", 0)
printer = log.New(os.Stdout, " ", 0)
if debug == true {
fmt.Println()

86
activityserve/snips.md Normal file
View File

@ -0,0 +1,86 @@
## When we follow someone from pherephone 1.00
``` json
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://floorb.qwazix.com/myAwesomeList1",
"id": "https://floorb.qwazix.com/myAwesomeList1/Xm9UHyJXyFYduqXz",
"object": "https://cybre.space/users/qwazix",
"to": "https://cybre.space/users/qwazix",
"type": "Follow"
}
```
``` yaml
Accept: application/activity+json
Accept-Charset: utf-8
Date: Tue, 10 Sep 2019 05:31:22 GMT
Digest: SHA-256=uL1LvGU4+gSDm8Qci6XibZODTaNCsXWXWgkMWAqBvG8=
Host: cybre.space
Signature: keyId="https://floorb.qwazix.com/myAwesomeList1#main-key",algorithm="rsa-sha256",headers="(request-target) date host digest",signature="c6oipeXu/2zqX3qZF1x7KLNTYifcyqwwDySoslAowjpYlKWO3qAZMU1A//trYm23AtnItXkH2mY3tPq8X7fy9P1+CMFmiTzV01MGwwwJLDtEXKoq8W7L7lWuQhDD5rjiZqWyei4T13FW7MOCRbAtC4kZqkHrp5Z3l8HhPvmgUV5VOuSGWrtbmCN3hlAEHVugQTMPC6UjlaHva6Qm/SNlFmpUdG7WmUUPJIZ6a/ysBk4cLkF1+Hb03grXKexLHAU4bPIRcjwFpUl06yp8fZ8CCLhNhIsBACiizV85D3votmdxAollE5JXSwBp4f6jrZbgiJEusFoxiVKKqZRHRESQBQ=="
```
## Pherephone 1 Accept Activity
``` yaml
Accept: application/activity+json
Accept-Charset: utf-8
Date: Tue, 10 Sep 2019 07:28:49 GMT
Digest: SHA-256=GTy9bhYjOnbeCJzAzpqI/HEw/5p81NnoPLJkVAiZ4K0=
Host: cybre.space
Signature: keyId="https://floorb.qwazix.com/activityserve_test_actor_1#main-key",algorithm="rsa-sha256",headers="(request-target) date host digest",signature="jAeTEy9v1t+bCwQJB2R4Cscu/fGu5i4luHXlzJcJVyRbsHGqxbNEOxlk/G0S5BGbX3Kuoerq2oMpkFV5kCWPlpAmfhz38NKIrWhjnEUpFOfiG+ZJBpQsb3VQp7M3RGPZ9K4hmV6BSzkC8npsFGPI/HkAaj9u/txW5Cp4v6dMOYteoRLcKc3UVPK9j4hCbjq6SPhpwfM+StARSDnUFfpDe4YYQiVnO2WoINPUr4xvELmCYdBclSBCKcG66g8sBpnx4McjIlu0VISeBxzIHZYOONPteLY2uZW3Axi9JIAq88Y2Ecw4vV6Ctp7KcmD7M3kAJLqao2p/XZNZ3ExsTGfrXA=="
User-Agent: activityserve 0.0
```
``` json
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://floorb.qwazix.com/myAwesomeList1",
"id": "https://floorb.qwazix.com/myAwesomeList1/SABRE7xlDAjtDcZb",
"object": {
"actor": "https://cybre.space/users/qwazix",
"id": "https://cybre.space/3e7336af-4bcd-4f77-aa69-6a145be824aa",
"object": "https://floorb.qwazix.com/myAwesomeList1",
"type": "Follow"
},
"to": "https://cybre.space/users/qwazix",
"type": "Accept"
}
```
## Pherephone 2 Accept Activity
``` yaml
Accept: application/activity+json
Accept-Charset: utf-8
Date: Tue, 10 Sep 2019 07:32:08 GMT
Digest: SHA-256=yKzA6srSMx0b5GXn9DyflXVdqWd6ADBGt5hO9t/yc44=
Host: cybre.space
Signature: keyId="https://floorb.qwazix.com/myAwesomeList1#main-key",algorithm="rsa-sha256",headers="(request-target) date host digest",signature="WERXWDRFS7aGiIoz+HSujtuv9XNFBPxHkJSsCPu7PNIUDoAB2jdwW3rZc5jbrSLxi9Aqhr2BiBV/VYELQ8gITPzzIYH5sizPcPyLyARPUw37t6zA3HinahpfBKXhf73q9u+CYE/7DMKQ2Pvv2lQPaZ8hl27R2KJmcc3Jhmn5nxrQ+kxAtn6qYpNT/BqLWlXKx5rpYM2r+mHjFyYRYsjlAmi+RQNDEmv/uwn+XuNKzEtrL8Oq7mM13Lsid0a3gJi/t0b/luoyRyvi3fHUM/b1epfVogG/FulsZ0A92310v8MbastceQjjUzTzjKHILl7qNewkqtlzn2ARm3cZlAprSg=="
User-Agent: pherephone (go-fed/activity v1.0.0)
```
``` json
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://floorb.qwazix.com/activityserve_test_actor_1",
"id": "https://floorb.qwazix.com/activityserve_test_actor_1/4wJ9DrBab4eIE3Bt",
"object": {
"actor": "https://cybre.space/users/qwazix",
"id": "https://cybre.space/9123da78-21a5-44bc-bce5-4039a4072e4c",
"object": "https://floorb.qwazix.com/activityserve_test_actor_1",
"type": "Follow"
},
"to": "https://cybre.space/users/qwazix",
"type": "Accept"
}
```

View File

@ -34,8 +34,19 @@ func PrettyPrintJSON(theJSON []byte) {
log.Info(dst)
}
func formatJSON(theJSON []byte) string{
func FormatJSON(theJSON []byte) string {
dst := new(bytes.Buffer)
json.Indent(dst, theJSON, "", "\t")
return dst.String()
}
// FormatHeaders to string for printing
func FormatHeaders(header http.Header) string {
buf := new(bytes.Buffer)
header.Write(buf)
return buf.String()
}
func context() [1]string {
return [1]string{"https://www.w3.org/ns/activitystreams"}
}

12
main.go
View File

@ -1,8 +1,9 @@
package main
import (
"fmt"
"flag"
"fmt"
// "os"
// "strings"
@ -26,7 +27,6 @@ import (
"./activityserve"
)
var err error
func main() {
@ -51,9 +51,11 @@ func main() {
activityserve.Setup("config.ini", *debugFlag)
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.CreateNote("Hello World!")
// actor.CreateNote("Hello World!")
activityserve.LoadActor("activityserve_test_actor_2")
activityserve.Serve()
}
}