114 lines
3.2 KiB
Go
114 lines
3.2 KiB
Go
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)
|
|
}
|