implemented federation

This commit is contained in:
2026-05-12 20:55:13 +02:00
parent e360f3697e
commit ab94775162
13 changed files with 874 additions and 30 deletions
+113
View File
@@ -0,0 +1,113 @@
package federation
import (
"encoding/json"
"fmt"
"log"
"time"
"xorm.io/xorm"
"github.com/forgeo/forgebucket/internal/models"
)
// Receive persists an inbound activity and dispatches it by type.
// The caller is responsible for verifying the HTTP signature before calling this.
func Receive(db *xorm.Engine, localActor *models.FederationActor, body []byte) error {
var activity map[string]any
if err := json.Unmarshal(body, &activity); err != nil {
return fmt.Errorf("parse activity: %w", err)
}
actType, _ := activity["type"].(string)
actorAPID, _ := activity["actor"].(string)
entry := &models.FederationActivity{
ActorAPID: localActor.APID,
Type: actType,
ObjectJSON: string(body),
Direction: "inbound",
RemoteActor: actorAPID,
Published: time.Now().UTC(),
}
db.Insert(entry) //nolint:errcheck
switch actType {
case "Follow":
return handleFollow(db, localActor, activity, actorAPID)
case "Accept":
handleAccept(db, localActor, activity)
case "Undo":
handleUndo(db, localActor, activity)
default:
log.Printf("federation: received unhandled activity type %q from %s", actType, actorAPID)
}
return nil
}
// handleFollow auto-accepts all incoming Follow activities and sends an Accept
// back to the sender's inbox.
func handleFollow(db *xorm.Engine, localActor *models.FederationActor, follow map[string]any, followerAPID string) error {
if followerAPID == "" {
return fmt.Errorf("Follow activity missing actor field")
}
// Fetch the follower's remote actor to get their inbox URL.
remote, err := FetchActor(db, followerAPID)
if err != nil {
return fmt.Errorf("fetch follower actor: %w", err)
}
if remote.InboxURL == "" {
return fmt.Errorf("follower has no inbox URL")
}
// Build Accept activity.
accept := map[string]any{
"@context": "https://www.w3.org/ns/activitystreams",
"id": localActor.APID + "/activities/accept-" + fmt.Sprint(time.Now().UnixNano()),
"type": "Accept",
"actor": localActor.APID,
"object": follow,
}
// Deliver asynchronously so inbox handler returns quickly.
go func() {
if err := DeliverActivity(localActor, accept, remote.InboxURL); err != nil {
log.Printf("federation: deliver Accept to %s: %v", remote.InboxURL, err)
return
}
// Store the outbound Accept.
db.Insert(&models.FederationActivity{ //nolint:errcheck
ActorAPID: localActor.APID,
Type: "Accept",
ObjectJSON: mustJSON(accept),
Direction: "outbound",
RemoteActor: followerAPID,
Published: time.Now().UTC(),
})
log.Printf("federation: accepted Follow from %s", followerAPID)
}()
return nil
}
func handleAccept(db *xorm.Engine, localActor *models.FederationActor, activity map[string]any) {
// A remote actor accepted our Follow. Nothing to store beyond the inbox entry.
log.Printf("federation: received Accept for actor %s", localActor.APID)
}
func handleUndo(db *xorm.Engine, localActor *models.FederationActor, activity map[string]any) {
// Common case: undo a Follow (unfollow).
obj, _ := activity["object"].(map[string]any)
if obj == nil {
return
}
if t, _ := obj["type"].(string); t == "Follow" {
log.Printf("federation: received Undo(Follow) for actor %s", localActor.APID)
}
}
func mustJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}