bot contesta con e2ee
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user