207 lines
6.2 KiB
Go
207 lines
6.2 KiB
Go
// Command register creates a Matrix bot user via the Synapse admin API
|
|
// and outputs the access token to store in .env.
|
|
//
|
|
// Usage:
|
|
//
|
|
// MATRIX_ADMIN_TOKEN=syt_... go run ./cmd/register \
|
|
// --homeserver https://matrix-af2f3d.organic-machine.com \
|
|
// --username assistant-bot \
|
|
// --displayname "Assistant Bot" \
|
|
// --env-var MATRIX_TOKEN_ASSISTANT
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func main() {
|
|
var (
|
|
homeserver string
|
|
username string
|
|
displayname string
|
|
envVar string
|
|
password string
|
|
)
|
|
|
|
root := &cobra.Command{
|
|
Use: "register",
|
|
Short: "Register a Matrix bot user via Synapse admin API",
|
|
Long: `Creates a bot user on your Synapse homeserver and prints its access token.
|
|
|
|
Requires MATRIX_ADMIN_TOKEN env var with an admin user's access token.
|
|
|
|
Example:
|
|
MATRIX_ADMIN_TOKEN=syt_... go run ./cmd/register \
|
|
--homeserver https://matrix.example.com \
|
|
--username my-bot \
|
|
--displayname "My Bot" \
|
|
--env-var MATRIX_TOKEN_MY_BOT`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
adminToken := os.Getenv("MATRIX_ADMIN_TOKEN")
|
|
if adminToken == "" {
|
|
return fmt.Errorf("MATRIX_ADMIN_TOKEN env var is not set")
|
|
}
|
|
|
|
// Strip trailing slash
|
|
homeserver = strings.TrimRight(homeserver, "/")
|
|
|
|
// Extract server name from homeserver URL
|
|
serverName := homeserver
|
|
serverName = strings.TrimPrefix(serverName, "https://")
|
|
serverName = strings.TrimPrefix(serverName, "http://")
|
|
|
|
userID := fmt.Sprintf("@%s:%s", username, serverName)
|
|
|
|
fmt.Printf("→ Registering user %s on %s\n", userID, homeserver)
|
|
|
|
// Generate password if not provided
|
|
if password == "" {
|
|
password = generatePassword()
|
|
}
|
|
|
|
// Step 1: Create/update user via admin API
|
|
if err := createUser(homeserver, adminToken, userID, displayname, password); err != nil {
|
|
return fmt.Errorf("create user: %w", err)
|
|
}
|
|
fmt.Printf("✓ User %s created/updated\n", userID)
|
|
|
|
// Step 2: Login as the bot to get an access token
|
|
token, deviceID, err := loginAs(homeserver, username, password)
|
|
if err != nil {
|
|
return fmt.Errorf("login as bot: %w", err)
|
|
}
|
|
fmt.Printf("✓ Logged in, device ID: %s\n", deviceID)
|
|
|
|
// Step 3: Generate pickle key for E2EE crypto store
|
|
pickleKey := generatePickleKey()
|
|
|
|
// Derive env var prefix from envVar (e.g. MATRIX_TOKEN_FOO → FOO)
|
|
norm := strings.TrimPrefix(envVar, "MATRIX_TOKEN_")
|
|
|
|
// Step 4: Print results — parseable lines for register.sh
|
|
fmt.Println("\n─── Add to your .env ───────────────────────────────")
|
|
fmt.Printf("%s=%s\n", envVar, token)
|
|
fmt.Printf("MATRIX_PASSWORD_%s=%s\n", norm, password)
|
|
fmt.Printf("PICKLE_KEY_%s=%s\n", norm, pickleKey)
|
|
fmt.Println("────────────────────────────────────────────────────")
|
|
fmt.Printf("\nUser ID: %s\n", userID)
|
|
fmt.Printf("Device ID: %s\n", deviceID)
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
root.Flags().StringVar(&homeserver, "homeserver", "", "Matrix homeserver URL (required)")
|
|
root.Flags().StringVar(&username, "username", "", "Bot username, without @ or server (required)")
|
|
root.Flags().StringVar(&displayname, "displayname", "", "Bot display name shown in Matrix")
|
|
root.Flags().StringVar(&envVar, "env-var", "MATRIX_TOKEN_BOT", "Name of the env var to output")
|
|
root.Flags().StringVar(&password, "password", "", "Bot password (auto-generated if empty)")
|
|
_ = root.MarkFlagRequired("homeserver")
|
|
_ = root.MarkFlagRequired("username")
|
|
|
|
if err := root.Execute(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// createUser calls PUT /_synapse/admin/v2/users/@user:server
|
|
func createUser(homeserver, adminToken, userID, displayname, password string) error {
|
|
body := map[string]any{
|
|
"password": password,
|
|
"admin": false,
|
|
"deactivated": false,
|
|
}
|
|
if displayname != "" {
|
|
body["displayname"] = displayname
|
|
}
|
|
|
|
raw, _ := json.Marshal(body)
|
|
url := fmt.Sprintf("%s/_synapse/admin/v2/users/%s", homeserver, userID)
|
|
|
|
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(raw))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("HTTP PUT %s: %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
return fmt.Errorf("admin API returned %d: %s", resp.StatusCode, respBody)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loginAs calls POST /_matrix/client/v3/login with the bot credentials.
|
|
func loginAs(homeserver, username, password string) (token, deviceID string, err error) {
|
|
body := map[string]any{
|
|
"type": "m.login.password",
|
|
"identifier": map[string]string{
|
|
"type": "m.id.user",
|
|
"user": username,
|
|
},
|
|
"password": password,
|
|
}
|
|
raw, _ := json.Marshal(body)
|
|
|
|
url := homeserver + "/_matrix/client/v3/login"
|
|
resp, err := http.Post(url, "application/json", bytes.NewReader(raw))
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("HTTP POST %s: %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", "", fmt.Errorf("login returned %d: %s", resp.StatusCode, respBody)
|
|
}
|
|
|
|
var result struct {
|
|
AccessToken string `json:"access_token"`
|
|
DeviceID string `json:"device_id"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return "", "", fmt.Errorf("parse login response: %w", err)
|
|
}
|
|
return result.AccessToken, result.DeviceID, nil
|
|
}
|
|
|
|
// generatePassword creates a random-enough password for the bot account.
|
|
func generatePassword() string {
|
|
f, err := os.Open("/dev/urandom")
|
|
if err != nil {
|
|
return "agent-bot-default-please-change"
|
|
}
|
|
defer f.Close()
|
|
buf := make([]byte, 24)
|
|
_, _ = io.ReadFull(f, buf)
|
|
return fmt.Sprintf("%x", buf)
|
|
}
|
|
|
|
// generatePickleKey creates a 32-byte hex-encoded key for E2EE crypto store encryption.
|
|
func generatePickleKey() string {
|
|
f, err := os.Open("/dev/urandom")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer f.Close()
|
|
buf := make([]byte, 32)
|
|
_, _ = io.ReadFull(f, buf)
|
|
return hex.EncodeToString(buf)
|
|
}
|