WIP mock amsto + tests

This commit is contained in:
Niko Abeler 2024-05-14 21:23:37 +02:00
parent 624f19a1d9
commit 652f81805d
16 changed files with 424 additions and 36 deletions

View File

@ -1,11 +1,11 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
tmp_dir = "/tmp"
[build]
args_bin = ["web"]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main owl-blogs/cmd/owl"
bin = "/tmp/main"
cmd = "go build -buildvcs=false -o /tmp/main owl-blogs/cmd/owl"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
e2e_tests/
tmp/
*.db

5
.gitignore vendored
View File

@ -27,4 +27,7 @@ users/
*.db
tmp/
tmp/
venv/
*.pyc

View File

@ -1,6 +1,7 @@
package app
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
@ -12,6 +13,7 @@ import (
"owl-blogs/config"
"owl-blogs/domain/model"
"owl-blogs/render"
"reflect"
"time"
vocab "github.com/go-ap/activitypub"
@ -56,6 +58,46 @@ func NewActivityPubService(followersRepo repository.FollowerRepository, configRe
}
}
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)
}

49
e2e_tests/conftest.py Normal file
View File

@ -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"]

View File

@ -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

View File

@ -0,0 +1,6 @@
FROM python:3.11
COPY . .
RUN pip install -r requirements.txt
CMD [ "python", "main.py" ]

View File

@ -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")

View File

@ -0,0 +1 @@
Flask==3.0.3

View File

@ -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

View File

View File

@ -0,0 +1 @@
ACCT_NAME = "acct:blog@localhost:3000"

View File

@ -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"]

View File

@ -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

View File

@ -6,9 +6,6 @@ import (
"net/http"
"net/url"
"owl-blogs/app"
"owl-blogs/app/repository"
"owl-blogs/config"
"owl-blogs/domain/model"
vocab "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
@ -18,9 +15,9 @@ import (
)
type ActivityPubServer struct {
configRepo repository.ConfigRepository
apService *app.ActivityPubService
entryService *app.EntryService
siteConfigService *app.SiteConfigService
apService *app.ActivityPubService
entryService *app.EntryService
}
type WebfingerResponse struct {
@ -35,19 +32,17 @@ type WebfingerLink struct {
Href string `json:"href"`
}
func NewActivityPubServer(configRepo repository.ConfigRepository, entryService *app.EntryService, apService *app.ActivityPubService) *ActivityPubServer {
func NewActivityPubServer(siteConfigService *app.SiteConfigService, entryService *app.EntryService, apService *app.ActivityPubService) *ActivityPubServer {
return &ActivityPubServer{
configRepo: configRepo,
entryService: entryService,
apService: apService,
siteConfigService: siteConfigService,
entryService: entryService,
apService: apService,
}
}
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
siteConfig, _ := s.siteConfigService.GetSiteConfig()
apConfig, _ := s.apService.GetApConfig()
domain, err := url.Parse(siteConfig.FullUrl)
if err != nil {
@ -55,7 +50,9 @@ func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
}
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)
}
@ -83,10 +80,8 @@ func (s *ActivityPubServer) Router(router fiber.Router) {
}
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
siteConfig, _ := s.siteConfigService.GetSiteConfig()
apConfig, _ := s.apService.GetApConfig()
actor := vocab.PersonNew(vocab.IRI(siteConfig.FullUrl + "/activitypub/actor"))
actor.PreferredUsername = vocab.NaturalLanguageValues{{Value: vocab.Content(apConfig.PreferredUsername)}}
@ -110,10 +105,8 @@ func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
}
func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
siteConfig, _ := s.siteConfigService.GetSiteConfig()
// apConfig, _ := s.apService.GetApConfig()
entries, err := s.entryService.FindAllByType(nil, true, false)
if err != nil {
@ -165,10 +158,8 @@ func (s *ActivityPubServer) processUndo(act *vocab.Activity) error {
}
func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
// siteConfig, _ := s.siteConfigService.GetSiteConfig()
// apConfig, _ := s.apService.GetApConfig()
body := ctx.Request().Body()
data, err := vocab.UnmarshalJSON(body)
@ -199,10 +190,8 @@ func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
}
func (s *ActivityPubServer) HandleFollowers(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
siteConfig, _ := s.siteConfigService.GetSiteConfig()
// apConfig, _ := s.apService.GetApConfig()
fs, err := s.apService.AllFollowers()
if err != nil {

View File

@ -135,7 +135,7 @@ func NewWebApp(
fiberApp.Get("/sitemap.xml", NewSiteMapHandler(entryService, siteConfigService).Handle)
// ActivityPub
activityPubServer := NewActivityPubServer(configRepo, entryService, apService)
activityPubServer := NewActivityPubServer(siteConfigService, entryService, apService)
configRegister.Register(config.ACT_PUB_CONF_NAME, &app.ActivityPubConfig{})
fiberApp.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
fiberApp.Route("/activitypub", activityPubServer.Router)