103 lines
3.3 KiB
Go
103 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Registration mirrors a Synapse application-service registration.yaml. Like the
|
|
// mautrix bridges, the bot GENERATES this file (random tokens) and then READS its
|
|
// own tokens back from it — so the same file is the single source of truth shared
|
|
// with Synapse, and tokens are never hand-copied into two places.
|
|
type Registration struct {
|
|
ID string `yaml:"id"`
|
|
URL string `yaml:"url"`
|
|
ASToken string `yaml:"as_token"`
|
|
HSToken string `yaml:"hs_token"`
|
|
SenderLocalpart string `yaml:"sender_localpart"`
|
|
RateLimited bool `yaml:"rate_limited"`
|
|
Namespaces RegNamespaces `yaml:"namespaces"`
|
|
}
|
|
|
|
type RegNamespaces struct {
|
|
Users []RegNamespace `yaml:"users"`
|
|
Aliases []RegNamespace `yaml:"aliases"`
|
|
Rooms []RegNamespace `yaml:"rooms"`
|
|
}
|
|
|
|
type RegNamespace struct {
|
|
Exclusive bool `yaml:"exclusive"`
|
|
Regex string `yaml:"regex"`
|
|
}
|
|
|
|
func randToken() (string, error) {
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// LoadRegistration reads and validates a registration.yaml.
|
|
func LoadRegistration(path string) (*Registration, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var reg Registration
|
|
if err := yaml.Unmarshal(data, ®); err != nil {
|
|
return nil, fmt.Errorf("parse registration %s: %w", path, err)
|
|
}
|
|
if reg.ASToken == "" || reg.HSToken == "" {
|
|
return nil, fmt.Errorf("registration %s missing as_token/hs_token", path)
|
|
}
|
|
return ®, nil
|
|
}
|
|
|
|
// GenerateRegistration writes a fresh registration.yaml with random tokens. It
|
|
// REFUSES to overwrite an existing file — regenerating would rotate the tokens
|
|
// and break the running Synapse binding; delete the file to intentionally regen.
|
|
func GenerateRegistration(path, asURL, localpart, serverName string) error {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return fmt.Errorf("registration already exists at %s (delete it to regenerate — that rotates tokens and breaks the live Synapse binding)", path)
|
|
}
|
|
asTok, err := randToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hsTok, err := randToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reg := &Registration{
|
|
ID: "ai-bot",
|
|
URL: asURL,
|
|
ASToken: asTok,
|
|
HSToken: hsTok,
|
|
SenderLocalpart: localpart,
|
|
RateLimited: false,
|
|
Namespaces: RegNamespaces{
|
|
Users: []RegNamespace{{Exclusive: true, Regex: "@" + regexp.QuoteMeta(localpart+":"+serverName)}},
|
|
Aliases: []RegNamespace{},
|
|
Rooms: []RegNamespace{},
|
|
},
|
|
}
|
|
body, err := yaml.Marshal(reg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header := "# Generated by `ai-bot generate-registration`. Mount this file into the\n" +
|
|
"# Synapse container and add it to app_service_config_files, then restart\n" +
|
|
"# Synapse. The bot reads its tokens from THIS file via REGISTRATION_PATH —\n" +
|
|
"# do not hand-copy the tokens elsewhere.\n"
|
|
// 0644, not 0600: this file is shared with the Synapse container, which runs
|
|
// as a DIFFERENT uid (non-root) and must be able to read it — same as the
|
|
// mautrix bridge registrations. (Token secrecy relies on host access control,
|
|
// not file mode, on the single-tenant VPS.)
|
|
return os.WriteFile(path, append([]byte(header), body...), 0o644)
|
|
}
|