commit
f67906c96a
|
@ -0,0 +1,2 @@
|
|||
*.snip
|
||||
storage
|
|
@ -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 <actor>.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
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
[general]
|
||||
|
||||
baseURL = https://floorb.qwazix.com
|
||||
storage = storage ; can be relative or absolute path
|
||||
userAgent = "pherephone"
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue