package federation import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "strings" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/models" ) // APID returns the canonical ActivityPub actor ID for a local username. func APID(instanceURL, username string) string { return strings.TrimRight(instanceURL, "/") + "/users/" + username } // GetOrCreate fetches the FederationActor for a user, creating it with a fresh // RSA-2048 key pair if none exists. Actor URLs are derived from instanceURL. func GetOrCreate(db *xorm.Engine, userID int64, username, instanceURL string) (*models.FederationActor, error) { var actor models.FederationActor if found, _ := db.Where("user_id = ?", userID).Get(&actor); found { return &actor, nil } priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, fmt.Errorf("generate rsa key: %w", err) } privPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv), }) pubDER, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) if err != nil { return nil, fmt.Errorf("marshal public key: %w", err) } pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER}) base := APID(instanceURL, username) actor = models.FederationActor{ UserID: userID, APID: base, InboxURL: base + "/inbox", OutboxURL: base + "/outbox", PublicKey: string(pubPEM), PrivateKey: string(privPEM), } if _, err := db.Insert(&actor); err != nil { // Race condition: another goroutine may have just created it. if found, _ := db.Where("user_id = ?", userID).Get(&actor); found { return &actor, nil } return nil, fmt.Errorf("insert actor: %w", err) } return &actor, nil } // ActorJSON builds the JSON-LD actor document returned by GET /users/{username}. func ActorJSON(actor *models.FederationActor, username, displayName string) map[string]any { return map[string]any{ "@context": []any{ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", }, "id": actor.APID, "type": "Person", "preferredUsername": username, "name": displayName, "inbox": actor.InboxURL, "outbox": actor.OutboxURL, "followers": actor.APID + "/followers", "following": actor.APID + "/following", "publicKey": map[string]any{ "id": actor.APID + "#main-key", "owner": actor.APID, "publicKeyPem": actor.PublicKey, }, } }