266 lines
7.3 KiB
Go
266 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/dchest/uniuri"
|
|
"github.com/go-fed/httpsig"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Actor struct {
|
|
Name, iri, feed string
|
|
followers map[string]interface{}
|
|
privateKey crypto.PrivateKey
|
|
publicKeyID string
|
|
}
|
|
|
|
type ActorToSave struct {
|
|
Name string
|
|
Followers map[string]interface{}
|
|
}
|
|
|
|
func GetActor(name, iri, feed, pk string) (Actor, error) {
|
|
// make sure users can't read hard drive
|
|
if strings.ContainsAny(name, "./ ") {
|
|
return Actor{}, errors.New("illegal characters in actor name")
|
|
}
|
|
// populate actor
|
|
pkfile, err := ioutil.ReadFile(pk)
|
|
if err != nil {
|
|
return Actor{}, errors.New("failed to read private key file")
|
|
}
|
|
privateKeyDecoded, rest := pem.Decode(pkfile)
|
|
if privateKeyDecoded == nil {
|
|
fmt.Println(rest)
|
|
return Actor{}, errors.New("failed to decode the private key PEM")
|
|
}
|
|
privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
|
if err != nil {
|
|
return Actor{}, errors.New("failed to parse the private key")
|
|
}
|
|
actor := Actor{
|
|
Name: name,
|
|
iri: iri,
|
|
feed: feed,
|
|
followers: make(map[string]interface{}),
|
|
privateKey: privateKey,
|
|
publicKeyID: iri + "#main-key",
|
|
}
|
|
jsonFile := storage + slash + "actors" + slash + name + slash + name + ".json"
|
|
fileHandle, err := os.Open(jsonFile)
|
|
defer func() {
|
|
_ = fileHandle.Close()
|
|
}()
|
|
if os.IsNotExist(err) {
|
|
// File doesn't exist, maybe it's a new actor
|
|
return actor, nil
|
|
}
|
|
savedActor := ActorToSave{}
|
|
err = json.NewDecoder(fileHandle).Decode(&savedActor)
|
|
if err != nil {
|
|
// Ignore error, but return actor
|
|
return actor, nil
|
|
}
|
|
if savedActor.Followers != nil {
|
|
actor.followers = savedActor.Followers
|
|
}
|
|
return actor, nil
|
|
}
|
|
|
|
// save the actor to file
|
|
func (a *Actor) save() error {
|
|
// check if directory already exists
|
|
dir := storage + slash + "actors" + slash + a.Name
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
_ = os.MkdirAll(dir, 0755)
|
|
}
|
|
actorToSave := ActorToSave{
|
|
Name: a.Name,
|
|
Followers: a.followers,
|
|
}
|
|
f, err := os.OpenFile(storage+slash+"actors"+slash+a.Name+slash+a.Name+".json", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_ = f.Close()
|
|
}()
|
|
jsonEncoder := json.NewEncoder(f)
|
|
jsonEncoder.SetIndent("", "\t")
|
|
err = jsonEncoder.Encode(actorToSave)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Actor) PostArticle(url string) error {
|
|
create := make(map[string]interface{})
|
|
note := make(map[string]interface{})
|
|
create["@context"] = context()
|
|
create["actor"] = a.iri
|
|
create["id"] = url
|
|
create["published"] = note["published"]
|
|
create["type"] = "Create"
|
|
article := make(map[string]interface{})
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return errors.New("failed to create request to fetch article")
|
|
}
|
|
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", libName, version))
|
|
req.Header.Add("Accept", ContentTypeAs2)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil || !isSuccess(resp.StatusCode) {
|
|
return errors.New("failed to fetch article")
|
|
}
|
|
err = json.NewDecoder(resp.Body).Decode(&article)
|
|
if err != nil {
|
|
return errors.New("failed to decode fetched article")
|
|
}
|
|
create["object"] = article
|
|
a.sendToFollowers(&create)
|
|
// Boost article if it contains "inReplyTo"
|
|
if article["inReplyTo"] != nil {
|
|
announce := make(map[string]interface{})
|
|
announce["@context"] = context()
|
|
announce["id"] = url + "#Announce"
|
|
announce["type"] = "Announce"
|
|
announce["object"] = url
|
|
announce["actor"] = a.iri
|
|
announce["to"] = []string{"https://www.w3.org/ns/activitystreams#Public"}
|
|
announce["published"] = article["published"]
|
|
a.sendToFollowers(&announce)
|
|
}
|
|
// Send update event if it contains "updated" and "updated" != "published"
|
|
if article["updated"] != nil && article["published"] != nil && article["updated"] != article["published"] {
|
|
update := make(map[string]interface{})
|
|
update["@context"] = context()
|
|
update["type"] = "Update"
|
|
update["object"] = url
|
|
update["actor"] = a.iri
|
|
a.sendToFollowers(&update)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// signedHTTPPost performs an HTTP post on behalf of Actor with the
|
|
// request-target, date, host and digest headers signed
|
|
// with the actor's private key.
|
|
func (a *Actor) signedHTTPPost(content *map[string]interface{}, to string) (err error) {
|
|
b, err := json.Marshal(*content)
|
|
if err != nil {
|
|
return
|
|
}
|
|
postSigner, _, _ := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, "SHA-256", []string{"(request-target)", "date", "host", "digest"}, httpsig.Signature)
|
|
byteCopy := make([]byte, len(b))
|
|
copy(byteCopy, b)
|
|
buf := bytes.NewBuffer(byteCopy)
|
|
req, err := http.NewRequest(http.MethodPost, to, buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
iri, err := url.Parse(to)
|
|
if err != nil {
|
|
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", libName, version))
|
|
req.Header.Add("Host", iri.Host)
|
|
req.Header.Add("Accept", "application/activity+json; charset=utf-8")
|
|
req.Header.Add("Content-Type", "application/activity+json; charset=utf-8")
|
|
err = postSigner.SignRequest(a.privateKey, a.publicKeyID, req, byteCopy)
|
|
if err != nil {
|
|
return
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !isSuccess(resp.StatusCode) {
|
|
err = errors.New("post request failed")
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// NewFollower records a new follower to the actor file
|
|
func (a *Actor) NewFollower(iri string, inbox string) error {
|
|
a.followers[iri] = inbox
|
|
return a.save()
|
|
}
|
|
|
|
func (a *Actor) RemoveFollower(iri string) error {
|
|
if _, ok := a.followers[iri]; ok {
|
|
delete(a.followers, iri)
|
|
return a.save()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// send to followers sends a batch of http posts to each one of the followers
|
|
func (a *Actor) sendToFollowers(activity *map[string]interface{}) {
|
|
recipients := make([]string, len(a.followers))
|
|
i := 0
|
|
for _, inbox := range a.followers {
|
|
recipients[i] = inbox.(string)
|
|
i++
|
|
}
|
|
for _, v := range recipients {
|
|
_ = a.signedHTTPPost(activity, v)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (a *Actor) newID() (hash string, url string) {
|
|
hash = uniuri.New()
|
|
return hash, a.iri + hash
|
|
}
|
|
|
|
// Accept a follow request
|
|
func (a *Actor) Accept(follow *map[string]interface{}) {
|
|
// it's a follow, write it down
|
|
newFollower := (*follow)["actor"].(string)
|
|
fmt.Println("New follow request:", newFollower)
|
|
// check we aren't following ourselves
|
|
if newFollower == (*follow)["object"] {
|
|
// actor and object are equal
|
|
return
|
|
}
|
|
follower, err := NewRemoteActor(newFollower)
|
|
if err != nil {
|
|
// Couldn't retrieve remote actor info
|
|
fmt.Println("Failed to retrieve remote actor info:", newFollower)
|
|
return
|
|
}
|
|
// Add or update follower
|
|
_ = a.NewFollower(newFollower, follower.inbox)
|
|
// remove @context from the inner activity
|
|
delete(*follow, "@context")
|
|
accept := make(map[string]interface{})
|
|
accept["@context"] = "https://www.w3.org/ns/activitystreams"
|
|
accept["to"] = (*follow)["actor"]
|
|
_, accept["id"] = a.newID()
|
|
accept["actor"] = a.iri
|
|
accept["object"] = *follow
|
|
accept["type"] = "Accept"
|
|
err = a.signedHTTPPost(&accept, follower.inbox)
|
|
if err != nil {
|
|
fmt.Println("Failed to accept:", follower.iri)
|
|
fmt.Println(err.Error())
|
|
} else {
|
|
fmt.Println("Accepted:", follower.iri)
|
|
}
|
|
}
|