implemented federation
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user