85 lines
2.5 KiB
Go
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,
|
|
},
|
|
}
|
|
}
|