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 }