From cd4c1d0f6ef4011be9528d00229c82b80e4998a3 Mon Sep 17 00:00:00 2001 From: Kenneth Zhao Date: Mon, 3 Aug 2026 17:20:04 +0000 Subject: [PATCH] feat(mattermost): separate group policy for threads vs channels --- nanobot/channels/mattermost/runtime.py | 21 ++++++--- .../tests/test_mattermost_channel.py | 47 +++++++++++++++++++ nanobot/channels/mattermost/webui/index.ts | 1 + .../channels/mattermost/webui/locales/en.json | 10 +++- .../channels/mattermost/webui/locales/es.json | 10 +++- .../channels/mattermost/webui/locales/fr.json | 12 ++++- .../channels/mattermost/webui/locales/id.json | 10 +++- .../channels/mattermost/webui/locales/ja.json | 10 +++- .../channels/mattermost/webui/locales/ko.json | 10 +++- .../mattermost/webui/locales/pt-BR.json | 10 +++- .../channels/mattermost/webui/locales/vi.json | 10 +++- .../mattermost/webui/locales/zh-CN.json | 10 +++- .../mattermost/webui/locales/zh-TW.json | 10 +++- 13 files changed, 154 insertions(+), 17 deletions(-) diff --git a/nanobot/channels/mattermost/runtime.py b/nanobot/channels/mattermost/runtime.py index cbd575423..141c82452 100644 --- a/nanobot/channels/mattermost/runtime.py +++ b/nanobot/channels/mattermost/runtime.py @@ -47,6 +47,7 @@ class MattermostConfig(Base): allow_from_match_mode: str = "id" allow_from: list[str] = Field(default_factory=list) group_policy: str = "mention" + group_policy_in_thread: str = "open" group_allow_from: list[str] = Field(default_factory=list) reply_in_thread: bool = True include_thread_context: bool = True @@ -244,8 +245,10 @@ class MattermostChannel(BaseChannel): ) return - if not is_dm and not self._should_respond_in_channel(message_text, channel_id): - return + if not is_dm: + in_thread = bool(root_id) + if not self._should_respond_in_channel(message_text, channel_id, in_thread=in_thread): + return message_text = self._strip_bot_mention(message_text) @@ -360,12 +363,18 @@ class MattermostChannel(BaseChannel): return chat_id in self.config.group_allow_from return True - def _should_respond_in_channel(self, text: str, chat_id: str) -> bool: - if self.config.group_policy == "open": + def _should_respond_in_channel( + self, text: str, chat_id: str, *, in_thread: bool = False, + ) -> bool: + policy = ( + self.config.group_policy_in_thread if in_thread + else self.config.group_policy + ) + if policy == "open": return True - if self.config.group_policy == "mention": + if policy == "mention": return self._is_mentioned(text) - if self.config.group_policy == "allowlist": + if policy == "allowlist": return chat_id in self.config.group_allow_from return False diff --git a/nanobot/channels/mattermost/tests/test_mattermost_channel.py b/nanobot/channels/mattermost/tests/test_mattermost_channel.py index f9afcdd15..a1fdefea0 100644 --- a/nanobot/channels/mattermost/tests/test_mattermost_channel.py +++ b/nanobot/channels/mattermost/tests/test_mattermost_channel.py @@ -375,6 +375,53 @@ async def test_group_policy_allowlist(): assert channel._should_respond_in_channel("msg", "c2") is False +@pytest.mark.asyncio +async def test_group_policy_in_thread_default_open(): + """Thread uses open policy by default, so no mention needed in threads.""" + channel, fake = _make_channel({"groupPolicy": "mention"}) + channel._self_username = "nanobot" + # In a main channel (not thread), mention is required + assert channel._should_respond_in_channel("hello", "c1", in_thread=False) is False + assert channel._should_respond_in_channel("@nanobot hello", "c1", in_thread=False) is True + # In a thread, no mention needed (default open policy) + assert channel._should_respond_in_channel("hello", "c1", in_thread=True) is True + + +@pytest.mark.asyncio +async def test_group_policy_in_thread_mention(): + """Thread can also use mention policy when configured.""" + channel, fake = _make_channel({ + "groupPolicy": "mention", + "groupPolicyInThread": "mention", + }) + channel._self_username = "nanobot" + # In a thread with mention policy, mention is required + assert channel._should_respond_in_channel("hello", "c1", in_thread=True) is False + assert channel._should_respond_in_channel("@nanobot hello", "c1", in_thread=True) is True + + +@pytest.mark.asyncio +async def test_group_policy_in_thread_open(): + """Thread uses open policy when explicitly configured.""" + channel, fake = _make_channel({ + "groupPolicy": "mention", + "groupPolicyInThread": "open", + }) + assert channel._should_respond_in_channel("hello", "c1", in_thread=True) is True + + +@pytest.mark.asyncio +async def test_group_policy_in_thread_allowlist(): + """Thread uses allowlist policy when configured.""" + channel, fake = _make_channel({ + "groupPolicy": "mention", + "groupPolicyInThread": "allowlist", + "groupAllowFrom": ["c1"], + }) + assert channel._should_respond_in_channel("msg", "c1", in_thread=True) is True + assert channel._should_respond_in_channel("msg", "c2", in_thread=True) is False + + # --------------------------------------------------------------------------- # Match mode: id / username / email # --------------------------------------------------------------------------- diff --git a/nanobot/channels/mattermost/webui/index.ts b/nanobot/channels/mattermost/webui/index.ts index 288ba3e7b..a6206cde0 100644 --- a/nanobot/channels/mattermost/webui/index.ts +++ b/nanobot/channels/mattermost/webui/index.ts @@ -15,6 +15,7 @@ export default { { key: "channels.mattermost.token" }, { key: "channels.mattermost.teamId" }, { key: "channels.mattermost.groupPolicy" }, + { key: "channels.mattermost.groupPolicyInThread" }, ], }, }, diff --git a/nanobot/channels/mattermost/webui/locales/en.json b/nanobot/channels/mattermost/webui/locales/en.json index 438db9d1a..5c2945211 100644 --- a/nanobot/channels/mattermost/webui/locales/en.json +++ b/nanobot/channels/mattermost/webui/locales/en.json @@ -27,13 +27,21 @@ "placeholder": "Optional team ID" }, "groupPolicy": { - "label": "Group behavior", + "label": "Channel behavior", "choices": { "mention": "Mention only", "open": "All messages", "allowlist": "Allowlist" } }, + "groupPolicyInThread": { + "label": "Thread behavior", + "choices": { + "mention": "Mention only", + "open": "All messages (no mention needed)", + "allowlist": "Allowlist" + } + }, "allowFrom": { "label": "Allowed users", "placeholder": "User IDs, comma separated" diff --git a/nanobot/channels/mattermost/webui/locales/es.json b/nanobot/channels/mattermost/webui/locales/es.json index 8600b1961..3ce9963bd 100644 --- a/nanobot/channels/mattermost/webui/locales/es.json +++ b/nanobot/channels/mattermost/webui/locales/es.json @@ -27,13 +27,21 @@ "placeholder": "ID de equipo opcional" }, "groupPolicy": { - "label": "Comportamiento en grupos", + "label": "Comportamiento en canales", "choices": { "mention": "Solo menciones", "open": "Todos los mensajes", "allowlist": "Lista permitida" } }, + "groupPolicyInThread": { + "label": "Comportamiento en hilos", + "choices": { + "mention": "Solo menciones", + "open": "Todos los mensajes (sin mención)", + "allowlist": "Lista permitida" + } + }, "allowFrom": { "label": "Usuarios permitidos", "placeholder": "ID de usuario separados por comas" diff --git a/nanobot/channels/mattermost/webui/locales/fr.json b/nanobot/channels/mattermost/webui/locales/fr.json index c59759fae..94a380c94 100644 --- a/nanobot/channels/mattermost/webui/locales/fr.json +++ b/nanobot/channels/mattermost/webui/locales/fr.json @@ -27,11 +27,19 @@ "placeholder": "ID d’équipe facultatif" }, "groupPolicy": { - "label": "Comportement en groupe", + "label": "Comportement en canal", "choices": { "mention": "Mentions uniquement", "open": "Tous les messages", - "allowlist": "Liste d’autorisation" + "allowlist": "Liste d'autorisation" + } + }, + "groupPolicyInThread": { + "label": "Comportement en fil", + "choices": { + "mention": "Mentions uniquement", + "open": "Tous les messages (sans mention)", + "allowlist": "Liste d'autorisation" } }, "allowFrom": { diff --git a/nanobot/channels/mattermost/webui/locales/id.json b/nanobot/channels/mattermost/webui/locales/id.json index de81e7c03..3f77caa11 100644 --- a/nanobot/channels/mattermost/webui/locales/id.json +++ b/nanobot/channels/mattermost/webui/locales/id.json @@ -27,13 +27,21 @@ "placeholder": "ID tim opsional" }, "groupPolicy": { - "label": "Perilaku grup", + "label": "Perilaku kanal", "choices": { "mention": "Hanya sebutan", "open": "Semua pesan", "allowlist": "Daftar izin" } }, + "groupPolicyInThread": { + "label": "Perilaku thread", + "choices": { + "mention": "Hanya sebutan", + "open": "Semua pesan (tanpa sebutan)", + "allowlist": "Daftar izin" + } + }, "allowFrom": { "label": "Pengguna yang diizinkan", "placeholder": "ID pengguna, dipisahkan koma" diff --git a/nanobot/channels/mattermost/webui/locales/ja.json b/nanobot/channels/mattermost/webui/locales/ja.json index 2db5e5110..721d22a2a 100644 --- a/nanobot/channels/mattermost/webui/locales/ja.json +++ b/nanobot/channels/mattermost/webui/locales/ja.json @@ -27,13 +27,21 @@ "placeholder": "任意のチーム ID" }, "groupPolicy": { - "label": "グループでの動作", + "label": "チャンネルでの動作", "choices": { "mention": "メンションのみ", "open": "すべてのメッセージ", "allowlist": "許可リスト" } }, + "groupPolicyInThread": { + "label": "スレッドでの動作", + "choices": { + "mention": "メンションのみ", + "open": "すべてのメッセージ (メンション不要)", + "allowlist": "許可リスト" + } + }, "allowFrom": { "label": "許可するユーザー", "placeholder": "ユーザー ID(カンマ区切り)" diff --git a/nanobot/channels/mattermost/webui/locales/ko.json b/nanobot/channels/mattermost/webui/locales/ko.json index dd2cf1588..08742b84d 100644 --- a/nanobot/channels/mattermost/webui/locales/ko.json +++ b/nanobot/channels/mattermost/webui/locales/ko.json @@ -27,13 +27,21 @@ "placeholder": "선택적 팀 ID" }, "groupPolicy": { - "label": "그룹 동작", + "label": "채널 동작", "choices": { "mention": "멘션만", "open": "모든 메시지", "allowlist": "허용 목록" } }, + "groupPolicyInThread": { + "label": "스레드 동작", + "choices": { + "mention": "멘션만", + "open": "모든 메시지 (언급 불필요)", + "allowlist": "허용 목록" + } + }, "allowFrom": { "label": "허용된 사용자", "placeholder": "사용자 ID, 쉼표로 구분" diff --git a/nanobot/channels/mattermost/webui/locales/pt-BR.json b/nanobot/channels/mattermost/webui/locales/pt-BR.json index f55c7d75c..837971318 100644 --- a/nanobot/channels/mattermost/webui/locales/pt-BR.json +++ b/nanobot/channels/mattermost/webui/locales/pt-BR.json @@ -27,13 +27,21 @@ "placeholder": "ID de equipe opcional" }, "groupPolicy": { - "label": "Comportamento em grupos", + "label": "Comportamento em canais", "choices": { "mention": "Somente menções", "open": "Todas as mensagens", "allowlist": "Lista de permissão" } }, + "groupPolicyInThread": { + "label": "Comportamento em threads", + "choices": { + "mention": "Somente menções", + "open": "Todas as mensagens (sem menção)", + "allowlist": "Lista de permissão" + } + }, "allowFrom": { "label": "Usuários permitidos", "placeholder": "IDs de usuário separados por vírgulas" diff --git a/nanobot/channels/mattermost/webui/locales/vi.json b/nanobot/channels/mattermost/webui/locales/vi.json index 5e978ef62..4fc510b3a 100644 --- a/nanobot/channels/mattermost/webui/locales/vi.json +++ b/nanobot/channels/mattermost/webui/locales/vi.json @@ -27,13 +27,21 @@ "placeholder": "ID nhóm tùy chọn" }, "groupPolicy": { - "label": "Hành vi trong nhóm", + "label": "Hành vi trong kênh", "choices": { "mention": "Chỉ khi được nhắc", "open": "Mọi tin nhắn", "allowlist": "Danh sách cho phép" } }, + "groupPolicyInThread": { + "label": "Hành vi trong thread", + "choices": { + "mention": "Chỉ khi được nhắc", + "open": "Mọi tin nhắn (không cần nhắc)", + "allowlist": "Danh sách cho phép" + } + }, "allowFrom": { "label": "Người dùng được phép", "placeholder": "ID người dùng, phân tách bằng dấu phẩy" diff --git a/nanobot/channels/mattermost/webui/locales/zh-CN.json b/nanobot/channels/mattermost/webui/locales/zh-CN.json index 0a8db0aea..521a5c06d 100644 --- a/nanobot/channels/mattermost/webui/locales/zh-CN.json +++ b/nanobot/channels/mattermost/webui/locales/zh-CN.json @@ -27,13 +27,21 @@ "placeholder": "可选的团队 ID" }, "groupPolicy": { - "label": "群组行为", + "label": "频道行为", "choices": { "mention": "仅提及时", "open": "所有消息", "allowlist": "白名单" } }, + "groupPolicyInThread": { + "label": "线程行为", + "choices": { + "mention": "仅提及时", + "open": "所有消息(无需提及)", + "allowlist": "白名单" + } + }, "allowFrom": { "label": "允许的用户", "placeholder": "用户 ID,用逗号分隔" diff --git a/nanobot/channels/mattermost/webui/locales/zh-TW.json b/nanobot/channels/mattermost/webui/locales/zh-TW.json index 78d15dff6..d8b4dd940 100644 --- a/nanobot/channels/mattermost/webui/locales/zh-TW.json +++ b/nanobot/channels/mattermost/webui/locales/zh-TW.json @@ -27,13 +27,21 @@ "placeholder": "可選的團隊 ID" }, "groupPolicy": { - "label": "群組行為", + "label": "頻道行為", "choices": { "mention": "僅提及時", "open": "所有訊息", "allowlist": "允許清單" } }, + "groupPolicyInThread": { + "label": "線程行為", + "choices": { + "mention": "僅提及時", + "open": "所有訊息(無需提及)", + "allowlist": "允許清單" + } + }, "allowFrom": { "label": "允許的使用者", "placeholder": "使用者 ID,以逗號分隔"