refactor(channels): make built-in channels self-contained (#4908)
* refactor(channels): own setup and instance contracts * refactor(channels): isolate management contracts * refactor(channels): normalize activation contracts * fix(channels): enforce management contracts * refactor(channels): finish setup ownership migration * fix(channels): harden management contracts * fix(channels): enforce lazy loading and runtime ownership * fix(feishu): make multi-instance startup idempotent * fix(webui): render channel setup contracts cleanly * fix(feishu): stop websocket clients cleanly * fix(channels): enforce persistence and activation gates * fix(channels): preserve global feature action scope * fix(channels): apply defaults for single plugins * fix(channels): enforce management contract boundaries * refactor(feishu): remove identity helper indirection * fix(channels): preserve management setup contracts * refactor(channels): generalize instance settings UI * refactor(channels): package channel plugins with web UI metadata * refactor(channels): make built-ins self-contained packages * test(channels): colocate tests with channel packages * fix(dingtalk): use official brand icon * feat(channels): colocate webui translations * docs(channels): clarify plugin ownership * test(exec): remove output wait race * refactor(channels): unify plugin descriptors * fix(channels): enforce descriptor-owned contracts * refactor(channels): finish package-owned plugin setup * refactor(channels): use repository-owned packages only * fix(channels): self-describe dependencies and runtime state * fix(channels): warn about legacy entry points
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Telegram channel package."""
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Telegram management contract."""
|
||||
|
||||
from nanobot.channels._manifest import GROUP_POLICIES, field, required
|
||||
from nanobot.channels.contracts import ChannelSetupSpec
|
||||
from nanobot.channels.plugin import ChannelPlugin
|
||||
from nanobot.channels.telegram.validation import validate
|
||||
|
||||
SETUP_SPEC = ChannelSetupSpec(
|
||||
fields={
|
||||
"token": field("secret"),
|
||||
"allowFrom": field("list"),
|
||||
"groupPolicy": field("enum", choices=GROUP_POLICIES, default="mention"),
|
||||
},
|
||||
required=(required("token"),),
|
||||
official_url="https://t.me/BotFather",
|
||||
validator=validate,
|
||||
)
|
||||
|
||||
PLUGIN = ChannelPlugin(
|
||||
name="telegram",
|
||||
display_name="Telegram",
|
||||
runtime=f"{__package__}.runtime:TelegramChannel",
|
||||
setup=SETUP_SPEC,
|
||||
dependencies=(
|
||||
"python-telegram-bot[socks,webhooks]>=22.6,<23.0",
|
||||
"socksio>=1.0.0,<2.0.0",
|
||||
"python-socks[asyncio]>=2.8.0,<3.0.0; sys_platform != 'win32'",
|
||||
),
|
||||
webui="webui/index.ts",
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
"""Tests for the Telegram channel package."""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from nanobot.channels.telegram import validation as telegram_validation
|
||||
from nanobot.channels.validation import validate_channel_config
|
||||
from nanobot.config.loader import save_config
|
||||
from nanobot.config.schema import Config
|
||||
|
||||
|
||||
def test_validate_telegram_bad_token_is_invalid(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
result = validate_channel_config("telegram", {"channels.telegram.token": "not-a-token"})
|
||||
|
||||
assert result["status"] == "invalid"
|
||||
assert result["can_enable"] is False
|
||||
assert result["missing_fields"] == []
|
||||
|
||||
|
||||
def test_validate_telegram_does_not_expose_saved_token_in_http_errors(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
token = "123456:abcdefghijklmnopqrstuvwxyz"
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(
|
||||
Config.model_validate({"channels": {"telegram": {"token": token}}}),
|
||||
config_path,
|
||||
)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
def raise_http_error(url: str, **_kwargs) -> dict:
|
||||
request = httpx.Request("GET", url)
|
||||
response = httpx.Response(401, request=request)
|
||||
raise httpx.HTTPStatusError("unauthorized", request=request, response=response)
|
||||
|
||||
monkeypatch.setattr(telegram_validation, "http_get", raise_http_error)
|
||||
|
||||
result = validate_channel_config("telegram", {"channels.telegram.token": ""})
|
||||
|
||||
assert token not in str(result)
|
||||
assert any("HTTP 401" in check.get("message", "") for check in result["checks"])
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Telegram setup validation owned by the channel package."""
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from nanobot.channels.contracts import ChannelValidationContext
|
||||
from nanobot.channels.validation import (
|
||||
check,
|
||||
http_get,
|
||||
message_from_response,
|
||||
payload,
|
||||
required_checks,
|
||||
status_from_checks,
|
||||
string_value,
|
||||
)
|
||||
|
||||
|
||||
def validate(values: dict[str, Any], _context: ChannelValidationContext) -> dict[str, Any]:
|
||||
checks, missing = required_checks("telegram", values)
|
||||
token = string_value(values.get("token"))
|
||||
if token:
|
||||
if not re.match(r"^\d+:[A-Za-z0-9_-]{20,}$", token):
|
||||
checks.append(
|
||||
check(
|
||||
"token_format",
|
||||
"Token format",
|
||||
"fail",
|
||||
"Telegram tokens look like 123456:ABC...",
|
||||
)
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
check("token_format", "Token format", "pass", "Looks like a BotFather token.")
|
||||
)
|
||||
try:
|
||||
data = http_get(f"https://api.telegram.org/bot{token}/getMe")
|
||||
if data.get("ok") and isinstance(data.get("result"), dict):
|
||||
bot = data["result"]
|
||||
identity = {
|
||||
"name": bot.get("username") or bot.get("first_name"),
|
||||
"account": str(bot.get("id") or ""),
|
||||
}
|
||||
checks.append(
|
||||
check("get_me", "Bot identity", "pass", "Telegram accepted the bot token.")
|
||||
)
|
||||
return payload(
|
||||
"telegram",
|
||||
"connected",
|
||||
checks,
|
||||
identity=identity,
|
||||
missing_fields=missing,
|
||||
)
|
||||
checks.append(
|
||||
check(
|
||||
"get_me",
|
||||
"Bot identity",
|
||||
"fail",
|
||||
message_from_response(data, "Telegram rejected the token."),
|
||||
)
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
checks.append(
|
||||
check(
|
||||
"get_me",
|
||||
"Bot identity",
|
||||
"warn",
|
||||
f"Telegram could not verify the token: HTTP {exc.response.status_code}.",
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
checks.append(
|
||||
check(
|
||||
"get_me",
|
||||
"Bot identity",
|
||||
"warn",
|
||||
"Could not reach Telegram now. Try again later.",
|
||||
)
|
||||
)
|
||||
return status_from_checks("telegram", checks, missing)
|
||||
|
||||
|
||||
__all__ = ["validate"]
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ChannelUiContribution } from "@/channel-plugins/types";
|
||||
import { chatAppGuideUrl } from "@/components/settings/channels/catalog";
|
||||
|
||||
export default {
|
||||
presentation: {
|
||||
displayName: "Telegram",
|
||||
initials: "TG",
|
||||
color: "#229ED9",
|
||||
logoUrl: "https://telegram.org/favicon.ico",
|
||||
setup: {
|
||||
mode: "credentials",
|
||||
docsUrl: chatAppGuideUrl("telegram"),
|
||||
fields: [
|
||||
{ key: "channels.telegram.token" },
|
||||
{ key: "channels.telegram.allowFrom" },
|
||||
{ key: "channels.telegram.groupPolicy" },
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies ChannelUiContribution;
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Chat with nanobot from Telegram chats.",
|
||||
"requirements": "Bot token, allowed users, gateway",
|
||||
"setup": {
|
||||
"docsLabel": "Open Telegram setup",
|
||||
"officialLabel": "Open BotFather",
|
||||
"tryIt": "Send /start or a short DM to your Telegram bot.",
|
||||
"summary": "Enable turns on Telegram support. Telegram still needs a BotFather token before messages can flow.",
|
||||
"steps": [
|
||||
"Create a bot with BotFather and copy its token.",
|
||||
"Add the token and choose who can message the bot.",
|
||||
"Save and enable Telegram, then send the bot a direct message or mention it in a group."
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "Bot token",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "Create it with BotFather."
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "Allowed users",
|
||||
"placeholder": "* or Telegram user IDs",
|
||||
"help": "Leave empty to use pairing codes."
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "Group behavior",
|
||||
"choices": {
|
||||
"mention": "Mention only",
|
||||
"open": "All messages",
|
||||
"allowlist": "Allowlist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Chatea con nanobot desde Telegram.",
|
||||
"requirements": "Token del bot, usuarios permitidos y gateway",
|
||||
"setup": {
|
||||
"docsLabel": "Abrir guía de Telegram",
|
||||
"officialLabel": "Abrir BotFather",
|
||||
"tryIt": "Envía /start o un DM corto a tu bot de Telegram.",
|
||||
"summary": "Activar habilita Telegram. Aún necesitas un token de BotFather para intercambiar mensajes.",
|
||||
"steps": [
|
||||
"Crea un bot con BotFather y copia su token.",
|
||||
"Añade el token y elige quién puede escribir al bot.",
|
||||
"Guarda y activa Telegram; después envía un DM o menciona el bot en un grupo."
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "Token del bot",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "Créalo con BotFather."
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "Usuarios permitidos",
|
||||
"placeholder": "* o ID de usuario de Telegram",
|
||||
"help": "Déjalo vacío para usar códigos de vinculación."
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "Comportamiento en grupos",
|
||||
"choices": {
|
||||
"mention": "Solo menciones",
|
||||
"open": "Todos los mensajes",
|
||||
"allowlist": "Lista permitida"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Discutez avec nanobot depuis Telegram.",
|
||||
"requirements": "Jeton du bot, utilisateurs autorisés et passerelle",
|
||||
"setup": {
|
||||
"docsLabel": "Ouvrir le guide Telegram",
|
||||
"officialLabel": "Ouvrir BotFather",
|
||||
"tryIt": "Envoyez /start ou un court message privé à votre bot Telegram.",
|
||||
"summary": "L’activation ouvre la prise en charge de Telegram. Un jeton BotFather reste nécessaire pour échanger des messages.",
|
||||
"steps": [
|
||||
"Créez un bot avec BotFather et copiez son jeton.",
|
||||
"Ajoutez le jeton et choisissez qui peut contacter le bot.",
|
||||
"Enregistrez et activez Telegram, puis envoyez un message privé ou mentionnez le bot dans un groupe."
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "Jeton du bot",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "Créez-le avec BotFather."
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "Utilisateurs autorisés",
|
||||
"placeholder": "* ou ID utilisateur Telegram",
|
||||
"help": "Laissez vide pour utiliser les codes d’association."
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "Comportement en groupe",
|
||||
"choices": {
|
||||
"mention": "Mentions uniquement",
|
||||
"open": "Tous les messages",
|
||||
"allowlist": "Liste d’autorisation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Mengobrol dengan nanobot dari Telegram.",
|
||||
"requirements": "Token bot, pengguna yang diizinkan, dan gateway",
|
||||
"setup": {
|
||||
"docsLabel": "Buka panduan Telegram",
|
||||
"officialLabel": "Buka BotFather",
|
||||
"tryIt": "Kirim /start atau DM singkat ke bot Telegram Anda.",
|
||||
"summary": "Mengaktifkan akan menyalakan dukungan Telegram. Token BotFather tetap diperlukan untuk bertukar pesan.",
|
||||
"steps": [
|
||||
"Buat bot dengan BotFather dan salin tokennya.",
|
||||
"Tambahkan token dan pilih siapa yang boleh mengirim pesan ke bot.",
|
||||
"Simpan dan aktifkan Telegram, lalu kirim DM atau sebut bot di grup."
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "Token bot",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "Buat dengan BotFather."
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "Pengguna yang diizinkan",
|
||||
"placeholder": "* atau ID pengguna Telegram",
|
||||
"help": "Kosongkan untuk memakai kode pairing."
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "Perilaku grup",
|
||||
"choices": {
|
||||
"mention": "Hanya sebutan",
|
||||
"open": "Semua pesan",
|
||||
"allowlist": "Daftar izin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Telegram のチャットから nanobot と会話します。",
|
||||
"requirements": "ボットトークン、許可するユーザー、ゲートウェイ",
|
||||
"setup": {
|
||||
"docsLabel": "Telegram 設定ガイドを開く",
|
||||
"officialLabel": "BotFather を開く",
|
||||
"tryIt": "Telegram ボットに /start または短い DM を送信します。",
|
||||
"summary": "有効化すると Telegram 対応がオンになります。メッセージの送受信には BotFather トークンが必要です。",
|
||||
"steps": [
|
||||
"BotFather でボットを作成し、トークンをコピーします。",
|
||||
"トークンを追加し、ボットに連絡できるユーザーを選びます。",
|
||||
"保存して Telegram を有効にし、DM を送るかグループでメンションします。"
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "ボットトークン",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "BotFather で作成します。"
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "許可するユーザー",
|
||||
"placeholder": "* または Telegram ユーザー ID",
|
||||
"help": "ペアリングコードを使う場合は空欄にします。"
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "グループでの動作",
|
||||
"choices": {
|
||||
"mention": "メンションのみ",
|
||||
"open": "すべてのメッセージ",
|
||||
"allowlist": "許可リスト"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Telegram 채팅에서 nanobot과 대화합니다.",
|
||||
"requirements": "봇 토큰, 허용된 사용자 및 게이트웨이",
|
||||
"setup": {
|
||||
"docsLabel": "Telegram 설정 가이드 열기",
|
||||
"officialLabel": "BotFather 열기",
|
||||
"tryIt": "Telegram 봇에 /start 또는 짧은 DM을 보내세요.",
|
||||
"summary": "활성화하면 Telegram 지원이 켜집니다. 메시지를 주고받으려면 BotFather 토큰이 필요합니다.",
|
||||
"steps": [
|
||||
"BotFather로 봇을 만들고 토큰을 복사하세요.",
|
||||
"토큰을 추가하고 봇에 메시지를 보낼 사용자를 선택하세요.",
|
||||
"저장하고 Telegram을 활성화한 다음 DM을 보내거나 그룹에서 멘션하세요."
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "봇 토큰",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "BotFather에서 생성하세요."
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "허용된 사용자",
|
||||
"placeholder": "* 또는 Telegram 사용자 ID",
|
||||
"help": "페어링 코드를 사용하려면 비워 두세요."
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "그룹 동작",
|
||||
"choices": {
|
||||
"mention": "멘션만",
|
||||
"open": "모든 메시지",
|
||||
"allowlist": "허용 목록"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Converse com o nanobot pelo Telegram.",
|
||||
"requirements": "Token do bot, usuários permitidos e gateway",
|
||||
"setup": {
|
||||
"docsLabel": "Abrir guia do Telegram",
|
||||
"officialLabel": "Abrir BotFather",
|
||||
"tryIt": "Envie /start ou uma DM curta ao seu bot do Telegram.",
|
||||
"summary": "Ativar liga o suporte ao Telegram. Um token do BotFather ainda é necessário para trocar mensagens.",
|
||||
"steps": [
|
||||
"Crie um bot com o BotFather e copie o token.",
|
||||
"Adicione o token e escolha quem pode enviar mensagens ao bot.",
|
||||
"Salve e ative o Telegram; depois, envie uma DM ou mencione o bot em um grupo."
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "Token do bot",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "Crie-o com o BotFather."
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "Usuários permitidos",
|
||||
"placeholder": "* ou IDs de usuário do Telegram",
|
||||
"help": "Deixe vazio para usar códigos de pareamento."
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "Comportamento em grupos",
|
||||
"choices": {
|
||||
"mention": "Somente menções",
|
||||
"open": "Todas as mensagens",
|
||||
"allowlist": "Lista de permissão"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "Trò chuyện với nanobot từ Telegram.",
|
||||
"requirements": "Token bot, người dùng được phép và gateway",
|
||||
"setup": {
|
||||
"docsLabel": "Mở hướng dẫn Telegram",
|
||||
"officialLabel": "Mở BotFather",
|
||||
"tryIt": "Gửi /start hoặc tin nhắn riêng ngắn đến bot Telegram.",
|
||||
"summary": "Bật sẽ kích hoạt hỗ trợ Telegram. Bạn vẫn cần token BotFather để trao đổi tin nhắn.",
|
||||
"steps": [
|
||||
"Tạo bot bằng BotFather và sao chép token.",
|
||||
"Thêm token và chọn người có thể nhắn cho bot.",
|
||||
"Lưu và bật Telegram, sau đó gửi tin nhắn riêng hoặc nhắc bot trong nhóm."
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "Token bot",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "Tạo bằng BotFather."
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "Người dùng được phép",
|
||||
"placeholder": "* hoặc ID người dùng Telegram",
|
||||
"help": "Để trống để dùng mã ghép nối."
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "Hành vi trong nhóm",
|
||||
"choices": {
|
||||
"mention": "Chỉ khi được nhắc",
|
||||
"open": "Mọi tin nhắn",
|
||||
"allowlist": "Danh sách cho phép"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "通过 Telegram 会话与 nanobot 聊天。",
|
||||
"requirements": "机器人令牌、允许的用户和网关",
|
||||
"setup": {
|
||||
"docsLabel": "打开 Telegram 配置指南",
|
||||
"officialLabel": "打开 BotFather",
|
||||
"tryIt": "向 Telegram 机器人发送 /start 或一条简短私信。",
|
||||
"summary": "启用只会打开 Telegram 支持;收发消息前仍需填写 BotFather 令牌。",
|
||||
"steps": [
|
||||
"使用 BotFather 创建机器人并复制令牌。",
|
||||
"填写令牌并选择哪些用户可以向机器人发送消息。",
|
||||
"保存并启用 Telegram,然后向机器人发送私信或在群组中提及它。"
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "机器人令牌",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "使用 BotFather 创建。"
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "允许的用户",
|
||||
"placeholder": "* 或 Telegram 用户 ID",
|
||||
"help": "留空则使用配对码。"
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "群组行为",
|
||||
"choices": {
|
||||
"mention": "仅提及时",
|
||||
"open": "所有消息",
|
||||
"allowlist": "白名单"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "透過 Telegram 對話與 nanobot 聊天。",
|
||||
"requirements": "機器人權杖、允許的使用者和閘道",
|
||||
"setup": {
|
||||
"docsLabel": "開啟 Telegram 設定指南",
|
||||
"officialLabel": "開啟 BotFather",
|
||||
"tryIt": "向 Telegram 機器人傳送 /start 或一則簡短私訊。",
|
||||
"summary": "啟用只會開啟 Telegram 支援;收發訊息前仍需填入 BotFather 權杖。",
|
||||
"steps": [
|
||||
"使用 BotFather 建立機器人並複製權杖。",
|
||||
"填入權杖並選擇哪些使用者可以向機器人傳送訊息。",
|
||||
"儲存並啟用 Telegram,然後傳送私訊或在群組中提及機器人。"
|
||||
],
|
||||
"fields": {
|
||||
"token": {
|
||||
"label": "機器人權杖",
|
||||
"placeholder": "123456:ABC...",
|
||||
"help": "使用 BotFather 建立。"
|
||||
},
|
||||
"allowFrom": {
|
||||
"label": "允許的使用者",
|
||||
"placeholder": "* 或 Telegram 使用者 ID",
|
||||
"help": "留空則使用配對碼。"
|
||||
},
|
||||
"groupPolicy": {
|
||||
"label": "群組行為",
|
||||
"choices": {
|
||||
"mention": "僅提及時",
|
||||
"open": "所有訊息",
|
||||
"allowlist": "允許清單"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user