dm: stop creating a duplicate DM room on every restart

GetDMRoom's lookup chain could never hit. The in-memory cache died with the
process, and the m.direct account data it fell back on was never written by
the bot -- room creation only ever populated the map. As an appservice user
there is no /sync to populate it either, so after each restart the first DM to
a user fell through both checks and created a fresh room.

Persist the mapping in a new dm_rooms table and read it before anything else.
Creation now also publishes m.direct, so clients label the room as a DM.

A stored room is checked for liveness before reuse, counting the user as
present while merely invited -- they often never accept, and treating that as
gone would recreate the room on every send, which is the original bug wearing
a different hat. The same check now guards the m.direct path, which previously
took the last entry on faith.

Two adoption paths keep existing rooms from being orphaned: user-initiated DM
invites are claimed on join, and a room the user is already talking in is
adopted on first message (negative-cached so group rooms cost one member
lookup, not one per message). For users predating the table, a one-shot sweep
of joined rooms runs only on the path that would otherwise create a duplicate.

The sweep cannot rank duplicates -- without /sync there are no timestamps, so
it takes the newest by room-list order. First message from the user corrects
any wrong pick.
This commit is contained in:
prosolis
2026-07-26 10:16:53 -07:00
parent 583616f9d0
commit 77dde5d133
3 changed files with 239 additions and 11 deletions
+12
View File
@@ -255,6 +255,9 @@ func main() {
// ---- Set up event handlers ----
// Minimal Base used only for DM-room bookkeeping from the event handlers.
dmLearner := &plugin.Base{Client: client}
// Auto-join on invite + moderation member tracking
sess.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
defer func() {
@@ -277,6 +280,11 @@ func main() {
slog.Error("failed to join room", "room", evt.RoomID, "err", err)
} else {
slog.Info("joined room", "room", evt.RoomID)
// A user-initiated DM invite: claim it now, before any
// outbound DM has a chance to create a rival room.
if mem.IsDirect {
dmLearner.RecordDMRoom(evt.Sender, evt.RoomID)
}
}
}
return
@@ -325,6 +333,10 @@ func main() {
body = strings.TrimSpace(body[idx+2:])
}
}
// Adopt the room the user is talking in if it's their DM room, so the
// bot replies there instead of opening a second one.
dmLearner.LearnDMRoom(evt.Sender, evt.RoomID)
msgCtx := plugin.MessageContext{
RoomID: evt.RoomID,
EventID: evt.ID,