diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f79c63e..ff1d87a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,7 +13,7 @@ jobs:
- name: Execute test and upload coverage
run: |
go version
- go test -coverprofile=coverage.txt -covermode=atomic -p 1 . ./worker ./cli ./State
+ go test -coverprofile=coverage.txt -covermode=atomic -p 1 ./api ./worker ./cli ./models
bash <(curl -s https://codecov.io/bash)
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/Dockerfile b/Dockerfile
index f9420ef..a6d4dfe 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,12 +5,12 @@ COPY . /Activity-Relay
RUN mkdir -p /rootfs/usr/bin && \
apk add -U --no-cache git && \
- go build -o /rootfs/usr/bin/server -ldflags "-X main.version=$(git describe --tags HEAD)" . && \
+ go build -o /rootfs/usr/bin/relay -ldflags "-X main.version=$(git describe --tags HEAD)" . && \
go build -o /rootfs/usr/bin/worker -ldflags "-X main.version=$(git describe --tags HEAD)" ./worker && \
go build -o /rootfs/usr/bin/ar-cli -ldflags "-X main.version=$(git describe --tags HEAD)" ./cli
FROM alpine
COPY --from=build /rootfs/usr/bin /usr/bin
-RUN chmod +x /usr/bin/server /usr/bin/worker /usr/bin/ar-cli && \
+RUN chmod +x /usr/bin/relay /usr/bin/worker /usr/bin/ar-cli && \
apk add -U --no-cache ca-certificates
diff --git a/KeyLoader/keyloader.go b/KeyLoader/keyloader.go
index 212b337..df73c6d 100644
--- a/KeyLoader/keyloader.go
+++ b/KeyLoader/keyloader.go
@@ -4,9 +4,7 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
- "fmt"
"io/ioutil"
- "os"
)
func ReadPrivateKeyRSAfromPath(path string) (*rsa.PrivateKey, error) {
@@ -21,29 +19,3 @@ func ReadPrivateKeyRSAfromPath(path string) (*rsa.PrivateKey, error) {
}
return priv, nil
}
-
-func ReadPublicKeyRSAfromString(pemString string) (*rsa.PublicKey, error) {
- pemByte := []byte(pemString)
- decoded, _ := pem.Decode(pemByte)
- defer func() {
- recover()
- }()
- keyInterface, err := x509.ParsePKIXPublicKey(decoded.Bytes)
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- return nil, err
- }
- pub := keyInterface.(*rsa.PublicKey)
- return pub, nil
-}
-
-func GeneratePublicKeyPEMString(publicKey *rsa.PublicKey) string {
- publicKeyByte := x509.MarshalPKCS1PublicKey(publicKey)
- publicKeyPem := pem.EncodeToMemory(
- &pem.Block{
- Type: "RSA PUBLIC KEY",
- Bytes: publicKeyByte,
- },
- )
- return string(publicKeyPem)
-}
diff --git a/api/api.go b/api/api.go
new file mode 100644
index 0000000..9a5511a
--- /dev/null
+++ b/api/api.go
@@ -0,0 +1,84 @@
+package api
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/RichardKnop/machinery/v1"
+ cache "github.com/patrickmn/go-cache"
+ "github.com/yukimochi/Activity-Relay/models"
+)
+
+var (
+ version string
+ globalConfig *models.RelayConfig
+
+ // Actor : Relay's Actor
+ Actor models.Actor
+
+ // WebfingerResource : Relay's Webfinger resource
+ WebfingerResource models.WebfingerResource
+
+ // Nodeinfo : Relay's Nodeinfo
+ Nodeinfo models.NodeinfoResources
+
+ hostURL *url.URL
+ relayState models.RelayState
+ machineryServer *machinery.Server
+ actorCache *cache.Cache
+)
+
+func Entrypoint(g *models.RelayConfig, v string) error {
+ var err error
+ globalConfig = g
+ version = v
+
+ err = initialize(globalConfig)
+ if err != nil {
+ return err
+ }
+
+ registResourceHandlers()
+
+ fmt.Println("Staring API Server at", globalConfig.ServerBind())
+ err = http.ListenAndServe(globalConfig.ServerBind(), nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func initialize(globalConfig *models.RelayConfig) error {
+ var err error
+
+ redisClient := globalConfig.RedisClient()
+ relayState = models.NewState(redisClient, true)
+ relayState.ListenNotify(nil)
+
+ machineryServer, err = models.NewMachineryServer(globalConfig)
+ if err != nil {
+ return err
+ }
+
+ Actor = models.NewActivityPubActorFromSelfKey(globalConfig)
+ actorCache = cache.New(5*time.Minute, 10*time.Minute)
+
+ hostURL = globalConfig.ServerHostname()
+ WebfingerResource.GenerateFromActor(hostURL, &Actor)
+ Nodeinfo.GenerateFromActor(hostURL, &Actor, version)
+
+ return nil
+}
+
+func registResourceHandlers() {
+ http.HandleFunc("/.well-known/nodeinfo", handleNodeinfoLink)
+ http.HandleFunc("/.well-known/webfinger", handleWebfinger)
+ http.HandleFunc("/nodeinfo/2.1", handleNodeinfo)
+ http.HandleFunc("/actor", handleActor)
+ http.HandleFunc("/inbox", func(w http.ResponseWriter, r *http.Request) {
+ handleInbox(w, r, decodeActivity)
+ })
+}
diff --git a/api/api_test.go b/api/api_test.go
new file mode 100644
index 0000000..3395abd
--- /dev/null
+++ b/api/api_test.go
@@ -0,0 +1,38 @@
+package api
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/yukimochi/Activity-Relay/models"
+)
+
+func TestMain(m *testing.M) {
+ var err error
+
+ testConfigPath := "../misc/config.yml"
+ file, _ := os.Open(testConfigPath)
+ defer file.Close()
+
+ viper.SetConfigType("yaml")
+ viper.ReadConfig(file)
+ viper.Set("actor_pem", "../misc/testKey.pem")
+
+ globalConfig, err = models.NewRelayConfig()
+ if err != nil {
+ fmt.Println(err.Error())
+ os.Exit(1)
+ }
+
+ err = initialize(globalConfig)
+ if err != nil {
+ fmt.Println(err.Error())
+ os.Exit(1)
+ }
+ relayState = models.NewState(relayState.RedisClient, false)
+ relayState.RedisClient.FlushAll().Result()
+ code := m.Run()
+ os.Exit(code)
+}
diff --git a/decode.go b/api/decode.go
similarity index 65%
rename from decode.go
rename to api/decode.go
index f9e3414..5edb274 100644
--- a/decode.go
+++ b/api/decode.go
@@ -1,4 +1,4 @@
-package main
+package api
import (
"crypto/sha256"
@@ -9,13 +9,11 @@ import (
"net/http"
"strconv"
- "github.com/spf13/viper"
- activitypub "github.com/yukimochi/Activity-Relay/ActivityPub"
- keyloader "github.com/yukimochi/Activity-Relay/KeyLoader"
+ "github.com/yukimochi/Activity-Relay/models"
"github.com/yukimochi/httpsig"
)
-func decodeActivity(request *http.Request) (*activitypub.Activity, *activitypub.Actor, []byte, error) {
+func decodeActivity(request *http.Request) (*models.Activity, *models.Actor, []byte, error) {
request.Header.Set("Host", request.Host)
dataLen, _ := strconv.Atoi(request.Header.Get("Content-Length"))
body := make([]byte, dataLen)
@@ -27,12 +25,12 @@ func decodeActivity(request *http.Request) (*activitypub.Activity, *activitypub.
return nil, nil, nil, err
}
KeyID := verifier.KeyId()
- keyOwnerActor := new(activitypub.Actor)
- err = keyOwnerActor.RetrieveRemoteActor(KeyID, fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", viper.GetString("relay_servicename"), version, hostURL.Host), actorCache)
+ keyOwnerActor := new(models.Actor)
+ err = keyOwnerActor.RetrieveRemoteActor(KeyID, fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", globalConfig.ServerServicename(), version, hostURL.Host), actorCache)
if err != nil {
return nil, nil, nil, err
}
- PubKey, err := keyloader.ReadPublicKeyRSAfromString(keyOwnerActor.PublicKey.PublicKeyPem)
+ PubKey, err := models.ReadPublicKeyRSAfromString(keyOwnerActor.PublicKey.PublicKeyPem)
if PubKey == nil {
return nil, nil, nil, errors.New("Failed parse PublicKey from string")
}
@@ -56,14 +54,14 @@ func decodeActivity(request *http.Request) (*activitypub.Activity, *activitypub.
}
// Parse Activity
- var activity activitypub.Activity
+ var activity models.Activity
err = json.Unmarshal(body, &activity)
if err != nil {
return nil, nil, nil, err
}
- var remoteActor activitypub.Actor
- err = remoteActor.RetrieveRemoteActor(activity.Actor, fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", viper.GetString("relay_servicename"), version, hostURL.Host), actorCache)
+ var remoteActor models.Actor
+ err = remoteActor.RetrieveRemoteActor(activity.Actor, fmt.Sprintf("%s (golang net/http; Activity-Relay %s; %s)", globalConfig.ServerServicename(), version, hostURL.Host), actorCache)
if err != nil {
return nil, nil, nil, err
}
diff --git a/decode_test.go b/api/decode_test.go
similarity index 91%
rename from decode_test.go
rename to api/decode_test.go
index 7580053..7782164 100644
--- a/decode_test.go
+++ b/api/decode_test.go
@@ -1,4 +1,4 @@
-package main
+package api
import (
"bytes"
@@ -9,18 +9,18 @@ import (
"strconv"
"testing"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/models"
)
func TestDecodeActivity(t *testing.T) {
relayState.RedisClient.FlushAll().Result()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: "innocent.yukimochi.io",
InboxURL: "https://innocent.yukimochi.io/inbox",
})
- file, _ := os.Open("./misc/create.json")
+ file, _ := os.Open("../misc/create.json")
body, _ := ioutil.ReadAll(file)
length := strconv.Itoa(len(body))
req, _ := http.NewRequest("POST", "/inbox", bytes.NewReader(body))
@@ -45,12 +45,12 @@ func TestDecodeActivity(t *testing.T) {
func TestDecodeActivityWithNoSignature(t *testing.T) {
relayState.RedisClient.FlushAll().Result()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: "innocent.yukimochi.io",
InboxURL: "https://innocent.yukimochi.io/inbox",
})
- file, _ := os.Open("./misc/create.json")
+ file, _ := os.Open("../misc/create.json")
body, _ := ioutil.ReadAll(file)
length := strconv.Itoa(len(body))
req, _ := http.NewRequest("POST", "/inbox", bytes.NewReader(body))
@@ -69,12 +69,12 @@ func TestDecodeActivityWithNoSignature(t *testing.T) {
func TestDecodeActivityWithNotFoundKeyId(t *testing.T) {
relayState.RedisClient.FlushAll().Result()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: "innocent.yukimochi.io",
InboxURL: "https://innocent.yukimochi.io/inbox",
})
- file, _ := os.Open("./misc/create.json")
+ file, _ := os.Open("../misc/create.json")
body, _ := ioutil.ReadAll(file)
length := strconv.Itoa(len(body))
req, _ := http.NewRequest("POST", "/inbox", bytes.NewReader(body))
@@ -94,12 +94,12 @@ func TestDecodeActivityWithNotFoundKeyId(t *testing.T) {
func TestDecodeActivityWithInvalidDigest(t *testing.T) {
relayState.RedisClient.FlushAll().Result()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: "innocent.yukimochi.io",
InboxURL: "https://innocent.yukimochi.io/inbox",
})
- file, _ := os.Open("./misc/create.json")
+ file, _ := os.Open("../misc/create.json")
body, _ := ioutil.ReadAll(file)
length := strconv.Itoa(len(body))
req, _ := http.NewRequest("POST", "/inbox", bytes.NewReader(body))
diff --git a/handle.go b/api/handle.go
similarity index 91%
rename from handle.go
rename to api/handle.go
index d6da6b8..2d79665 100644
--- a/handle.go
+++ b/api/handle.go
@@ -1,4 +1,4 @@
-package main
+package api
import (
"encoding/json"
@@ -9,8 +9,7 @@ import (
"os"
"github.com/RichardKnop/machinery/v1/tasks"
- activitypub "github.com/yukimochi/Activity-Relay/ActivityPub"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/models"
)
func handleWebfinger(writer http.ResponseWriter, request *http.Request) {
@@ -95,7 +94,7 @@ func contains(entries interface{}, finder string) bool {
}
}
return false
- case []state.Subscription:
+ case []models.Subscription:
for i := 0; i < len(entry); i++ {
if entry[i].Domain == finder {
return true
@@ -156,7 +155,7 @@ func pushRegistorJob(inboxURL string, body []byte) {
}
}
-func followAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) error {
+func followAcceptable(activity *models.Activity, actor *models.Actor) error {
if contains(activity.Object, "https://www.w3.org/ns/activitystreams#Public") {
return nil
} else {
@@ -164,7 +163,7 @@ func followAcceptable(activity *activitypub.Activity, actor *activitypub.Actor)
}
}
-func unFollowAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) error {
+func unFollowAcceptable(activity *models.Activity, actor *models.Actor) error {
if contains(activity.Object, "https://www.w3.org/ns/activitystreams#Public") {
return nil
} else {
@@ -172,7 +171,7 @@ func unFollowAcceptable(activity *activitypub.Activity, actor *activitypub.Actor
}
}
-func suitableFollow(activity *activitypub.Activity, actor *activitypub.Actor) bool {
+func suitableFollow(activity *models.Activity, actor *models.Actor) bool {
domain, _ := url.Parse(activity.Actor)
if contains(relayState.BlockedDomains, domain.Host) {
return false
@@ -180,7 +179,7 @@ func suitableFollow(activity *activitypub.Activity, actor *activitypub.Actor) bo
return true
}
-func relayAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) error {
+func relayAcceptable(activity *models.Activity, actor *models.Actor) error {
if !contains(activity.To, "https://www.w3.org/ns/activitystreams#Public") && !contains(activity.Cc, "https://www.w3.org/ns/activitystreams#Public") {
return errors.New("Activity should contain https://www.w3.org/ns/activitystreams#Public as receiver")
}
@@ -191,7 +190,7 @@ func relayAcceptable(activity *activitypub.Activity, actor *activitypub.Actor) e
return errors.New("To use the relay service, Subscribe me in advance")
}
-func suitableRelay(activity *activitypub.Activity, actor *activitypub.Actor) bool {
+func suitableRelay(activity *models.Activity, actor *models.Actor) bool {
domain, _ := url.Parse(activity.Actor)
if contains(relayState.LimitedDomains, domain.Host) {
return false
@@ -202,7 +201,7 @@ func suitableRelay(activity *activitypub.Activity, actor *activitypub.Actor) boo
return true
}
-func handleInbox(writer http.ResponseWriter, request *http.Request, activityDecoder func(*http.Request) (*activitypub.Activity, *activitypub.Actor, []byte, error)) {
+func handleInbox(writer http.ResponseWriter, request *http.Request, activityDecoder func(*http.Request) (*models.Activity, *models.Actor, []byte, error)) {
switch request.Method {
case "POST":
activity, actor, body, err := activityDecoder(request)
@@ -237,7 +236,7 @@ func handleInbox(writer http.ResponseWriter, request *http.Request, activityDeco
resp := activity.GenerateResponse(hostURL, "Accept")
jsonData, _ := json.Marshal(&resp)
go pushRegistorJob(actor.Inbox, jsonData)
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: actor.Endpoints.SharedInbox,
ActivityID: activity.ID,
diff --git a/handle_test.go b/api/handle_test.go
similarity index 92%
rename from handle_test.go
rename to api/handle_test.go
index 6171309..19c84b4 100644
--- a/handle_test.go
+++ b/api/handle_test.go
@@ -1,4 +1,4 @@
-package main
+package api
import (
"encoding/json"
@@ -11,12 +11,11 @@ import (
"strconv"
"testing"
- activitypub "github.com/yukimochi/Activity-Relay/ActivityPub"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/models"
)
const (
- BlockService state.Config = iota
+ BlockService models.Config = iota
ManuallyAccept
CreateAsAnnounce
)
@@ -43,7 +42,7 @@ func TestHandleWebfingerGet(t *testing.T) {
defer r.Body.Close()
data, _ := ioutil.ReadAll(r.Body)
- var wfresource activitypub.WebfingerResource
+ var wfresource models.WebfingerResource
err = json.Unmarshal(data, &wfresource)
if err != nil {
t.Fatalf("WebfingerResource response is not valid.")
@@ -92,7 +91,7 @@ func TestHandleNodeinfoLinkGet(t *testing.T) {
defer r.Body.Close()
data, _ := ioutil.ReadAll(r.Body)
- var nodeinfoLinks activitypub.NodeinfoLinks
+ var nodeinfoLinks models.NodeinfoLinks
err = json.Unmarshal(data, &nodeinfoLinks)
if err != nil {
t.Fatalf("NodeinfoLinks response is not valid.")
@@ -133,7 +132,7 @@ func TestHandleNodeinfoGet(t *testing.T) {
defer r.Body.Close()
data, _ := ioutil.ReadAll(r.Body)
- var nodeinfo activitypub.Nodeinfo
+ var nodeinfo models.Nodeinfo
err = json.Unmarshal(data, &nodeinfo)
if err != nil {
t.Fatalf("Nodeinfo response is not valid.")
@@ -187,7 +186,7 @@ func TestHandleActorGet(t *testing.T) {
defer r.Body.Close()
data, _ := ioutil.ReadAll(r.Body)
- var actor activitypub.Actor
+ var actor models.Actor
err = json.Unmarshal(data, &actor)
if err != nil {
t.Fatalf("Actor response is not valid.")
@@ -241,8 +240,8 @@ func TestContains(t *testing.T) {
}
}
-func mockActivityDecoderProvider(activity *activitypub.Activity, actor *activitypub.Actor) func(r *http.Request) (*activitypub.Activity, *activitypub.Actor, []byte, error) {
- return func(r *http.Request) (*activitypub.Activity, *activitypub.Actor, []byte, error) {
+func mockActivityDecoderProvider(activity *models.Activity, actor *models.Actor) func(r *http.Request) (*models.Activity, *models.Actor, []byte, error) {
+ return func(r *http.Request) (*models.Activity, *models.Actor, []byte, error) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
@@ -252,57 +251,57 @@ func mockActivityDecoderProvider(activity *activitypub.Activity, actor *activity
}
}
-func mockActivity(req string) activitypub.Activity {
+func mockActivity(req string) models.Activity {
switch req {
case "Follow":
- file, _ := os.Open("./misc/follow.json")
+ file, _ := os.Open("../misc/follow.json")
body, _ := ioutil.ReadAll(file)
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal(body, &activity)
return activity
case "Invalid-Follow":
- file, _ := os.Open("./misc/followAsActor.json")
+ file, _ := os.Open("../misc/followAsActor.json")
body, _ := ioutil.ReadAll(file)
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal(body, &activity)
return activity
case "Unfollow":
- file, _ := os.Open("./misc/unfollow.json")
+ file, _ := os.Open("../misc/unfollow.json")
body, _ := ioutil.ReadAll(file)
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal(body, &activity)
return activity
case "Invalid-Unfollow":
body := "{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.test.yukimochi.io/c125e836-e622-478e-a22d-2d9fbf2f496f\",\"type\":\"Undo\",\"actor\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"object\":{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://hacked.test.yukimochi.io/c125e836-e622-478e-a22d-2d9fbf2f496f\",\"type\":\"Follow\",\"actor\":\"https://hacked.test.yukimochi.io/users/yukimochi\",\"object\":\"https://www.w3.org/ns/activitystreams#Public\"}}"
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal([]byte(body), &activity)
return activity
case "UnfollowAsActor":
body := "{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.test.yukimochi.io/c125e836-e622-478e-a22d-2d9fbf2f496f\",\"type\":\"Undo\",\"actor\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"object\":{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://hacked.test.yukimochi.io/c125e836-e622-478e-a22d-2d9fbf2f496f\",\"type\":\"Follow\",\"actor\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"object\":\"https://relay.yukimochi.example.org/actor\"}}"
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal([]byte(body), &activity)
return activity
case "Create":
- file, _ := os.Open("./misc/create.json")
+ file, _ := os.Open("../misc/create.json")
body, _ := ioutil.ReadAll(file)
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal(body, &activity)
return activity
case "Create-Article":
body := "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"sensitive\":\"as:sensitive\",\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"Hashtag\":\"as:Hashtag\",\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"toot\":\"http://joinmastodon.org/ns#\",\"Emoji\":\"toot:Emoji\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"},\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\"}],\"id\":\"https://mastodon.test.yukimochi.io/users/yukimochi/statuses/101075045564444857/activity\",\"type\":\"Create\",\"actor\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"published\":\"2018-11-15T11:07:26Z\",\"to\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"cc\":[\"https://mastodon.test.yukimochi.io/users/yukimochi/followers\"],\"object\":{\"id\":\"https://mastodon.test.yukimochi.io/users/yukimochi/statuses/101075045564444857\",\"type\":\"Article\",\"summary\":null,\"inReplyTo\":null,\"published\":\"2018-11-15T11:07:26Z\",\"url\":\"https://mastodon.test.yukimochi.io/@yukimochi/101075045564444857\",\"attributedTo\":\"https://mastodon.test.yukimochi.io/users/yukimochi\",\"to\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"cc\":[\"https://mastodon.test.yukimochi.io/users/yukimochi/followers\"],\"sensitive\":false,\"atomUri\":\"https://mastodon.test.yukimochi.io/users/yukimochi/statuses/101075045564444857\",\"inReplyToAtomUri\":null,\"conversation\":\"tag:mastodon.test.yukimochi.io,2018-11-15:objectId=68:objectType=Conversation\",\"content\":\"
Actvity-Relay
\",\"contentMap\":{\"en\":\"Actvity-Relay
\"},\"attachment\":[],\"tag\":[]},\"signature\":{\"type\":\"RsaSignature2017\",\"creator\":\"https://mastodon.test.yukimochi.io/users/yukimochi#main-key\",\"created\":\"2018-11-15T11:07:26Z\",\"signatureValue\":\"mMgl2GgVPgb1Kw6a2iDIZc7r0j3ob+Cl9y+QkCxIe6KmnUzb15e60UuhkE5j3rJnoTwRKqOFy1PMkSxlYW6fPG/5DBxW9I4kX+8sw8iH/zpwKKUOnXUJEqfwRrNH2ix33xcs/GkKPdedY6iAPV9vGZ10MSMOdypfYgU9r+UI0sTaaC2iMXH0WPnHQuYAI+Q1JDHIbDX5FH1WlDL6+8fKAicf3spBMxDwPHGPK8W2jmDLWdN2Vz4ffsCtWs5BCuqOKZrtTW0Rdd4HWzo40MnRXvBjv7yNlnnKzokANBqiOLWT7kNfK0+Vtnt6c/bNX64KBro53KR7wL3ZBvPVuv5rdQ==\"}}"
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal([]byte(body), &activity)
return activity
case "Announce":
- file, _ := os.Open("./misc/announce.json")
+ file, _ := os.Open("../misc/announce.json")
body, _ := ioutil.ReadAll(file)
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal(body, &activity)
return activity
case "Undo":
- file, _ := os.Open("./misc/undo.json")
+ file, _ := os.Open("../misc/undo.json")
body, _ := ioutil.ReadAll(file)
- var activity activitypub.Activity
+ var activity models.Activity
json.Unmarshal(body, &activity)
return activity
default:
@@ -310,24 +309,24 @@ func mockActivity(req string) activitypub.Activity {
}
}
-func mockActor(req string) activitypub.Actor {
+func mockActor(req string) models.Actor {
switch req {
case "Person":
- file, _ := os.Open("./misc/person.json")
+ file, _ := os.Open("../misc/person.json")
body, _ := ioutil.ReadAll(file)
- var actor activitypub.Actor
+ var actor models.Actor
json.Unmarshal(body, &actor)
return actor
case "Service":
- file, _ := os.Open("./misc/service.json")
+ file, _ := os.Open("../misc/service.json")
body, _ := ioutil.ReadAll(file)
- var actor activitypub.Actor
+ var actor models.Actor
json.Unmarshal(body, &actor)
return actor
case "Application":
- file, _ := os.Open("./misc/application.json")
+ file, _ := os.Open("../misc/application.json")
body, _ := ioutil.ReadAll(file)
- var actor activitypub.Actor
+ var actor models.Actor
json.Unmarshal(body, &actor)
return actor
default:
@@ -529,7 +528,7 @@ func TestHandleInboxValidUnfollow(t *testing.T) {
}))
defer s.Close()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: "https://mastodon.test.yukimochi.io/inbox",
})
@@ -559,7 +558,7 @@ func TestHandleInboxInvalidUnfollow(t *testing.T) {
}))
defer s.Close()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: "https://mastodon.test.yukimochi.io/inbox",
})
@@ -589,7 +588,7 @@ func TestHandleInboxUnfollowAsActor(t *testing.T) {
}))
defer s.Close()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: "https://mastodon.test.yukimochi.io/inbox",
})
@@ -619,11 +618,11 @@ func TestHandleInboxValidCreate(t *testing.T) {
}))
defer s.Close()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: "https://mastodon.test.yukimochi.io/inbox",
})
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: "example.org",
InboxURL: "https://example.org/inbox",
})
@@ -652,7 +651,7 @@ func TestHandleInboxlimitedCreate(t *testing.T) {
}))
defer s.Close()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: "https://mastodon.test.yukimochi.io/inbox",
})
@@ -680,11 +679,11 @@ func TestHandleInboxValidCreateAsAnnounceNote(t *testing.T) {
}))
defer s.Close()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: "https://mastodon.test.yukimochi.io/inbox",
})
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: "example.org",
InboxURL: "https://example.org/inbox",
})
@@ -713,11 +712,11 @@ func TestHandleInboxValidCreateAsAnnounceNoNote(t *testing.T) {
}))
defer s.Close()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: "https://mastodon.test.yukimochi.io/inbox",
})
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: "example.org",
InboxURL: "https://example.org/inbox",
})
@@ -765,7 +764,7 @@ func TestHandleInboxUndo(t *testing.T) {
}))
defer s.Close()
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain.Host,
InboxURL: "https://mastodon.test.yukimochi.io/inbox",
})
diff --git a/cli/cli.go b/cli/cli.go
index 8aeefaf..818c085 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -10,20 +10,19 @@ import (
"github.com/go-redis/redis"
"github.com/spf13/cobra"
"github.com/spf13/viper"
- activitypub "github.com/yukimochi/Activity-Relay/ActivityPub"
keyloader "github.com/yukimochi/Activity-Relay/KeyLoader"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/models"
)
var (
version string
// Actor : Relay's Actor
- Actor activitypub.Actor
+ Actor models.Actor
hostname *url.URL
hostkey *rsa.PrivateKey
- relayState state.RelayState
+ relayState models.RelayState
machineryServer *machinery.Server
)
@@ -40,8 +39,8 @@ func initConfig() {
viper.BindEnv("relay_servicename")
} else {
Actor.Summary = viper.GetString("relay_summary")
- Actor.Icon = activitypub.Image{URL: viper.GetString("relay_icon")}
- Actor.Image = activitypub.Image{URL: viper.GetString("relay_image")}
+ Actor.Icon = &models.Image{URL: viper.GetString("relay_icon")}
+ Actor.Image = &models.Image{URL: viper.GetString("relay_image")}
}
Actor.Name = viper.GetString("relay_servicename")
@@ -58,7 +57,7 @@ func initConfig() {
panic(err)
}
redisClient := redis.NewClient(redisOption)
- relayState = state.NewState(redisClient, true)
+ relayState = models.NewState(redisClient, true)
var machineryConfig = &config.Config{
Broker: viper.GetString("redis_url"),
DefaultQueue: "relay",
diff --git a/cli/cli_test.go b/cli/cli_test.go
index d12f523..63dd6d3 100644
--- a/cli/cli_test.go
+++ b/cli/cli_test.go
@@ -5,14 +5,14 @@ import (
"testing"
"github.com/spf13/viper"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/models"
)
func TestMain(m *testing.M) {
viper.Set("actor_pem", "../misc/testKey.pem")
viper.Set("relay_domain", "relay.yukimochi.example.org")
initConfig()
- relayState = state.NewState(relayState.RedisClient, false)
+ relayState = models.NewState(relayState.RedisClient, false)
relayState.RedisClient.FlushAll().Result()
code := m.Run()
diff --git a/cli/config.go b/cli/config.go
index 856162b..2041347 100644
--- a/cli/config.go
+++ b/cli/config.go
@@ -7,11 +7,11 @@ import (
"os"
"github.com/spf13/cobra"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/models"
)
const (
- BlockService state.Config = iota
+ BlockService models.Config = iota
ManuallyAccept
CreateAsAnnounce
)
@@ -126,7 +126,7 @@ func importConfig(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, err)
return
}
- var data state.RelayState
+ var data models.RelayState
err = json.Unmarshal(jsonData, &data)
if err != nil {
fmt.Fprintln(os.Stderr, err)
@@ -154,7 +154,7 @@ func importConfig(cmd *cobra.Command, args []string) {
cmd.Println("Set [" + BlockedDomain + "] as blocked domain")
}
for _, Subscription := range data.Subscriptions {
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: Subscription.Domain,
InboxURL: Subscription.InboxURL,
ActivityID: Subscription.ActivityID,
diff --git a/cli/contain.go b/cli/contain.go
index 7c122e0..e995587 100644
--- a/cli/contain.go
+++ b/cli/contain.go
@@ -1,6 +1,6 @@
package main
-import state "github.com/yukimochi/Activity-Relay/State"
+import "github.com/yukimochi/Activity-Relay/models"
func contains(entries interface{}, finder string) bool {
switch entry := entries.(type) {
@@ -12,7 +12,7 @@ func contains(entries interface{}, finder string) bool {
return true
}
}
- case []state.Subscription:
+ case []models.Subscription:
for i := 0; i < len(entry); i++ {
if entry[i].Domain == finder {
return true
diff --git a/cli/domain.go b/cli/domain.go
index 6e1d443..b86ba26 100644
--- a/cli/domain.go
+++ b/cli/domain.go
@@ -5,8 +5,7 @@ import (
"fmt"
"github.com/spf13/cobra"
- activitypub "github.com/yukimochi/Activity-Relay/ActivityPub"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/models"
)
func domainCmdInit() *cobra.Command {
@@ -48,8 +47,8 @@ func domainCmdInit() *cobra.Command {
return domain
}
-func createUnfollowRequestResponse(subscription state.Subscription) error {
- activity := activitypub.Activity{
+func createUnfollowRequestResponse(subscription models.Subscription) error {
+ activity := models.Activity{
Context: []string{"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"},
ID: subscription.ActivityID,
Actor: subscription.ActorID,
diff --git a/cli/follow.go b/cli/follow.go
index 0d5f08d..6237eac 100644
--- a/cli/follow.go
+++ b/cli/follow.go
@@ -9,8 +9,7 @@ import (
"github.com/RichardKnop/machinery/v1/tasks"
uuid "github.com/satori/go.uuid"
"github.com/spf13/cobra"
- activitypub "github.com/yukimochi/Activity-Relay/ActivityPub"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/models"
)
func followCmdInit() *cobra.Command {
@@ -85,7 +84,7 @@ func createFollowRequestResponse(domain string, response string) error {
if err != nil {
return err
}
- activity := activitypub.Activity{
+ activity := models.Activity{
Context: []string{"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"},
ID: data["activity_id"],
Actor: data["actor"],
@@ -101,7 +100,7 @@ func createFollowRequestResponse(domain string, response string) error {
pushRegistorJob(data["inbox_url"], jsonData)
relayState.RedisClient.Del("relay:pending:" + domain)
if response == "Accept" {
- relayState.AddSubscription(state.Subscription{
+ relayState.AddSubscription(models.Subscription{
Domain: domain,
InboxURL: data["inbox_url"],
ActivityID: data["activity_id"],
@@ -112,8 +111,8 @@ func createFollowRequestResponse(domain string, response string) error {
return nil
}
-func createUpdateActorActivity(subscription state.Subscription) error {
- activity := activitypub.Activity{
+func createUpdateActorActivity(subscription models.Subscription) error {
+ activity := models.Activity{
Context: []string{"https://www.w3.org/ns/activitystreams"},
ID: hostname.String() + "/activities/" + uuid.NewV4().String(),
Actor: hostname.String() + "/actor",
diff --git a/docker-compose.yml b/docker-compose.yml
index 8215bf3..1a88ecb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -31,7 +31,7 @@ services:
image: yukimochi/activity-relay
restart: always
init: true
- command: server
+ command: relay server
environment:
- "ACTOR_PEM=/actor.pem"
- "RELAY_DOMAIN=relay.toot.yukimochi.jp"
diff --git a/main.go b/main.go
index 2310c0d..4c88e3e 100644
--- a/main.go
+++ b/main.go
@@ -1,112 +1,108 @@
+/*
+Yet another powerful customizable ActivityPub relay server written in Go.
+Run Activity-Relay
+API Server
+ ./Activity-Relay -c server
+Config
+YAML Format
+ ACTOR_PEM: actor.pem
+ REDIS_URL: redis://localhost:6379
+ RELAY_BIND: 0.0.0.0:8080
+ RELAY_DOMAIN: relay.toot.yukimochi.jp
+ RELAY_SERVICENAME: YUKIMOCHI Toot Relay Service
+ JOB_CONCURRENCY: 50
+ RELAY_SUMMARY: |
+ YUKIMOCHI Toot Relay Service is Running by Activity-Relay
+ RELAY_ICON: https://example.com/example_icon.png
+ RELAY_IMAGE: https://example.com/example_image.png
+Environment Variable
+This is Optional : When config file not exist, use environment variables.
+ - ACTOR_PEM
+ - REDIS_URL
+ - RELAY_BIND
+ - RELAY_DOMAIN
+ - RELAY_SERVICENAME
+ - JOB_CONCURRENCY
+ - RELAY_SUMMARY
+ - RELAY_ICON
+ - RELAY_IMAGE
+*/
package main
import (
- "crypto/rsa"
"fmt"
- "net/http"
- "net/url"
- "time"
+ "os"
- "github.com/RichardKnop/machinery/v1"
- "github.com/RichardKnop/machinery/v1/config"
- "github.com/go-redis/redis"
- cache "github.com/patrickmn/go-cache"
+ "github.com/spf13/cobra"
"github.com/spf13/viper"
- activitypub "github.com/yukimochi/Activity-Relay/ActivityPub"
- keyloader "github.com/yukimochi/Activity-Relay/KeyLoader"
- state "github.com/yukimochi/Activity-Relay/State"
+ "github.com/yukimochi/Activity-Relay/api"
+ "github.com/yukimochi/Activity-Relay/models"
)
var (
version string
- // Actor : Relay's Actor
- Actor activitypub.Actor
-
- // WebfingerResource : Relay's Webfinger resource
- WebfingerResource activitypub.WebfingerResource
-
- // Nodeinfo : Relay's Nodeinfo
- Nodeinfo activitypub.NodeinfoResources
-
- hostURL *url.URL
- hostPrivatekey *rsa.PrivateKey
- relayState state.RelayState
- machineryServer *machinery.Server
- actorCache *cache.Cache
+ globalConfig *models.RelayConfig
)
-func initConfig() {
- viper.SetConfigName("config")
- viper.AddConfigPath(".")
- err := viper.ReadInConfig()
- if err != nil {
- fmt.Println("Config file is not exists. Use environment variables.")
- viper.BindEnv("actor_pem")
- viper.BindEnv("redis_url")
- viper.BindEnv("relay_bind")
- viper.BindEnv("relay_domain")
- viper.BindEnv("relay_servicename")
- } else {
- Actor.Summary = viper.GetString("relay_summary")
- Actor.Icon = activitypub.Image{URL: viper.GetString("relay_icon")}
- Actor.Image = activitypub.Image{URL: viper.GetString("relay_image")}
- }
- Actor.Name = viper.GetString("relay_servicename")
-
- hostURL, _ = url.Parse("https://" + viper.GetString("relay_domain"))
- hostPrivatekey, _ = keyloader.ReadPrivateKeyRSAfromPath(viper.GetString("actor_pem"))
- redisOption, err := redis.ParseURL(viper.GetString("redis_url"))
- if err != nil {
- panic(err)
- }
- redisClient := redis.NewClient(redisOption)
- relayState = state.NewState(redisClient, true)
- relayState.ListenNotify(nil)
- machineryConfig := &config.Config{
- Broker: viper.GetString("redis_url"),
- DefaultQueue: "relay",
- ResultBackend: viper.GetString("redis_url"),
- ResultsExpireIn: 5,
- }
- machineryServer, err = machinery.NewServer(machineryConfig)
- if err != nil {
- panic(err)
- }
-
- Actor.GenerateSelfKey(hostURL, &hostPrivatekey.PublicKey)
- actorCache = cache.New(5*time.Minute, 10*time.Minute)
- WebfingerResource.GenerateFromActor(hostURL, &Actor)
- Nodeinfo.GenerateFromActor(hostURL, &Actor, version)
-
- fmt.Println("Welcome to YUKIMOCHI Activity-Relay [Server]", version)
- fmt.Println(" - Configurations")
- fmt.Println("RELAY DOMAIN : ", hostURL.Host)
- fmt.Println("REDIS URL : ", viper.GetString("redis_url"))
- fmt.Println("BIND ADDRESS : ", viper.GetString("relay_bind"))
- fmt.Println(" - Blocked Domain")
- domains, _ := redisClient.HKeys("relay:config:blockedDomain").Result()
- for _, domain := range domains {
- fmt.Println(domain)
- }
- fmt.Println(" - Limited Domain")
- domains, _ = redisClient.HKeys("relay:config:limitedDomain").Result()
- for _, domain := range domains {
- fmt.Println(domain)
- }
-}
-
func main() {
- // Load Config
- initConfig()
+ var app = buildCommand()
+ app.PersistentFlags().StringP("config", "c", "config.yml", "Path of config file.")
- http.HandleFunc("/.well-known/nodeinfo", handleNodeinfoLink)
- http.HandleFunc("/.well-known/webfinger", handleWebfinger)
- http.HandleFunc("/nodeinfo/2.1", handleNodeinfo)
- http.HandleFunc("/actor", handleActor)
- http.HandleFunc("/inbox", func(w http.ResponseWriter, r *http.Request) {
- handleInbox(w, r, decodeActivity)
- })
-
- http.ListenAndServe(viper.GetString("relay_bind"), nil)
+ app.Execute()
+}
+
+func buildCommand() *cobra.Command {
+ var server = &cobra.Command{
+ Use: "server",
+ Short: "Activity-Relay API Server",
+ Long: "Activity-Relay API Server is providing WebFinger API, ActivityPub inbox",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ initConfig(cmd, args)
+ fmt.Println(globalConfig.DumpWelcomeMessage("API Server", version))
+ err := api.Entrypoint(globalConfig, version)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ os.Exit(1)
+ }
+ return nil
+ },
+ }
+
+ var app = &cobra.Command{
+ Short: "YUKIMOCHI Activity-Relay",
+ Long: "YUKIMOCHI Activity-Relay - ActivityPub Relay Server",
+ }
+ app.AddCommand(server)
+
+ return app
+}
+
+func initConfig(cmd *cobra.Command, args []string) {
+ configPath := cmd.Flag("config").Value.String()
+ file, err := os.Open(configPath)
+ defer file.Close()
+
+ if err == nil {
+ viper.SetConfigType("yaml")
+ viper.ReadConfig(file)
+ } else {
+ fmt.Fprintln(os.Stderr, "Config file not exist. Use environment variables.")
+
+ viper.BindEnv("ACTOR_PEM")
+ viper.BindEnv("REDIS_URL")
+ viper.BindEnv("RELAY_BIND")
+ viper.BindEnv("RELAY_DOMAIN")
+ viper.BindEnv("RELAY_SERVICENAME")
+ viper.BindEnv("JOB_CONCURRENCY")
+ viper.BindEnv("RELAY_SUMMARY")
+ viper.BindEnv("RELAY_ICON")
+ viper.BindEnv("RELAY_IMAGE")
+ }
+
+ globalConfig, err = models.NewRelayConfig()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ os.Exit(1)
+ }
}
diff --git a/main_test.go b/main_test.go
deleted file mode 100644
index 12d4dd3..0000000
--- a/main_test.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package main
-
-import (
- "os"
- "testing"
-
- "github.com/spf13/viper"
- state "github.com/yukimochi/Activity-Relay/State"
-)
-
-func TestMain(m *testing.M) {
- viper.Set("actor_pem", "misc/testKey.pem")
- viper.Set("relay_domain", "relay.yukimochi.example.org")
- initConfig()
- relayState = state.NewState(relayState.RedisClient, false)
- relayState.RedisClient.FlushAll().Result()
-
- // Load Config
- code := m.Run()
- os.Exit(code)
-}
diff --git a/misc/config.yml b/misc/config.yml
new file mode 100644
index 0000000..fd71ee6
--- /dev/null
+++ b/misc/config.yml
@@ -0,0 +1,10 @@
+# ACTOR_PEM: FILL_WITH_EACH_TEST
+REDIS_URL: redis://localhost:6379
+
+RELAY_BIND: 0.0.0.0:8080
+RELAY_DOMAIN: relay.toot.yukimochi.jp
+RELAY_SERVICENAME: YUKIMOCHI Toot Relay Service
+JOB_CONCURRENCY: 50
+RELAY_SUMMARY: YUKIMOCHI Toot Relay Service is Running by Activity-Relay
+RELAY_ICON: https://example.com/example_icon.png
+RELAY_IMAGE: https://example.com/example_image.png
\ No newline at end of file
diff --git a/misc/header b/misc/header
deleted file mode 100644
index 6787e94..0000000
--- a/misc/header
+++ /dev/null
@@ -1,17 +0,0 @@
-map[User-Agent:[http.rb/3.3.0 (Mastodon/2.6.5; +https://innocent.yukimochi.io/)] Content-Length:[2248] Accept-Encoding:[gzip] Connection:[close] X-Real-Ip:[202.182.118.242] X-Forwarded-For:[202.182.118.242] X-Forwarded-Proto:[https] Host:[relay.01.cloudgarage.yukimochi.io] Content-Type:[application/activity+json] Date:[Sun, 23 Dec 2018 07:39:37 GMT] Digest:[SHA-256=mxgIzbPwBuNYxmjhQeH0vWeEedQGqR1R7zMwR/XTfX8=] Signature:[keyId="https://innocent.yukimochi.io/users/YUKIMOCHI#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="MhxXhL21RVp8VmALER2U/oJlWldJAB2COiU2QmwGopLD2pw1c32gQvg0PaBRHfMBBOsidZuRRnj43Kn488zW2xV3n3DYWcGscSh527/hhRzcpLVX2kBqbf/WeQzJmfJVuOX4SzivVhnnUB8PvlPj5LRHpw4n/ctMTq37strKDl9iZg9rej1op1YFJagDxm3iPzAhnv8lzO4RI9dstt2i/sN5EfjXai97oS7EgI//Kj1wJCRk9Pw1iTsGfPTkbk/aVZwDt7QGGvGDdO0JJjsCqtIyjojoyD9hFY9GzMqvTwVIYJrh54AUHq2i80veybaOBbCFcEaK0RpKoLs101r5Uw=="]]
-{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":{"@id":"as:movedTo","@type":"@id"},"Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"featured":{"@id":"toot:featured","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value"}],"id":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289215743686309/activity","type":"Create","actor":"https://innocent.yukimochi.io/users/YUKIMOCHI","published":"2018-12-23T07:39:37Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://innocent.yukimochi.io/users/YUKIMOCHI/followers"],"object":{"id":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289215743686309","type":"Note","summary":null,"inReplyTo":null,"published":"2018-12-23T07:39:37Z","url":"https://innocent.yukimochi.io/@YUKIMOCHI/101289215743686309","attributedTo":"https://innocent.yukimochi.io/users/YUKIMOCHI","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://innocent.yukimochi.io/users/YUKIMOCHI/followers"],"sensitive":false,"atomUri":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289215743686309","inReplyToAtomUri":null,"conversation":"tag:innocent.yukimochi.io,2018-12-23:objectId=113387:objectType=Conversation","content":"\u003cp\u003eてすてす\u003c/p\u003e","contentMap":{"ja":"\u003cp\u003eてすてす\u003c/p\u003e"},"attachment":[],"tag":[]},"signature":{"type":"RsaSignature2017","creator":"https://innocent.yukimochi.io/users/YUKIMOCHI#main-key","created":"2018-12-23T07:39:37Z","signatureValue":"TvpvX96xZpAXorHCkoUdBRVq53geGvJjZtFt0971PO2AvqeHouHOVKKL9Q/WCH2raZdFnC8bsBPeWHZ+XVRxS/6poXyZ5sx+LrOEugng9+J0HwuI97GJFpcfltzXPvEKGyeScpGxQoVzbMwH5WO8jddEXA6Qxmr5LNleSEEamwB+ZQRab7Xm2KVkGkdPW/gA0n9sVdpPTjcayrDSIF7HZrUr7lMVfUsWJctpVs45YkIkn2GOdmkYmbbQ5Mg0B4bYKI06p9e7EQ0WiCmO+zHvCh6QSWWx1qZNWm3j10ia1gP/FKpEBLhZkBoC7TJxNe/6pW5L03yT7F72rf8Ztxb76A=="}}
-
-map[Content-Length:[1694] Accept-Encoding:[gzip] Connection:[close] Content-Type:[application/activity+json] Date:[Sun, 23 Dec 2018 07:48:31 GMT] Signature:[keyId="https://innocent.yukimochi.io/users/YUKIMOCHI#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="XCzIDqdA2SG1VQp5yNveHUL6OE0yrVrClMonMMUO+dFKsgZ+Z+7d+tRLSVKrp5WkQMzMaM48DGSUetX3hRZeRSLwGKFbYHSPafjTpUI11p+JPnPF268kGmYOne75FEoANPTRyurK7e7cZFK5Xo+O8+tpOXUE74+eTUxPxrSidc3w/JvGX6hfFVzjbKUqMZKp3Xo9uvypamZqSC4WAQHRJ5ibuymzhnNVU03Jx5M26kSPPZ8pz1hUdwCqmi0/DKPXLEIn+VHlyOccCULbcGrU334iC0FJJURlfAlQYkoUHeF8aL8soKQPh2XkiTj+mXdE31T/Pxy0XeyLgfM3e52Fgg=="] X-Forwarded-Proto:[https] Host:[relay.01.cloudgarage.yukimochi.io] X-Real-Ip:[202.182.118.242] Digest:[SHA-256=M3C0pD195sMKhWkeXJW11+chE3mxV7bDB9sb/g9lE8g=] X-Forwarded-For:[202.182.118.242] User-Agent:[http.rb/3.3.0 (Mastodon/2.6.5; +https://innocent.yukimochi.io/)]]
-{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":{"@id":"as:movedTo","@type":"@id"},"Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"featured":{"@id":"toot:featured","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value"}],"id":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289250732181320/activity","type":"Announce","actor":"https://innocent.yukimochi.io/users/YUKIMOCHI","published":"2018-12-23T07:48:31Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://innocent.yukimochi.io/users/YUKIMOCHI","https://innocent.yukimochi.io/users/YUKIMOCHI/followers"],"object":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289215743686309","atomUri":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289250732181320/activity","signature":{"type":"RsaSignature2017","creator":"https://innocent.yukimochi.io/users/YUKIMOCHI#main-key","created":"2018-12-23T07:48:31Z","signatureValue":"BV9zw6w0fESP03/DAY185Qk74FIOGDkuX5o1ASRK/OjAEdH2gm7wXQVZ5vYzjJo1AG6CJyNE/XFVdqCqakJCpzJ6QJcTmm+//hq7J9VFlkpIgIGUBUtOOaVe5lWTi+z+pN23jQ0dGnYyBMxihIVMbrSYh0IelgcyhMkRwwhLHWB8/AmOhnyK+VvFD+g99f3e92f72mD86lE2xZjoxXG/ErS56U75pKqp7OUSRo5yu8uG6vCPFoOqu6lrNSm4jAGUwHY82j4IpCElwdahDu3TM+frw+AnZUjlj7EJMbZQyYJ/C6nE5HsoMT13Ph5AJtJif03At5XYgVDv5Eesh10n1w=="}}
-
-map[Digest:[SHA-256=1aObUKpTAdKZyH7b6D+SEcRDPTuukXb71uNGyRciD04=] X-Forwarded-For:[202.182.118.242] X-Forwarded-Proto:[https] Host:[relay.01.cloudgarage.yukimochi.io] Accept-Encoding:[gzip] Connection:[close] Date:[Sun, 23 Dec 2018 07:50:53 GMT] Signature:[keyId="https://innocent.yukimochi.io/users/YUKIMOCHI#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="pefvsRNMnKV0/qmxEXXChcLPjvQF0pPRgOy0/EKK0B+AR1ExoaFsSGUHNsfw/MlizpE6IvKG93k84JkpNwtPZqaO4QdCFu7UjOayAeZ1h7YmXGo0COnTs0Z5WxRDdr4t4NaCCoW441FhCp2lLJOnzn9N6Kh5+GK1A2+wwCQRqy7YYYm2QKGLoJ6sZlDk7DI8KWZVhHzvzykfCw7ehXUaQYZA56i8q6l6FbENNEnk6l3TZOWIAAlg+3b8WdCMVqNYvG7Q0ZUYF4oPSlVkO1jI5xxVDq/6pNjtqBicr59rKRmoMYHRsKUjZOrKDAHXpgiTbSni42rd89yuXobUliTZ9A=="] X-Real-Ip:[202.182.118.242] User-Agent:[http.rb/3.3.0 (Mastodon/2.6.5; +https://innocent.yukimochi.io/)] Content-Length:[1916] Content-Type:[application/activity+json]]
-{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":{"@id":"as:movedTo","@type":"@id"},"Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"featured":{"@id":"toot:featured","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value"}],"id":"https://innocent.yukimochi.io/users/YUKIMOCHI#announces/101289250732181320/undo","type":"Undo","actor":"https://innocent.yukimochi.io/users/YUKIMOCHI","to":["https://www.w3.org/ns/activitystreams#Public"],"object":{"id":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289250732181320/activity","type":"Announce","actor":"https://innocent.yukimochi.io/users/YUKIMOCHI","published":"2018-12-23T07:48:31Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://innocent.yukimochi.io/users/YUKIMOCHI","https://innocent.yukimochi.io/users/YUKIMOCHI/followers"],"object":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289215743686309","atomUri":"https://innocent.yukimochi.io/users/YUKIMOCHI/statuses/101289250732181320/activity"},"signature":{"type":"RsaSignature2017","creator":"https://innocent.yukimochi.io/users/YUKIMOCHI#main-key","created":"2018-12-23T07:50:53Z","signatureValue":"mPq1BaRWwJGnoAKssfolfhRB/MTFhnZTbxi5IHFast+EvoYqjir/ZDgGJwVo07Zkrok6yLSESALolXzGOoteV+BC+Idmb1c8iWX52kZSKaPqFTOwDWI0tumtTACWnluK0WdGxgmFQxmhfkyO7iz9yka6FA0Gbn3dLfaMWmOCJUJwrDRdS7tlsXe2W3cGqQGpXrabKUol5jZv0BojUVEWiVzlrfVtVmE/38+mttydcMpPYw9WBNtomm3kHBDwU7FbszRigUAO3MOI1ABGb3Zi67mihDfC1RoWgxwn4ke2/z6bzxvy6g8Biy0cSjUbDSf3xHypKJGSU62Es+DdKCPpSQ=="}}
-
-map[Connection:[close] Date:[Sun, 23 Dec 2018 07:53:13 GMT] Digest:[SHA-256=sVu5mw+OWfi86NmAWm6rs+VZhsRLwla+uJqeM/DxL1Y=] X-Real-Ip:[202.182.118.242] User-Agent:[http.rb/3.3.0 (Mastodon/2.6.5; +https://innocent.yukimochi.io/)] Content-Length:[403] Accept-Encoding:[gzip] X-Forwarded-For:[202.182.118.242] X-Forwarded-Proto:[https] Host:[relay.01.cloudgarage.yukimochi.io] Content-Type:[application/activity+json] Signature:[keyId="https://innocent.yukimochi.io/users/mayaeh#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="fnUhNMp421FitTE9NNEzD27MkwjKwa0OJiDggZ6vCDTj8EKNdfu/kMut9RWfyKY6c7TkXEuTJC78x7pmO05WtLllwAcxqXOf3dNKuO4S5KlhI6K/NPxNT7JwyQgTvEUpxmL4334rfUkfj8kyPg2IPAru+ilA3LRApJiyvOzw0hR3t2+mtwRiMrWyAQjQbo2B44gMGbs39pD+vNFp5ASliwUhs+YVAFq9IGWG9JZ1JNhqPGCU7L2tY8++ctbyO1YBbahxu+gto5EZodFHiefupQjVRa0DfD2QORYmxB+R+EX+jZJazEa9iqKmlV5Qx4DylEvBnbqpQSKG3zcDHAhnxg=="]]
-{"@context":"https://www.w3.org/ns/activitystreams","id":"https://innocent.yukimochi.io/d4028c5c-a794-4dcf-b2a8-0eaa41a086a1","type":"Undo","actor":"https://innocent.yukimochi.io/users/mayaeh","object":{"id":"https://innocent.yukimochi.io/102e3bf7-8a15-42d1-9e99-590e8e436f8e","type":"Follow","actor":"https://innocent.yukimochi.io/users/mayaeh","object":"https://www.w3.org/ns/activitystreams#Public"}}
-
-map[Content-Type:[application/activity+json] Date:[Sun, 23 Dec 2018 07:53:26 GMT] Signature:[keyId="https://innocent.yukimochi.io/users/mayaeh#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="wWoNIarha2rc6gMeesYl35xcsxpiZQ76iUQihZwAfa24QxOQsRjWaaspJuSPyuj2Gz3bZ3xixhD9/im2EtDG9++zf2Ww3nc05s5qeHX94E/5aUmMlkKbavkLjcIeOPoDZYGr4eOTrhEWnWbYElyVAb9cgNrPRwxCllGEynf9jsV+ByH2EfQzKDW5QpQzan4Z/91Un/8dtjBZRZ7+LpMpeIGAbqMBrNIkKogDAQEEELGPToAvXwM00CgSZR+FxA7+Gk3ST5shwiB2ij5hOWvYlDefe+zSUJVgnjYO0t7c3qi4mojzLM9BeQZI8K5jBN0O8WbAVzVY7RRtD8fSWT819w=="] X-Forwarded-For:[202.182.118.242] X-Real-Ip:[202.182.118.242] Digest:[SHA-256=okLYHQWxAJY1ELwOGKPKhOkEfbD4Hfds2bskdFdcfj0=] X-Forwarded-Proto:[https] Host:[relay.01.cloudgarage.yukimochi.io] User-Agent:[http.rb/3.3.0 (Mastodon/2.6.5; +https://innocent.yukimochi.io/)] Content-Length:[251] Accept-Encoding:[gzip] Connection:[close]]
-{"@context":"https://www.w3.org/ns/activitystreams","id":"https://innocent.yukimochi.io/be0802b1-8648-4598-b794-2ed19532100d","type":"Follow","actor":"https://innocent.yukimochi.io/users/mayaeh","object":"https://www.w3.org/ns/activitystreams#Public"}
-
-map[Date:[Sun, 23 Dec 2018 07:57:33 GMT] Digest:[SHA-256=0LrPvX1QoMb03H+4bmkJ82qS1iR4Z8K33Rp4WLzHbt0=] Signature:[keyId="https://innocent.yukimochi.io/users/YUKIMOCHI#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="wD4fCoTpc2iUGy/J+7PYZ08tS6DVf7TCqY3dgwSlG8H4UMtmfT0e8BQV4uRlr/sQlEJflp3RBeWXGsz3Y+2NclxO0xoVcA5+N5F8V5k3Uf6U1dtddm2Y8iUbt8hxT9qcNFC56NRKqtl3Ecj8yA9qs0LbesqLGs+wIlNUZUQLK/fC6d20TeGZwPwrC1LHig6bps8qTNyIaiVcDck3QzOXcwwGOokroSGf9PpdaOSMimHTMFEHdjqxclrYysVBl9yNxSP5oSmdOM55OnNzfaRkPqeTh1NOsSLZ/tCFV1owP/47Lu6lAwsjMU4586qokuWLwGUSx4NSgJ6fSj4Azj+umQ=="] X-Forwarded-For:[202.182.118.242] X-Forwarded-Proto:[https] Host:[relay.01.cloudgarage.yukimochi.io] User-Agent:[http.rb/3.3.0 (Mastodon/2.6.5; +https://innocent.yukimochi.io/)] Accept-Encoding:[gzip] X-Real-Ip:[202.182.118.242] Content-Length:[1353] Connection:[close] Content-Type:[application/activity+json]]
-{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":{"@id":"as:movedTo","@type":"@id"},"Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"featured":{"@id":"toot:featured","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value"}],"id":"https://innocent.yukimochi.io/5cfe3380-6cf0-4a1a-a4dd-283b96999a9e","type":"Follow","actor":"https://innocent.yukimochi.io/users/YUKIMOCHI","object":"https://relay.01.cloudgarage.yukimochi.io/actor","signature":{"type":"RsaSignature2017","creator":"https://innocent.yukimochi.io/users/YUKIMOCHI#main-key","created":"2018-12-23T07:57:33Z","signatureValue":"t61d9Y2FispoIXDIxJH1eOs0/GAkIkCnESQv9ganfTVvDqaS9+jgW7o2/P1jeITIfOapqJlYuko3XtcxaGPbR/V4pL19xM8qaSLP1HO9COwnqy+CuWD7PKZ/E0y6Dnm/PETrn72yxxLRh95lsY0iwsD+ClFyLr9PoIRsVAV98ng1G23sQvAA7unapUjJMIgCVtNa3nylWHopcvdGLG5kqXVoXIfYN4H8HwiNoMzU4336bNSc1UIclnGcAjbfZtXvS3rEuSHIwBHGxnXHr3bKmclm5cwYmDHzfuwkCIJduehRfdLnSP1JGQig1GM2qX+/UIC4uEiD1tTWBIV6vR1i8g=="}}
diff --git a/models/config.go b/models/config.go
new file mode 100644
index 0000000..5f78cbb
--- /dev/null
+++ b/models/config.go
@@ -0,0 +1,121 @@
+package models
+
+import (
+ "crypto/rsa"
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+
+ "github.com/RichardKnop/machinery/v1"
+ "github.com/RichardKnop/machinery/v1/config"
+ "github.com/go-redis/redis"
+ "github.com/spf13/viper"
+)
+
+// RelayConfig contains valid configuration.
+type RelayConfig struct {
+ actorKey *rsa.PrivateKey
+ domain *url.URL
+ redisClient *redis.Client
+ redisURL string
+ serverBind string
+ serviceName string
+ serviceSummary string
+ serviceIconURL *url.URL
+ serviceImageURL *url.URL
+}
+
+// NewRelayConfig create valid RelayConfig from viper configuration. If invalid configuration detected, return error.
+func NewRelayConfig() (*RelayConfig, error) {
+ domain, err := url.ParseRequestURI("https://" + viper.GetString("RELAY_DOMAIN"))
+ if err != nil {
+ return nil, errors.New("RELAY_DOMAIN: " + err.Error())
+ }
+
+ iconURL, err := url.ParseRequestURI(viper.GetString("RELAY_ICON"))
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "RELAY_ICON IS INVALID OR EMPTY. THIS COLUMN IS DISABLED.")
+ iconURL = nil
+ }
+
+ imageURL, err := url.ParseRequestURI(viper.GetString("RELAY_IMAGE"))
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "RELAY_IMAGE IS INVALID OR EMPTY. THIS COLUMN IS DISABLED.")
+ imageURL = nil
+ }
+
+ privateKey, err := readPrivateKeyRSA(viper.GetString("ACTOR_PEM"))
+ if err != nil {
+ return nil, errors.New("ACTOR_PEM: " + err.Error())
+ }
+
+ redisURL := viper.GetString("REDIS_URL")
+ redisOption, err := redis.ParseURL(redisURL)
+ if err != nil {
+ return nil, errors.New("REDIS_URL: " + err.Error())
+ }
+ redisClient := redis.NewClient(redisOption)
+ err = redisClient.Ping().Err()
+ if err != nil {
+ return nil, errors.New("Redis Connection Test: " + err.Error())
+ }
+
+ serverBind := viper.GetString("RELAY_BIND")
+
+ return &RelayConfig{
+ actorKey: privateKey,
+ domain: domain,
+ redisClient: redisClient,
+ redisURL: redisURL,
+ serverBind: serverBind,
+ serviceName: viper.GetString("RELAY_SERVICENAME"),
+ serviceSummary: viper.GetString("RELAY_SUMMARY"),
+ serviceIconURL: iconURL,
+ serviceImageURL: imageURL,
+ }, nil
+}
+
+// ServerBind is API Server's bind interface definition.
+func (relayConfig *RelayConfig) ServerBind() string {
+ return relayConfig.serverBind
+}
+
+// ServerHostname is API Server's hostname definition.
+func (relayConfig *RelayConfig) ServerHostname() *url.URL {
+ return relayConfig.domain
+}
+
+// ServerHostname is API Server's hostname definition.
+func (relayConfig *RelayConfig) ServerServicename() string {
+ return relayConfig.serviceName
+}
+
+// CreateRedisClient is create new redis client from RelayConfig.
+func (relayConfig *RelayConfig) RedisClient() *redis.Client {
+ return relayConfig.redisClient
+}
+
+// DumpWelcomeMessage provide build and config information string.
+func (relayConfig *RelayConfig) DumpWelcomeMessage(moduleName string, version string) string {
+ return fmt.Sprintf(`Welcome to YUKIMOCHI Activity-Relay %s - %s
+ - Configuration
+RELAY NAME : %s
+RELAY DOMAIN : %s
+REDIS URL : %s
+BIND ADDRESS : %s
+`, version, moduleName, relayConfig.serviceName, relayConfig.domain.Host, relayConfig.redisURL, relayConfig.serverBind)
+}
+
+// NewMachineryServer create Redis backed Machinery Server from RelayConfig.
+func NewMachineryServer(globalConfig *RelayConfig) (*machinery.Server, error) {
+ cnf := &config.Config{
+ Broker: globalConfig.redisURL,
+ DefaultQueue: "relay",
+ ResultBackend: globalConfig.redisURL,
+ ResultsExpireIn: 1,
+ }
+ newServer, err := machinery.NewServer(cnf)
+
+ return newServer, err
+}
diff --git a/models/config_test.go b/models/config_test.go
new file mode 100644
index 0000000..1db3c47
--- /dev/null
+++ b/models/config_test.go
@@ -0,0 +1,109 @@
+package models
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/spf13/viper"
+)
+
+func TestNewRelayConfig(t *testing.T) {
+ t.Run("success valid configuration", func(t *testing.T) {
+ relayConfig, err := NewRelayConfig()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if relayConfig.serverBind != "0.0.0.0:8080" {
+ t.Error("Failed parse: RelayConfig.serverBind")
+ }
+ if relayConfig.domain.Host != "relay.toot.yukimochi.jp" {
+ t.Error("Failed parse: RelayConfig.domain")
+ }
+ if relayConfig.serviceName != "YUKIMOCHI Toot Relay Service" {
+ t.Error("Failed parse: RelayConfig.serviceName")
+ }
+ if relayConfig.serviceSummary != "YUKIMOCHI Toot Relay Service is Running by Activity-Relay" {
+ t.Error("Failed parse: RelayConfig.serviceSummary")
+ }
+ if relayConfig.serviceIconURL.String() != "https://example.com/example_icon.png" {
+ t.Error("Failed parse: RelayConfig.serviceIconURL")
+ }
+ if relayConfig.serviceImageURL.String() != "https://example.com/example_image.png" {
+ t.Error("Failed parse: RelayConfig.serviceImageURL")
+ }
+ })
+
+ t.Run("fail invalid configuration", func(t *testing.T) {
+ invalidConfig := map[string]string{
+ "ACTOR_PEM@notFound": "../misc/test/notfound.pem",
+ "ACTOR_PEM@invalidKey": "../misc/test/actor.dh.pem",
+ "REDIS_URL@invalidURL": "",
+ "REDIS_URL@unreachableHost": "redis://localhost:6380",
+ }
+
+ for key, value := range invalidConfig {
+ viperKey := strings.Split(key, "@")[0]
+ valid := viper.GetString(viperKey)
+
+ viper.Set(viperKey, value)
+ _, err := NewRelayConfig()
+ if err == nil {
+ t.Error("Failed catch error: " + key)
+ }
+
+ viper.Set(viperKey, valid)
+ }
+ })
+}
+
+func createRelayConfig(t *testing.T) *RelayConfig {
+ relayConfig, err := NewRelayConfig()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return relayConfig
+}
+
+func TestRelayConfig_ServerBind(t *testing.T) {
+ relayConfig := createRelayConfig(t)
+ if relayConfig.ServerBind() != relayConfig.serverBind {
+ t.Error("Failed accessor: ServerBind()")
+ }
+}
+
+func TestRelayConfig_ServerHostname(t *testing.T) {
+ relayConfig := createRelayConfig(t)
+ if relayConfig.ServerHostname() != relayConfig.domain {
+ t.Error("Failed accessor: ServerHostname()")
+ }
+}
+
+func TestRelayConfig_DumpWelcomeMessage(t *testing.T) {
+ relayconfig := createRelayConfig(t)
+ w := relayconfig.DumpWelcomeMessage("Testing", "")
+
+ informations := map[string]string{
+ "module NAME": "Testing",
+ "RELAY NANE": relayconfig.serviceName,
+ "RELAY DOMAIN": relayconfig.domain.Host,
+ "REDIS URL": relayconfig.redisURL,
+ "BIND ADDRESS": relayconfig.serverBind,
+ }
+
+ for key, information := range informations {
+ if !strings.Contains(w, information) {
+ t.Error("Missed welcome message information: ", key)
+ }
+ }
+}
+
+func TestNewMachineryServer(t *testing.T) {
+ relayConfig := createRelayConfig(t)
+
+ _, err := NewMachineryServer(relayConfig)
+ if err != nil {
+ t.Error("Failed create machinery server: ", err)
+ }
+}
diff --git a/ActivityPub/models.go b/models/models.go
similarity index 85%
rename from ActivityPub/models.go
rename to models/models.go
index 6b7e283..c141398 100644
--- a/ActivityPub/models.go
+++ b/models/models.go
@@ -1,4 +1,4 @@
-package activitypub
+package models
import (
"crypto/rsa"
@@ -11,7 +11,6 @@ import (
cache "github.com/patrickmn/go-cache"
uuid "github.com/satori/go.uuid"
- keyloader "github.com/yukimochi/Activity-Relay/KeyLoader"
)
// PublicKey : Activity Certificate.
@@ -42,8 +41,8 @@ type Actor struct {
Inbox string `json:"inbox,omitempty"`
Endpoints *Endpoints `json:"endpoints,omitempty"`
PublicKey PublicKey `json:"publicKey,omitempty"`
- Icon Image `json:"icon,omitempty"`
- Image Image `json:"image,omitempty"`
+ Icon *Image `json:"icon,omitempty"`
+ Image *Image `json:"image,omitempty"`
}
// GenerateSelfKey : Generate relay Actor from Publickey.
@@ -56,10 +55,48 @@ func (actor *Actor) GenerateSelfKey(hostname *url.URL, publickey *rsa.PublicKey)
actor.PublicKey = PublicKey{
hostname.String() + "/actor#main-key",
hostname.String() + "/actor",
- keyloader.GeneratePublicKeyPEMString(publickey),
+ generatePublicKeyPEMString(publickey),
}
}
+func NewActivityPubActorFromSelfKey(globalConfig *RelayConfig) Actor {
+ hostname := globalConfig.domain.String()
+ publicKey := &globalConfig.actorKey.PublicKey
+ publicKeyPemString := generatePublicKeyPEMString(publicKey)
+
+ newActor := Actor{
+ Context: []string{"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"},
+ ID: hostname + "/actor",
+ Type: "Service",
+ Name: globalConfig.serviceName,
+ PreferredUsername: "relay",
+ Summary: globalConfig.serviceSummary,
+ Inbox: hostname + "/inbox",
+ PublicKey: struct {
+ ID string `json:"id,omitempty"`
+ Owner string `json:"owner,omitempty"`
+ PublicKeyPem string `json:"publicKeyPem,omitempty"`
+ }{
+ ID: hostname + "/actor#main-key",
+ Owner: hostname + "/actor",
+ PublicKeyPem: publicKeyPemString,
+ },
+ }
+
+ if globalConfig.serviceIconURL != nil {
+ newActor.Icon = &Image{
+ URL: globalConfig.serviceIconURL.String(),
+ }
+ }
+ if globalConfig.serviceImageURL != nil {
+ newActor.Image = &Image{
+ URL: globalConfig.serviceImageURL.String(),
+ }
+ }
+
+ return newActor
+}
+
// RetrieveRemoteActor : Retrieve Actor from remote instance.
func (actor *Actor) RetrieveRemoteActor(url string, uaString string, cache *cache.Cache) error {
var err error
diff --git a/models/models_test.go b/models/models_test.go
new file mode 100644
index 0000000..d589936
--- /dev/null
+++ b/models/models_test.go
@@ -0,0 +1,38 @@
+package models
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/spf13/viper"
+)
+
+var globalConfig *RelayConfig
+var relayState RelayState
+var ch chan bool
+
+func TestMain(m *testing.M) {
+ var err error
+
+ testConfigPath := "../misc/config.yml"
+ file, _ := os.Open(testConfigPath)
+ defer file.Close()
+
+ viper.SetConfigType("yaml")
+ viper.ReadConfig(file)
+ viper.Set("actor_pem", "../misc/testKey.pem")
+
+ globalConfig, err = NewRelayConfig()
+ if err != nil {
+ fmt.Println(err.Error())
+ os.Exit(1)
+ }
+
+ relayState = NewState(globalConfig.RedisClient(), true)
+ ch = make(chan bool)
+ relayState.ListenNotify(ch)
+ relayState.RedisClient.FlushAll().Result()
+ code := m.Run()
+ os.Exit(code)
+}
diff --git a/State/state.go b/models/state.go
similarity index 99%
rename from State/state.go
rename to models/state.go
index b1055d6..95b834f 100644
--- a/State/state.go
+++ b/models/state.go
@@ -1,4 +1,4 @@
-package state
+package models
import (
"fmt"
diff --git a/State/state_test.go b/models/state_test.go
similarity index 79%
rename from State/state_test.go
rename to models/state_test.go
index 5eb0e4b..d86634d 100644
--- a/State/state_test.go
+++ b/models/state_test.go
@@ -1,45 +1,12 @@
-package state
+package models
import (
- "fmt"
- "os"
"testing"
-
- "github.com/go-redis/redis"
- "github.com/spf13/viper"
)
-var redisClient *redis.Client
-var relayState RelayState
-var ch chan bool
-
-func TestMain(m *testing.M) {
- viper.SetConfigName("config")
- viper.AddConfigPath(".")
- err := viper.ReadInConfig()
- if err != nil {
- fmt.Println("Config file is not exists. Use environment variables.")
- viper.BindEnv("redis_url")
- }
- redisOption, err := redis.ParseURL(viper.GetString("redis_url"))
- if err != nil {
- panic(err)
- }
- redisClient = redis.NewClient(redisOption)
- redisClient.FlushAll().Result()
-
- ch = make(chan bool)
- relayState = NewState(redisClient, true)
- relayState.ListenNotify(ch)
-
- code := m.Run()
- redisClient.FlushAll().Result()
-
- os.Exit(code)
-}
-
func TestLoadEmpty(t *testing.T) {
- redisClient.FlushAll().Result()
+ relayState.RedisClient.FlushAll().Result()
+ relayState.Load()
if relayState.RelayConfig.BlockService != false {
t.Fatalf("Failed read config.")
@@ -53,7 +20,7 @@ func TestLoadEmpty(t *testing.T) {
}
func TestSetConfig(t *testing.T) {
- redisClient.FlushAll().Result()
+ relayState.RedisClient.FlushAll().Result()
relayState.SetConfig(BlockService, true)
<-ch
@@ -89,7 +56,7 @@ func TestSetConfig(t *testing.T) {
}
func TestTreatSubscriptionNotify(t *testing.T) {
- redisClient.FlushAll().Result()
+ relayState.RedisClient.FlushAll().Result()
relayState.AddSubscription(Subscription{
Domain: "example.com",
@@ -121,7 +88,7 @@ func TestTreatSubscriptionNotify(t *testing.T) {
}
func TestSelectDomain(t *testing.T) {
- redisClient.FlushAll().Result()
+ relayState.RedisClient.FlushAll().Result()
exampleSubscription := Subscription{
Domain: "example.com",
@@ -143,7 +110,7 @@ func TestSelectDomain(t *testing.T) {
}
func TestBlockedDomain(t *testing.T) {
- redisClient.FlushAll().Result()
+ relayState.RedisClient.FlushAll().Result()
relayState.SetBlockedDomain("example.com", true)
<-ch
@@ -172,7 +139,7 @@ func TestBlockedDomain(t *testing.T) {
}
func TestLimitedDomain(t *testing.T) {
- redisClient.FlushAll().Result()
+ relayState.RedisClient.FlushAll().Result()
relayState.SetLimitedDomain("example.com", true)
<-ch
@@ -201,7 +168,7 @@ func TestLimitedDomain(t *testing.T) {
}
func TestLoadCompatiSubscription(t *testing.T) {
- redisClient.FlushAll().Result()
+ relayState.RedisClient.FlushAll().Result()
relayState.AddSubscription(Subscription{
Domain: "example.com",
diff --git a/models/utils.go b/models/utils.go
new file mode 100644
index 0000000..6447255
--- /dev/null
+++ b/models/utils.go
@@ -0,0 +1,71 @@
+package models
+
+import (
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "github.com/go-redis/redis"
+)
+
+func ReadPublicKeyRSAfromString(pemString string) (*rsa.PublicKey, error) {
+ pemByte := []byte(pemString)
+ decoded, _ := pem.Decode(pemByte)
+ defer func() {
+ recover()
+ }()
+ keyInterface, err := x509.ParsePKIXPublicKey(decoded.Bytes)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return nil, err
+ }
+ pub := keyInterface.(*rsa.PublicKey)
+ return pub, nil
+}
+
+func redisHGetOrCreateWithDefault(redisClient *redis.Client, key string, field string, defaultValue string) (string, error) {
+ keyExist, err := redisClient.HExists(key, field).Result()
+ if err != nil {
+ return "", err
+ }
+ if keyExist {
+ value, err := redisClient.HGet(key, field).Result()
+ if err != nil {
+ return "", err
+ }
+ return value, nil
+ } else {
+ _, err := redisClient.HSet(key, field, defaultValue).Result()
+ if err != nil {
+ return "", err
+ }
+ return defaultValue, nil
+ }
+}
+
+func readPrivateKeyRSA(keyPath string) (*rsa.PrivateKey, error) {
+ file, err := ioutil.ReadFile(keyPath)
+ if err != nil {
+ return nil, err
+ }
+ decoded, _ := pem.Decode(file)
+ privateKey, err := x509.ParsePKCS1PrivateKey(decoded.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ return privateKey, nil
+}
+
+func generatePublicKeyPEMString(publicKey *rsa.PublicKey) string {
+ publicKeyByte := x509.MarshalPKCS1PublicKey(publicKey)
+ publicKeyPem := pem.EncodeToMemory(
+ &pem.Block{
+ Type: "RSA PUBLIC KEY",
+ Bytes: publicKeyByte,
+ },
+ )
+ return string(publicKeyPem)
+}
diff --git a/models/utils_test.go b/models/utils_test.go
new file mode 100644
index 0000000..dfca80b
--- /dev/null
+++ b/models/utils_test.go
@@ -0,0 +1,51 @@
+package models
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestRedisHGetOrCreateWithDefault(t *testing.T) {
+ relayConfig := createRelayConfig(t)
+
+ t.Run("Execute HGet when value exist", func(t *testing.T) {
+ _, err := relayConfig.redisClient.HSet("gotest:redis:hget:or:create:with:default", "exist", "1").Result()
+ if err != nil {
+ t.Error(err)
+ }
+
+ value, err := redisHGetOrCreateWithDefault(relayConfig.redisClient, "gotest:redis:hget:or:create:with:default", "exist", "2")
+ if err != nil {
+ t.Error(err)
+ }
+ if value != "1" {
+ t.Error(errors.New("value is override by redisHGetOrCreateWithDefault"))
+ }
+
+ _, err = relayConfig.redisClient.HDel("gotest:redis:hget:or:create:with:default", "exist").Result()
+ if err != nil {
+ t.Error(err)
+ }
+ })
+
+ t.Run("Execute HGet when value not exist", func(t *testing.T) {
+ _, err := redisHGetOrCreateWithDefault(relayConfig.redisClient, "gotest:redis:hget:or:create:with:default", "not_exist", "2")
+ if err != nil {
+ t.Error(err)
+ }
+
+ value, err := relayConfig.redisClient.HGet("gotest:redis:hget:or:create:with:default", "not_exist").Result()
+ if err != nil {
+ t.Error(err)
+ }
+
+ if value != "2" {
+ t.Error(errors.New("redisHGetOrCreateWithDefault is not write default value successfully"))
+ }
+
+ _, err = relayConfig.redisClient.HDel("gotest:redis:hget:or:create:with:default", "not_exist").Result()
+ if err != nil {
+ t.Error(err)
+ }
+ })
+}
diff --git a/readme.md b/readme.md
index 598f452..a7948e6 100644
--- a/readme.md
+++ b/readme.md
@@ -27,29 +27,32 @@ See [GitHub wiki](https://github.com/yukimochi/Activity-Relay/wiki)
### `config.yml`
```yaml config.yml
-actor_pem: /actor.pem
-redis_url: redis://redis:6379
+ACTOR_PEM: /actor.pem
+REDIS_URL: redis://redis:6379
-relay_bind: 0.0.0.0:8080
-relay_domain: relay.toot.yukimochi.jp
-relay_servicename: YUKIMOCHI Toot Relay Service
-job_concurrency: 50
-# relay_summary: |
+RELAY_BIND: 0.0.0.0:8080
+RELAY_DOMAIN: relay.toot.yukimochi.jp
+RELAY_SERVICENAME: YUKIMOCHI Toot Relay Service
+JOB_CONCURRENCY: 50
+# RELAY_SUMMARY: |
-# relay_icon: https://
-# relay_image: https://
+# RELAY_ICON: https://
+# RELAY_IMAGE: https://
```
### `Environment Variable`
This is **Optional** : When `config.yml` not exists, use environment variable.
- - `ACTOR_PEM` (ex. `/actor.pem`)
- - `REDIS_URL` (ex. `redis://127.0.0.1:6379/0`)
- - `RELAY_BIND` (ex. `0.0.0.0:8080`)
- - `RELAY_DOMAIN` (ex. `relay.toot.yukimochi.jp`)
- - `RELAY_SERVICENAME` (ex. `YUKIMOCHI Toot Relay Service`)
- - `JOB_CONCURRENCY` (ex. `50`)
+ - ACTOR_PEM
+ - REDIS_URL
+ - RELAY_BIND
+ - RELAY_DOMAIN
+ - RELAY_SERVICENAME
+ - JOB_CONCURRENCY
+ - RELAY_SUMMARY
+ - RELAY_ICON
+ - RELAY_IMAGE
## License
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fyukimochi%2FActivity-Relay?ref=badge_large)
\ No newline at end of file
diff --git a/worker/worker.go b/worker/worker.go
index 95c1f6e..a930aae 100644
--- a/worker/worker.go
+++ b/worker/worker.go
@@ -14,15 +14,15 @@ import (
"github.com/go-redis/redis"
uuid "github.com/satori/go.uuid"
"github.com/spf13/viper"
- activitypub "github.com/yukimochi/Activity-Relay/ActivityPub"
keyloader "github.com/yukimochi/Activity-Relay/KeyLoader"
+ "github.com/yukimochi/Activity-Relay/models"
)
var (
version string
// Actor : Relay's Actor
- Actor activitypub.Actor
+ Actor models.Actor
hostURL *url.URL
hostPrivatekey *rsa.PrivateKey
@@ -66,8 +66,8 @@ func initConfig() {
viper.BindEnv("job_concurrency")
} else {
Actor.Summary = viper.GetString("relay_summary")
- Actor.Icon = activitypub.Image{URL: viper.GetString("relay_icon")}
- Actor.Image = activitypub.Image{URL: viper.GetString("relay_image")}
+ Actor.Icon = &models.Image{URL: viper.GetString("relay_icon")}
+ Actor.Image = &models.Image{URL: viper.GetString("relay_image")}
}
Actor.Name = viper.GetString("relay_servicename")