|
@@ -429,8 +429,6 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
|
|
|
|
|
# Member directory cache: user_id -> user info dict
|
|
# Member directory cache: user_id -> user info dict
|
|
|
self._user_cache: dict[str, User] = {}
|
|
self._user_cache: dict[str, User] = {}
|
|
|
- # Handle -> does a user hold it. Cached both ways; see _mentions_someone_else.
|
|
|
|
|
- self._known_handles: dict[str, bool] = {}
|
|
|
|
|
|
|
|
|
|
# Chattolib client cache and lock for async access.
|
|
# Chattolib client cache and lock for async access.
|
|
|
self._chatto_client: ChattoClient | None = None
|
|
self._chatto_client: ChattoClient | None = None
|
|
@@ -730,39 +728,25 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
async def _handle_belongs_to_a_user(self, handle: str) -> bool:
|
|
async def _handle_belongs_to_a_user(self, handle: str) -> bool:
|
|
|
"""Whether ``handle`` is the login of a real Chatto user.
|
|
"""Whether ``handle`` is the login of a real Chatto user.
|
|
|
|
|
|
|
|
- The API carries no mention entities — ``mention_confirmation_token`` is
|
|
|
|
|
- reserved in the message descriptor — so an @-token is only a candidate
|
|
|
|
|
- until the directory confirms it. Results are cached both ways under the
|
|
|
|
|
- lowercased handle (the mention namespace is case-insensitive per
|
|
|
|
|
- FDR-006), since the same handles recur and a miss is as reusable as a
|
|
|
|
|
- hit. Only logins are looked up: matching another user's display name,
|
|
|
|
|
- as the web frontend does against its room member list, has no directory
|
|
|
|
|
- equivalent here.
|
|
|
|
|
|
|
+ Resolved strictly from local knowledge — the member cache the adapter
|
|
|
|
|
+ already keeps — never from the admin directory: ``admin_get_member``
|
|
|
|
|
+ needs admin permissions a bot account must not be assumed to hold,
|
|
|
|
|
+ and in the field its ``permission_denied`` failed every lookup, which
|
|
|
|
|
+ fail-open let the bot answer every @colleague message. The cache is
|
|
|
|
|
+ filled by the admission pipeline itself (room rosters are fetched for
|
|
|
|
|
+ every channel turn), presence patching and the edited-message path.
|
|
|
|
|
+
|
|
|
|
|
+ An unknown handle is "not confirmed", not "confirmed absent": Chatto
|
|
|
|
|
+ carries no mention entities (``mention_confirmation_token`` is
|
|
|
|
|
+ reserved in the message descriptor), so a @-token stays a candidate
|
|
|
|
|
+ we cannot verify — and staying silent on a false positive is worse
|
|
|
|
|
+ than answering one.
|
|
|
"""
|
|
"""
|
|
|
- cache_key = handle.lower()
|
|
|
|
|
- known = self._known_handles.get(cache_key)
|
|
|
|
|
- if known is not None:
|
|
|
|
|
- return known
|
|
|
|
|
- client = await self._get_chatto_client()
|
|
|
|
|
- if client is None:
|
|
|
|
|
- # Unresolved means "not confirmed", so the message goes through.
|
|
|
|
|
- return False
|
|
|
|
|
- try:
|
|
|
|
|
- member = await client.admin_get_member(login=handle)
|
|
|
|
|
- except Exception as exc:
|
|
|
|
|
- logger.debug("Chatto: could not resolve handle @%s: %s", handle, exc)
|
|
|
|
|
- return False
|
|
|
|
|
- # admin_get_member returns pb_to_dict(GetMemberResponse):
|
|
|
|
|
- # {"member": {"user": {...}, ...}, "roles": [...]} when found,
|
|
|
|
|
- # or {} when not found.
|
|
|
|
|
- if not member.get("member"):
|
|
|
|
|
- self._known_handles[cache_key] = False
|
|
|
|
|
- return False
|
|
|
|
|
- from chattolib.types import User
|
|
|
|
|
-
|
|
|
|
|
- user = User.parse(member["member"].get("user"))
|
|
|
|
|
- self._known_handles[cache_key] = user is not None
|
|
|
|
|
- return user is not None
|
|
|
|
|
|
|
+ lowered = handle.lower()
|
|
|
|
|
+ return any(
|
|
|
|
|
+ user is not None and (user.login or "").lower() == lowered
|
|
|
|
|
+ for user in self._user_cache.values()
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
async def _mentions_someone_else(self, body: str) -> bool:
|
|
async def _mentions_someone_else(self, body: str) -> bool:
|
|
|
"""Whether the message @-mentions a person who is not this bot.
|
|
"""Whether the message @-mentions a person who is not this bot.
|
|
@@ -2014,6 +1998,13 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
)
|
|
)
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
+ # Handle resolution reads the member cache, which the roster
|
|
|
|
|
+ # projection fills. Fetch it now so the someone-else check below
|
|
|
|
|
+ # sees the room's members — the same projection is reused for
|
|
|
|
|
+ # this turn's channel_context further down, one fetch per room.
|
|
|
|
|
+ if policy == RoomPolicy.OPEN:
|
|
|
|
|
+ await self._roster_for_room(client, room_id=room_id)
|
|
|
|
|
+
|
|
|
# In an open channel we see every message, including ones plainly
|
|
# In an open channel we see every message, including ones plainly
|
|
|
# aimed at a named colleague. Answering those would be barging in,
|
|
# aimed at a named colleague. Answering those would be barging in,
|
|
|
# so acknowledge that we read it and stay quiet. Checked after the
|
|
# so acknowledge that we read it and stay quiet. Checked after the
|