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>
This commit is contained in:
210
db/routes/activitypub.go
Normal file
210
db/routes/activitypub.go
Normal file
@@ -0,0 +1,210 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user