fix(matrix): use member_count as DM signal for named DM rooms

Most Matrix clients auto-set a room name when creating a DM (e.g.
"Alice & Bot" from participant display names), so the old
`is_direct and not has_explicit_name` heuristic classified virtually
all client-created DM rooms as "room", forcing require_mention gating
in legitimate one-on-one DMs.

member_count is now the primary DM signal: <=2 members means the room
is necessarily a 1:1 conversation, regardless of m.direct or an explicit
name. A room that grew to 3+ members but is still in stale m.direct is
still classified as a room (conflict flag set). Falls back to the
m.direct + name heuristic when the count is unavailable.

Also hardens _get_room_member_count with a joined_members API fallback
when the cache-backed state_store is empty.

Salvaged from #48554 by @justemu onto the current plugin adapter path
(gateway/platforms/matrix.py -> plugins/platforms/matrix/adapter.py).

Fixes #48551
This commit is contained in:
justemu 2026-06-23 23:45:19 -07:00 committed by Teknium
parent 0ef86febe2
commit 4aa793345e
3 changed files with 78 additions and 21 deletions

View file

@ -3573,21 +3573,29 @@ class MatrixAdapter(BasePlatformAdapter):
return None
async def _get_room_member_count(self, room_id: str) -> Optional[int]:
# Tier 1: state_store (fast, cache-backed).
state_store = (
getattr(self._client, "state_store", None) if self._client else None
)
if not state_store:
return None
try:
members = await state_store.get_members(room_id)
except Exception:
return None
if members is None:
return None
try:
return len(members)
except TypeError:
return None
if state_store:
try:
members = await state_store.get_members(room_id)
if members is not None:
return len(members)
except Exception:
pass
# Tier 2: API fallback (direct server query) when the cache is empty.
client = getattr(self, "_client", None)
if client is not None and hasattr(client, "joined_members"):
try:
resp = await client.joined_members(room_id)
if getattr(resp, "members", None) is not None:
return len(resp.members)
except Exception:
pass
return None
async def _get_room_name(self, room_id: str) -> Optional[str]:
if not self._client or not hasattr(self._client, "get_state_event"):
@ -3674,8 +3682,23 @@ class MatrixAdapter(BasePlatformAdapter):
member_count = await self._get_room_member_count(room_id)
has_explicit_name = bool(room_name)
is_direct = bool(self._dm_rooms.get(room_id, False))
conflict = bool(is_direct and has_explicit_name)
chat_type = "dm" if is_direct and not has_explicit_name else "room"
# member_count is the primary DM signal: <=2 members means this is
# necessarily a 1:1 conversation (or self-DM), regardless of m.direct
# or room name. Most Matrix clients auto-name DM rooms (e.g.
# "Alice & Bot"), so the old `not has_explicit_name` check
# misclassified virtually all client-created DMs as rooms. Falls back
# to the m.direct + name heuristic when the count is unavailable (e.g.
# state_store and API query both fail). A room that grew to 3+ members
# but is still in stale m.direct is correctly classified as a room.
is_likely_dm = (member_count is not None and member_count <= 2) or (
is_direct and not has_explicit_name
)
conflict = bool(
is_direct
and has_explicit_name
and (member_count is None or member_count > 2)
)
chat_type = "dm" if is_likely_dm else "room"
display_name = room_name or canonical_alias or room_id
identity = MatrixRoomIdentity(