Files
agents_and_robots/shell/matrix/client.go
T

127 lines
4.0 KiB
Go

// Package matrix wraps mautrix-go for agent use.
package matrix
import (
"context"
"crypto/sha256"
"encoding/hex"
"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. "./agents/<id>/data/crypto/crypto.db").
// pickleKeyHex is a hex-encoded key for encrypting crypto material at rest. If empty,
// falls back to sha256(access_token) for backward compatibility.
// agentID namespaces the crypto state within the 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, pickleKeyHex, 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
// Use explicit pickle key if provided, otherwise derive from access token.
var pickleKey []byte
if pickleKeyHex != "" {
pickleKey, err = hex.DecodeString(pickleKeyHex)
if err != nil {
return nil, fmt.Errorf("decode pickle_key_env: %w", err)
}
} else {
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
}