owl-blogs/web/activity_pub_handler.go

232 lines
6.0 KiB
Go
Raw Normal View History

2023-07-22 18:34:17 +00:00
package web
import (
2024-05-13 17:03:21 +00:00
"errors"
"log/slog"
"net/http"
2023-07-26 19:28:57 +00:00
"net/url"
"owl-blogs/app"
2023-07-22 18:34:17 +00:00
2023-07-26 19:28:57 +00:00
vocab "github.com/go-ap/activitypub"
2024-02-26 19:17:53 +00:00
"github.com/go-ap/jsonld"
2023-07-26 19:28:57 +00:00
2023-07-22 18:34:17 +00:00
"github.com/gofiber/fiber/v2"
2024-05-13 17:03:21 +00:00
"github.com/gofiber/fiber/v2/middleware/adaptor"
2023-07-22 18:34:17 +00:00
)
type ActivityPubServer struct {
2024-05-14 19:23:37 +00:00
siteConfigService *app.SiteConfigService
apService *app.ActivityPubService
entryService *app.EntryService
2023-07-22 18:34:17 +00:00
}
type WebfingerResponse struct {
2023-07-26 19:28:57 +00:00
Subject string `json:"subject"`
2024-02-26 19:17:53 +00:00
Aliases []string `json:"aliases"`
2023-07-26 19:28:57 +00:00
Links []WebfingerLink `json:"links"`
2023-07-22 18:34:17 +00:00
}
2023-07-26 19:28:57 +00:00
type WebfingerLink struct {
2023-07-22 18:34:17 +00:00
Rel string `json:"rel"`
Type string `json:"type"`
Href string `json:"href"`
}
2024-05-14 19:23:37 +00:00
func NewActivityPubServer(siteConfigService *app.SiteConfigService, entryService *app.EntryService, apService *app.ActivityPubService) *ActivityPubServer {
2023-07-22 18:34:17 +00:00
return &ActivityPubServer{
2024-05-14 19:23:37 +00:00
siteConfigService: siteConfigService,
entryService: entryService,
apService: apService,
2023-07-22 18:34:17 +00:00
}
}
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
2024-05-14 19:23:37 +00:00
siteConfig, _ := s.siteConfigService.GetSiteConfig()
apConfig, _ := s.apService.GetApConfig()
2023-07-22 18:34:17 +00:00
2024-02-26 19:17:53 +00:00
domain, err := url.Parse(siteConfig.FullUrl)
if err != nil {
return err
}
subject := ctx.Query("resource", "")
2024-05-14 19:23:37 +00:00
blogSubject := "acct:" + apConfig.PreferredUsername + "@" + domain.Host
slog.Info("webfinger request", "for", subject, "required", blogSubject)
if subject != blogSubject {
2024-02-26 19:17:53 +00:00
return ctx.Status(404).JSON(nil)
}
2023-07-22 18:34:17 +00:00
webfinger := WebfingerResponse{
2024-02-26 19:17:53 +00:00
Subject: subject,
2023-07-22 18:34:17 +00:00
2023-07-26 19:28:57 +00:00
Links: []WebfingerLink{
2023-07-22 18:34:17 +00:00
{
Rel: "self",
Type: "application/activity+json",
Href: siteConfig.FullUrl + "/activitypub/actor",
},
},
}
return ctx.JSON(webfinger)
}
func (s *ActivityPubServer) Router(router fiber.Router) {
router.Get("/actor", s.HandleActor)
2023-07-26 19:28:57 +00:00
router.Get("/outbox", s.HandleOutbox)
2024-05-16 18:27:06 +00:00
router.Post("/inbox", s.HandleInbox)
2024-05-13 17:03:21 +00:00
router.Get("/followers", s.HandleFollowers)
2023-07-22 18:34:17 +00:00
}
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
2024-05-14 19:23:37 +00:00
siteConfig, _ := s.siteConfigService.GetSiteConfig()
apConfig, _ := s.apService.GetApConfig()
2023-07-22 18:34:17 +00:00
2023-07-26 19:28:57 +00:00
actor := vocab.PersonNew(vocab.IRI(siteConfig.FullUrl + "/activitypub/actor"))
actor.PreferredUsername = vocab.NaturalLanguageValues{{Value: vocab.Content(apConfig.PreferredUsername)}}
actor.Inbox = vocab.IRI(siteConfig.FullUrl + "/activitypub/inbox")
actor.Outbox = vocab.IRI(siteConfig.FullUrl + "/activitypub/outbox")
actor.Followers = vocab.IRI(siteConfig.FullUrl + "/activitypub/followers")
actor.PublicKey = vocab.PublicKey{
ID: vocab.ID(siteConfig.FullUrl + "/activitypub/actor#main-key"),
Owner: vocab.IRI(siteConfig.FullUrl + "/activitypub/actor"),
PublicKeyPem: apConfig.PublicKeyPem,
}
2024-02-26 19:17:53 +00:00
data, err := jsonld.WithContext(
jsonld.IRI(vocab.ActivityBaseURI),
jsonld.IRI(vocab.SecurityContextURI),
).Marshal(actor)
2023-07-26 19:28:57 +00:00
if err != nil {
return err
}
ctx.Set("Content-Type", "application/activity+json")
return ctx.Send(data)
}
func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
2024-05-14 19:23:37 +00:00
siteConfig, _ := s.siteConfigService.GetSiteConfig()
// apConfig, _ := s.apService.GetApConfig()
2023-07-26 19:28:57 +00:00
entries, err := s.entryService.FindAllByType(nil, true, false)
if err != nil {
return err
}
items := make([]vocab.Item, len(entries))
for i, entry := range entries {
url, _ := url.JoinPath(siteConfig.FullUrl, "/posts/"+entry.ID()+"/")
items[i] = *vocab.ActivityNew(vocab.IRI(url), vocab.CreateType, vocab.Object{
ID: vocab.ID(url),
Type: vocab.ArticleType,
Content: vocab.NaturalLanguageValues{
{Value: vocab.Content(entry.Content())},
},
})
}
outbox := vocab.OrderedCollectionNew(vocab.IRI(siteConfig.FullUrl + "/activitypub/outbox"))
outbox.TotalItems = uint(len(items))
outbox.OrderedItems = items
data, err := outbox.MarshalJSON()
if err != nil {
return err
2023-07-22 18:34:17 +00:00
}
2023-07-26 19:28:57 +00:00
ctx.Set("Content-Type", "application/activity+json")
return ctx.Send(data)
2024-05-13 17:03:21 +00:00
}
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 {
2024-05-16 18:27:06 +00:00
slog.Error("wrong signature", "err", err)
2024-05-13 17:03:21 +00:00
return err
}
err = s.apService.AddFollower(follower)
if err != nil {
return err
}
// go acpub.Accept(gameName, act)
return nil
}
2024-05-16 18:27:06 +00:00
func (s *ActivityPubServer) processUndo(r *http.Request, act *vocab.Activity) error {
follower := act.Actor.GetID().String()
err := s.apService.VerifySignature(r, follower)
if err != nil {
slog.Error("wrong signature", "err", err)
return err
}
err = s.apService.RemoveFollower(follower)
if err != nil {
return err
}
// go acpub.Accept(gameName, act)
2024-05-13 17:03:21 +00:00
return nil
}
func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
2024-05-14 19:23:37 +00:00
// siteConfig, _ := s.siteConfigService.GetSiteConfig()
// apConfig, _ := s.apService.GetApConfig()
2023-07-22 18:34:17 +00:00
2024-05-13 17:03:21 +00:00
body := ctx.Request().Body()
data, err := vocab.UnmarshalJSON(body)
if err != nil {
2024-05-16 18:27:06 +00:00
slog.Error("failed to parse request body", "body", body, "err", err)
2024-05-13 17:03:21 +00:00
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")
2024-05-16 18:27:06 +00:00
return s.processUndo(r, act)
2024-05-13 17:03:21 +00:00
}
return errors.New("only follow and undo actions supported")
})
return err
}
func (s *ActivityPubServer) HandleFollowers(ctx *fiber.Ctx) error {
2024-05-14 19:23:37 +00:00
siteConfig, _ := s.siteConfigService.GetSiteConfig()
// apConfig, _ := s.apService.GetApConfig()
2024-05-13 17:03:21 +00:00
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)
2023-07-22 18:34:17 +00:00
}