feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user