// 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/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: Print results fmt.Println("\n─── Add to your .env ───────────────────────────────") fmt.Printf("%s=%s\n", envVar, token) fmt.Printf("MATRIX_HOMESERVER=%s\n", homeserver) fmt.Printf("MATRIX_SERVER_NAME=%s\n", serverName) 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 { // Simple: use os.ReadFile on /dev/urandom, encode hex 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) }