Repo iniciado

This commit is contained in:
2026-03-03 23:19:23 +00:00
commit c126187c5a
32 changed files with 2719 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
// Package matrix wraps mautrix-go for agent use.
package matrix
import (
"context"
"fmt"
"os"
"maunium.net/go/mautrix"
"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
}
// SendText sends a plain-text message to a room.
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
}
+122
View File
@@ -0,0 +1,122 @@
package matrix
import (
"context"
"log/slog"
"strings"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/pkg/decision"
"github.com/enmanuel/agents/pkg/message"
)
// EventHandler is called for each incoming Matrix message that passes filters.
type EventHandler func(ctx context.Context, msgCtx decision.MessageContext, evt *event.Event)
// Listener attaches to a mautrix syncer and dispatches events to an EventHandler.
type Listener struct {
client *Client
cfg config.MatrixCfg
handler EventHandler
logger *slog.Logger
}
// NewListener creates a Listener for the given client.
func NewListener(client *Client, cfg config.MatrixCfg, handler EventHandler, logger *slog.Logger) *Listener {
return &Listener{
client: client,
cfg: cfg,
handler: handler,
logger: logger,
}
}
// Run starts the Matrix sync loop. Blocks until ctx is cancelled.
func (l *Listener) Run(ctx context.Context) error {
syncer := l.client.raw.Syncer.(*mautrix.DefaultSyncer)
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
if !l.shouldHandle(evt) {
return
}
body, ok := evt.Content.Raw["body"].(string)
if !ok || body == "" {
return
}
// Determine power level (simplified — full impl fetches from room state)
powerLevel := 0
isDM := false // TODO: detect DMs via room member count
opts := message.ParseOptions{
CommandPrefix: l.cfg.Filters.CommandPrefix,
BotUserID: l.cfg.UserID,
}
msgCtx := message.Parse(
body,
evt.Sender.String(),
evt.RoomID.String(),
powerLevel,
isDM,
opts,
)
go l.handler(ctx, msgCtx, evt)
})
l.logger.Info("starting matrix sync")
return l.client.raw.SyncWithContext(ctx)
}
// shouldHandle applies the configured filters to an event.
func (l *Listener) shouldHandle(evt *event.Event) bool {
f := l.cfg.Filters
// Don't handle our own messages
if evt.Sender == id.UserID(l.cfg.UserID) {
return false
}
// Ignore bots
if f.IgnoreBots && strings.HasSuffix(evt.Sender.String(), "-bot:"+serverName(l.cfg.UserID)) {
return false
}
// Ignore specific users
for _, u := range f.IgnoreUsers {
if evt.Sender.String() == u {
return false
}
}
// Check if room is in the listen list
if len(l.cfg.Rooms.Listen) > 0 {
allowed := false
for _, r := range l.cfg.Rooms.Listen {
if evt.RoomID.String() == r {
allowed = true
break
}
}
if !allowed {
return false
}
}
return true
}
func serverName(userID string) string {
parts := strings.SplitN(userID, ":", 2)
if len(parts) == 2 {
return parts[1]
}
return ""
}