From f67906c96a3a50a71df17d4cecaa946b12a6921b Mon Sep 17 00:00:00 2001 From: Michael Demetriou Date: Wed, 4 Sep 2019 12:33:32 +0300 Subject: [PATCH] Inital commit It tries to reply to a mastodon post but gets 500 --- .gitignore | 2 + TODO | 75 +++++++ activityserve/actor.go | 435 +++++++++++++++++++++++++++++++++++++++++ activityserve/http.go | 153 +++++++++++++++ activityserve/setup.go | 82 ++++++++ activityserve/util.go | 41 ++++ config.ini | 5 + main.go | 59 ++++++ 8 files changed, 852 insertions(+) create mode 100644 .gitignore create mode 100644 TODO create mode 100644 activityserve/actor.go create mode 100644 activityserve/http.go create mode 100644 activityserve/setup.go create mode 100644 activityserve/util.go create mode 100644 config.ini create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b19c37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.snip +storage \ No newline at end of file diff --git a/TODO b/TODO new file mode 100644 index 0000000..4be5df8 --- /dev/null +++ b/TODO @@ -0,0 +1,75 @@ +[ ] 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) +[ ] Actor should have pubActor not the other way around +[ ] Handle more than one local actors +[ ] Fix the json to host those multiple actors +[ ] Fix the json unmarshalling code to read multiple actors +[ ] Handle the /actor endpoint +[ ] Create configuration file +[ ] Implement database backend + [ ] Create a file with the actors we have, their following + and their followers. + [ ] `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 + [ ] 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) + [ ] Check why I get calls to get with an id that consists only of an actor's name + [ ] Implement `db.followers` and `db.following` +[ ] Write all the announcements (boosts) to the database to + their correct actors +[ ] Check if we are already following users +[ ] On GetOutbox read the database and present a list of the + last posts. +[ ] 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 + through in our inbox +[ ] Boost not only articles but other things too +[ ] Handle post uri's +[ ] Sanitize input, never allow slashes or dots +[ ] Add summary to actors.json +[ ] Check local actor names for characters illegal for filenames and ban them +[ ] 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 Unfollow +[ ] Implement accept (accept when other follow us) + (done but can't test it pending http signatures) + Works in pleroma/pixelfed not working on masto + (nothing works on masto) +[ ] Implement nodeinfo and statistics +[ ] Accept even if already follows us +[ ] Implement db.Update +[ ] Implement db.Delete +[ ] Handle paging +[ ] Handle http signatures + masto can't find the signature +[ ] Verify http signatures +[ ] Why doesn't our outbox being fetched by others? +[ ] Refactor, comment and clean up +[ ] Make sure we never show .json to the public +[ ] 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? +[ ] 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 \ No newline at end of file diff --git a/activityserve/actor.go b/activityserve/actor.go new file mode 100644 index 0000000..faacafc --- /dev/null +++ b/activityserve/actor.go @@ -0,0 +1,435 @@ +package activityserve + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/gologme/log" + + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + + "github.com/dchest/uniuri" + "github.com/go-fed/httpsig" +) + +// 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 +} + +// ActorToSave is a stripped down actor representation +// with exported properties in order for json to be +// able to marshal it. +// see https://stackoverflow.com/questions/26327391/json-marshalstruct-returns +type ActorToSave struct { + Name, Summary, ActorType, IRI, PublicKey, PrivateKey string + Followers, Following map[string]interface{} +} + +// MakeActor returns a new local actor we can act +// on behalf of +func MakeActor(name, summary, actorType string) (Actor, error) { + followers := make(map[string]interface{}) + following := make(map[string]interface{}) + followersIRI := baseURL + name + "/followers" + publicKeyID := baseURL + name + "#main-key" + 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") + return Actor{}, err + } + actor := Actor{ + name: name, + summary: summary, + actorType: actorType, + iri: iri, + nuIri: nuIri, + followers: followers, + following: following, + followersIRI: followersIRI, + publicKeyID: publicKeyID, + } + + // create actor's keypair + rng := rand.Reader + privateKey, err := rsa.GenerateKey(rng, 2048) + publicKey := privateKey.PublicKey + + actor.publicKey = publicKey + actor.privateKey = privateKey + + // marshal the crypto to pem + privateKeyDer := x509.MarshalPKCS1PrivateKey(privateKey) + privateKeyBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privateKeyDer, + } + actor.privateKeyPem = string(pem.EncodeToMemory(&privateKeyBlock)) + + publicKeyDer, err := x509.MarshalPKIXPublicKey(&publicKey) + if err != nil { + log.Info("Can't marshal public key") + return Actor{}, err + } + + publicKeyBlock := pem.Block{ + Type: "PUBLIC KEY", + Headers: nil, + Bytes: publicKeyDer, + } + actor.publicKeyPem = string(pem.EncodeToMemory(&publicKeyBlock)) + + err = actor.save() + if err != nil { + return actor, err + } + + return actor, nil +} + +// GetOutboxIRI returns the outbox iri in net/url +func (a *Actor) GetOutboxIRI() *url.URL { + iri := a.iri + "/outbox" + nuiri, _ := url.Parse(iri) + return nuiri +} + +// LoadActor searches the filesystem and creates an Actor +// from the data in name.json +func LoadActor(name string) (Actor, error) { + // make sure our users can't read our hard drive + if strings.ContainsAny(name, "./ ") { + log.Info("Illegal characters in actor name") + return Actor{}, errors.New("Illegal characters in actor name") + } + jsonFile := storage + slash + "actors" + slash + name + slash + name + ".json" + fileHandle, err := os.Open(jsonFile) + if os.IsNotExist(err) { + log.Info("We don't have this kind of actor stored") + return Actor{}, err + } + byteValue, err := ioutil.ReadAll(fileHandle) + if err != nil { + log.Info("Error reading actor file") + return Actor{}, err + } + jsonData := make(map[string]interface{}) + json.Unmarshal(byteValue, &jsonData) + + nuIri, err := url.Parse(jsonData["IRI"].(string)) + if err != nil { + log.Info("Something went wrong when parsing the local actor uri into net/url") + 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) + panic("failed to parse PEM block containing the public key") + } + publicKey, err := x509.ParsePKIXPublicKey(publicKeyDecoded.Bytes) + if err != nil { + log.Info("Can't parse public keys") + log.Info(err) + return Actor{}, err + } + privateKeyDecoded, rest := pem.Decode([]byte(jsonData["PrivateKey"].(string))) + if privateKeyDecoded == nil { + log.Info(rest) + panic("failed to parse PEM block containing the private key") + } + privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes) + if err != nil { + log.Info("Can't parse private keys") + log.Info(err) + return Actor{}, err + } + + actor := Actor{ + name: name, + summary: jsonData["Summary"].(string), + actorType: jsonData["ActorType"].(string), + iri: jsonData["IRI"].(string), + nuIri: nuIri, + followers: jsonData["Followers"].(map[string]interface{}), + following: jsonData["Following"].(map[string]interface{}), + publicKey: publicKey, + privateKey: privateKey, + publicKeyPem: jsonData["PublicKey"].(string), + privateKeyPem: jsonData["PrivateKey"].(string), + followersIRI: baseURL + name + "/followers", + publicKeyID: baseURL + name + "#main-key", + } + + return actor, nil +} + +// save the actor to file +func (a *Actor) save() error { + + // check if we already have a directory to save actors + // and if not, create it + dir := storage + slash + "actors" + slash + a.name + slash + "items" + if _, err := os.Stat(dir); os.IsNotExist(err) { + os.MkdirAll(dir, 0755) + } + + actorToSave := ActorToSave{ + Name: a.name, + Summary: a.summary, + ActorType: a.actorType, + IRI: a.iri, + Followers: a.followers, + Following: a.following, + PublicKey: a.publicKeyPem, + PrivateKey: a.privateKeyPem, + } + + actorJSON, err := json.MarshalIndent(actorToSave, "", "\t") + if err != nil { + log.Info("error Marshalling actor json") + return err + } + // log.Info(actorToSave) + // log.Info(string(actorJSON)) + err = ioutil.WriteFile(storage+slash+"actors"+slash+a.name+slash+a.name+".json", actorJSON, 0644) + if err != nil { + log.Printf("WriteFileJson ERROR: %+v", err) + return err + } + + return nil +} + +func (a *Actor) whoAmI() string { + return `{"@context": "https://www.w3.org/ns/activitystreams", + "type": "` + a.actorType + `", + "id": "` + baseURL + a.name + `", + "name": "` + a.name + `", + "preferredUsername": "` + a.name + `", + "summary": "` + a.summary + `", + "inbox": "` + baseURL + a.name + `/inbox", + "outbox": "` + baseURL + a.name + `/outbox", + "followers": "` + baseURL + a.name + `/followers", + "following": "` + baseURL + a.name + `/following", + "liked": "` + baseURL + a.name + `/liked", + "publicKey": { + "id": "` + baseURL + a.name + `#main-key", + "owner": "` + baseURL + a.name + `", + "publicKeyPem": "` + strings.ReplaceAll(a.publicKeyPem, "\n", "\\n") + `" + } + }` +} + +func (a *Actor) newID() string { + return uniuri.New() +} + +// 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() + 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["actor"] = baseURL + a.name + create["cc"] = a.followersIRI + create["id"] = baseURL + a.name + "/" + id + 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["published"] = time.Now().Format(time.RFC3339) + note["url"] = create["id"] + note["type"] = "Note" + 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) +} + +func (a *Actor) saveItem(id 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) + if err != nil { + log.Printf("WriteFileJson ERROR: %+v", err) + return err + } + return nil +} + +// 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) { + return a.signedHTTPPost(content, to.String()) +} + +func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err error) { + b, err := json.Marshal(content) + if err != nil { + log.Info("Can't marshal JSON") + log.Info(err) + return + } + postSigner, _, _ := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, []string{"(request-target)", "date", "host", "digest"}, httpsig.Signature) + + byteCopy := make([]byte, len(b)) + copy(byteCopy, b) + buf := bytes.NewBuffer(byteCopy) + req, err := http.NewRequest("POST", to, buf) + if err != nil { + log.Info(err) + return + } + + // I prefer to deal with strings and just parse to net/url if and when + // needed, even if here we do one extra round trip + iri, err := url.Parse(to) + if err != nil { + log.Error("cannot parse url for POST, check your syntax") + return err + } + 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("Host", iri.Host) + req.Header.Add("Accept", "application/activity+json") + sum := sha256.Sum256(b) + req.Header.Add("Digest", + fmt.Sprintf("SHA-256=%s", + base64.StdEncoding.EncodeToString(sum[:]))) + err = postSigner.SignRequest(a.privateKey, a.publicKeyID, req) + if err != nil { + log.Info(err) + return + } + resp, err := client.Do(req) + if err != nil { + log.Info(err) + return + } + 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) + 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) + return +} + +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") + return "", err + } + + iri, err := url.Parse(address) + if err != nil { + log.Error("cannot parse url for GET, check your syntax") + return "", err + } + + 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("%s %s %s", userAgent, libName, version)) + req.Header.Add("host", iri.Host) + req.Header.Add("digest", "") + req.Header.Add("Accept", "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"") + + // set up the http signer + signer, _, _ := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, []string{"(request-target)", "date", "host", "digest"}, httpsig.Signature) + err = signer.SignRequest(a.privateKey, a.publicKeyID, req) + if err != nil { + log.Error("Can't sign the request") + return "", err + } + + resp, err := client.Do(req) + if err != nil { + log.Error("Cannot perform the GET request") + log.Error(err) + return "", err + } + 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)) + } + + responseData, _ := ioutil.ReadAll(resp.Body) + fmt.Println("GET request succeeded:", iri.String(), req.Header, resp.StatusCode, resp.Status, "\n", formatJSON(responseData)) + + responseText := string(responseData) + return responseText, nil +} \ No newline at end of file diff --git a/activityserve/http.go b/activityserve/http.go new file mode 100644 index 0000000..daee436 --- /dev/null +++ b/activityserve/http.go @@ -0,0 +1,153 @@ +package activityserve + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gologme/log" + "github.com/gorilla/mux" + + "encoding/json" +) + +// SetupHTTP starts an http server with all the required handlers +func Serve() { + + var webfingerHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/jrd+json; charset=utf-8") + account := r.URL.Query().Get("resource") // should be something like acct:user@example.com + account = strings.Replace(account, "acct:", "", 1) // remove acct: + server := strings.Split(baseURL, "://")[1] // remove protocol from baseURL. Should get example.com + server = strings.TrimSuffix(server, "/") // remove protocol from baseURL. Should get example.com + account = strings.Replace(account, "@"+server, "", 1) // remove server from handle. Should get user + actor, err := LoadActor(account) + // error out if this actor does not exist + if err != nil { + log.Info("No such actor") + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "404 - actor not found") + return + } + // response := `{"subject":"acct:` + actor.name + `@` + server + `","aliases":["` + baseURL + actor.name + `","` + baseURL + actor.name + `"],"links":[{"href":"` + baseURL + `","type":"text/html","rel":"https://webfinger.net/rel/profile-page"},{"href":"` + baseURL + actor.name + `","type":"application/activity+json","rel":"self"}]}` + + 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 + responseMap["links"] = links + + response, err := json.Marshal(responseMap) + if err != nil { + log.Error("problem creating the webfinger response json") + } + log.Info(string(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") + username := mux.Vars(r)["actor"] + log.Info(username) + if username == ".well-known" || username == "favicon.ico" { + log.Info("well-known, skipping...") + return + } + actor, err := LoadActor(username) + // error out if this actor does not exist (or there are dots or slashes in his name) + if err != nil { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "404 - page not found") + log.Info("Can't create local actor") + return + } + fmt.Fprintf(w, actor.whoAmI()) + log.Info(r.RemoteAddr) + log.Info(r.Body) + log.Info(r.Header) + } + + var outboxHandler 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"] // get the needed actor from the muxer (url variable {actor} below) + actor, err := LoadActor(username) // load the actor from disk + if err != nil { // either actor requested has illegal characters or + log.Info("Can't load local actor") // we don't have such actor + fmt.Fprintf(w, "404 - page not found") + w.WriteHeader(http.StatusNotFound) + return + } + var response string + if r.URL.Query().Get("page") == "" { + //TODO fix total items + response = `{ + "@context" : "https://www.w3.org/ns/activitystreams", + "first" : "` + baseURL + actor.name + `/outbox?page=true", + "id" : "` + baseURL + actor.name + `/outbox", + "last" : "` + baseURL + actor.name + `/outbox?min_id=0&page=true", + "totalItems" : 10, + "type" : "OrderedCollection" + }` + } else { + content := "Hello, World!" + id := "asfdasdf" + response = ` + { + "@context" : "https://www.w3.org/ns/activitystreams", + "id" : "` + baseURL + actor.name + `/outbox?min_id=0&page=true", + "next" : "` + baseURL + actor.name + `/outbox?max_id=99524642494530460&page=true", + "orderedItems" :[ + { + "actor" : "https://` + baseURL + actor.name + `", + "cc" : [ + "https://` + baseURL + actor.name + `/followers" + ], + "id" : "https://` + baseURL + actor.name + `/` + id + `", + "object" : { + "attributedTo" : "https://` + baseURL + actor.name + `", + "cc" : [ + "https://` + baseURL + actor.name + `/followers" + ], + "content" : "` + content + `", + "id" : "https://` + baseURL + actor.name + `/` + id + `", + "inReplyTo" : null, + "published" : "2019-08-26T16:25:26Z", + "to" : [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type" : "Note", + "url" : "https://` + baseURL + actor.name + `/` + id + `" + }, + "published" : "2019-08-26T16:25:26Z", + "to" : [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type" : "Create" + } + ], + "partOf" : "` + baseURL + actor.name + `/outbox", + "prev" : "` + baseURL + actor.name + `/outbox?min_id=99982453036184436&page=true", + "type" : "OrderedCollectionPage" + }` + } + w.Write([]byte(response)) + } + + // Add the handlers to a HTTP server + gorilla := mux.NewRouter() + gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler) + gorilla.HandleFunc("/{actor}/outbox", outboxHandler) + gorilla.HandleFunc("/{actor}/outbox/", outboxHandler) + // gorilla.HandleFunc("/{actor}/inbox", inboxHandler) + // gorilla.HandleFunc("/{actor}/inbox/", inboxHandler) + gorilla.HandleFunc("/{actor}/", actorHandler) + gorilla.HandleFunc("/{actor}", actorHandler) + // gorilla.HandleFunc("/{actor}/post/{hash}", postHandler) + http.Handle("/", gorilla) + + log.Fatal(http.ListenAndServe(":8081", nil)) +} diff --git a/activityserve/setup.go b/activityserve/setup.go new file mode 100644 index 0000000..267c810 --- /dev/null +++ b/activityserve/setup.go @@ -0,0 +1,82 @@ +package activityserve + +import ( + "fmt" + "os" + "net/http" + + "github.com/gologme/log" + "gopkg.in/ini.v1" +) + +var slash = string(os.PathSeparator) +var baseURL = "http://example.com/" +var storage = "storage" +var userAgent = "activityserve" + +const libName = "activityserve" +const version = "0.99" + +var client = http.Client{} + +// Setup sets our environment up +func Setup(configurationFile string, debug bool) { + // read configuration file (config.ini) + + if configurationFile == "" { + configurationFile = "config.ini" + } + + cfg, err := ini.Load("config.ini") + if err != nil { + fmt.Printf("Fail to read file: %v", err) + os.Exit(1) + } + + // Load base url from configuration file + baseURL = cfg.Section("general").Key("baseURL").String() + // check if it ends with a / and append one if not + if baseURL[len(baseURL)-1:] != "/" { + baseURL += "/" + } + // print it for our users + fmt.Println() + fmt.Println("Domain Name:", baseURL) + + // Load storage location (only local filesystem supported for now) from config + storage = cfg.Section("general").Key("storage").String() + cwd, err := os.Getwd() + fmt.Println("Storage Location:", cwd+slash+storage) + fmt.Println() + + SetupStorage(storage) + + // Load user agent + userAgent = cfg.Section("general").Key("userAgent").String() + + // I prefer long file so that I can click it in the terminal and open it + // in the editor above + log.SetFlags(log.Llongfile) + // log.SetFlags(log.LstdFlags | log.Lshortfile) + log.EnableLevel("warn") + // create a logger with levels but without prefixes for easier to read + // debug output + printer := log.New(os.Stdout, " ", 0) + + if debug == true { + fmt.Println() + fmt.Println("debug mode on") + log.EnableLevel("info") + printer.EnableLevel("info") + } +} + +// SetupStorage creates storage +func SetupStorage(storage string) { + // prepare storage for foreign activities (activities we store that don't + // belong to us) + foreignDir := storage + slash + "foreign" + if _, err := os.Stat(foreignDir); os.IsNotExist(err) { + os.MkdirAll(foreignDir, 0755) + } +} diff --git a/activityserve/util.go b/activityserve/util.go new file mode 100644 index 0000000..ba937a6 --- /dev/null +++ b/activityserve/util.go @@ -0,0 +1,41 @@ +package activityserve + +import ( + "net/http" + // "net/url" + "bytes" + "encoding/json" + + // "time" + // "fmt" + "github.com/gologme/log" + // "github.com/go-fed/httpsig" +) + +func isSuccess(code int) bool { + return code == http.StatusOK || + code == http.StatusCreated || + code == http.StatusAccepted +} + +//PrettyPrint maps +func PrettyPrint(themap map[string]interface{}) { + b, err := json.MarshalIndent(themap, "", " ") + if err != nil { + log.Info("error:", err) + } + log.Print(string(b)) +} + +//PrettyPrintJSON does what it's name says +func PrettyPrintJSON(theJSON []byte) { + dst := new(bytes.Buffer) + json.Indent(dst, theJSON, "", "\t") + log.Info(dst) +} + +func formatJSON(theJSON []byte) string{ + dst := new(bytes.Buffer) + json.Indent(dst, theJSON, "", "\t") + return dst.String() +} diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..377292d --- /dev/null +++ b/config.ini @@ -0,0 +1,5 @@ +[general] + +baseURL = https://floorb.qwazix.com +storage = storage ; can be relative or absolute path +userAgent = "pherephone" \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..536f5be --- /dev/null +++ b/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "flag" + // "os" + // "strings" + + // "errors" + + // "encoding/json" + // "io/ioutil" + // "net/http" + + // "net/url" + // "context" + // "html" + + "github.com/gologme/log" + + // "github.com/go-fed/activity/streams" + // "github.com/gorilla/mux" + // "gopkg.in/ini.v1" + // "github.com/davecgh/go-spew/spew" + + "./activityserve" +) + + +var err error + +func main() { + + // This is here for debugging purposes. I want to be able to easily spot in the terminal + // when a single execution starts + fmt.Println() + fmt.Println("======================= PHeRePHoNe ==========================") + + // introduce ourselves + fmt.Println() + fmt.Println("Pherephone follows some accounts and boosts") + fmt.Println("whatever they post to our followers. See config.ini ") + fmt.Println("for more information and how to set up. ") + + debugFlag := flag.Bool("debug", false, "set to true to get debugging information in the console") + flag.Parse() + + if *debugFlag == true { + log.EnableLevel("info") + } + + activityserve.Setup("config.ini", *debugFlag) + + 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!") + + activityserve.Serve() +} \ No newline at end of file