|
|
@@ -202,7 +202,7 @@ def chat_type_for_room_kind(kind: RoomKind | None) -> HermesChatType:
|
|
|
return _ROOM_KIND_TO_CHAT_TYPE.get(kind, HermesChatType.GROUP)
|
|
|
|
|
|
|
|
|
-class ChannelPolicy(StrEnum):
|
|
|
+class RoomPolicy(StrEnum):
|
|
|
"""How a channel-kind room treats an inbound message.
|
|
|
|
|
|
Derived per room from ``CHATTO_REQUIRE_MENTION_ROOMS`` /
|
|
|
@@ -805,7 +805,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
needs no JoinRoom call — it only gets seeded and added to the list.
|
|
|
Channel-kind rooms additionally report their mention-list status,
|
|
|
because a channel on neither list stays silent and this reply is
|
|
|
- where users copy the room ID from (see ``_channel_join_hint``).
|
|
|
+ where users copy the room ID from (see ``_room_join_hint``).
|
|
|
"""
|
|
|
room_obj = state.room
|
|
|
if room_obj is None:
|
|
|
@@ -839,7 +839,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
if state.viewer_state.is_member
|
|
|
else f"Joined {label}."
|
|
|
)
|
|
|
- return f"{head}\n{self._channel_join_hint(room_obj.id)}"
|
|
|
+ return f"{head}\n{self._room_join_hint(room_obj.id)}"
|
|
|
if state.viewer_state.is_member:
|
|
|
return f"Already a member of {label} — listening there."
|
|
|
return f"Joined {label}."
|
|
|
@@ -876,7 +876,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
self._joined_room_ids.remove(room_obj.id)
|
|
|
return f"Left {label}."
|
|
|
|
|
|
- def _channel_join_hint(self, room_id: str) -> str:
|
|
|
+ def _room_join_hint(self, room_id: str) -> str:
|
|
|
"""The mention-list status appended to a channel-kind /join reply.
|
|
|
|
|
|
Users do not know their room IDs by heart — this reply is where they
|
|
|
@@ -885,12 +885,12 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
gateway startup, hence the restart note.
|
|
|
"""
|
|
|
policy = self._room_policy(room_id)
|
|
|
- if policy == ChannelPolicy.REQUIRE_MENTION:
|
|
|
+ if policy == RoomPolicy.REQUIRE_MENTION:
|
|
|
return (
|
|
|
"This channel answers only @mentions "
|
|
|
"(listed in CHATTO_REQUIRE_MENTION_ROOMS)."
|
|
|
)
|
|
|
- if policy == ChannelPolicy.OPEN:
|
|
|
+ if policy == RoomPolicy.OPEN:
|
|
|
return (
|
|
|
"This channel answers every message "
|
|
|
"(listed in CHATTO_OPTIONAL_MENTION_ROOMS)."
|
|
|
@@ -1033,7 +1033,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
self._room_kinds[room_id] = kind
|
|
|
return kind
|
|
|
|
|
|
- def _room_policy(self, room_id: str) -> ChannelPolicy:
|
|
|
+ def _room_policy(self, room_id: str) -> RoomPolicy:
|
|
|
"""Which of the mention lists a channel-kind room is on.
|
|
|
|
|
|
The two lists are mutually exclusive (enforced by
|
|
|
@@ -1042,10 +1042,10 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
neither list stays silent.
|
|
|
"""
|
|
|
if room_id in self.chatto_config.optional_mention_rooms.value:
|
|
|
- return ChannelPolicy.OPEN
|
|
|
+ return RoomPolicy.OPEN
|
|
|
if room_id in self.chatto_config.require_mention_rooms.value:
|
|
|
- return ChannelPolicy.REQUIRE_MENTION
|
|
|
- return ChannelPolicy.SILENT
|
|
|
+ return RoomPolicy.REQUIRE_MENTION
|
|
|
+ return RoomPolicy.SILENT
|
|
|
|
|
|
def _answers_in_room(self, room_id: str) -> bool:
|
|
|
"""Whether inbound messages from this room reach the agent pipeline.
|
|
|
@@ -1059,7 +1059,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
"""
|
|
|
if self._room_kinds.get(room_id) == RoomKind.DM:
|
|
|
return True
|
|
|
- return self._room_policy(room_id) != ChannelPolicy.SILENT
|
|
|
+ return self._room_policy(room_id) != RoomPolicy.SILENT
|
|
|
|
|
|
async def _dispatch_message_posted(self, payload: MessagePostedPayload) -> None:
|
|
|
# Respond-room gate first: read-only memberships must not cost a
|
|
|
@@ -1308,7 +1308,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
# below on one definition of "addressed", broadcast handles included.
|
|
|
if room_kind != RoomKind.DM:
|
|
|
policy = self._room_policy(room_id)
|
|
|
- if policy == ChannelPolicy.REQUIRE_MENTION and not self._mentions_me(
|
|
|
+ if policy == RoomPolicy.REQUIRE_MENTION and not self._mentions_me(
|
|
|
message_body
|
|
|
):
|
|
|
logger.debug(
|
|
|
@@ -1324,7 +1324,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
# bot-mention test above, so a message naming us *and* someone
|
|
|
# else still counts as ours.
|
|
|
if (
|
|
|
- policy == ChannelPolicy.OPEN
|
|
|
+ policy == RoomPolicy.OPEN
|
|
|
and not self._mentions_me(message_body)
|
|
|
and await self._mentions_someone_else(message_body)
|
|
|
):
|
|
|
@@ -1594,6 +1594,37 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
home_id,
|
|
|
)
|
|
|
|
|
|
+ def _joined_room_label(self, room_id: str) -> str:
|
|
|
+ """One entry for the joined-rooms summary, with how-we-answer tags.
|
|
|
+
|
|
|
+ The tag names the room's participation mode at a glance: DMs answer
|
|
|
+ unconditionally, listed channels answer on mentions or every message,
|
|
|
+ and an unlisted channel is read-only.
|
|
|
+ """
|
|
|
+ name = self._room_names.get(room_id, room_id)
|
|
|
+ if self._room_kinds.get(room_id) == RoomKind.DM:
|
|
|
+ tags = "[dm]"
|
|
|
+ else:
|
|
|
+ policy = self._room_policy(room_id)
|
|
|
+ if policy == RoomPolicy.REQUIRE_MENTION:
|
|
|
+ tags = "[on-mention]"
|
|
|
+ elif policy == RoomPolicy.OPEN:
|
|
|
+ tags = "[every-message]"
|
|
|
+ else:
|
|
|
+ tags = "[read-only]"
|
|
|
+ if room_id in self._universal_room_ids:
|
|
|
+ tags += " [universal]"
|
|
|
+ return f"{name} ({room_id}) {tags}"
|
|
|
+
|
|
|
+ def _log_joined_rooms(self) -> None:
|
|
|
+ """Log the joined rooms and how the bot answers in each of them."""
|
|
|
+ labels = [self._joined_room_label(rid) for rid in self._joined_room_ids]
|
|
|
+ logger.info(
|
|
|
+ "Chatto WS: currently joined in %d room(s): %s",
|
|
|
+ len(labels),
|
|
|
+ ", ".join(labels),
|
|
|
+ )
|
|
|
+
|
|
|
async def _refresh_rooms(self) -> None:
|
|
|
"""Refresh room list via ConnectRPC, join and seed any newly discovered rooms."""
|
|
|
client = await self._get_chatto_client()
|
|
|
@@ -1642,44 +1673,29 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
|
|
|
self._warn_if_home_channel_unjoined(member_ids)
|
|
|
|
|
|
- if not new_room_ids:
|
|
|
- return
|
|
|
-
|
|
|
- logger.info(
|
|
|
- "Chatto WS: discovered %d new room(s): %s",
|
|
|
- len(new_room_ids),
|
|
|
- new_room_ids,
|
|
|
- )
|
|
|
-
|
|
|
- for rid in new_room_ids:
|
|
|
- # Membership came straight from the directory scan
|
|
|
- # (viewer_state.is_member); natively invited rooms need no
|
|
|
- # JoinRoom call — same rule as _run_join.
|
|
|
- if self._answers_in_room(rid):
|
|
|
- await self._seed_room(rid)
|
|
|
- else:
|
|
|
- logger.info(
|
|
|
- "Chatto WS: %s (%s) is on neither mention list -"
|
|
|
- " joined read-only",
|
|
|
- self._room_names.get(rid, rid),
|
|
|
- rid,
|
|
|
- )
|
|
|
- self._joined_room_ids.append(rid)
|
|
|
+ if new_room_ids:
|
|
|
+ logger.info(
|
|
|
+ "Chatto WS: discovered %d new room(s): %s",
|
|
|
+ len(new_room_ids),
|
|
|
+ new_room_ids,
|
|
|
+ )
|
|
|
|
|
|
- joined_room_names: list[str] = []
|
|
|
- for rid in self._joined_room_ids:
|
|
|
- name = self._room_names[rid]
|
|
|
- if not self._answers_in_room(rid):
|
|
|
- name += " [read-only]"
|
|
|
- if rid in self._universal_room_ids:
|
|
|
- name += " [universal]"
|
|
|
- joined_room_names.append(name + " (" + rid + ")")
|
|
|
+ for rid in new_room_ids:
|
|
|
+ # Membership came straight from the directory scan
|
|
|
+ # (viewer_state.is_member); natively invited rooms need no
|
|
|
+ # JoinRoom call — same rule as _run_join.
|
|
|
+ if self._answers_in_room(rid):
|
|
|
+ await self._seed_room(rid)
|
|
|
+ else:
|
|
|
+ logger.info(
|
|
|
+ "Chatto WS: %s (%s) is on neither mention list -"
|
|
|
+ " joined read-only",
|
|
|
+ self._room_names.get(rid, rid),
|
|
|
+ rid,
|
|
|
+ )
|
|
|
+ self._joined_room_ids.append(rid)
|
|
|
|
|
|
- logger.info(
|
|
|
- "Chatto WS: currently joined in %d room(s): %s",
|
|
|
- len(self._joined_room_ids),
|
|
|
- ", ".join(joined_room_names),
|
|
|
- )
|
|
|
+ self._log_joined_rooms()
|
|
|
|
|
|
except Exception:
|
|
|
logger.warning("Chatto WS: _refresh_rooms failed", exc_info=True)
|
|
|
@@ -1687,6 +1703,10 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
# ------------------------------------------------------------------ #
|
|
|
# Read state & notification dismissal (best-effort, Chatto-unique)
|
|
|
# ------------------------------------------------------------------ #
|
|
|
+ # Deliberately outside the try block above: this sweep runs on every
|
|
|
+ # refresh — including refreshes that discovered no new rooms. The
|
|
|
+ # old early return starved it to "only when something changed",
|
|
|
+ # leaving silent rooms unmarked for whole connection lifetimes.
|
|
|
# Best-effort: mark all joined rooms as read (room_id may be undefined here)
|
|
|
for _rid in list(self._joined_room_ids):
|
|
|
try:
|
|
|
@@ -2817,9 +2837,9 @@ def hermes_validate_config(config: PlatformConfig) -> bool:
|
|
|
)
|
|
|
return False
|
|
|
|
|
|
- require_channels = set(chatto_config.require_mention_rooms.value)
|
|
|
- optional_channels = set(chatto_config.optional_mention_rooms.value)
|
|
|
- overlap = sorted(require_channels & optional_channels)
|
|
|
+ require_rooms = set(chatto_config.require_mention_rooms.value)
|
|
|
+ optional_rooms = set(chatto_config.optional_mention_rooms.value)
|
|
|
+ overlap = sorted(require_rooms & optional_rooms)
|
|
|
if overlap:
|
|
|
logger.error(
|
|
|
"Chatto: Conflicting configuration. Room(s) %s are on both "
|