Spaces:
Running
Running
SilverElixir commited on
Commit ·
eb80878
1
Parent(s): 398ccda
Add owner ban commands, dependabot PR cap, seed docs
Browse files- .github/dependabot.yml +4 -0
- README.md +3 -1
- bot.py +21 -0
- docs/ENVIRONMENT.md +1 -1
- lumen_chat_state.py +59 -0
- lumen_commands.py +70 -0
- lumen_lang.py +216 -0
- tests/test_bot_commands.py +102 -0
.github/dependabot.yml
CHANGED
|
@@ -7,14 +7,18 @@ updates:
|
|
| 7 |
directory: "/"
|
| 8 |
schedule:
|
| 9 |
interval: "weekly"
|
|
|
|
| 10 |
groups:
|
| 11 |
actions:
|
| 12 |
patterns:
|
| 13 |
- "*"
|
|
|
|
|
|
|
| 14 |
- package-ecosystem: "pip"
|
| 15 |
directory: "/"
|
| 16 |
schedule:
|
| 17 |
interval: "weekly"
|
|
|
|
| 18 |
groups:
|
| 19 |
pip-minor:
|
| 20 |
patterns:
|
|
|
|
| 7 |
directory: "/"
|
| 8 |
schedule:
|
| 9 |
interval: "weekly"
|
| 10 |
+
open-pull-requests-limit: 3
|
| 11 |
groups:
|
| 12 |
actions:
|
| 13 |
patterns:
|
| 14 |
- "*"
|
| 15 |
+
# pip смотрит все requirements*.txt в каталоге: requirements.txt и
|
| 16 |
+
# requirements-dev.txt оба покрыты, отдельных записей не нужно.
|
| 17 |
- package-ecosystem: "pip"
|
| 18 |
directory: "/"
|
| 19 |
schedule:
|
| 20 |
interval: "weekly"
|
| 21 |
+
open-pull-requests-limit: 3
|
| 22 |
groups:
|
| 23 |
pip-minor:
|
| 24 |
patterns:
|
README.md
CHANGED
|
@@ -115,13 +115,15 @@ All three endpoints below require `Authorization: Bearer <ADMIN_PANEL_KEY>` (`/a
|
|
| 115 |
| `/tts [text]` | anyone | Reads text out loud. |
|
| 116 |
| `/lang` | anyone in DMs; group admins/owner in groups | Bot language menu (system messages; AI answers always follow your language). |
|
| 117 |
|
| 118 |
-
|
| 119 |
|
| 120 |
| Command | Access | Purpose |
|
| 121 |
|---|---|---|
|
| 122 |
| `/stats` | bot owner, DMs only | Active chat count, process uptime, per-model quota usage. |
|
| 123 |
| `/logs` | bot owner, DMs only | Sends `bot.log` with secrets redacted. |
|
| 124 |
| `/selftest [gemini]` | bot owner, DMs only, max once a minute | Live probe of route heads (Groq, light OpenRouter, Telegram proxy, TikWM; Gemini only with the `gemini` argument). Successful probes count against quota. |
|
|
|
|
|
|
|
| 125 |
|
| 126 |
In groups the bot keeps recent messages (up to 100) as context and sends them to the model providers together with the question; `/reset` (group admins/owner) clears them.
|
| 127 |
|
|
|
|
| 115 |
| `/tts [text]` | anyone | Reads text out loud. |
|
| 116 |
| `/lang` | anyone in DMs; group admins/owner in groups | Bot language menu (system messages; AI answers always follow your language). |
|
| 117 |
|
| 118 |
+
More owner-only commands stay out of the menu on purpose, since they surface internal details that shouldn't be visible in a group chat:
|
| 119 |
|
| 120 |
| Command | Access | Purpose |
|
| 121 |
|---|---|---|
|
| 122 |
| `/stats` | bot owner, DMs only | Active chat count, process uptime, per-model quota usage. |
|
| 123 |
| `/logs` | bot owner, DMs only | Sends `bot.log` with secrets redacted. |
|
| 124 |
| `/selftest [gemini]` | bot owner, DMs only, max once a minute | Live probe of route heads (Groq, light OpenRouter, Telegram proxy, TikWM; Gemini only with the `gemini` argument). Successful probes count against quota. |
|
| 125 |
+
| `/ban <ID>` / `/unban <ID>` (or as a reply) | bot owner, DMs only | Block/unblock a user by Telegram ID. Banned users are fully ignored: no answers, no group context, no limit counters. Survives restarts. |
|
| 126 |
+
| `/banlist` | bot owner, DMs only | Shows banned user IDs. |
|
| 127 |
|
| 128 |
In groups the bot keeps recent messages (up to 100) as context and sends them to the model providers together with the question; `/reset` (group admins/owner) clears them.
|
| 129 |
|
bot.py
CHANGED
|
@@ -481,6 +481,11 @@ from lumen_chat_state import (
|
|
| 481 |
_evict_orphan_chat_locks,
|
| 482 |
_is_owner,
|
| 483 |
_notify_owner,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
_is_privileged_in_chat,
|
| 485 |
_quota_entry,
|
| 486 |
_mark_quota_exhausted,
|
|
@@ -888,6 +893,11 @@ __all__ = [
|
|
| 888 |
"GLOBAL_QUOTA",
|
| 889 |
"_is_owner",
|
| 890 |
"_is_privileged_in_chat",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
"_mark_quota_exhausted",
|
| 892 |
"_mark_rate_limited",
|
| 893 |
"_record_quota_usage",
|
|
@@ -1201,6 +1211,9 @@ from lumen_commands import (
|
|
| 1201 |
cmd_logs,
|
| 1202 |
cmd_stats,
|
| 1203 |
cmd_selftest,
|
|
|
|
|
|
|
|
|
|
| 1204 |
_build_version,
|
| 1205 |
_webhook_info_text,
|
| 1206 |
_process_memory_text,
|
|
@@ -1217,6 +1230,9 @@ dp.callback_query.register(handle_lang_callback, F.data.startswith("lang:"))
|
|
| 1217 |
dp.message.register(cmd_logs, Command("logs"))
|
| 1218 |
dp.message.register(cmd_stats, Command("stats"))
|
| 1219 |
dp.message.register(cmd_selftest, Command("selftest"))
|
|
|
|
|
|
|
|
|
|
| 1220 |
dp.callback_query.register(handle_pick_callback, F.data.startswith("pick:"))
|
| 1221 |
|
| 1222 |
|
|
@@ -1248,6 +1264,11 @@ async def global_error_handler(event: Any) -> bool:
|
|
| 1248 |
|
| 1249 |
@dp.message()
|
| 1250 |
async def handle_message(message: Message) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1251 |
if message.media_group_id:
|
| 1252 |
mgid = message.media_group_id
|
| 1253 |
_mg_buffers.setdefault(mgid, []).append(message)
|
|
|
|
| 481 |
_evict_orphan_chat_locks,
|
| 482 |
_is_owner,
|
| 483 |
_notify_owner,
|
| 484 |
+
BANNED_KEY,
|
| 485 |
+
_is_banned,
|
| 486 |
+
_ban_user,
|
| 487 |
+
_unban_user,
|
| 488 |
+
_banned_list,
|
| 489 |
_is_privileged_in_chat,
|
| 490 |
_quota_entry,
|
| 491 |
_mark_quota_exhausted,
|
|
|
|
| 893 |
"GLOBAL_QUOTA",
|
| 894 |
"_is_owner",
|
| 895 |
"_is_privileged_in_chat",
|
| 896 |
+
"BANNED_KEY",
|
| 897 |
+
"_is_banned",
|
| 898 |
+
"_ban_user",
|
| 899 |
+
"_unban_user",
|
| 900 |
+
"_banned_list",
|
| 901 |
"_mark_quota_exhausted",
|
| 902 |
"_mark_rate_limited",
|
| 903 |
"_record_quota_usage",
|
|
|
|
| 1211 |
cmd_logs,
|
| 1212 |
cmd_stats,
|
| 1213 |
cmd_selftest,
|
| 1214 |
+
cmd_ban,
|
| 1215 |
+
cmd_unban,
|
| 1216 |
+
cmd_banlist,
|
| 1217 |
_build_version,
|
| 1218 |
_webhook_info_text,
|
| 1219 |
_process_memory_text,
|
|
|
|
| 1230 |
dp.message.register(cmd_logs, Command("logs"))
|
| 1231 |
dp.message.register(cmd_stats, Command("stats"))
|
| 1232 |
dp.message.register(cmd_selftest, Command("selftest"))
|
| 1233 |
+
dp.message.register(cmd_ban, Command("ban"))
|
| 1234 |
+
dp.message.register(cmd_unban, Command("unban"))
|
| 1235 |
+
dp.message.register(cmd_banlist, Command("banlist"))
|
| 1236 |
dp.callback_query.register(handle_pick_callback, F.data.startswith("pick:"))
|
| 1237 |
|
| 1238 |
|
|
|
|
| 1264 |
|
| 1265 |
@dp.message()
|
| 1266 |
async def handle_message(message: Message) -> None:
|
| 1267 |
+
# Владельческая блокировка: ни ответа, ни контекста группы, ни счётчиков —
|
| 1268 |
+
# отсекаем раньше всего, включая медиагруппы и пассивный фон.
|
| 1269 |
+
if message.from_user is not None and _is_banned(message.from_user.id):
|
| 1270 |
+
return
|
| 1271 |
+
|
| 1272 |
if message.media_group_id:
|
| 1273 |
mgid = message.media_group_id
|
| 1274 |
_mg_buffers.setdefault(mgid, []).append(message)
|
docs/ENVIRONMENT.md
CHANGED
|
@@ -132,7 +132,7 @@ These ceilings are fixed in code; change them only with a code edit:
|
|
| 132 |
| `BOT_LOG_PATH` | `/app/bot.log` | Log file path. Mainly relevant for tests; leave it alone in production. |
|
| 133 |
| `LOG_LEVEL` | `INFO` | Standard logging level (`DEBUG`/`INFO`/`WARNING`/...). |
|
| 134 |
| `DIAG_TOTAL_BUDGET_SEC` | `25s` | Total budget for the `/diag` network check across all probed hosts, so a hanging host cannot hang the diagnostic. |
|
| 135 |
-
| `ADMIN_SECRET_SEED` | — | Secret seed for deriving `WEBHOOK_SECRET` and `ADMIN_PANEL_KEY`. **Set it.** Without it both keys are derived from `BOT_TOKEN`, so a leaked token also exposes `/export_state` and `/webhook` (the bot logs a warning at startup).
|
| 136 |
|
| 137 |
Sentry setup: create a free Python project at [sentry.io](https://sentry.io) (Developer tier: 5,000 events/month), copy the DSN from the project settings, and add it as a Space secret.
|
| 138 |
|
|
|
|
| 132 |
| `BOT_LOG_PATH` | `/app/bot.log` | Log file path. Mainly relevant for tests; leave it alone in production. |
|
| 133 |
| `LOG_LEVEL` | `INFO` | Standard logging level (`DEBUG`/`INFO`/`WARNING`/...). |
|
| 134 |
| `DIAG_TOTAL_BUDGET_SEC` | `25s` | Total budget for the `/diag` network check across all probed hosts, so a hanging host cannot hang the diagnostic. |
|
| 135 |
+
| `ADMIN_SECRET_SEED` | — | Secret seed for deriving `WEBHOOK_SECRET` and `ADMIN_PANEL_KEY`. **Set it to its own random value, separate from `BOT_TOKEN`.** Without it both keys are derived from `BOT_TOKEN`, so a leaked token also exposes `/export_state` and `/webhook` (the bot logs a warning at startup). Changing the seed (or setting it for the first time) rotates both keys at once: on restart the bot re-registers the Telegram webhook with the new `WEBHOOK_SECRET` by itself, but anything using the old `ADMIN_PANEL_KEY` (browser bookmarks, cron export scripts) gets `401` until updated — fetch the new key via `GET /admin_keys` with Bearer `BOT_TOKEN`. |
|
| 136 |
|
| 137 |
Sentry setup: create a free Python project at [sentry.io](https://sentry.io) (Developer tier: 5,000 events/month), copy the DSN from the project settings, and add it as a Space secret.
|
| 138 |
|
lumen_chat_state.py
CHANGED
|
@@ -294,6 +294,16 @@ def load_global_quota() -> None:
|
|
| 294 |
except (TypeError, ValueError):
|
| 295 |
clean_stats[field] = 0
|
| 296 |
GLOBAL_QUOTA[STATS_KEY] = clean_stats
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
raw_ts = loaded.get("last_storage_write_ts")
|
| 298 |
if isinstance(raw_ts, (int, float)) and not isinstance(raw_ts, bool) and raw_ts > 0:
|
| 299 |
GLOBAL_QUOTA["last_storage_write_ts"] = float(raw_ts)
|
|
@@ -835,6 +845,55 @@ def _is_owner(user_id: int | None) -> bool:
|
|
| 835 |
import bot
|
| 836 |
return bot.OWNER_ID is not None and user_id is not None and user_id == bot.OWNER_ID
|
| 837 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 838 |
async def _notify_owner(text: str) -> None:
|
| 839 |
"""ЛС владельцу о редких важных событиях; с троттлингом у вызывателя, никогда не кидает."""
|
| 840 |
import bot
|
|
|
|
| 294 |
except (TypeError, ValueError):
|
| 295 |
clean_stats[field] = 0
|
| 296 |
GLOBAL_QUOTA[STATS_KEY] = clean_stats
|
| 297 |
+
# Владельческая блокировка переживает рестарт тем же файлом/ключом.
|
| 298 |
+
raw_banned = loaded.get(BANNED_KEY)
|
| 299 |
+
if isinstance(raw_banned, dict):
|
| 300 |
+
clean_banned: dict[str, Any] = {}
|
| 301 |
+
for uid, ts in raw_banned.items():
|
| 302 |
+
try:
|
| 303 |
+
clean_banned[str(int(uid))] = max(0, int(ts or 0))
|
| 304 |
+
except (TypeError, ValueError):
|
| 305 |
+
continue
|
| 306 |
+
GLOBAL_QUOTA[BANNED_KEY] = clean_banned
|
| 307 |
raw_ts = loaded.get("last_storage_write_ts")
|
| 308 |
if isinstance(raw_ts, (int, float)) and not isinstance(raw_ts, bool) and raw_ts > 0:
|
| 309 |
GLOBAL_QUOTA["last_storage_write_ts"] = float(raw_ts)
|
|
|
|
| 845 |
import bot
|
| 846 |
return bot.OWNER_ID is not None and user_id is not None and user_id == bot.OWNER_ID
|
| 847 |
|
| 848 |
+
# ── Владельческая блокировка (/ban): тот же персистентный файл/ключ, что квота.
|
| 849 |
+
BANNED_KEY = "banned_users"
|
| 850 |
+
|
| 851 |
+
def _banned_map() -> dict[str, Any]:
|
| 852 |
+
import bot
|
| 853 |
+
raw = bot.GLOBAL_QUOTA.get(BANNED_KEY)
|
| 854 |
+
if not isinstance(raw, dict):
|
| 855 |
+
raw = {}
|
| 856 |
+
bot.GLOBAL_QUOTA[BANNED_KEY] = raw
|
| 857 |
+
return raw
|
| 858 |
+
|
| 859 |
+
def _is_banned(user_id: int | None) -> bool:
|
| 860 |
+
"""Заблокирован ли пользователь. Владельца забанить нельзя — всегда False."""
|
| 861 |
+
import bot
|
| 862 |
+
if user_id is None or bot._is_owner(user_id):
|
| 863 |
+
return False
|
| 864 |
+
return str(user_id) in _banned_map()
|
| 865 |
+
|
| 866 |
+
def _ban_user(user_id: int) -> bool:
|
| 867 |
+
"""True если добавился сейчас, False если уже был (или это владелец)."""
|
| 868 |
+
import bot
|
| 869 |
+
if bot._is_owner(user_id):
|
| 870 |
+
return False
|
| 871 |
+
entry = _banned_map()
|
| 872 |
+
if str(user_id) in entry:
|
| 873 |
+
return False
|
| 874 |
+
entry[str(user_id)] = int(time.time())
|
| 875 |
+
bot.mark_quota_dirty()
|
| 876 |
+
return True
|
| 877 |
+
|
| 878 |
+
def _unban_user(user_id: int) -> bool:
|
| 879 |
+
"""True если был в списке и убран."""
|
| 880 |
+
import bot
|
| 881 |
+
if str(user_id) not in _banned_map():
|
| 882 |
+
return False
|
| 883 |
+
del _banned_map()[str(user_id)]
|
| 884 |
+
bot.mark_quota_dirty()
|
| 885 |
+
return True
|
| 886 |
+
|
| 887 |
+
def _banned_list() -> list[int]:
|
| 888 |
+
"""ID по возрастанию — для /banlist."""
|
| 889 |
+
ids = []
|
| 890 |
+
for key in _banned_map():
|
| 891 |
+
try:
|
| 892 |
+
ids.append(int(key))
|
| 893 |
+
except (TypeError, ValueError):
|
| 894 |
+
continue
|
| 895 |
+
return sorted(ids)
|
| 896 |
+
|
| 897 |
async def _notify_owner(text: str) -> None:
|
| 898 |
"""ЛС владельцу о редких важных событиях; с троттлингом у вызывателя, никогда не кидает."""
|
| 899 |
import bot
|
lumen_commands.py
CHANGED
|
@@ -720,6 +720,76 @@ async def cmd_selftest(message: Message) -> None:
|
|
| 720 |
log.info("[selftest] chat=%s gemini_arg=%s %s", message.chat.id, want_gemini, " ".join(summary))
|
| 721 |
|
| 722 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
_BUILD_VERSION_CACHED: str | None = None
|
| 724 |
|
| 725 |
|
|
|
|
| 720 |
log.info("[selftest] chat=%s gemini_arg=%s %s", message.chat.id, want_gemini, " ".join(summary))
|
| 721 |
|
| 722 |
|
| 723 |
+
def _ban_target_id(message: Message) -> int | None:
|
| 724 |
+
"""ID цели /ban//unban: ответ на сообщение или числовой аргумент."""
|
| 725 |
+
replied = getattr(message, "reply_to_message", None)
|
| 726 |
+
replied_user = getattr(replied, "from_user", None) if replied is not None else None
|
| 727 |
+
replied_id = getattr(replied_user, "id", None)
|
| 728 |
+
if isinstance(replied_id, int) and not isinstance(replied_id, bool):
|
| 729 |
+
return replied_id
|
| 730 |
+
args = (getattr(message, "text", "") or "").split()[1:]
|
| 731 |
+
if args:
|
| 732 |
+
raw = args[0].split("@")[0].lstrip("+")
|
| 733 |
+
if raw.isdigit():
|
| 734 |
+
return int(raw)
|
| 735 |
+
return None
|
| 736 |
+
|
| 737 |
+
async def cmd_ban(message: Message) -> None:
|
| 738 |
+
"""Владельческая блокировка (только владелец, только личка)."""
|
| 739 |
+
import bot
|
| 740 |
+
if not bot._is_owner(message.from_user.id if message.from_user else None):
|
| 741 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "stats_deny"))
|
| 742 |
+
return
|
| 743 |
+
if message.chat.type != ChatType.PRIVATE:
|
| 744 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "ban_group_only"))
|
| 745 |
+
return
|
| 746 |
+
target = _ban_target_id(message)
|
| 747 |
+
if target is None:
|
| 748 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "ban_usage"))
|
| 749 |
+
return
|
| 750 |
+
if bot._is_owner(target):
|
| 751 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "ban_owner_refuse"))
|
| 752 |
+
return
|
| 753 |
+
bot._ban_user(target)
|
| 754 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "ban_done", user_id=target))
|
| 755 |
+
log.info("[ban] Owner banned user %s", target)
|
| 756 |
+
|
| 757 |
+
async def cmd_unban(message: Message) -> None:
|
| 758 |
+
"""Снятие владельческой блокировки (только владелец, только личка)."""
|
| 759 |
+
import bot
|
| 760 |
+
if not bot._is_owner(message.from_user.id if message.from_user else None):
|
| 761 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "stats_deny"))
|
| 762 |
+
return
|
| 763 |
+
if message.chat.type != ChatType.PRIVATE:
|
| 764 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "ban_group_only"))
|
| 765 |
+
return
|
| 766 |
+
target = _ban_target_id(message)
|
| 767 |
+
if target is None:
|
| 768 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "ban_usage"))
|
| 769 |
+
return
|
| 770 |
+
if bot._unban_user(target):
|
| 771 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "unban_done", user_id=target))
|
| 772 |
+
log.info("[ban] Owner unbanned user %s", target)
|
| 773 |
+
else:
|
| 774 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "unban_missing", user_id=target))
|
| 775 |
+
|
| 776 |
+
async def cmd_banlist(message: Message) -> None:
|
| 777 |
+
"""Список заблокированных (только владелец, только личка)."""
|
| 778 |
+
import bot
|
| 779 |
+
if not bot._is_owner(message.from_user.id if message.from_user else None):
|
| 780 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "stats_deny"))
|
| 781 |
+
return
|
| 782 |
+
if message.chat.type != ChatType.PRIVATE:
|
| 783 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "ban_group_only"))
|
| 784 |
+
return
|
| 785 |
+
ids = bot._banned_list()
|
| 786 |
+
if not ids:
|
| 787 |
+
await bot._tg_call(message.reply, bot._t(message.chat.id, "banlist_empty"))
|
| 788 |
+
return
|
| 789 |
+
text = bot._t(message.chat.id, "banlist_header") + "\n" + "\n".join(f"• {uid}" for uid in ids)
|
| 790 |
+
await bot._tg_call(message.reply, text)
|
| 791 |
+
|
| 792 |
+
|
| 793 |
_BUILD_VERSION_CACHED: str | None = None
|
| 794 |
|
| 795 |
|
lumen_lang.py
CHANGED
|
@@ -569,6 +569,70 @@ STRINGS: dict[str, dict[str, str]] = {
|
|
| 569 |
"ru": "(Gemini пропущен — допиши gemini, квота дефицитная)",
|
| 570 |
"uk": "(Gemini пропущено — допиши gemini, квота дефіцитна)",
|
| 571 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
"stats_no_data": {
|
| 573 |
"be": " няма даных",
|
| 574 |
"en": " no data",
|
|
@@ -837,6 +901,14 @@ PACK_PT: dict[str, str] = {
|
|
| 837 |
"selftest_cooldown": "Espera {sec}s antes da próxima execução.",
|
| 838 |
"selftest_header": "Autoteste:",
|
| 839 |
"selftest_gemini_skipped": "(Gemini ignorado — adiciona o argumento gemini, a quota dele é escassa)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
"stats_no_data": " sem dados",
|
| 841 |
"stats_limit_used": " (limite esgotado)",
|
| 842 |
"tiktok_sound_recognized": "Link de som do TikTok reconhecido.",
|
|
@@ -930,6 +1002,14 @@ PACK_AR: dict[str, str] = {
|
|
| 930 |
"selftest_cooldown": "انتظر {sec} ث قبل التشغيل التالي.",
|
| 931 |
"selftest_header": "فحص ذاتي:",
|
| 932 |
"selftest_gemini_skipped": "(تم تخطي Gemini — أضف الوسيط gemini، حصته شحيحة)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
"stats_no_data": " لا بيانات",
|
| 934 |
"stats_limit_used": " (الحد مستنفد)",
|
| 935 |
"tiktok_sound_recognized": "تم التعرف على رابط صوت TikTok.",
|
|
@@ -1023,6 +1103,14 @@ PACK_TR: dict[str, str] = {
|
|
| 1023 |
"selftest_cooldown": "Sonraki çalıştırmadan önce {sec} sn bekle.",
|
| 1024 |
"selftest_header": "Öz test:",
|
| 1025 |
"selftest_gemini_skipped": "(Gemini atlandı — gemini argümanını ekle, kotası kıt)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1026 |
"stats_no_data": " veri yok",
|
| 1027 |
"stats_limit_used": " (limit doldu)",
|
| 1028 |
"tiktok_sound_recognized": "TikTok ses bağlantısı tanındı.",
|
|
@@ -1116,6 +1204,14 @@ PACK_DE: dict[str, str] = {
|
|
| 1116 |
"selftest_cooldown": "Warte {sec} s vor dem nächsten Start.",
|
| 1117 |
"selftest_header": "Selbsttest:",
|
| 1118 |
"selftest_gemini_skipped": "(Gemini übersprungen — füge das Argument gemini hinzu, sein Kontingent ist knapp)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1119 |
"stats_no_data": " keine Daten",
|
| 1120 |
"stats_limit_used": " (Limit erschöpft)",
|
| 1121 |
"tiktok_sound_recognized": "TikTok-Soundlink erkannt.",
|
|
@@ -1209,6 +1305,14 @@ PACK_FR: dict[str, str] = {
|
|
| 1209 |
"selftest_cooldown": "Attends {sec} s avant le prochain lancement.",
|
| 1210 |
"selftest_header": "Auto-test :",
|
| 1211 |
"selftest_gemini_skipped": "(Gemini ignoré — ajoute l'argument gemini, son quota est limité)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1212 |
"stats_no_data": " pas de données",
|
| 1213 |
"stats_limit_used": " (limite épuisée)",
|
| 1214 |
"tiktok_sound_recognized": "Lien de son TikTok reconnu.",
|
|
@@ -1302,6 +1406,14 @@ PACK_IT: dict[str, str] = {
|
|
| 1302 |
"selftest_cooldown": "Attendi {sec} s prima del prossimo avvio.",
|
| 1303 |
"selftest_header": "Autotest:",
|
| 1304 |
"selftest_gemini_skipped": "(Gemini saltato — aggiungi l'argomento gemini, la sua quota è scarsa)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
"stats_no_data": " nessun dato",
|
| 1306 |
"stats_limit_used": " (limite esaurito)",
|
| 1307 |
"tiktok_sound_recognized": "Link audio di TikTok riconosciuto.",
|
|
@@ -1395,6 +1507,14 @@ PACK_HI: dict[str, str] = {
|
|
| 1395 |
"selftest_cooldown": "अगली बार चलाने से पहले {sec} सेकंड रुको।",
|
| 1396 |
"selftest_header": "स्व-जांच:",
|
| 1397 |
"selftest_gemini_skipped": "(Gemini छोड़ा गया — gemini आर्गुमेंट जोड़ो, उसका कोटा सीमित है)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1398 |
"stats_no_data": " कोई डेटा नहीं",
|
| 1399 |
"stats_limit_used": " (सीमा समाप्त)",
|
| 1400 |
"tiktok_sound_recognized": "TikTok ध्वनि लिंक पहचानी गई।",
|
|
@@ -1488,6 +1608,14 @@ PACK_ID: dict[str, str] = {
|
|
| 1488 |
"selftest_cooldown": "Tunggu {sec} dtk sebelum menjalankan lagi.",
|
| 1489 |
"selftest_header": "Uji mandiri:",
|
| 1490 |
"selftest_gemini_skipped": "(Gemini dilewati — tambahkan argumen gemini, kuotanya terbatas)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1491 |
"stats_no_data": " tidak ada data",
|
| 1492 |
"stats_limit_used": " (batas habis)",
|
| 1493 |
"tiktok_sound_recognized": "Tautan suara TikTok dikenali.",
|
|
@@ -1581,6 +1709,14 @@ PACK_MS: dict[str, str] = {
|
|
| 1581 |
"selftest_cooldown": "Tunggu {sec}s sebelum larian seterusnya.",
|
| 1582 |
"selftest_header": "Ujian kendiri:",
|
| 1583 |
"selftest_gemini_skipped": "(Gemini dilangkau — tambah argumen gemini, kuotanya terhad)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1584 |
"stats_no_data": " tiada data",
|
| 1585 |
"stats_limit_used": " (had habis)",
|
| 1586 |
"tiktok_sound_recognized": "Pautan bunyi TikTok dikenal pasti.",
|
|
@@ -1674,6 +1810,14 @@ PACK_VI: dict[str, str] = {
|
|
| 1674 |
"selftest_cooldown": "Đợi {sec} giây trước lần chạy tiếp theo.",
|
| 1675 |
"selftest_header": "Tự kiểm tra:",
|
| 1676 |
"selftest_gemini_skipped": "(Bỏ qua Gemini — thêm đối số gemini, hạn ngạch của nó khan hiếm)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1677 |
"stats_no_data": " không có dữ liệu",
|
| 1678 |
"stats_limit_used": " (hết hạn mức)",
|
| 1679 |
"tiktok_sound_recognized": "Đã nhận ra liên kết âm thanh TikTok.",
|
|
@@ -1767,6 +1911,14 @@ PACK_TH: dict[str, str] = {
|
|
| 1767 |
"selftest_cooldown": "รอ {sec} วินาทีก่อนรันครั้งถัดไป",
|
| 1768 |
"selftest_header": "ทดสอบตัวเอง:",
|
| 1769 |
"selftest_gemini_skipped": "(ข้าม Gemini — เติมอาร์กิวเมนต์ gemini โควต้าของมันมีจำกัด)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1770 |
"stats_no_data": " ไม่มีข้อมูล",
|
| 1771 |
"stats_limit_used": " (โควต้าหมด)",
|
| 1772 |
"tiktok_sound_recognized": "รู้จักลิงก์เสียง TikTok แล้ว",
|
|
@@ -1860,6 +2012,14 @@ PACK_FA: dict[str, str] = {
|
|
| 1860 |
"selftest_cooldown": "{sec} ثانیه تا اجرای بعدی صبر کن.",
|
| 1861 |
"selftest_header": "خودآزمایی:",
|
| 1862 |
"selftest_gemini_skipped": "(Gemini رد شد — آرگومان gemini را اضافه کن، سهمیهاش کم است)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1863 |
"stats_no_data": " دادهای نیست",
|
| 1864 |
"stats_limit_used": " (سقف تمام شد)",
|
| 1865 |
"tiktok_sound_recognized": "لینک صدای TikTok شناخته شد.",
|
|
@@ -1953,6 +2113,14 @@ PACK_UR: dict[str, str] = {
|
|
| 1953 |
"selftest_cooldown": "اگلی بار چلانے سے پہلے {sec} سیکنڈ انتظار کرو۔",
|
| 1954 |
"selftest_header": "خود جانچ:",
|
| 1955 |
"selftest_gemini_skipped": "(Gemini چھوڑ دیا گیا — gemini دلیل شامل کرو، اس کا کوٹہ محدود ہے)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1956 |
"stats_no_data": " کوئی ڈیٹا نہیں",
|
| 1957 |
"stats_limit_used": " (حد ختم)",
|
| 1958 |
"tiktok_sound_recognized": "TikTok آواز کا لنک پہچانا گیا۔",
|
|
@@ -2046,6 +2214,14 @@ PACK_UZ: dict[str, str] = {
|
|
| 2046 |
"selftest_cooldown": "Keyingi ishga tushirishdan oldin {sec} s kuting.",
|
| 2047 |
"selftest_header": "O‘z-o‘zini tekshirish:",
|
| 2048 |
"selftest_gemini_skipped": "(Gemini o‘tkazib yuborildi — gemini argumentini qo‘shing, kvotasi tanqis)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2049 |
"stats_no_data": " ma'lumot yo'q",
|
| 2050 |
"stats_limit_used": " (limit tugadi)",
|
| 2051 |
"tiktok_sound_recognized": "TikTok tovush havolasi tanildi.",
|
|
@@ -2139,6 +2315,14 @@ PACK_BN: dict[str, str] = {
|
|
| 2139 |
"selftest_cooldown": "পরের বার চালানোর আগে {sec} সেকেন্ড অপেক্ষা করো।",
|
| 2140 |
"selftest_header": "স্ব-পরীক্ষা:",
|
| 2141 |
"selftest_gemini_skipped": "(Gemini বাদ দেওয়া হয়েছে — gemini আর্গুমেন্ট যোগ করো, কোটা সীমিত)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2142 |
"stats_no_data": " কোনো তথ্য নেই",
|
| 2143 |
"stats_limit_used": " (সীমা শেষ)",
|
| 2144 |
"tiktok_sound_recognized": "TikTok সাউন্ড লিংক চেনা গেছে।",
|
|
@@ -2232,6 +2416,14 @@ PACK_FIL: dict[str, str] = {
|
|
| 2232 |
"selftest_cooldown": "Maghintay ng {sec}s bago sa susunod na pag-run.",
|
| 2233 |
"selftest_header": "Self-test:",
|
| 2234 |
"selftest_gemini_skipped": "(Nilaktawan ang Gemini — idagdag ang argument na gemini, kakaunti ang quota nito)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2235 |
"stats_no_data": " walang data",
|
| 2236 |
"stats_limit_used": " (ubos ang limit)",
|
| 2237 |
"tiktok_sound_recognized": "Nakilala ang TikTok sound link.",
|
|
@@ -2325,6 +2517,14 @@ PACK_PL: dict[str, str] = {
|
|
| 2325 |
"selftest_cooldown": "Poczekaj {sec} s przed kolejnym uruchomieniem.",
|
| 2326 |
"selftest_header": "Autotest:",
|
| 2327 |
"selftest_gemini_skipped": "(Gemini pominięte — dopisz argument gemini, limit jest niski)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2328 |
"stats_no_data": " brak danych",
|
| 2329 |
"stats_limit_used": " (limit wyczerpany)",
|
| 2330 |
"tiktok_sound_recognized": "Rozpoznano link do dźwięku TikToka.",
|
|
@@ -2418,6 +2618,14 @@ PACK_NL: dict[str, str] = {
|
|
| 2418 |
"selftest_cooldown": "Wacht {sec} s voor de volgende run.",
|
| 2419 |
"selftest_header": "Zelftest:",
|
| 2420 |
"selftest_gemini_skipped": "(Gemini overgeslagen — voeg het argument gemini toe, zijn quotum is schaars)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2421 |
"stats_no_data": " geen data",
|
| 2422 |
"stats_limit_used": " (limiet op)",
|
| 2423 |
"tiktok_sound_recognized": "TikTok-geluidslink herkend.",
|
|
@@ -2511,6 +2719,14 @@ PACK_ZU: dict[str, str] = {
|
|
| 2511 |
"selftest_cooldown": "Linda u-{sec}s ngaphambi kokusebenza okulandelayo.",
|
| 2512 |
"selftest_header": "Ukuzihlola:",
|
| 2513 |
"selftest_gemini_skipped": "(I-Gemini yeqiwe — engeza i-agumenti ethi gemini, i-quota yayo yincane)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2514 |
"stats_no_data": " ayikho idatha",
|
| 2515 |
"stats_limit_used": " (umkhawulo uphelile)",
|
| 2516 |
"tiktok_sound_recognized": "Isixhumanisi somsindo se-TikTok saziwe.",
|
|
|
|
| 569 |
"ru": "(Gemini пропущен — допиши gemini, квота дефицитная)",
|
| 570 |
"uk": "(Gemini пропущено — допиши gemini, квота дефіцитна)",
|
| 571 |
},
|
| 572 |
+
"ban_group_only": {
|
| 573 |
+
"be": "Гэтая каманда кіруе блакіроўкай — даступна толькі ў асабістых паведамленнях з ботам, не ў групах.",
|
| 574 |
+
"en": "This command manages the blocklist — available only in direct messages with the bot, not in groups.",
|
| 575 |
+
"es": "Este comando gestiona la lista de bloqueados — disponible solo en mensajes directos con el bot, no en grupos.",
|
| 576 |
+
"kk": "Бұл команда бұғаттау тізімін басқарады — тек ботпен жеке хабарламаларда қолжетімді, топтарда емес.",
|
| 577 |
+
"ru": "Эта команда управляет блокировкой — доступна только в личных сообщениях с ботом, не в группах.",
|
| 578 |
+
"uk": "Ця команда керує блокуванням — доступна лише в особистих повідомленнях з ботом, не в групах.",
|
| 579 |
+
},
|
| 580 |
+
"ban_usage": {
|
| 581 |
+
"be": "Выкарыстанне: /ban <ID> або адказам на паведамленне; /unban <ID> або адказам.",
|
| 582 |
+
"en": "Usage: /ban <ID> or as a reply; /unban <ID> or as a reply.",
|
| 583 |
+
"es": "Uso: /ban <ID> o como respuesta; /unban <ID> o como respuesta.",
|
| 584 |
+
"kk": "Қолдану: /ban <ID> немесе хабарламаға жауап ретінде; /unban <ID> немесе жауап ретінде.",
|
| 585 |
+
"ru": "Использование: /ban <ID> или ответом на сообщение; /unban <ID> или ответом.",
|
| 586 |
+
"uk": "Використання: /ban <ID> або відповіддю на повідомлення; /unban <ID> або відповіддю.",
|
| 587 |
+
},
|
| 588 |
+
"ban_owner_refuse": {
|
| 589 |
+
"be": "Уладальніка заблакаваць нельга.",
|
| 590 |
+
"en": "The owner cannot be banned.",
|
| 591 |
+
"es": "No se puede bloquear al dueño.",
|
| 592 |
+
"kk": "Иесін бұғаттау мүмкін емес.",
|
| 593 |
+
"ru": "Владельца заблокировать нельзя.",
|
| 594 |
+
"uk": "Власника заблокувати не можна.",
|
| 595 |
+
},
|
| 596 |
+
"ban_done": {
|
| 597 |
+
"be": "Карыстальнік {user_id} заблакаваны.",
|
| 598 |
+
"en": "User {user_id} banned.",
|
| 599 |
+
"es": "Usuario {user_id} bloqueado.",
|
| 600 |
+
"kk": "Пайдаланушы {user_id} бұғатталды.",
|
| 601 |
+
"ru": "Пользователь {user_id} заблокирован.",
|
| 602 |
+
"uk": "Користувача {user_id} заблоковано.",
|
| 603 |
+
},
|
| 604 |
+
"unban_done": {
|
| 605 |
+
"be": "Карыстальнік {user_id} разблакаваны.",
|
| 606 |
+
"en": "User {user_id} unbanned.",
|
| 607 |
+
"es": "Usuario {user_id} desbloqueado.",
|
| 608 |
+
"kk": "Пайдаланушы {user_id} бұғаттан шығарылды.",
|
| 609 |
+
"ru": "Пользователь {user_id} разблокирован.",
|
| 610 |
+
"uk": "Користувача {user_id} розблоковано.",
|
| 611 |
+
},
|
| 612 |
+
"unban_missing": {
|
| 613 |
+
"be": "Карыстальніка {user_id} няма ў спісе.",
|
| 614 |
+
"en": "User {user_id} is not on the list.",
|
| 615 |
+
"es": "El usuario {user_id} no está en la lista.",
|
| 616 |
+
"kk": "Пайдаланушы {user_id} тізімде жоқ.",
|
| 617 |
+
"ru": "Пользователя {user_id} нет в списке.",
|
| 618 |
+
"uk": "Користувача {user_id} немає у списку.",
|
| 619 |
+
},
|
| 620 |
+
"banlist_empty": {
|
| 621 |
+
"be": "Спіс пусты — нікога не заблакавана.",
|
| 622 |
+
"en": "The list is empty — nobody is banned.",
|
| 623 |
+
"es": "La lista está vacía — nadie bloqueado.",
|
| 624 |
+
"kk": "Тізім бос — ешкім бұғатталмаған.",
|
| 625 |
+
"ru": "Список пуст — никто не заблокирован.",
|
| 626 |
+
"uk": "Список порожній — нікого не заблоковано.",
|
| 627 |
+
},
|
| 628 |
+
"banlist_header": {
|
| 629 |
+
"be": "Заблакаваныя:",
|
| 630 |
+
"en": "Banned:",
|
| 631 |
+
"es": "Bloqueados:",
|
| 632 |
+
"kk": "Бұғатталғандар:",
|
| 633 |
+
"ru": "Заблокированные:",
|
| 634 |
+
"uk": "Заблоковані:",
|
| 635 |
+
},
|
| 636 |
"stats_no_data": {
|
| 637 |
"be": " няма даных",
|
| 638 |
"en": " no data",
|
|
|
|
| 901 |
"selftest_cooldown": "Espera {sec}s antes da próxima execução.",
|
| 902 |
"selftest_header": "Autoteste:",
|
| 903 |
"selftest_gemini_skipped": "(Gemini ignorado — adiciona o argumento gemini, a quota dele é escassa)",
|
| 904 |
+
"ban_group_only": "Este comando gere a lista de bloqueados — disponível apenas em mensagens diretas com o bot, não em grupos.",
|
| 905 |
+
"ban_usage": "Uso: /ban <ID> ou como resposta; /unban <ID> ou como resposta.",
|
| 906 |
+
"ban_owner_refuse": "O dono não pode ser bloqueado.",
|
| 907 |
+
"ban_done": "Utilizador {user_id} bloqueado.",
|
| 908 |
+
"unban_done": "Utilizador {user_id} desbloqueado.",
|
| 909 |
+
"unban_missing": "O utilizador {user_id} não está na lista.",
|
| 910 |
+
"banlist_empty": "A lista está vazia — ninguém bloqueado.",
|
| 911 |
+
"banlist_header": "Bloqueados:",
|
| 912 |
"stats_no_data": " sem dados",
|
| 913 |
"stats_limit_used": " (limite esgotado)",
|
| 914 |
"tiktok_sound_recognized": "Link de som do TikTok reconhecido.",
|
|
|
|
| 1002 |
"selftest_cooldown": "انتظر {sec} ث قبل التشغيل التالي.",
|
| 1003 |
"selftest_header": "فحص ذاتي:",
|
| 1004 |
"selftest_gemini_skipped": "(تم تخطي Gemini — أضف الوسيط gemini، حصته شحيحة)",
|
| 1005 |
+
"ban_group_only": "هذا الأمر يدير قائمة الحظر — متاح فقط في الرسائل الخاصة مع البوت وليس في المجموعات.",
|
| 1006 |
+
"ban_usage": "الاستخدام: /ban <ID> أو بالرد؛ /unban <ID> أو بالرد.",
|
| 1007 |
+
"ban_owner_refuse": "لا يمكن حظر المالك.",
|
| 1008 |
+
"ban_done": "تم حظر المستخدم {user_id}.",
|
| 1009 |
+
"unban_done": "تم إلغاء حظر المستخدم {user_id}.",
|
| 1010 |
+
"unban_missing": "المستخدم {user_id} ليس في القائمة.",
|
| 1011 |
+
"banlist_empty": "القائمة فارغة — لا أحد محظور.",
|
| 1012 |
+
"banlist_header": "محظورون:",
|
| 1013 |
"stats_no_data": " لا بيانات",
|
| 1014 |
"stats_limit_used": " (الحد مستنفد)",
|
| 1015 |
"tiktok_sound_recognized": "تم التعرف على رابط صوت TikTok.",
|
|
|
|
| 1103 |
"selftest_cooldown": "Sonraki çalıştırmadan önce {sec} sn bekle.",
|
| 1104 |
"selftest_header": "Öz test:",
|
| 1105 |
"selftest_gemini_skipped": "(Gemini atlandı — gemini argümanını ekle, kotası kıt)",
|
| 1106 |
+
"ban_group_only": "Bu komut engel listesini yönetir — yalnızca botla direkt mesajlarda kullanılabilir, gruplarda değil.",
|
| 1107 |
+
"ban_usage": "Kullanım: /ban <ID> veya yanıt olarak; /unban <ID> veya yanıt olarak.",
|
| 1108 |
+
"ban_owner_refuse": "Sahip engellenemez.",
|
| 1109 |
+
"ban_done": "Kullanıcı {user_id} engellendi.",
|
| 1110 |
+
"unban_done": "Kullanıcı {user_id} engeli kaldırıldı.",
|
| 1111 |
+
"unban_missing": "Kullanıcı {user_id} listede yok.",
|
| 1112 |
+
"banlist_empty": "Liste boş — kimse engelli değil.",
|
| 1113 |
+
"banlist_header": "Engellenenler:",
|
| 1114 |
"stats_no_data": " veri yok",
|
| 1115 |
"stats_limit_used": " (limit doldu)",
|
| 1116 |
"tiktok_sound_recognized": "TikTok ses bağlantısı tanındı.",
|
|
|
|
| 1204 |
"selftest_cooldown": "Warte {sec} s vor dem nächsten Start.",
|
| 1205 |
"selftest_header": "Selbsttest:",
|
| 1206 |
"selftest_gemini_skipped": "(Gemini übersprungen — füge das Argument gemini hinzu, sein Kontingent ist knapp)",
|
| 1207 |
+
"ban_group_only": "Dieser Befehl verwaltet die Sperrliste — nur in Direktnachrichten mit dem Bot verfügbar, nicht in Gruppen.",
|
| 1208 |
+
"ban_usage": "Verwendung: /ban <ID> oder als Antwort; /unban <ID> oder als Antwort.",
|
| 1209 |
+
"ban_owner_refuse": "Der Besitzer kann nicht gesperrt werden.",
|
| 1210 |
+
"ban_done": "Benutzer {user_id} gesperrt.",
|
| 1211 |
+
"unban_done": "Benutzer {user_id} entsperrt.",
|
| 1212 |
+
"unban_missing": "Benutzer {user_id} steht nicht auf der Liste.",
|
| 1213 |
+
"banlist_empty": "Die Liste ist leer — niemand gesperrt.",
|
| 1214 |
+
"banlist_header": "Gesperrt:",
|
| 1215 |
"stats_no_data": " keine Daten",
|
| 1216 |
"stats_limit_used": " (Limit erschöpft)",
|
| 1217 |
"tiktok_sound_recognized": "TikTok-Soundlink erkannt.",
|
|
|
|
| 1305 |
"selftest_cooldown": "Attends {sec} s avant le prochain lancement.",
|
| 1306 |
"selftest_header": "Auto-test :",
|
| 1307 |
"selftest_gemini_skipped": "(Gemini ignoré — ajoute l'argument gemini, son quota est limité)",
|
| 1308 |
+
"ban_group_only": "Cette commande gère la liste des bloqués — disponible uniquement en messages privés avec le bot, pas en groupes.",
|
| 1309 |
+
"ban_usage": "Usage : /ban <ID> ou en réponse ; /unban <ID> ou en réponse.",
|
| 1310 |
+
"ban_owner_refuse": "Le propriétaire ne peut pas être bloqué.",
|
| 1311 |
+
"ban_done": "Utilisateur {user_id} bloqué.",
|
| 1312 |
+
"unban_done": "Utilisateur {user_id} débloqué.",
|
| 1313 |
+
"unban_missing": "L'utilisateur {user_id} n'est pas dans la liste.",
|
| 1314 |
+
"banlist_empty": "La liste est vide — personne n'est bloqué.",
|
| 1315 |
+
"banlist_header": "Bloqués :",
|
| 1316 |
"stats_no_data": " pas de données",
|
| 1317 |
"stats_limit_used": " (limite épuisée)",
|
| 1318 |
"tiktok_sound_recognized": "Lien de son TikTok reconnu.",
|
|
|
|
| 1406 |
"selftest_cooldown": "Attendi {sec} s prima del prossimo avvio.",
|
| 1407 |
"selftest_header": "Autotest:",
|
| 1408 |
"selftest_gemini_skipped": "(Gemini saltato — aggiungi l'argomento gemini, la sua quota è scarsa)",
|
| 1409 |
+
"ban_group_only": "Questo comando gestisce la lista dei bloccati — disponibile solo nei messaggi diretti con il bot, non nei gruppi.",
|
| 1410 |
+
"ban_usage": "Uso: /ban <ID> o come risposta; /unban <ID> o come risposta.",
|
| 1411 |
+
"ban_owner_refuse": "Il proprietario non può essere bloccato.",
|
| 1412 |
+
"ban_done": "Utente {user_id} bloccato.",
|
| 1413 |
+
"unban_done": "Utente {user_id} sbloccato.",
|
| 1414 |
+
"unban_missing": "L'utente {user_id} non è nella lista.",
|
| 1415 |
+
"banlist_empty": "La lista è vuota — nessuno bloccato.",
|
| 1416 |
+
"banlist_header": "Bloccati:",
|
| 1417 |
"stats_no_data": " nessun dato",
|
| 1418 |
"stats_limit_used": " (limite esaurito)",
|
| 1419 |
"tiktok_sound_recognized": "Link audio di TikTok riconosciuto.",
|
|
|
|
| 1507 |
"selftest_cooldown": "अगली बार चलाने से पहले {sec} सेकंड रुको।",
|
| 1508 |
"selftest_header": "स्व-जांच:",
|
| 1509 |
"selftest_gemini_skipped": "(Gemini छोड़ा गया — gemini आर्गुमेंट जोड़ो, उसका कोटा सीमित है)",
|
| 1510 |
+
"ban_group_only": "यह कमांड ब्लॉक सूची संभालता है — केवल बॉट के साथ सीधे संदेशों में, समूहों में नहीं।",
|
| 1511 |
+
"ban_usage": "उपयोग: /ban <ID> या जवाब में; /unban <ID> या जवाब में।",
|
| 1512 |
+
"ban_owner_refuse": "मालिक को बैन नहीं किया जा सकता।",
|
| 1513 |
+
"ban_done": "उपयोगकर्ता {user_id} बैन हुआ।",
|
| 1514 |
+
"unban_done": "उपयोगकर्ता {user_id} अनबैन हुआ।",
|
| 1515 |
+
"unban_missing": "उपयोगकर्ता {user_id} सूची में नहीं है।",
|
| 1516 |
+
"banlist_empty": "सूची खाली है — कोई बैन नहीं है।",
|
| 1517 |
+
"banlist_header": "बैन किए गए:",
|
| 1518 |
"stats_no_data": " कोई डेटा नहीं",
|
| 1519 |
"stats_limit_used": " (सीमा समाप्त)",
|
| 1520 |
"tiktok_sound_recognized": "TikTok ध्वनि लिंक पहचानी गई।",
|
|
|
|
| 1608 |
"selftest_cooldown": "Tunggu {sec} dtk sebelum menjalankan lagi.",
|
| 1609 |
"selftest_header": "Uji mandiri:",
|
| 1610 |
"selftest_gemini_skipped": "(Gemini dilewati — tambahkan argumen gemini, kuotanya terbatas)",
|
| 1611 |
+
"ban_group_only": "Perintah ini mengelola daftar blokir — hanya tersedia di pesan langsung dengan bot, bukan di grup.",
|
| 1612 |
+
"ban_usage": "Penggunaan: /ban <ID> atau sebagai balasan; /unban <ID> atau sebagai balasan.",
|
| 1613 |
+
"ban_owner_refuse": "Pemilik tidak bisa diblokir.",
|
| 1614 |
+
"ban_done": "Pengguna {user_id} diblokir.",
|
| 1615 |
+
"unban_done": "Pengguna {user_id} dibuka blokirnya.",
|
| 1616 |
+
"unban_missing": "Pengguna {user_id} tidak ada dalam daftar.",
|
| 1617 |
+
"banlist_empty": "Daftarnya kosong — tidak ada yang diblokir.",
|
| 1618 |
+
"banlist_header": "Diblokir:",
|
| 1619 |
"stats_no_data": " tidak ada data",
|
| 1620 |
"stats_limit_used": " (batas habis)",
|
| 1621 |
"tiktok_sound_recognized": "Tautan suara TikTok dikenali.",
|
|
|
|
| 1709 |
"selftest_cooldown": "Tunggu {sec}s sebelum larian seterusnya.",
|
| 1710 |
"selftest_header": "Ujian kendiri:",
|
| 1711 |
"selftest_gemini_skipped": "(Gemini dilangkau — tambah argumen gemini, kuotanya terhad)",
|
| 1712 |
+
"ban_group_only": "Perintah ini mengurus senarai sekat — hanya tersedia dalam mesej langsung dengan bot, bukan dalam kumpulan.",
|
| 1713 |
+
"ban_usage": "Penggunaan: /ban <ID> atau sebagai balasan; /unban <ID> atau sebagai balasan.",
|
| 1714 |
+
"ban_owner_refuse": "Pemilik tidak boleh disekat.",
|
| 1715 |
+
"ban_done": "Pengguna {user_id} disekat.",
|
| 1716 |
+
"unban_done": "Pengguna {user_id} dinyahsekat.",
|
| 1717 |
+
"unban_missing": "Pengguna {user_id} tiada dalam senarai.",
|
| 1718 |
+
"banlist_empty": "Senarai kosong — tiada siapa disekat.",
|
| 1719 |
+
"banlist_header": "Disekat:",
|
| 1720 |
"stats_no_data": " tiada data",
|
| 1721 |
"stats_limit_used": " (had habis)",
|
| 1722 |
"tiktok_sound_recognized": "Pautan bunyi TikTok dikenal pasti.",
|
|
|
|
| 1810 |
"selftest_cooldown": "Đợi {sec} giây trước lần chạy tiếp theo.",
|
| 1811 |
"selftest_header": "Tự kiểm tra:",
|
| 1812 |
"selftest_gemini_skipped": "(Bỏ qua Gemini — thêm đối số gemini, hạn ngạch của nó khan hiếm)",
|
| 1813 |
+
"ban_group_only": "Lệnh này quản lý danh sách chặn — chỉ dùng được trong tin nhắn riêng với bot, không dùng trong nhóm.",
|
| 1814 |
+
"ban_usage": "Cách dùng: /ban <ID> hoặc trả lời tin nhắn; /unban <ID> hoặc trả lời tin nhắn.",
|
| 1815 |
+
"ban_owner_refuse": "Không thể chặn chủ sở hữu.",
|
| 1816 |
+
"ban_done": "Đã chặn người dùng {user_id}.",
|
| 1817 |
+
"unban_done": "Đã bỏ chặn người dùng {user_id}.",
|
| 1818 |
+
"unban_missing": "Người dùng {user_id} không có trong danh sách.",
|
| 1819 |
+
"banlist_empty": "Danh sách trống — không ai bị chặn.",
|
| 1820 |
+
"banlist_header": "Đã chặn:",
|
| 1821 |
"stats_no_data": " không có dữ liệu",
|
| 1822 |
"stats_limit_used": " (hết hạn mức)",
|
| 1823 |
"tiktok_sound_recognized": "Đã nhận ra liên kết âm thanh TikTok.",
|
|
|
|
| 1911 |
"selftest_cooldown": "รอ {sec} วินาทีก่อนรันครั้งถัดไป",
|
| 1912 |
"selftest_header": "ทดสอบตัวเอง:",
|
| 1913 |
"selftest_gemini_skipped": "(ข้าม Gemini — เติมอาร์กิวเมนต์ gemini โควต้าของมันมีจำกัด)",
|
| 1914 |
+
"ban_group_only": "คำสั่งนี้จัดการรายชื่อที่ถูกบล็อก — ใช้ได้เฉพาะในข้อความส่วนตัวกับบอท ไม่ใช่ในกลุ่ม",
|
| 1915 |
+
"ban_usage": "วิธีใช้: /ban <ID> หรือตอบกลับ; /unban <ID> หรือตอบกลับ",
|
| 1916 |
+
"ban_owner_refuse": "บล็อกเจ้าของไม่ได้",
|
| 1917 |
+
"ban_done": "แบนผู้ใช้ {user_id} แล้ว",
|
| 1918 |
+
"unban_done": "ปลดบล็อกผู้ใช้ {user_id} แล้ว",
|
| 1919 |
+
"unban_missing": "ผู้ใช้ {user_id} ไม่ได้อยู่ในรายชื่อ",
|
| 1920 |
+
"banlist_empty": "รายชื่อว่าง — ไม่มีใครถูกบล็อก",
|
| 1921 |
+
"banlist_header": "ถูกบล็อก:",
|
| 1922 |
"stats_no_data": " ไม่มีข้อมูล",
|
| 1923 |
"stats_limit_used": " (โควต้าหมด)",
|
| 1924 |
"tiktok_sound_recognized": "รู้จักลิงก์เสียง TikTok แล้ว",
|
|
|
|
| 2012 |
"selftest_cooldown": "{sec} ثانیه تا اجرای بعدی صبر کن.",
|
| 2013 |
"selftest_header": "خودآزمایی:",
|
| 2014 |
"selftest_gemini_skipped": "(Gemini رد شد — آرگومان gemini را اضافه کن، سهمیهاش کم است)",
|
| 2015 |
+
"ban_group_only": "این دستور فهرست مسدودیها را مدیریت میکند — فقط در پیامهای خصوصی با ربات، نه در گروهها.",
|
| 2016 |
+
"ban_usage": "کاربرد: /ban <ID> یا با پاسخ؛ /unban <ID> یا با پاسخ.",
|
| 2017 |
+
"ban_owner_refuse": "مالک را نمیتوان مسدود کرد.",
|
| 2018 |
+
"ban_done": "کاربر {user_id} مسدود شد.",
|
| 2019 |
+
"unban_done": "کاربر {user_id} رفع مسدودیت شد.",
|
| 2020 |
+
"unban_missing": "کاربر {user_id} در فهرست نیست.",
|
| 2021 |
+
"banlist_empty": "فهرست خالی است — هیچکس مسدود نیست.",
|
| 2022 |
+
"banlist_header": "مسدودشدهها:",
|
| 2023 |
"stats_no_data": " دادهای نیست",
|
| 2024 |
"stats_limit_used": " (سقف تمام شد)",
|
| 2025 |
"tiktok_sound_recognized": "لینک صدای TikTok شناخته شد.",
|
|
|
|
| 2113 |
"selftest_cooldown": "اگلی بار چلانے سے پہلے {sec} سیکنڈ انتظار کرو۔",
|
| 2114 |
"selftest_header": "خود جانچ:",
|
| 2115 |
"selftest_gemini_skipped": "(Gemini چھوڑ دیا گیا — gemini دلیل شامل کرو، اس کا کوٹہ محدود ہے)",
|
| 2116 |
+
"ban_group_only": "یہ کمانڈ بلاک لسٹ کا نظم کرتی ہے — صرف بوٹ کے ساتھ براہ راست پیغامات میں، گروپس میں نہیں۔",
|
| 2117 |
+
"ban_usage": "استعمال: /ban <ID> یا جواب میں؛ /unban <ID> یا جواب میں۔",
|
| 2118 |
+
"ban_owner_refuse": "مالک کو بین نہیں کیا جا سکتا۔",
|
| 2119 |
+
"ban_done": "صارف {user_id} بین ہو گیا۔",
|
| 2120 |
+
"unban_done": "صارف {user_id} ان بین ہو گیا۔",
|
| 2121 |
+
"unban_missing": "صارف {user_id} فہرست میں نہیں ہے۔",
|
| 2122 |
+
"banlist_empty": "فہرست خالی ہے — کوئی بین نہیں ہے۔",
|
| 2123 |
+
"banlist_header": "بین شدہ:",
|
| 2124 |
"stats_no_data": " کوئی ڈیٹا نہیں",
|
| 2125 |
"stats_limit_used": " (حد ختم)",
|
| 2126 |
"tiktok_sound_recognized": "TikTok آواز کا لنک پہچانا گیا۔",
|
|
|
|
| 2214 |
"selftest_cooldown": "Keyingi ishga tushirishdan oldin {sec} s kuting.",
|
| 2215 |
"selftest_header": "O‘z-o‘zini tekshirish:",
|
| 2216 |
"selftest_gemini_skipped": "(Gemini o‘tkazib yuborildi — gemini argumentini qo‘shing, kvotasi tanqis)",
|
| 2217 |
+
"ban_group_only": "Bu buyruq bloklanganlar ro‘yxatini boshqaradi — faqat bot bilan shaxsiy xabarlarda, guruhlarda emas.",
|
| 2218 |
+
"ban_usage": "Foydalanish: /ban <ID> yoki javob tariqasida; /unban <ID> yoki javob tariqasida.",
|
| 2219 |
+
"ban_owner_refuse": "Egani bloklab bo‘lmaydi.",
|
| 2220 |
+
"ban_done": "Foydalanuvchi {user_id} bloklandi.",
|
| 2221 |
+
"unban_done": "Foydalanuvchi {user_id} blokdan chiqarildi.",
|
| 2222 |
+
"unban_missing": "Foydalanuvchi {user_id} ro‘yxatda yo‘q.",
|
| 2223 |
+
"banlist_empty": "Ro‘yxat bo‘sh — hech kim bloklanmagan.",
|
| 2224 |
+
"banlist_header": "Bloklanganlar:",
|
| 2225 |
"stats_no_data": " ma'lumot yo'q",
|
| 2226 |
"stats_limit_used": " (limit tugadi)",
|
| 2227 |
"tiktok_sound_recognized": "TikTok tovush havolasi tanildi.",
|
|
|
|
| 2315 |
"selftest_cooldown": "পরের বার চালানোর আগে {sec} সেকেন্ড অপেক্ষা করো।",
|
| 2316 |
"selftest_header": "স্ব-পরীক্ষা:",
|
| 2317 |
"selftest_gemini_skipped": "(Gemini বাদ দেওয়া হয়েছে — gemini আর্গুমেন্ট যোগ করো, কোটা সীমিত)",
|
| 2318 |
+
"ban_group_only": "এই কমান্ড ব্লকলিস্ট পরিচালনা করে — শুধু বটের সাথে ব্যক্তিগত বার্তায়, গ্রুপে নয়।",
|
| 2319 |
+
"ban_usage": "ব্যবহার: /ban <ID> বা উত্তরে; /unban <ID> বা উত্তরে।",
|
| 2320 |
+
"ban_owner_refuse": "মালিককে ব্যান করা যাবে না।",
|
| 2321 |
+
"ban_done": "ব্যবহারকারী {user_id} ব্যান হয়েছে।",
|
| 2322 |
+
"unban_done": "ব্যবহারকারী {user_id} আনব্যান হয়েছে।",
|
| 2323 |
+
"unban_missing": "ব্যবহারকারী {user_id} তালিকায় নেই।",
|
| 2324 |
+
"banlist_empty": "তালিকা খালি — কেউ ব্যান নয়।",
|
| 2325 |
+
"banlist_header": "ব্যানড:",
|
| 2326 |
"stats_no_data": " কোনো তথ্য নেই",
|
| 2327 |
"stats_limit_used": " (সীমা শেষ)",
|
| 2328 |
"tiktok_sound_recognized": "TikTok সাউন্ড লিংক চেনা গেছে।",
|
|
|
|
| 2416 |
"selftest_cooldown": "Maghintay ng {sec}s bago sa susunod na pag-run.",
|
| 2417 |
"selftest_header": "Self-test:",
|
| 2418 |
"selftest_gemini_skipped": "(Nilaktawan ang Gemini — idagdag ang argument na gemini, kakaunti ang quota nito)",
|
| 2419 |
+
"ban_group_only": "Pinamamahalaan ng command na ito ang blocklist — sa direktang mensahe lang sa bot, hindi sa mga grupo.",
|
| 2420 |
+
"ban_usage": "Paggamit: /ban <ID> o bilang reply; /unban <ID> o bilang reply.",
|
| 2421 |
+
"ban_owner_refuse": "Hindi puwedeng i-ban ang may-ari.",
|
| 2422 |
+
"ban_done": "Na-ban ang user na {user_id}.",
|
| 2423 |
+
"unban_done": "Na-unban ang user na {user_id}.",
|
| 2424 |
+
"unban_missing": "Wala sa listahan ang user na {user_id}.",
|
| 2425 |
+
"banlist_empty": "Walang laman ang listahan — walang naka-ban.",
|
| 2426 |
+
"banlist_header": "Naka-ban:",
|
| 2427 |
"stats_no_data": " walang data",
|
| 2428 |
"stats_limit_used": " (ubos ang limit)",
|
| 2429 |
"tiktok_sound_recognized": "Nakilala ang TikTok sound link.",
|
|
|
|
| 2517 |
"selftest_cooldown": "Poczekaj {sec} s przed kolejnym uruchomieniem.",
|
| 2518 |
"selftest_header": "Autotest:",
|
| 2519 |
"selftest_gemini_skipped": "(Gemini pominięte — dopisz argument gemini, limit jest niski)",
|
| 2520 |
+
"ban_group_only": "Ta komenda zarządza listą zablokowanych — dostępna tylko w wiadomościach prywatnych z botem, nie w grupach.",
|
| 2521 |
+
"ban_usage": "Użycie: /ban <ID> lub w odpowiedzi; /unban <ID> lub w odpowiedzi.",
|
| 2522 |
+
"ban_owner_refuse": "Właściciela nie można zablokować.",
|
| 2523 |
+
"ban_done": "Użytkownik {user_id} zablokowany.",
|
| 2524 |
+
"unban_done": "Użytkownik {user_id} odblokowany.",
|
| 2525 |
+
"unban_missing": "Użytkownika {user_id} nie ma na liście.",
|
| 2526 |
+
"banlist_empty": "Lista jest pusta — nikt nie jest zablokowany.",
|
| 2527 |
+
"banlist_header": "Zablokowani:",
|
| 2528 |
"stats_no_data": " brak danych",
|
| 2529 |
"stats_limit_used": " (limit wyczerpany)",
|
| 2530 |
"tiktok_sound_recognized": "Rozpoznano link do dźwięku TikToka.",
|
|
|
|
| 2618 |
"selftest_cooldown": "Wacht {sec} s voor de volgende run.",
|
| 2619 |
"selftest_header": "Zelftest:",
|
| 2620 |
"selftest_gemini_skipped": "(Gemini overgeslagen — voeg het argument gemini toe, zijn quotum is schaars)",
|
| 2621 |
+
"ban_group_only": "Deze opdracht beheert de blokkeerlijst — alleen beschikbaar in directe berichten met de bot, niet in groepen.",
|
| 2622 |
+
"ban_usage": "Gebruik: /ban <ID> of als antwoord; /unban <ID> of als antwoord.",
|
| 2623 |
+
"ban_owner_refuse": "De eigenaar kan niet worden geblokkeerd.",
|
| 2624 |
+
"ban_done": "Gebruiker {user_id} geblokkeerd.",
|
| 2625 |
+
"unban_done": "Gebruiker {user_id} gedeblokkeerd.",
|
| 2626 |
+
"unban_missing": "Gebruiker {user_id} staat niet op de lijst.",
|
| 2627 |
+
"banlist_empty": "De lijst is leeg — niemand geblokkeerd.",
|
| 2628 |
+
"banlist_header": "Geblokkeerd:",
|
| 2629 |
"stats_no_data": " geen data",
|
| 2630 |
"stats_limit_used": " (limiet op)",
|
| 2631 |
"tiktok_sound_recognized": "TikTok-geluidslink herkend.",
|
|
|
|
| 2719 |
"selftest_cooldown": "Linda u-{sec}s ngaphambi kokusebenza okulandelayo.",
|
| 2720 |
"selftest_header": "Ukuzihlola:",
|
| 2721 |
"selftest_gemini_skipped": "(I-Gemini yeqiwe — engeza i-agumenti ethi gemini, i-quota yayo yincane)",
|
| 2722 |
+
"ban_group_only": "Lo myalo uphatha uhlu lwavinjiwe — utholakala kuphela emilayezweni eqondile ne-bot, hhayi emaqenjini.",
|
| 2723 |
+
"ban_usage": "Ukusebenzisa: /ban <ID> noma ngempendulo; /unban <ID> noma ngempendulo.",
|
| 2724 |
+
"ban_owner_refuse": "Umnikazi akakwazi ukuvinjwa.",
|
| 2725 |
+
"ban_done": "Umsebenzisi {user_id} uvinjiwe.",
|
| 2726 |
+
"unban_done": "Umsebenzisi {user_id} ukhTshirwe.",
|
| 2727 |
+
"unban_missing": "Umsebenzisi {user_id} akekho ohlwini.",
|
| 2728 |
+
"banlist_empty": "Uhlu alunamuntu — akekho ovinjiwe.",
|
| 2729 |
+
"banlist_header": "Abavinjiwe:",
|
| 2730 |
"stats_no_data": " ayikho idatha",
|
| 2731 |
"stats_limit_used": " (umkhawulo uphelile)",
|
| 2732 |
"tiktok_sound_recognized": "Isixhumanisi somsindo se-TikTok saziwe.",
|
tests/test_bot_commands.py
CHANGED
|
@@ -1445,3 +1445,105 @@ def test_cmd_stats_shows_quarantine(monkeypatch):
|
|
| 1445 |
asyncio.run(bot.cmd_stats(incoming))
|
| 1446 |
assert "Карантин моделей" in sent["text"]
|
| 1447 |
assert "qs-model:free" in sent["text"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1445 |
asyncio.run(bot.cmd_stats(incoming))
|
| 1446 |
assert "Карантин моделей" in sent["text"]
|
| 1447 |
assert "qs-model:free" in sent["text"]
|
| 1448 |
+
|
| 1449 |
+
|
| 1450 |
+
def _make_ban_message(chat_id, user_id, text, group=False, reply_to=None):
|
| 1451 |
+
incoming = _FakeIncomingMessage(chat_id)
|
| 1452 |
+
if group:
|
| 1453 |
+
incoming.chat.type = bot.ChatType.GROUP
|
| 1454 |
+
incoming.from_user = SimpleNamespace(id=user_id)
|
| 1455 |
+
incoming.text = text
|
| 1456 |
+
incoming.reply_to_message = reply_to
|
| 1457 |
+
return incoming
|
| 1458 |
+
|
| 1459 |
+
|
| 1460 |
+
def _capture_replies(monkeypatch):
|
| 1461 |
+
sent = {}
|
| 1462 |
+
|
| 1463 |
+
async def fake_tg_call(method, *args, **kwargs):
|
| 1464 |
+
sent["text"] = args[0] if args else kwargs.get("text", "")
|
| 1465 |
+
return SimpleNamespace()
|
| 1466 |
+
|
| 1467 |
+
monkeypatch.setattr(bot, "_tg_call", fake_tg_call)
|
| 1468 |
+
return sent
|
| 1469 |
+
|
| 1470 |
+
|
| 1471 |
+
def test_ban_flow_by_id_unban_and_list(monkeypatch):
|
| 1472 |
+
# Полный цикл: бан по ID, список, разбан, повторный разбан — мимо.
|
| 1473 |
+
monkeypatch.setattr(bot, "OWNER_ID", 101001)
|
| 1474 |
+
sent = _capture_replies(monkeypatch)
|
| 1475 |
+
assert bot._is_banned(777002) is False
|
| 1476 |
+
asyncio.run(bot.cmd_ban(_make_ban_message(999801, 101001, "/ban 777002")))
|
| 1477 |
+
assert sent["text"] == bot._t(999801, "ban_done", user_id=777002)
|
| 1478 |
+
assert bot._is_banned(777002) is True
|
| 1479 |
+
asyncio.run(bot.cmd_banlist(_make_ban_message(999801, 101001, "/banlist")))
|
| 1480 |
+
assert "777002" in sent["text"]
|
| 1481 |
+
asyncio.run(bot.cmd_unban(_make_ban_message(999801, 101001, "/unban 777002")))
|
| 1482 |
+
assert sent["text"] == bot._t(999801, "unban_done", user_id=777002)
|
| 1483 |
+
assert bot._is_banned(777002) is False
|
| 1484 |
+
asyncio.run(bot.cmd_unban(_make_ban_message(999801, 101001, "/unban 777002")))
|
| 1485 |
+
assert sent["text"] == bot._t(999801, "unban_missing", user_id=777002)
|
| 1486 |
+
asyncio.run(bot.cmd_banlist(_make_ban_message(999801, 101001, "/banlist")))
|
| 1487 |
+
assert sent["text"] == bot._t(999801, "banlist_empty")
|
| 1488 |
+
|
| 1489 |
+
|
| 1490 |
+
def test_ban_by_reply_and_usage_and_owner_refuse(monkeypatch):
|
| 1491 |
+
# Цель ответом на сообщение; мусор вместо ID — подсказка; владельца — отказ.
|
| 1492 |
+
monkeypatch.setattr(bot, "OWNER_ID", 102001)
|
| 1493 |
+
sent = _capture_replies(monkeypatch)
|
| 1494 |
+
replied = SimpleNamespace(from_user=SimpleNamespace(id=555003))
|
| 1495 |
+
asyncio.run(bot.cmd_ban(_make_ban_message(999802, 102001, "/ban", reply_to=replied)))
|
| 1496 |
+
assert bot._is_banned(555003) is True
|
| 1497 |
+
asyncio.run(bot.cmd_ban(_make_ban_message(999802, 102001, "/ban вообще")))
|
| 1498 |
+
assert sent["text"] == bot._t(999802, "ban_usage")
|
| 1499 |
+
asyncio.run(bot.cmd_ban(_make_ban_message(999802, 102001, "/ban 102001")))
|
| 1500 |
+
assert sent["text"] == bot._t(999802, "ban_owner_refuse")
|
| 1501 |
+
assert bot._is_banned(102001) is False
|
| 1502 |
+
|
| 1503 |
+
|
| 1504 |
+
def test_ban_denies_non_owner_and_group(monkeypatch):
|
| 1505 |
+
# Чужой и группа: только отказ, список не меняется.
|
| 1506 |
+
monkeypatch.setattr(bot, "OWNER_ID", 103001)
|
| 1507 |
+
sent = _capture_replies(monkeypatch)
|
| 1508 |
+
asyncio.run(bot.cmd_ban(_make_ban_message(999803, 999999, "/ban 123")))
|
| 1509 |
+
assert sent["text"] == bot._t(999803, "stats_deny")
|
| 1510 |
+
asyncio.run(bot.cmd_ban(_make_ban_message(999803, 103001, "/ban 123", group=True)))
|
| 1511 |
+
assert sent["text"] == bot._t(999803, "ban_group_only")
|
| 1512 |
+
assert bot._is_banned(123) is False
|
| 1513 |
+
|
| 1514 |
+
|
| 1515 |
+
def test_handle_message_ignores_banned_user(monkeypatch):
|
| 1516 |
+
# Заблокированный: ни ответа, ни ядра (там контекст группы и счётчики лимитов).
|
| 1517 |
+
from unittest.mock import AsyncMock
|
| 1518 |
+
monkeypatch.setattr(bot, "OWNER_ID", 104001)
|
| 1519 |
+
bot._ban_user(605001)
|
| 1520 |
+
fake_core = AsyncMock()
|
| 1521 |
+
monkeypatch.setattr(bot, "_handle_message_core", fake_core)
|
| 1522 |
+
|
| 1523 |
+
def _msg(chat_type, text="привет"):
|
| 1524 |
+
return SimpleNamespace(
|
| 1525 |
+
media_group_id=None,
|
| 1526 |
+
chat=SimpleNamespace(id=999804, type=chat_type),
|
| 1527 |
+
from_user=SimpleNamespace(id=605001),
|
| 1528 |
+
text=text, caption=None, reply_to_message=None,
|
| 1529 |
+
)
|
| 1530 |
+
|
| 1531 |
+
asyncio.run(bot.handle_message(_msg(bot.ChatType.PRIVATE)))
|
| 1532 |
+
asyncio.run(bot.handle_message(_msg(bot.ChatType.GROUP)))
|
| 1533 |
+
assert fake_core.await_count == 0
|
| 1534 |
+
bot._unban_user(605001)
|
| 1535 |
+
asyncio.run(bot.handle_message(_msg(bot.ChatType.GROUP)))
|
| 1536 |
+
assert fake_core.await_count == 1
|
| 1537 |
+
|
| 1538 |
+
|
| 1539 |
+
def test_banned_list_survives_quota_reload(monkeypatch):
|
| 1540 |
+
# Список живёт в том же файле, что квота: рестарт его не сбрасывает.
|
| 1541 |
+
import json
|
| 1542 |
+
monkeypatch.setattr(bot, "OWNER_ID", 105001)
|
| 1543 |
+
bot._ban_user(606001)
|
| 1544 |
+
snapshot = json.dumps(dict(bot.GLOBAL_QUOTA), ensure_ascii=False)
|
| 1545 |
+
monkeypatch.setattr(bot, "_storage_read_text", lambda *args, **kwargs: snapshot)
|
| 1546 |
+
bot.GLOBAL_QUOTA.pop(bot.BANNED_KEY, None)
|
| 1547 |
+
assert bot._is_banned(606001) is False
|
| 1548 |
+
bot.load_global_quota()
|
| 1549 |
+
assert bot._is_banned(606001) is True
|