Activity Pub Implementation #58
|
@ -0,0 +1,169 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"
|
||||||
|
"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 (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,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import "os"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SITE_CONFIG = "site_config"
|
SITE_CONFIG = "site_config"
|
||||||
|
ACT_PUB_CONF_NAME = "activity_pub"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config interface {
|
type Config interface {
|
||||||
|
|
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,46 +1,28 @@
|
||||||
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/app/repository"
|
||||||
"owl-blogs/config"
|
"owl-blogs/config"
|
||||||
"owl-blogs/domain/model"
|
"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
|
configRepo repository.ConfigRepository
|
||||||
|
apService *app.ActivityPubService
|
||||||
entryService *app.EntryService
|
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 {
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Aliases []string `json:"aliases"`
|
Aliases []string `json:"aliases"`
|
||||||
|
@ -53,17 +35,18 @@ type WebfingerLink struct {
|
||||||
Href string `json:"href"`
|
Href string `json:"href"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewActivityPubServer(configRepo repository.ConfigRepository, entryService *app.EntryService) *ActivityPubServer {
|
func NewActivityPubServer(configRepo repository.ConfigRepository, entryService *app.EntryService, apService *app.ActivityPubService) *ActivityPubServer {
|
||||||
return &ActivityPubServer{
|
return &ActivityPubServer{
|
||||||
configRepo: configRepo,
|
configRepo: configRepo,
|
||||||
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 := model.SiteConfig{}
|
||||||
apConfig := ActivityPubConfig{}
|
apConfig := app.ActivityPubConfig{}
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
domain, err := url.Parse(siteConfig.FullUrl)
|
domain, err := url.Parse(siteConfig.FullUrl)
|
||||||
|
@ -95,12 +78,14 @@ 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 := model.SiteConfig{}
|
||||||
apConfig := ActivityPubConfig{}
|
apConfig := app.ActivityPubConfig{}
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
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"))
|
||||||
|
@ -126,8 +111,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 := model.SiteConfig{}
|
||||||
apConfig := ActivityPubConfig{}
|
apConfig := app.ActivityPubConfig{}
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
entries, err := s.entryService.FindAllByType(nil, true, false)
|
entries, err := s.entryService.FindAllByType(nil, true, false)
|
||||||
|
@ -157,5 +142,86 @@ 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 := model.SiteConfig{}
|
||||||
|
apConfig := app.ActivityPubConfig{}
|
||||||
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
|
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 := model.SiteConfig{}
|
||||||
|
apConfig := app.ActivityPubConfig{}
|
||||||
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
|
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(configRepo, 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