Files
2026-05-12 22:34:26 +02:00

135 lines
3.9 KiB
Go

package federation
import (
"encoding/json"
"fmt"
"log"
"strings"
"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)
case "Create":
if IsCreatePullRequest(body) {
// Derive instanceURL from the local actor's APID.
instanceURL := extractInstanceURL(localActor.APID)
if err := HandleCreatePullRequest(db, body, instanceURL); err != nil {
log.Printf("federation: handle Create(PullRequest): %v", err)
}
} else {
log.Printf("federation: received Create activity from %s (non-PR, skipped)", actorAPID)
}
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)
}
func extractInstanceURL(apid string) string {
// apid is like "https://example.com/users/alice"
// Return "https://example.com"
parts := strings.SplitN(apid, "/", 4)
if len(parts) >= 3 {
return parts[0] + "//" + parts[2]
}
return apid
}