Files
2026-05-12 20:55:13 +02:00

85 lines
2.5 KiB
Go

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,
},
}
}