bot contesta con e2ee

This commit is contained in:
2026-03-04 00:59:10 +00:00
parent bd8e1432e5
commit 396fc39b90
12 changed files with 316 additions and 46 deletions
+41
View File
@@ -3,10 +3,14 @@ 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"
@@ -39,7 +43,44 @@ func New(cfg config.MatrixCfg) (*Client, error) {
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
+86 -4
View File
@@ -4,6 +4,7 @@ import (
"context"
"log/slog"
"strings"
"sync"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
@@ -23,6 +24,8 @@ type Listener struct {
cfg config.MatrixCfg
handler EventHandler
logger *slog.Logger
dmCache map[id.RoomID]bool
mu sync.RWMutex
}
// NewListener creates a Listener for the given client.
@@ -32,6 +35,7 @@ func NewListener(client *Client, cfg config.MatrixCfg, handler EventHandler, log
cfg: cfg,
handler: handler,
logger: logger,
dmCache: make(map[id.RoomID]bool),
}
}
@@ -39,24 +43,55 @@ func NewListener(client *Client, cfg config.MatrixCfg, handler EventHandler, log
func (l *Listener) Run(ctx context.Context) error {
syncer := l.client.raw.Syncer.(*mautrix.DefaultSyncer)
// Auto-join rooms when invited. Without this, the bot stays in "invited"
// state and never receives m.room.message events.
syncer.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
if evt.GetStateKey() != l.cfg.UserID {
return
}
if evt.Content.AsMember().Membership != event.MembershipInvite {
return
}
l.logger.Info("received room invite, joining", "room", evt.RoomID, "inviter", evt.Sender)
if _, err := l.client.raw.JoinRoom(ctx, evt.RoomID.String(), "", nil); err != nil {
l.logger.Error("failed to auto-join room", "room", evt.RoomID, "err", err)
} else {
l.logger.Info("auto-joined room", "room", evt.RoomID)
}
})
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
l.logger.Debug("event received", "sender", evt.Sender, "room", evt.RoomID)
if !l.shouldHandle(evt) {
return
}
body, ok := evt.Content.Raw["body"].(string)
if !ok || body == "" {
l.logger.Debug("event has no body, skipping", "room", evt.RoomID)
return
}
// Determine power level (simplified — full impl fetches from room state)
powerLevel := 0
isDM := l.checkIsDM(ctx, evt.RoomID)
isDM := false // TODO: detect DMs via room member count
// Extract m.mentions for reliable mention detection (modern Matrix spec).
var mentionedUsers []string
if mentions, ok := evt.Content.Raw["m.mentions"].(map[string]any); ok {
if userIDs, ok := mentions["user_ids"].([]any); ok {
for _, uid := range userIDs {
if s, ok := uid.(string); ok {
mentionedUsers = append(mentionedUsers, s)
}
}
}
}
opts := message.ParseOptions{
CommandPrefix: l.cfg.Filters.CommandPrefix,
BotUserID: l.cfg.UserID,
CommandPrefix: l.cfg.Filters.CommandPrefix,
BotUserID: l.cfg.UserID,
MentionedUserIDs: mentionedUsers,
}
msgCtx := message.Parse(
@@ -68,6 +103,15 @@ func (l *Listener) Run(ctx context.Context) error {
opts,
)
l.logger.Debug("message parsed",
"sender", msgCtx.SenderID,
"room", msgCtx.RoomID,
"is_dm", msgCtx.IsDirectMsg,
"is_mention", msgCtx.IsMention,
"command", msgCtx.Command,
"content_preview", truncate(msgCtx.Content, 80),
)
go l.handler(ctx, msgCtx, evt)
})
@@ -75,23 +119,51 @@ func (l *Listener) Run(ctx context.Context) error {
return l.client.raw.SyncWithContext(ctx)
}
// checkIsDM returns true if the room has exactly 2 joined members.
// The result is cached so the API is only called once per room.
func (l *Listener) checkIsDM(ctx context.Context, roomID id.RoomID) bool {
l.mu.RLock()
if v, ok := l.dmCache[roomID]; ok {
l.mu.RUnlock()
return v
}
l.mu.RUnlock()
members, err := l.client.raw.JoinedMembers(ctx, roomID)
if err != nil {
l.logger.Warn("could not fetch room members for DM check", "room", roomID, "err", err)
return false
}
isDM := len(members.Joined) == 2
l.mu.Lock()
l.dmCache[roomID] = isDM
l.mu.Unlock()
return isDM
}
// 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) {
l.logger.Debug("ignoring own message", "room", evt.RoomID)
return false
}
// Ignore bots
if f.IgnoreBots && strings.HasSuffix(evt.Sender.String(), "-bot:"+serverName(l.cfg.UserID)) {
l.logger.Debug("ignoring bot sender", "sender", evt.Sender)
return false
}
// Ignore specific users
for _, u := range f.IgnoreUsers {
if evt.Sender.String() == u {
l.logger.Debug("ignoring blocked user", "sender", evt.Sender)
return false
}
}
@@ -106,6 +178,7 @@ func (l *Listener) shouldHandle(evt *event.Event) bool {
}
}
if !allowed {
l.logger.Debug("ignoring event: room not in listen list", "room", evt.RoomID)
return false
}
}
@@ -113,6 +186,15 @@ func (l *Listener) shouldHandle(evt *event.Event) bool {
return true
}
// truncate shortens s to at most n runes for log preview.
func truncate(s string, n int) string {
runes := []rune(s)
if len(runes) <= n {
return s
}
return string(runes[:n]) + "…"
}
func serverName(userID string) string {
parts := strings.SplitN(userID, ":", 2)
if len(parts) == 2 {