jlelse
/
jsonpub
Archived
1
Fork 0
This repository has been archived on 2020-04-25. You can view files and clone it, but cannot push or open issues or pull requests.
jsonpub/actor.go

271 lines
7.5 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"
"sync"
"time"
)
type Actor struct {
Name, iri, feed string
followers map[string]interface{}
privateKey crypto.PrivateKey
publicKeyID string
postSigner httpsig.Signer
postSignMutex *sync.Mutex
}
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")
}
postSigner, _, _ := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, "SHA-256", []string{"(request-target)", "date", "host", "digest"}, httpsig.Signature)
actor := Actor{
Name: name,
iri: iri,
feed: feed,
followers: make(map[string]interface{}),
privateKey: privateKey,
publicKeyID: iri + "#main-key",
postSigner: postSigner,
postSignMutex: &sync.Mutex{},
}
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)
defer func() { _ = f.Close() }()
if err != nil {
return err
}
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)
_ = resp.Body.Close()
if err != nil {
return errors.New("failed to decode fetched article")
}
// create["object"] = url
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
}
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
}
a.postSignMutex.Lock()
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 = a.postSigner.SignRequest(a.privateKey, a.publicKeyID, req, byteCopy)
a.postSignMutex.Unlock()
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{}) {
for _, follower := range a.followers {
inbox := follower.(string)
go func() { _ = a.signedHTTPPost(activity, inbox) }()
}
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)
if telegramBot != nil {
_ = telegramBot.Post(follower.iri + " followed")
}
}
}