Files
wanderer/db/routes/activitypub.go
Flomp d2ac49470a Federation Refactoring & Architecture Improvements (#930)
* initial commit

* add lists

* update permissions

* fix waypoint create

* needs_full_sync for activitypub trails

* fixes build issues

* fix list get

* further CSRF protection

* update API docs

* adds rate limiter

* fix tiptap mentions

* a bit more cleanup of main.go

* Fix migration order

* fixes reviewed notes

* require context for activitpub server calls

* improve hashing for identifier

* fix copy paste error

* fix sync trail/list issues

* fix summit log/comment duplicates

* adaptions after trail merge

* fix dockerignore

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-05-09 17:30:34 +02:00

211 lines
5.3 KiB
Go

package routes
import (
"database/sql"
"errors"
"fmt"
"io"
"net/http"
"os"
"pocketbase/federation"
"pocketbase/util"
"strconv"
"strings"
pub "github.com/go-ap/activitypub"
"github.com/pocketbase/pocketbase/core"
)
func ActivitypubActor(e *core.RequestEvent) error {
resource := e.Request.URL.Query().Get("resource")
resource = strings.TrimPrefix(resource, "acct:")
iri := e.Request.URL.Query().Get("iri")
follows := e.Request.URL.Query().Get("follows") == "true"
var userActor *core.Record
var err error
if e.Auth != nil {
userActor, err = e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
if err != nil {
return err
}
}
ctx, err := util.GetSafeActorContext(e.Request, userActor)
if err != nil {
return err
}
var actor *core.Record
if resource != "" {
actor, err = federation.GetActorByHandle(e.App, ctx, resource, follows)
} else {
actor, err = federation.GetActorByIRI(e.App, ctx, iri, follows)
}
if err != nil && actor == nil {
if strings.HasPrefix(err.Error(), "webfinger") {
return e.NotFoundError("Not found", err)
}
return err
} else if err != nil && actor != nil {
if errors.Is(err, federation.ErrProfilePrivate) {
// this is our own profile
if e.Auth != nil && actor.GetString("user") == e.Auth.Id {
return e.JSON(http.StatusOK, map[string]any{"actor": actor, "error": nil})
} else {
return e.JSON(http.StatusNotFound, map[string]any{"error": "profile is private"})
}
}
// we could not fetch the remote actor so we return our local cached copy
return e.JSON(http.StatusOK, map[string]any{"actor": actor, "error": err.Error()})
}
return e.JSON(http.StatusOK, map[string]any{"actor": actor, "error": nil})
}
func ActivitypubActivityProcess(e *core.RequestEvent) error {
origin := os.Getenv("ORIGIN")
if origin == "" {
return fmt.Errorf("ORIGIN not set")
}
body, err := io.ReadAll(e.Request.Body)
if err != nil {
return err
}
var activity pub.Activity
err = activity.UnmarshalJSON(body)
if err != nil {
return err
}
inbox := fmt.Sprintf("%s%s", origin, e.Request.Header.Get("X-Forwarded-Path"))
recipient, err := e.App.FindFirstRecordByData("activitypub_actors", "inbox", inbox)
if err != nil {
return err
}
actor, err := e.App.FindFirstRecordByData("activitypub_actors", "iri", activity.Actor.GetID().String())
if err != nil {
if err == sql.ErrNoRows {
ctx, err := util.GetSafeActorContext(e.Request, recipient)
if err != nil {
return err
}
actor, err = federation.GetActorByIRI(e.App, ctx, activity.Actor.GetID().String(), false)
if err != nil {
return err
}
} else {
return err
}
}
verified, err := util.VerifySignature(e.App, e.Request, actor.GetString("public_key"))
if err != nil || !verified {
e.App.Logger().Error(err.Error())
return e.UnauthorizedError("Invalid http signature", err)
}
switch activity.Type {
case pub.FollowType:
err = federation.ProcessFollowActivity(e.App, actor, activity)
case pub.AcceptType:
err = federation.ProcessAcceptActivity(e.App, actor, activity)
case pub.UndoType:
err = federation.ProcessUndoActivity(e.App, actor, activity)
case pub.UpdateType:
fallthrough
case pub.CreateType:
err = federation.ProcessCreateOrUpdateActivity(e.App, actor, recipient, activity)
case pub.DeleteType:
err = federation.ProcessDeleteActivity(e.App, actor, activity)
case pub.AnnounceType:
err = federation.ProcessAnnounceActivity(e.App, actor, activity)
case pub.LikeType:
err = federation.ProcessLikeActivity(e.App, actor, activity)
}
return e.JSON(http.StatusOK, err)
}
func ActivitypubActorFollow(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
followType := e.Request.PathValue("follow")
page := e.Request.URL.Query().Get("page")
intPage := 0
if page != "" {
var err error
intPage, err = strconv.Atoi(page)
if err != nil {
return err
}
}
actor, err := e.App.FindRecordById("activitypub_actors", id)
if err != nil {
return err
}
var userActor *core.Record
if e.Auth != nil {
userActor, err = e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
if err != nil {
return err
}
}
ctx, err := util.GetSafeActorContext(e.Request, userActor)
if err != nil {
return err
}
url := actor.GetString(followType)
if url == "" {
return e.BadRequestError("unknown type: "+followType, nil)
}
collection, err := federation.FetchCollection(e.App, ctx, fmt.Sprintf("%s?page=%d", url, intPage))
if err != nil {
if errors.Is(err, federation.ErrProfilePrivate) {
return e.JSON(http.StatusNotFound, map[string]any{"error": "profile is private"})
} else if errors.Is(err, util.ErrRateLimited) {
return e.TooManyRequestsError("Too many requests", err)
}
return err
}
return e.JSON(http.StatusOK, collection)
}
func ActivitypubTrail(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
trail, err := e.App.FindRecordById("trails", id)
if err != nil {
return err
}
trailObject, err := util.ObjectFromTrail(e.App, trail, nil)
if err != nil {
return err
}
return e.JSON(http.StatusOK, trailObject)
}
func ActivitypubComment(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
comment, err := e.App.FindRecordById("comments", id)
if err != nil {
return err
}
commentObject, err := util.ObjectFromComment(e.App, comment, nil)
if err != nil {
return err
}
return e.JSON(http.StatusOK, commentObject)
}