// Package matrix wraps mautrix-go for agent use. package matrix import ( "context" "crypto/sha256" "fmt" "io" "os" "path/filepath" "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto/cryptohelper" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "github.com/enmanuel/agents/internal/config" ) // Client wraps a mautrix client with agent-relevant helpers. type Client struct { raw *mautrix.Client cfg config.MatrixCfg } // New creates and authenticates a Matrix client from config. // The access token is read from the env var specified in cfg.AccessTokenEnv. func New(cfg config.MatrixCfg) (*Client, error) { token := os.Getenv(cfg.AccessTokenEnv) if token == "" { return nil, fmt.Errorf("env var %s is not set", cfg.AccessTokenEnv) } raw, err := mautrix.NewClient(cfg.Homeserver, id.UserID(cfg.UserID), token) if err != nil { return nil, fmt.Errorf("create matrix client: %w", err) } if cfg.DeviceID != "" { raw.DeviceID = id.DeviceID(cfg.DeviceID) } return &Client{raw: raw, cfg: cfg}, nil } // InitCrypto sets up end-to-end encryption using the mautrix cryptohelper. // storePath is the SQLite file path for crypto material (e.g. "./data/crypto/crypto.db"). // agentID is used to namespace the crypto state so multiple agents can share a database. // Returns an io.Closer that must be called on agent shutdown to flush the crypto store. func (c *Client) InitCrypto(ctx context.Context, storePath, agentID string) (io.Closer, error) { // Resolve the actual device ID from the server — the value in config may differ // from what the registration process assigned. whoami, err := c.raw.Whoami(ctx) if err != nil { return nil, fmt.Errorf("whoami for crypto init: %w", err) } c.raw.DeviceID = whoami.DeviceID // Derive a stable pickle key from the access token. // If the token changes (bot re-registered), delete the crypto store to reset. sum := sha256.Sum256([]byte(c.raw.AccessToken)) pickleKey := sum[:] if err := os.MkdirAll(filepath.Dir(storePath), 0700); err != nil { return nil, fmt.Errorf("create crypto store dir: %w", err) } helper, err := cryptohelper.NewCryptoHelper(c.raw, pickleKey, storePath) if err != nil { return nil, fmt.Errorf("create crypto helper: %w", err) } helper.DBAccountID = agentID if err := helper.Init(ctx); err != nil { return nil, fmt.Errorf("init e2ee: %w", err) } c.raw.Crypto = helper return helper, nil } // SendText sends a plain-text message to a room. // If the room has E2EE enabled and crypto is initialized, the message is encrypted automatically. func (c *Client) SendText(ctx context.Context, roomID, text string) error { _, err := c.raw.SendText(ctx, id.RoomID(roomID), text) return err } // SendMarkdown sends a formatted (Markdown) message to a room. func (c *Client) SendMarkdown(ctx context.Context, roomID, markdown string) error { content := event.MessageEventContent{ MsgType: event.MsgText, Body: markdown, Format: event.FormatHTML, FormattedBody: markdown, // mautrix can render markdown -> HTML if needed } _, err := c.raw.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content) return err } // SendReaction sends a reaction to an event. func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error { _, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction) return err } // SendTyping sets the typing indicator in a room. func (c *Client) SendTyping(ctx context.Context, roomID string, typing bool) error { _, err := c.raw.UserTyping(ctx, id.RoomID(roomID), typing, 5000) return err } // Raw returns the underlying mautrix.Client for advanced use. func (c *Client) Raw() *mautrix.Client { return c.raw }