fix: detectar thread en eventos E2EE via cache de eventos cifrados

Añade un tercer mecanismo de deteccion de thread en listener.go para cubrir el caso en que mautrix-go no propaga m.relates_to al payload descifrado.

El problema ocurria cuando Element Web (matrix-js-sdk versiones antiguas) no incluia m.relates_to en el contenido exterior del evento m.room.encrypted. mautrix-go solo copia m.relates_to al payload descifrado si EncryptedEventContent.RelatesTo != nil, por lo que los dos mecanismos existentes (raw map + typed content) fallaban.

La solucion registra un listener global (OnEvent) que captura m.relates_to del evento cifrado ANTES de que CryptoHelper lo descifre y re-despache (los listeners globales se ejecutan antes que los de tipo especifico segun DefaultSyncer.Dispatch). El valor se guarda en un sync.Map keyed por event ID y se consume con LoadAndDelete en el handler EventMessage.
This commit is contained in:
2026-03-08 18:05:58 +00:00
parent 00a7c00020
commit f289729ccf
+45 -10
View File
@@ -29,14 +29,15 @@ type MembershipNotifyFunc func(ctx context.Context, roomID, userID, membership s
// 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
dmCache map[id.RoomID]bool
mu sync.RWMutex
interceptor InterceptFunc // if set and returns true, event is forwarded to orchestrator
membershipNotify MembershipNotifyFunc // if set, called on all StateMember events
client *Client
cfg config.MatrixCfg
handler EventHandler
logger *slog.Logger
dmCache map[id.RoomID]bool
mu sync.RWMutex
interceptor InterceptFunc // if set and returns true, event is forwarded to orchestrator
membershipNotify MembershipNotifyFunc // if set, called on all StateMember events
encryptedRelatesTo sync.Map // id.EventID → map[string]any: cache m.relates_to from encrypted events
}
// NewListener creates a Listener for the given client.
@@ -66,6 +67,22 @@ func (l *Listener) SetMembershipNotify(fn MembershipNotifyFunc) {
func (l *Listener) Run(ctx context.Context) error {
syncer := l.client.raw.Syncer.(*mautrix.DefaultSyncer)
// Cache m.relates_to from encrypted events BEFORE they are decrypted.
// mautrix-go only copies m.relates_to into the decrypted content when
// EncryptedEventContent.RelatesTo != nil (i.e. the outer event has it).
// Older Element / matrix-js-sdk versions may not include it in the outer
// event, so we store it here and use it as a fallback for thread detection.
// Global listeners fire before type-specific ones, so this runs before
// CryptoHelper decrypts and dispatches the event.EventMessage.
syncer.OnEvent(func(ctx context.Context, evt *event.Event) {
if evt.Type != event.EventEncrypted {
return
}
if relatesTo, ok := evt.Content.Raw["m.relates_to"].(map[string]any); ok {
l.encryptedRelatesTo.Store(evt.ID, relatesTo)
}
})
// 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) {
@@ -154,7 +171,10 @@ func (l *Listener) Run(ctx context.Context) error {
msgCtx.EventID = evt.ID.String()
// Extract thread root from m.relates_to (Matrix thread support).
// Two methods: raw map (fast) + typed content fallback (robust for E2EE).
// Three methods in order of preference:
// 1. Raw map from decrypted content (fast path)
// 2. Typed parsed content (robust after E2EE decryption)
// 3. Encrypted event cache (for older clients that omit outer m.relates_to)
if l.cfg.Threads.Enabled {
if relatesTo, ok := evt.Content.Raw["m.relates_to"].(map[string]any); ok {
if relType, _ := relatesTo["rel_type"].(string); relType == "m.thread" {
@@ -163,7 +183,7 @@ func (l *Listener) Run(ctx context.Context) error {
}
}
}
// Fallback: use typed content (more robust after E2EE decryption)
// Fallback 2: use typed content (more robust after E2EE decryption)
if msgCtx.ThreadID == "" {
if msgContent, ok := evt.Content.Parsed.(*event.MessageEventContent); ok && msgContent.RelatesTo != nil {
if threadParent := msgContent.RelatesTo.GetThreadParent(); threadParent != "" {
@@ -172,6 +192,21 @@ func (l *Listener) Run(ctx context.Context) error {
}
}
}
// Fallback 3: encrypted event cache — for E2EE messages where the outer
// m.relates_to was NOT propagated into the decrypted content by mautrix-go
// (happens with older Element / matrix-js-sdk versions).
if msgCtx.ThreadID == "" {
if cached, found := l.encryptedRelatesTo.LoadAndDelete(evt.ID); found {
if rt, ok := cached.(map[string]any); ok {
if relType, _ := rt["rel_type"].(string); relType == "m.thread" {
if threadRoot, _ := rt["event_id"].(string); threadRoot != "" {
msgCtx.ThreadID = threadRoot
l.logger.Debug("thread detected via encrypted event cache", "thread_id", msgCtx.ThreadID)
}
}
}
}
}
}
l.logger.Debug("message parsed",