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