Compare commits
2 Commits
10ca2bdcd9
...
652f81805d
Author | SHA1 | Date |
---|---|---|
Niko Abeler | 652f81805d | |
Niko Abeler | 624f19a1d9 |
|
@ -1,11 +1,11 @@
|
||||||
root = "."
|
root = "."
|
||||||
testdata_dir = "testdata"
|
testdata_dir = "testdata"
|
||||||
tmp_dir = "tmp"
|
tmp_dir = "/tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
args_bin = ["web"]
|
args_bin = ["web"]
|
||||||
bin = "./tmp/main"
|
bin = "/tmp/main"
|
||||||
cmd = "go build -o ./tmp/main owl-blogs/cmd/owl"
|
cmd = "go build -buildvcs=false -o /tmp/main owl-blogs/cmd/owl"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
e2e_tests/
|
||||||
|
tmp/
|
||||||
|
*.db
|
|
@ -27,4 +27,7 @@ users/
|
||||||
|
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
|
venv/
|
||||||
|
*.pyc
|
|
@ -0,0 +1,211 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/config"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
vocab "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityPubConfig struct {
|
||||||
|
PreferredUsername string
|
||||||
|
PublicKeyPem string
|
||||||
|
PrivateKeyPem string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form implements app.AppConfig.
|
||||||
|
func (cfg *ActivityPubConfig) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
|
f, _ := render.RenderTemplateToString("forms/ActivityPubConfig", cfg)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFormData implements app.AppConfig.
|
||||||
|
func (cfg *ActivityPubConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
|
cfg.PreferredUsername = data.FormValue("PreferredUsername")
|
||||||
|
cfg.PublicKeyPem = data.FormValue("PublicKeyPem")
|
||||||
|
cfg.PrivateKeyPem = data.FormValue("PrivateKeyPem")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *ActivityPubConfig) PrivateKey() *rsa.PrivateKey {
|
||||||
|
block, _ := pem.Decode([]byte(cfg.PrivateKeyPem))
|
||||||
|
privKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
return privKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityPubService struct {
|
||||||
|
followersRepo repository.FollowerRepository
|
||||||
|
configRepo repository.ConfigRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityPubService(followersRepo repository.FollowerRepository, configRepo repository.ConfigRepository) *ActivityPubService {
|
||||||
|
return &ActivityPubService{
|
||||||
|
followersRepo: followersRepo,
|
||||||
|
configRepo: configRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) defaultConfig() ActivityPubConfig {
|
||||||
|
privKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
pubKey := privKey.Public().(*rsa.PublicKey)
|
||||||
|
|
||||||
|
pubKeyPem := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PUBLIC KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PublicKey(pubKey),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
privKeyPrm := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return ActivityPubConfig{
|
||||||
|
PreferredUsername: "blog",
|
||||||
|
PublicKeyPem: string(pubKeyPem),
|
||||||
|
PrivateKeyPem: string(privKeyPrm),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) GetApConfig() (ActivityPubConfig, error) {
|
||||||
|
apConfig := ActivityPubConfig{}
|
||||||
|
err := svc.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
if err != nil {
|
||||||
|
println("ERROR IN ACTIVITY PUB CONFIG")
|
||||||
|
return ActivityPubConfig{}, err
|
||||||
|
}
|
||||||
|
if reflect.ValueOf(apConfig).IsZero() {
|
||||||
|
cfg := svc.defaultConfig()
|
||||||
|
svc.configRepo.Update(config.ACT_PUB_CONF_NAME, cfg)
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
return apConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) AddFollower(follower string) error {
|
||||||
|
return s.followersRepo.Add(follower)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) RemoveFollower(follower string) error {
|
||||||
|
return s.followersRepo.Remove(follower)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) AllFollowers() ([]string, error) {
|
||||||
|
return s.followersRepo.All()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) sign(privateKey *rsa.PrivateKey, pubKeyId string, body []byte, r *http.Request) error {
|
||||||
|
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||||
|
digestAlgorithm := httpsig.DigestSha256
|
||||||
|
// The "Date" and "Digest" headers must already be set on r, as well as r.URL.
|
||||||
|
headersToSign := []string{httpsig.RequestTarget, "host", "date"}
|
||||||
|
if body != nil {
|
||||||
|
headersToSign = append(headersToSign, "digest")
|
||||||
|
}
|
||||||
|
signer, _, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// To sign the digest, we need to give the signer a copy of the body...
|
||||||
|
// ...but it is optional, no digest will be signed if given "nil"
|
||||||
|
// If r were a http.ResponseWriter, call SignResponse instead.
|
||||||
|
err = signer.SignRequest(privateKey, pubKeyId, r, body)
|
||||||
|
|
||||||
|
slog.Info("Signed Request", "req", r.Header)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) GetActor(reqUrl string, fromGame string) (vocab.Actor, error) {
|
||||||
|
c := http.Client{}
|
||||||
|
|
||||||
|
parsedUrl, err := url.Parse(reqUrl)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("parse error", "err", err)
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", reqUrl, nil)
|
||||||
|
req.Header.Set("Accept", "application/ld+json")
|
||||||
|
req.Header.Set("Date", time.Now().Format(http.TimeFormat))
|
||||||
|
req.Header.Set("Host", parsedUrl.Host)
|
||||||
|
|
||||||
|
siteConfig := model.SiteConfig{}
|
||||||
|
apConfig := ActivityPubConfig{}
|
||||||
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
|
err = s.sign(apConfig.PrivateKey(), siteConfig.FullUrl+"/games/"+fromGame+"#main-key", nil, req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Signing error", "err", err)
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := vocab.UnmarshalJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var actor vocab.Actor
|
||||||
|
|
||||||
|
err = vocab.OnActor(item, func(o *vocab.Actor) error {
|
||||||
|
actor = *o
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return actor, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) VerifySignature(r *http.Request, sender string) error {
|
||||||
|
siteConfig := model.SiteConfig{}
|
||||||
|
apConfig := ActivityPubConfig{}
|
||||||
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
|
actor, err := s.GetActor(sender, siteConfig.FullUrl+"/activitypub/actor")
|
||||||
|
// actor does not have a pub key -> don't verify
|
||||||
|
if actor.PublicKey.PublicKeyPem == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
|
||||||
|
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("retrieved pub key of sender", "actor", actor, "pubKey", pubKey)
|
||||||
|
|
||||||
|
verifier, err := httpsig.NewVerifier(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return verifier.Verify(pubKey, httpsig.RSA_SHA256)
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ func App(db infra.Database) *web.WebApp {
|
||||||
authorRepo := infra.NewDefaultAuthorRepo(db)
|
authorRepo := infra.NewDefaultAuthorRepo(db)
|
||||||
configRepo := infra.NewConfigRepo(db)
|
configRepo := infra.NewConfigRepo(db)
|
||||||
interactionRepo := infra.NewInteractionRepo(db, interactionRegister)
|
interactionRepo := infra.NewInteractionRepo(db, interactionRegister)
|
||||||
|
followersRepo := infra.NewFollowerRepository(db)
|
||||||
|
|
||||||
// Create External Services
|
// Create External Services
|
||||||
httpClient := &infra.OwlHttpClient{}
|
httpClient := &infra.OwlHttpClient{}
|
||||||
|
@ -65,6 +66,7 @@ func App(db infra.Database) *web.WebApp {
|
||||||
webmentionService := app.NewWebmentionService(
|
webmentionService := app.NewWebmentionService(
|
||||||
siteConfigService, interactionRepo, entryRepo, httpClient, eventBus,
|
siteConfigService, interactionRepo, entryRepo, httpClient, eventBus,
|
||||||
)
|
)
|
||||||
|
apService := app.NewActivityPubService(followersRepo, configRepo)
|
||||||
|
|
||||||
// setup render functions
|
// setup render functions
|
||||||
render.SiteConfigService = siteConfigService
|
render.SiteConfigService = siteConfigService
|
||||||
|
@ -80,6 +82,7 @@ func App(db infra.Database) *web.WebApp {
|
||||||
entryService, entryRegister, binaryService,
|
entryService, entryRegister, binaryService,
|
||||||
authorService, configRepo, configRegister,
|
authorService, configRepo, configRegister,
|
||||||
siteConfigService, webmentionService, interactionRepo,
|
siteConfigService, webmentionService, interactionRepo,
|
||||||
|
apService,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ package config
|
||||||
import "os"
|
import "os"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SITE_CONFIG = "site_config"
|
SITE_CONFIG = "site_config"
|
||||||
|
ACT_PUB_CONF_NAME = "activity_pub"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config interface {
|
type Config interface {
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import pytest
|
||||||
|
from requests import Session
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from tests.fixtures import ACCT_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class LiveServerSession(Session):
|
||||||
|
def __init__(self, base_url=None):
|
||||||
|
super().__init__()
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
def request(self, method, url, *args, **kwargs):
|
||||||
|
joined_url = urljoin(self.base_url, url)
|
||||||
|
return super().request(method, joined_url, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return LiveServerSession("http://localhost:3000")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def actor_url(client):
|
||||||
|
resp = client.get(f"/.well-known/webfinger?resource={ACCT_NAME}")
|
||||||
|
data = resp.json()
|
||||||
|
self_link = [x for x in data["links"] if x["rel"] == "self"][0]
|
||||||
|
return self_link["href"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def actor(client):
|
||||||
|
resp = client.get(actor_url, headers={"Content-Type": "application/activity+json"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inbox(actor):
|
||||||
|
return actor["inbox"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def outbox(actor):
|
||||||
|
return actor["outbox"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def followers(actor):
|
||||||
|
return actor["followers"]
|
|
@ -0,0 +1,24 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: Dockerfile.test
|
||||||
|
volumes:
|
||||||
|
- ../app:/go/owl/app
|
||||||
|
- ../assets:/go/owl/assets
|
||||||
|
- ../cmd:/go/owl/cmd
|
||||||
|
- ../config:/go/owl/config
|
||||||
|
- ../domain:/go/owl/domain
|
||||||
|
- ../entry_types:/go/owl/entry_types
|
||||||
|
- ../importer:/go/owl/importer
|
||||||
|
- ../infra:/go/owl/infra
|
||||||
|
- ../interactions:/go/owl/interactions
|
||||||
|
- ../plugings:/go/owl/plugings
|
||||||
|
- ../render:/go/owl/render
|
||||||
|
- ../web:/go/owl/web
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
mock_masto:
|
||||||
|
build: mock_masto
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
|
@ -0,0 +1,6 @@
|
||||||
|
FROM python:3.11
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
CMD [ "python", "main.py" ]
|
|
@ -0,0 +1,208 @@
|
||||||
|
import json
|
||||||
|
from flask import Flask, request
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
PRIV_KEY_PEM = """-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCni8P4bvkC/3Sx
|
||||||
|
NTrDw1qw0vWtJKZMsyJ3Mcs4+1apoVqOQhujUqGqFSiRT7Vmc7OEhB0vikdiTkCk
|
||||||
|
1PcoTd/rOa/0WbG8385JcLzaJfTIG+rrRtHwZ1TwxwBju43jpGEZjpbA0dqoXMkr
|
||||||
|
J1MyD7aPLoAiVe0ikw2czSZumv4ncemOtk0VG3b2TnIxo3CMKtUOWu8xT08MMIuo
|
||||||
|
3cZRnpI6Xr/ULzvjv8e3EjIpwRJqMPECtGsfdcjFmR0yFIrjrlmkZTiW31z/Dk7i
|
||||||
|
xRGD0ADy3WEQ3lA4l3mNZeyG4S0Wi4iYe9/wegESMZcakLoME7ks+KNS388Mdbcd
|
||||||
|
DKy9NmWvAgMBAAECggEABLQAA0hHhdWv6+Lc9xkpFuTvxTV4fuyvCf4u1eGlnstg
|
||||||
|
ZF/nW1/6w8XQ8WCgbJ4mKuZz1J14FYKxfoRaj8S9MA2Ff+wd+M77gRpAuDWajRzO
|
||||||
|
LQk8OW2yd7POXKkAzvln9F9eofkCFKR4zSpPGTenCJaQkuYrQEOKfUf7oofdRzQi
|
||||||
|
w9kmp3wAxM/EseHZpknYDCgDQV7MDQAaMD7kbynL2WfXPxebktwpRlKUwgtGrevj
|
||||||
|
gagQL8J/GX6wO3ymw9sln4BhlI2+3LuiMXQdQc1tamkXFCguCuOZCu/2VRdCHmiS
|
||||||
|
nnpu+FMspBHbvxO+RXo3Cu/S6jjJgoQxD2WZTE0gqQKBgQDM6AQdqBYjISdkI9Gl
|
||||||
|
6ZLLjwZRJSYpopujtX7pun61l9kUwQevaR2Z39rMWxX62DD6arazi/ygIUBw6Kgp
|
||||||
|
s/qBEb29ec+0cESdC8aJYb3dGvDzh/8C05p7ozxj8JZQcxq5W5jql/BELlSsUONO
|
||||||
|
jfqQv8RGZNSkD9uy6TxOr4eWIwKBgQDRUuO/XRDLt8Mp10mTshxTznSQ3gAJYKeG
|
||||||
|
0WfEC3kPEukHBQb8huqFcQDiQ71oBWuEdOQWgT3aBS6L+nIMyZMT5u+BejQm7/E5
|
||||||
|
pMM+z0VRpfFSsIrCvU8yKam0aemQGlKQAfhTct1gCg+wKnYsSQMlNHKWEfDbw9I/
|
||||||
|
cns/IN+dBQKBgQC6/Of0oFVDTZgC3GUPAO3C8QwUtM/0or1hUdk1Nck3shCZzeVT
|
||||||
|
f5tRtmSWpHCUbwGTJBsCEjdBcda6srXzCJkLe8Moy6ZtxR34KqzM5fM7eMB1nJ9s
|
||||||
|
Vunc9gPAN+cUF1ZF3H7ZZjoOHjGK5m3oW8xSl41np9Acv5P/2rP8Ilaa/QKBgQDJ
|
||||||
|
YwISfitGk8mEW8hB/L4cMykapztJyl/i6Vz31EHoKr1fL4sFMZg4QfwjtCBqD6zd
|
||||||
|
hshajoU/WHTr30wS2WxTXX9YBoZeX8KpPsdJioiagRioAYm+yfuDu2m2VZ+MMIb2
|
||||||
|
Xa7YOk6Zs5RcXL3M5YHNLaSAlUoxZTjGKhJBLhN1MQKBgQCbo3ngBl7Qjjx4WJ93
|
||||||
|
2WEEKvSDCv69eecNQDuKWKEiFqBN23LheNrN8DXMWFTtE4miY106dzQ0dUMh418x
|
||||||
|
K98rXSX3VvY4w48AznvPMKVLqesFjcvwnBdvk/NqXod20CMSpOEVj6W/nGoTBQt2
|
||||||
|
0PuW3IUym9KvO0WX9E+1Qw8mbw==
|
||||||
|
-----END PRIVATE KEY-----"""
|
||||||
|
|
||||||
|
PUB_KEY_PEM = """-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp4vD+G75Av90sTU6w8Na
|
||||||
|
sNL1rSSmTLMidzHLOPtWqaFajkIbo1KhqhUokU+1ZnOzhIQdL4pHYk5ApNT3KE3f
|
||||||
|
6zmv9FmxvN/OSXC82iX0yBvq60bR8GdU8McAY7uN46RhGY6WwNHaqFzJKydTMg+2
|
||||||
|
jy6AIlXtIpMNnM0mbpr+J3HpjrZNFRt29k5yMaNwjCrVDlrvMU9PDDCLqN3GUZ6S
|
||||||
|
Ol6/1C8747/HtxIyKcESajDxArRrH3XIxZkdMhSK465ZpGU4lt9c/w5O4sURg9AA
|
||||||
|
8t1hEN5QOJd5jWXshuEtFouImHvf8HoBEjGXGpC6DBO5LPijUt/PDHW3HQysvTZl
|
||||||
|
rwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----"""
|
||||||
|
|
||||||
|
INBOX = []
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/.well-known/webfinger")
|
||||||
|
def webfinger():
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"subject": "acct:h4kor@mock_masto",
|
||||||
|
"aliases": [
|
||||||
|
"http://mock_masto/@h4kor",
|
||||||
|
"http://mock_masto/users/h4kor",
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.net/rel/profile-page",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": "http://mock_masto/@h4kor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": "http://mock_masto/users/h4kor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
"template": "http://mock_masto/authorize_interaction?uri={uri}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.net/rel/avatar",
|
||||||
|
"type": "image/png",
|
||||||
|
"href": "http://assets.mock_masto/accounts/avatars/000/082/056/original/a4be9944e3b03229.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/users/h4kor")
|
||||||
|
def actor():
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"http://www.w3.org/ns/activitystreams",
|
||||||
|
"http://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"featured": {"@id": "toot:featured", "@type": "@id"},
|
||||||
|
"featuredTags": {"@id": "toot:featuredTags", "@type": "@id"},
|
||||||
|
"alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"},
|
||||||
|
"movedTo": {"@id": "as:movedTo", "@type": "@id"},
|
||||||
|
"schema": "http://schema.org#",
|
||||||
|
"PropertyValue": "schema:PropertyValue",
|
||||||
|
"value": "schema:value",
|
||||||
|
"discoverable": "toot:discoverable",
|
||||||
|
"Device": "toot:Device",
|
||||||
|
"Ed25519Signature": "toot:Ed25519Signature",
|
||||||
|
"Ed25519Key": "toot:Ed25519Key",
|
||||||
|
"Curve25519Key": "toot:Curve25519Key",
|
||||||
|
"EncryptedMessage": "toot:EncryptedMessage",
|
||||||
|
"publicKeyBase64": "toot:publicKeyBase64",
|
||||||
|
"deviceId": "toot:deviceId",
|
||||||
|
"claim": {"@type": "@id", "@id": "toot:claim"},
|
||||||
|
"fingerprintKey": {"@type": "@id", "@id": "toot:fingerprintKey"},
|
||||||
|
"identityKey": {"@type": "@id", "@id": "toot:identityKey"},
|
||||||
|
"devices": {"@type": "@id", "@id": "toot:devices"},
|
||||||
|
"messageFranking": "toot:messageFranking",
|
||||||
|
"messageType": "toot:messageType",
|
||||||
|
"cipherText": "toot:cipherText",
|
||||||
|
"suspended": "toot:suspended",
|
||||||
|
"memorial": "toot:memorial",
|
||||||
|
"indexable": "toot:indexable",
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"focalPoint": {"@container": "@list", "@id": "toot:focalPoint"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"id": "http://mock_masto/users/h4kor",
|
||||||
|
"type": "Person",
|
||||||
|
"following": "http://mock_masto/users/h4kor/following",
|
||||||
|
"followers": "http://mock_masto/users/h4kor/followers",
|
||||||
|
"inbox": "http://mock_masto/users/h4kor/inbox",
|
||||||
|
"outbox": "http://mock_masto/users/h4kor/outbox",
|
||||||
|
"featured": "http://mock_masto/users/h4kor/collections/featured",
|
||||||
|
"featuredTags": "http://mock_masto/users/h4kor/collections/tags",
|
||||||
|
"preferredUsername": "h4kor",
|
||||||
|
"name": "Niko",
|
||||||
|
"summary": '<p>Teaching computers to do things with arguable efficiency.</p><p>he/him</p><p><a href="http://mock_masto/tags/vegan" class="mention hashtag" rel="tag">#<span>vegan</span></a> <a href="http://mock_masto/tags/cooking" class="mention hashtag" rel="tag">#<span>cooking</span></a> <a href="http://mock_masto/tags/programming" class="mention hashtag" rel="tag">#<span>programming</span></a> <a href="http://mock_masto/tags/politics" class="mention hashtag" rel="tag">#<span>politics</span></a> <a href="http://mock_masto/tags/climate" class="mention hashtag" rel="tag">#<span>climate</span></a></p>',
|
||||||
|
"url": "http://mock_masto/@h4kor",
|
||||||
|
"manuallyApprovesFollowers": False,
|
||||||
|
"discoverable": True,
|
||||||
|
"indexable": False,
|
||||||
|
"published": "2018-08-16T00:00:00Z",
|
||||||
|
"memorial": False,
|
||||||
|
"devices": "http://mock_masto/users/h4kor/collections/devices",
|
||||||
|
"publicKey": {
|
||||||
|
"id": "http://mock_masto/users/h4kor#main-key",
|
||||||
|
"owner": "http://mock_masto/users/h4kor",
|
||||||
|
"publicKeyPem": PUB_KEY_PEM,
|
||||||
|
},
|
||||||
|
"tag": [
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto/tags/politics",
|
||||||
|
"name": "#politics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto/tags/climate",
|
||||||
|
"name": "#climate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto/tags/vegan",
|
||||||
|
"name": "#vegan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto/tags/programming",
|
||||||
|
"name": "#programming",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto/tags/cooking",
|
||||||
|
"name": "#cooking",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"attachment": [
|
||||||
|
{
|
||||||
|
"type": "PropertyValue",
|
||||||
|
"name": "Me",
|
||||||
|
"value": '<a href="http://rerere.org" target="_blank" rel="nofollow noopener noreferrer me" translate="no"><span class="invisible">http://</span><span class="">rerere.org</span><span class="invisible"></span></a>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PropertyValue",
|
||||||
|
"name": "Blog",
|
||||||
|
"value": '<a href="http://blog.libove.org" target="_blank" rel="nofollow noopener noreferrer me" translate="no"><span class="invisible">http://</span><span class="">blog.libove.org</span><span class="invisible"></span></a>',
|
||||||
|
},
|
||||||
|
{"type": "PropertyValue", "name": "Location", "value": "Münster"},
|
||||||
|
{
|
||||||
|
"type": "PropertyValue",
|
||||||
|
"name": "Current Project",
|
||||||
|
"value": '<a href="http://git.libove.org/h4kor/owl-blogs" target="_blank" rel="nofollow noopener noreferrer me" translate="no"><span class="invisible">http://</span><span class="">git.libove.org/h4kor/owl-blogs</span><span class="invisible"></span></a>',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"endpoints": {"sharedInbox": "http://mock_masto/inbox"},
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"mediaType": "image/png",
|
||||||
|
"url": "http://assets.mock_masto/accounts/avatars/000/082/056/original/a4be9944e3b03229.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/users/h4kor/inbox")
|
||||||
|
def inbox():
|
||||||
|
if request.method == "POST":
|
||||||
|
INBOX.append(request.get_json())
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True, host="0.0.0.0", port="8000")
|
|
@ -0,0 +1 @@
|
||||||
|
Flask==3.0.3
|
|
@ -0,0 +1,11 @@
|
||||||
|
certifi==2024.2.2
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
exceptiongroup==1.2.1
|
||||||
|
idna==3.7
|
||||||
|
iniconfig==2.0.0
|
||||||
|
packaging==24.0
|
||||||
|
pluggy==1.5.0
|
||||||
|
pytest==8.2.0
|
||||||
|
requests==2.31.0
|
||||||
|
tomli==2.0.1
|
||||||
|
urllib3==2.2.1
|
|
@ -0,0 +1 @@
|
||||||
|
ACCT_NAME = "acct:blog@localhost:3000"
|
|
@ -0,0 +1,24 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_actor(client, actor_url):
|
||||||
|
resp = client.get(actor_url, headers={"Content-Type": "application/activity+json"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert "type" in data
|
||||||
|
assert "inbox" in data
|
||||||
|
assert "outbox" in data
|
||||||
|
assert "followers" in data
|
||||||
|
assert "preferredUsername" in data
|
||||||
|
assert "publicKey" in data
|
||||||
|
assert len(data["publicKey"])
|
||||||
|
|
||||||
|
pubKey = data["publicKey"]
|
||||||
|
assert "id" in pubKey
|
||||||
|
assert "owner" in pubKey
|
||||||
|
assert "publicKeyPem" in pubKey
|
||||||
|
|
||||||
|
assert pubKey["owner"] == data["id"]
|
||||||
|
assert pubKey["id"] != data["id"]
|
||||||
|
assert "-----BEGIN RSA PUBLIC KEY-----" in pubKey["publicKeyPem"]
|
|
@ -0,0 +1,27 @@
|
||||||
|
import pytest
|
||||||
|
from .fixtures import ACCT_NAME
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["query", "status"],
|
||||||
|
[
|
||||||
|
["", 404],
|
||||||
|
["?foo=bar", 404],
|
||||||
|
["?resource=lol@bar.com", 404],
|
||||||
|
[f"?resource={ACCT_NAME}", 200],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_webfinger_status(client, query, status):
|
||||||
|
resp = client.get("/.well-known/webfinger" + query)
|
||||||
|
assert resp.status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
def test_webfinger(client):
|
||||||
|
resp = client.get(f"/.well-known/webfinger?resource={ACCT_NAME}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["subject"] == ACCT_NAME
|
||||||
|
assert len(data["links"]) > 0
|
||||||
|
self_link = [x for x in data["links"] if x["rel"] == "self"][0]
|
||||||
|
assert self_link["type"] == "application/activity+json"
|
||||||
|
assert "href" in self_link
|
1
go.mod
1
go.mod
|
@ -26,6 +26,7 @@ require (
|
||||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect
|
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect
|
||||||
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -22,6 +22,8 @@ github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 h1:H9MGShwybHLSln6K8R
|
||||||
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI=
|
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI=
|
||||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
|
||||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
||||||
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
@ -84,15 +86,21 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
|
|
@ -1,44 +1,23 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
|
||||||
"owl-blogs/render"
|
|
||||||
|
|
||||||
vocab "github.com/go-ap/activitypub"
|
vocab "github.com/go-ap/activitypub"
|
||||||
"github.com/go-ap/jsonld"
|
"github.com/go-ap/jsonld"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/adaptor"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ACT_PUB_CONF_NAME = "activity_pub"
|
|
||||||
|
|
||||||
type ActivityPubServer struct {
|
type ActivityPubServer struct {
|
||||||
configRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
entryService *app.EntryService
|
apService *app.ActivityPubService
|
||||||
}
|
entryService *app.EntryService
|
||||||
|
|
||||||
type ActivityPubConfig struct {
|
|
||||||
PreferredUsername string
|
|
||||||
PublicKeyPem string
|
|
||||||
PrivateKeyPem string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form implements app.AppConfig.
|
|
||||||
func (cfg *ActivityPubConfig) Form(binSvc model.BinaryStorageInterface) string {
|
|
||||||
f, _ := render.RenderTemplateToString("forms/ActivityPubConfig", cfg)
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseFormData implements app.AppConfig.
|
|
||||||
func (cfg *ActivityPubConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
|
||||||
cfg.PreferredUsername = data.FormValue("PreferredUsername")
|
|
||||||
cfg.PublicKeyPem = data.FormValue("PublicKeyPem")
|
|
||||||
cfg.PrivateKeyPem = data.FormValue("PrivateKeyPem")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebfingerResponse struct {
|
type WebfingerResponse struct {
|
||||||
|
@ -53,18 +32,17 @@ type WebfingerLink struct {
|
||||||
Href string `json:"href"`
|
Href string `json:"href"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewActivityPubServer(configRepo repository.ConfigRepository, entryService *app.EntryService) *ActivityPubServer {
|
func NewActivityPubServer(siteConfigService *app.SiteConfigService, entryService *app.EntryService, apService *app.ActivityPubService) *ActivityPubServer {
|
||||||
return &ActivityPubServer{
|
return &ActivityPubServer{
|
||||||
configRepo: configRepo,
|
siteConfigService: siteConfigService,
|
||||||
entryService: entryService,
|
entryService: entryService,
|
||||||
|
apService: apService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, _ := s.siteConfigService.GetSiteConfig()
|
||||||
apConfig := ActivityPubConfig{}
|
apConfig, _ := s.apService.GetApConfig()
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
domain, err := url.Parse(siteConfig.FullUrl)
|
domain, err := url.Parse(siteConfig.FullUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -72,7 +50,9 @@ func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
subject := ctx.Query("resource", "")
|
subject := ctx.Query("resource", "")
|
||||||
if subject != "acct:"+apConfig.PreferredUsername+"@"+domain.Host {
|
blogSubject := "acct:" + apConfig.PreferredUsername + "@" + domain.Host
|
||||||
|
slog.Info("webfinger request", "for", subject, "required", blogSubject)
|
||||||
|
if subject != blogSubject {
|
||||||
return ctx.Status(404).JSON(nil)
|
return ctx.Status(404).JSON(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,13 +75,13 @@ func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
||||||
func (s *ActivityPubServer) Router(router fiber.Router) {
|
func (s *ActivityPubServer) Router(router fiber.Router) {
|
||||||
router.Get("/actor", s.HandleActor)
|
router.Get("/actor", s.HandleActor)
|
||||||
router.Get("/outbox", s.HandleOutbox)
|
router.Get("/outbox", s.HandleOutbox)
|
||||||
|
router.Get("/inbox", s.HandleInbox)
|
||||||
|
router.Get("/followers", s.HandleFollowers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
|
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, _ := s.siteConfigService.GetSiteConfig()
|
||||||
apConfig := ActivityPubConfig{}
|
apConfig, _ := s.apService.GetApConfig()
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
actor := vocab.PersonNew(vocab.IRI(siteConfig.FullUrl + "/activitypub/actor"))
|
actor := vocab.PersonNew(vocab.IRI(siteConfig.FullUrl + "/activitypub/actor"))
|
||||||
actor.PreferredUsername = vocab.NaturalLanguageValues{{Value: vocab.Content(apConfig.PreferredUsername)}}
|
actor.PreferredUsername = vocab.NaturalLanguageValues{{Value: vocab.Content(apConfig.PreferredUsername)}}
|
||||||
|
@ -125,10 +105,8 @@ func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
|
func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, _ := s.siteConfigService.GetSiteConfig()
|
||||||
apConfig := ActivityPubConfig{}
|
// apConfig, _ := s.apService.GetApConfig()
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
entries, err := s.entryService.FindAllByType(nil, true, false)
|
entries, err := s.entryService.FindAllByType(nil, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -157,5 +135,82 @@ func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
ctx.Set("Content-Type", "application/activity+json")
|
ctx.Set("Content-Type", "application/activity+json")
|
||||||
return ctx.Send(data)
|
return ctx.Send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) processFollow(r *http.Request, act *vocab.Activity) error {
|
||||||
|
follower := act.Actor.GetID().String()
|
||||||
|
err := s.apService.VerifySignature(r, follower)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.apService.AddFollower(follower)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// go acpub.Accept(gameName, act)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) processUndo(act *vocab.Activity) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
|
||||||
|
// siteConfig, _ := s.siteConfigService.GetSiteConfig()
|
||||||
|
// apConfig, _ := s.apService.GetApConfig()
|
||||||
|
|
||||||
|
body := ctx.Request().Body()
|
||||||
|
data, err := vocab.UnmarshalJSON(body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = vocab.OnActivity(data, func(act *vocab.Activity) error {
|
||||||
|
slog.Info("activity retrieved", "activity", act, "type", act.Type)
|
||||||
|
|
||||||
|
r, err := adaptor.ConvertRequest(ctx, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if act.Type == vocab.FollowType {
|
||||||
|
return s.processFollow(r, act)
|
||||||
|
}
|
||||||
|
|
||||||
|
if act.Type == vocab.UndoType {
|
||||||
|
slog.Info("processing undo")
|
||||||
|
return s.processUndo(act)
|
||||||
|
}
|
||||||
|
return errors.New("only follow and undo actions supported")
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) HandleFollowers(ctx *fiber.Ctx) error {
|
||||||
|
siteConfig, _ := s.siteConfigService.GetSiteConfig()
|
||||||
|
// apConfig, _ := s.apService.GetApConfig()
|
||||||
|
|
||||||
|
fs, err := s.apService.AllFollowers()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := vocab.Collection{}
|
||||||
|
for _, f := range fs {
|
||||||
|
followers.Append(vocab.IRI(f))
|
||||||
|
}
|
||||||
|
followers.TotalItems = uint(len(fs))
|
||||||
|
followers.ID = vocab.IRI(siteConfig.FullUrl + "/activitypub/followers")
|
||||||
|
data, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(vocab.ActivityBaseURI),
|
||||||
|
).Marshal(followers)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.Set("Content-Type", "application/activity+json")
|
||||||
|
return ctx.Send(data)
|
||||||
|
}
|
||||||
|
|
44
web/app.go
44
web/app.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/config"
|
||||||
"owl-blogs/web/middleware"
|
"owl-blogs/web/middleware"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
@ -35,11 +36,12 @@ func NewWebApp(
|
||||||
siteConfigService *app.SiteConfigService,
|
siteConfigService *app.SiteConfigService,
|
||||||
webmentionService *app.WebmentionService,
|
webmentionService *app.WebmentionService,
|
||||||
interactionRepo repository.InteractionRepository,
|
interactionRepo repository.InteractionRepository,
|
||||||
|
apService *app.ActivityPubService,
|
||||||
) *WebApp {
|
) *WebApp {
|
||||||
app := fiber.New(fiber.Config{
|
fiberApp := fiber.New(fiber.Config{
|
||||||
BodyLimit: 50 * 1024 * 1024, // 50MB in bytes
|
BodyLimit: 50 * 1024 * 1024, // 50MB in bytes
|
||||||
})
|
})
|
||||||
app.Use(middleware.NewUserMiddleware(authorService).Handle)
|
fiberApp.Use(middleware.NewUserMiddleware(authorService).Handle)
|
||||||
|
|
||||||
indexHandler := NewIndexHandler(entryService, siteConfigService)
|
indexHandler := NewIndexHandler(entryService, siteConfigService)
|
||||||
listHandler := NewListHandler(entryService, siteConfigService)
|
listHandler := NewListHandler(entryService, siteConfigService)
|
||||||
|
@ -51,15 +53,15 @@ func NewWebApp(
|
||||||
webmentionHandler := NewWebmentionHandler(webmentionService, configRepo)
|
webmentionHandler := NewWebmentionHandler(webmentionService, configRepo)
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
app.Get("/auth/login", loginHandler.HandleGet)
|
fiberApp.Get("/auth/login", loginHandler.HandleGet)
|
||||||
app.Post("/auth/login", loginHandler.HandlePost)
|
fiberApp.Post("/auth/login", loginHandler.HandlePost)
|
||||||
|
|
||||||
// admin
|
// admin
|
||||||
adminHandler := NewAdminHandler(configRepo, configRegister, typeRegistry)
|
adminHandler := NewAdminHandler(configRepo, configRegister, typeRegistry)
|
||||||
draftHandler := NewDraftHandler(entryService, siteConfigService)
|
draftHandler := NewDraftHandler(entryService, siteConfigService)
|
||||||
binaryManageHandler := NewBinaryManageHandler(configRepo, binService)
|
binaryManageHandler := NewBinaryManageHandler(configRepo, binService)
|
||||||
adminInteractionHandler := NewAdminInteractionHandler(configRepo, interactionRepo)
|
adminInteractionHandler := NewAdminInteractionHandler(configRepo, interactionRepo)
|
||||||
admin := app.Group("/admin")
|
admin := fiberApp.Group("/admin")
|
||||||
admin.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
admin.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
||||||
admin.Get("/", adminHandler.Handle)
|
admin.Get("/", adminHandler.Handle)
|
||||||
admin.Get("/drafts/", draftHandler.Handle)
|
admin.Get("/drafts/", draftHandler.Handle)
|
||||||
|
@ -75,7 +77,7 @@ func NewWebApp(
|
||||||
adminApi.Post("/binaries", binaryManageHandler.HandleUploadApi)
|
adminApi.Post("/binaries", binaryManageHandler.HandleUploadApi)
|
||||||
|
|
||||||
// Editor
|
// Editor
|
||||||
editor := app.Group("/editor")
|
editor := fiberApp.Group("/editor")
|
||||||
editor.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
editor.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
||||||
editor.Get("/new/:editor/", editorHandler.HandleGetNew)
|
editor.Get("/new/:editor/", editorHandler.HandleGetNew)
|
||||||
editor.Post("/new/:editor/", editorHandler.HandlePostNew)
|
editor.Post("/new/:editor/", editorHandler.HandlePostNew)
|
||||||
|
@ -85,7 +87,7 @@ func NewWebApp(
|
||||||
editor.Post("/unpublish/:id/", editorHandler.HandlePostUnpublish)
|
editor.Post("/unpublish/:id/", editorHandler.HandlePostUnpublish)
|
||||||
|
|
||||||
// SiteConfig
|
// SiteConfig
|
||||||
siteConfig := app.Group("/site-config")
|
siteConfig := fiberApp.Group("/site-config")
|
||||||
siteConfig.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
siteConfig.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
||||||
|
|
||||||
siteConfigHandler := NewSiteConfigHandler(siteConfigService)
|
siteConfigHandler := NewSiteConfigHandler(siteConfigService)
|
||||||
|
@ -107,39 +109,39 @@ func NewWebApp(
|
||||||
siteConfig.Post("/menus/create/", siteConfigMenusHandler.HandleCreate)
|
siteConfig.Post("/menus/create/", siteConfigMenusHandler.HandleCreate)
|
||||||
siteConfig.Post("/menus/delete/", siteConfigMenusHandler.HandleDelete)
|
siteConfig.Post("/menus/delete/", siteConfigMenusHandler.HandleDelete)
|
||||||
|
|
||||||
app.Use("/static", filesystem.New(filesystem.Config{
|
fiberApp.Use("/static", filesystem.New(filesystem.Config{
|
||||||
Root: http.FS(embedDirStatic),
|
Root: http.FS(embedDirStatic),
|
||||||
PathPrefix: "static",
|
PathPrefix: "static",
|
||||||
Browse: false,
|
Browse: false,
|
||||||
}))
|
}))
|
||||||
app.Get("/", indexHandler.Handle)
|
fiberApp.Get("/", indexHandler.Handle)
|
||||||
app.Get("/lists/:list/", listHandler.Handle)
|
fiberApp.Get("/lists/:list/", listHandler.Handle)
|
||||||
// Media
|
// Media
|
||||||
app.Get("/media/+", mediaHandler.Handle)
|
fiberApp.Get("/media/+", mediaHandler.Handle)
|
||||||
// RSS
|
// RSS
|
||||||
app.Get("/index.xml", rssHandler.Handle)
|
fiberApp.Get("/index.xml", rssHandler.Handle)
|
||||||
// Posts
|
// Posts
|
||||||
app.Get("/posts/:post/", entryHandler.Handle)
|
fiberApp.Get("/posts/:post/", entryHandler.Handle)
|
||||||
// Webmention
|
// Webmention
|
||||||
app.Post("/webmention/", webmentionHandler.Handle)
|
fiberApp.Post("/webmention/", webmentionHandler.Handle)
|
||||||
// robots.txt
|
// robots.txt
|
||||||
app.Get("/robots.txt", func(c *fiber.Ctx) error {
|
fiberApp.Get("/robots.txt", func(c *fiber.Ctx) error {
|
||||||
siteConfig, _ := siteConfigService.GetSiteConfig()
|
siteConfig, _ := siteConfigService.GetSiteConfig()
|
||||||
sitemapUrl, _ := url.JoinPath(siteConfig.FullUrl, "/sitemap.xml")
|
sitemapUrl, _ := url.JoinPath(siteConfig.FullUrl, "/sitemap.xml")
|
||||||
c.Set("Content-Type", "text/plain")
|
c.Set("Content-Type", "text/plain")
|
||||||
return c.SendString(fmt.Sprintf("User-agent: GPTBot\nDisallow: /\n\nUser-agent: *\nAllow: /\n\nSitemap: %s\n", sitemapUrl))
|
return c.SendString(fmt.Sprintf("User-agent: GPTBot\nDisallow: /\n\nUser-agent: *\nAllow: /\n\nSitemap: %s\n", sitemapUrl))
|
||||||
})
|
})
|
||||||
// sitemap.xml
|
// sitemap.xml
|
||||||
app.Get("/sitemap.xml", NewSiteMapHandler(entryService, siteConfigService).Handle)
|
fiberApp.Get("/sitemap.xml", NewSiteMapHandler(entryService, siteConfigService).Handle)
|
||||||
|
|
||||||
// ActivityPub
|
// ActivityPub
|
||||||
activityPubServer := NewActivityPubServer(configRepo, entryService)
|
activityPubServer := NewActivityPubServer(siteConfigService, entryService, apService)
|
||||||
configRegister.Register(ACT_PUB_CONF_NAME, &ActivityPubConfig{})
|
configRegister.Register(config.ACT_PUB_CONF_NAME, &app.ActivityPubConfig{})
|
||||||
app.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
|
fiberApp.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
|
||||||
app.Route("/activitypub", activityPubServer.Router)
|
fiberApp.Route("/activitypub", activityPubServer.Router)
|
||||||
|
|
||||||
return &WebApp{
|
return &WebApp{
|
||||||
FiberApp: app,
|
FiberApp: fiberApp,
|
||||||
EntryService: entryService,
|
EntryService: entryService,
|
||||||
Registry: typeRegistry,
|
Registry: typeRegistry,
|
||||||
BinaryService: binService,
|
BinaryService: binService,
|
||||||
|
|
Loading…
Reference in New Issue