feat: sync bot keyboards and callbacks
Sync telesrv b96f2dd (feat(bot): complete keyboards callbacks and durable delivery). Skipped private docs and preserved public README files per sync rules; normalized the appearance seed log label for public naming.
This commit is contained in:
parent
0c99ae0a9d
commit
bf965f610c
80 changed files with 7212 additions and 349 deletions
63
cmd/bots/aiogramecho/README.md
Normal file
63
cmd/bots/aiogramecho/README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# aiogram 3 echo demo
|
||||
|
||||
该示例使用标准 aiogram 3 API,仅把 API server 指向 telesrv。aiogram 的
|
||||
`TelegramAPIServer.from_base()` 会自动拼出 telesrv 已支持的
|
||||
`/bot<TOKEN>/<method>` 与 `/file/bot<TOKEN>/<path>`。
|
||||
|
||||
```powershell
|
||||
python -m pip install -r .\cmd\bots\aiogramecho\requirements.txt
|
||||
$env:TELESRV_BOT_TOKEN = "<bot_id>:<secret>"
|
||||
python .\cmd\bots\aiogramecho\echo.py --drop-pending
|
||||
```
|
||||
|
||||
发送三种语义色的 reply keyboard 与 inline callback 按钮:
|
||||
|
||||
```powershell
|
||||
python .\cmd\bots\aiogramecho\echo.py `
|
||||
--buttons-chat-id 1780243200 `
|
||||
--drop-pending
|
||||
```
|
||||
|
||||
可选的 `--button-icon-id <custom_emoji_document_id>` 同时验证按钮自定义 emoji
|
||||
图标。Telegram 官方会按 bot owner Premium / Fragment 权限限制图标使用;颜色只接受
|
||||
`primary`(蓝)、`success`(绿)、`danger`(红),不接受任意 RGB。
|
||||
|
||||
只主动发消息、不启动轮询:
|
||||
|
||||
```powershell
|
||||
python .\cmd\bots\aiogramecho\echo.py `
|
||||
--send-only `
|
||||
--send-chat-id 1780243200 `
|
||||
--send-text "hello from aiogram"
|
||||
```
|
||||
|
||||
默认 API server 是 `http://127.0.0.1:8081`,可用 `--base-url` 或
|
||||
`TELESRV_BOT_API_SERVER` 覆盖。不要在这里追加 `/bot`;这与 ptbecho 的
|
||||
`--base-url http://127.0.0.1:8081/bot` 参数格式不同。
|
||||
|
||||
轮询模式会回答本示例的 `aiogram-*` callback,也会对同一测试 bot 先前由
|
||||
其它 demo 创建的 inline callback 给出兜底确认,避免 TDesktop 按钮一直转圈。
|
||||
|
||||
## Webhook 模式
|
||||
|
||||
telesrv 现在会持久化 webhook 配置,通过跨实例租约投递,并且只在目标返回 2xx
|
||||
后推进 `update_id`。aiogram 可监听本机 HTTP,由 Caddy/Nginx/Tunnel 提供公网 HTTPS:
|
||||
|
||||
```powershell
|
||||
$env:TELESRV_BOT_WEBHOOK_URL = "https://bot.example.com/webhook"
|
||||
$env:TELESRV_BOT_WEBHOOK_SECRET = "replace_with_a_random_secret"
|
||||
python .\cmd\bots\aiogramecho\echo.py `
|
||||
--mode webhook `
|
||||
--listen-host 127.0.0.1 `
|
||||
--listen-port 8082 `
|
||||
--webhook-path /webhook `
|
||||
--drop-pending
|
||||
```
|
||||
|
||||
公网 URL 必须是 HTTPS,端口限 Telegram 标准的 443/80/88/8443;本机监听地址
|
||||
可以是 HTTP,因为 TLS 通常在反向代理终止。`secret_token` 会由 telesrv 放入
|
||||
`X-Telegram-Bot-Api-Secret-Token`,aiogram 会自动校验。若希望进程退出时删除配置,
|
||||
再加 `--delete-webhook-on-exit`;默认保留配置,以免普通重启造成更新丢窗。
|
||||
|
||||
同一个 token 的 polling 与 webhook 互斥;切回轮询时直接以默认模式启动,示例会先
|
||||
调用 `deleteWebhook`,再开始 `getUpdates`。
|
||||
273
cmd/bots/aiogramecho/echo.py
Normal file
273
cmd/bots/aiogramecho/echo.py
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
#!/usr/bin/env python3
|
||||
"""aiogram 3 echo/button demo for the telesrv Bot API endpoint."""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from aiogram import Bot, Dispatcher, F, Router
|
||||
from aiogram.client.session.aiohttp import AiohttpSession
|
||||
from aiogram.client.telegram import TelegramAPIServer
|
||||
from aiogram.enums import ButtonStyle
|
||||
from aiogram.filters import Command, CommandStart
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
Message,
|
||||
ReplyKeyboardMarkup,
|
||||
)
|
||||
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
LOG = logging.getLogger("aiogramecho")
|
||||
|
||||
|
||||
def env_int(name: str) -> int | None:
|
||||
raw = os.getenv(name)
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError as exc:
|
||||
raise SystemExit(f"{name} must be an integer, got {raw!r}") from exc
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="aiogram 3 echo bot against telesrv")
|
||||
parser.add_argument("--token", default=os.getenv("TELESRV_BOT_TOKEN"))
|
||||
parser.add_argument(
|
||||
"--base-url",
|
||||
default=os.getenv("TELESRV_BOT_API_SERVER", "http://127.0.0.1:8081"),
|
||||
help="API server origin; aiogram adds /bot<TOKEN> and /file/bot<TOKEN>",
|
||||
)
|
||||
parser.add_argument("--prefix", default="aiogram echo: ")
|
||||
parser.add_argument("--drop-pending", action="store_true")
|
||||
parser.add_argument("--mode", choices=("polling", "webhook"), default=os.getenv("TELESRV_BOT_MODE", "polling"))
|
||||
parser.add_argument(
|
||||
"--webhook-url",
|
||||
default=os.getenv("TELESRV_BOT_WEBHOOK_URL", ""),
|
||||
help="Public HTTPS URL including the webhook path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--webhook-path",
|
||||
default=os.getenv("TELESRV_BOT_WEBHOOK_PATH", "/webhook"),
|
||||
help="Local aiohttp route, normally the path part of --webhook-url",
|
||||
)
|
||||
parser.add_argument("--webhook-secret", default=os.getenv("TELESRV_BOT_WEBHOOK_SECRET", "telesrv-aiogram-demo"))
|
||||
parser.add_argument("--listen-host", default=os.getenv("TELESRV_BOT_LISTEN_HOST", "127.0.0.1"))
|
||||
parser.add_argument("--listen-port", type=int, default=env_int("TELESRV_BOT_LISTEN_PORT") or 8082)
|
||||
parser.add_argument("--delete-webhook-on-exit", action="store_true")
|
||||
parser.add_argument("--timeout", type=int, default=30)
|
||||
parser.add_argument("--send-chat-id", type=int, default=env_int("TELESRV_BOT_DEMO_CHAT_ID"))
|
||||
parser.add_argument("--send-text", default=os.getenv("TELESRV_BOT_DEMO_SEND_TEXT", ""))
|
||||
parser.add_argument("--buttons-chat-id", type=int, default=env_int("TELESRV_BOT_DEMO_BUTTONS_CHAT_ID"))
|
||||
parser.add_argument(
|
||||
"--button-icon-id",
|
||||
default=os.getenv("TELESRV_BOT_DEMO_BUTTON_ICON_ID"),
|
||||
help="Optional custom emoji document id used as the button icon",
|
||||
)
|
||||
parser.add_argument("--send-only", action="store_true")
|
||||
parser.add_argument("--log-level", default="INFO")
|
||||
args = parser.parse_args()
|
||||
if not args.token:
|
||||
parser.error("missing --token or TELESRV_BOT_TOKEN")
|
||||
if args.timeout < 0 or args.timeout > 50:
|
||||
parser.error("--timeout must be between 0 and 50")
|
||||
if args.send_text and args.send_chat_id is None:
|
||||
parser.error("--send-chat-id is required with --send-text")
|
||||
if args.send_only and not args.send_text and args.buttons_chat_id is None:
|
||||
parser.error("--send-only requires --send-text or --buttons-chat-id")
|
||||
if args.button_icon_id:
|
||||
try:
|
||||
if int(args.button_icon_id) <= 0:
|
||||
raise ValueError
|
||||
except ValueError as exc:
|
||||
raise SystemExit("--button-icon-id must be a positive integer") from exc
|
||||
if args.mode == "webhook" and not args.webhook_url:
|
||||
parser.error("--webhook-url or TELESRV_BOT_WEBHOOK_URL is required in webhook mode")
|
||||
if not args.webhook_path.startswith("/") or "?" in args.webhook_path or "#" in args.webhook_path:
|
||||
parser.error("--webhook-path must be an absolute path without query or fragment")
|
||||
if args.listen_port < 1 or args.listen_port > 65535:
|
||||
parser.error("--listen-port must be between 1 and 65535")
|
||||
return args
|
||||
|
||||
|
||||
def reply_keyboard(icon_id: str | None) -> ReplyKeyboardMarkup:
|
||||
return ReplyKeyboardMarkup(
|
||||
keyboard=[
|
||||
[
|
||||
KeyboardButton(text="Primary", style=ButtonStyle.PRIMARY, icon_custom_emoji_id=icon_id),
|
||||
KeyboardButton(text="Success", style=ButtonStyle.SUCCESS, icon_custom_emoji_id=icon_id),
|
||||
KeyboardButton(text="Danger", style=ButtonStyle.DANGER, icon_custom_emoji_id=icon_id),
|
||||
]
|
||||
],
|
||||
resize_keyboard=True,
|
||||
one_time_keyboard=True,
|
||||
input_field_placeholder="Tap a colored reply button",
|
||||
)
|
||||
|
||||
|
||||
def inline_keyboard(icon_id: str | None) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Primary",
|
||||
callback_data="aiogram-primary",
|
||||
style=ButtonStyle.PRIMARY,
|
||||
icon_custom_emoji_id=icon_id,
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="Success",
|
||||
callback_data="aiogram-success",
|
||||
style=ButtonStyle.SUCCESS,
|
||||
icon_custom_emoji_id=icon_id,
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="Danger",
|
||||
callback_data="aiogram-danger",
|
||||
style=ButtonStyle.DANGER,
|
||||
icon_custom_emoji_id=icon_id,
|
||||
),
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def send_button_messages(bot: Bot, chat_id: int, icon_id: str | None) -> None:
|
||||
reply = await bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text="TELESRV_AIOGRAM_REPLY_STYLES_20260719",
|
||||
reply_markup=reply_keyboard(icon_id),
|
||||
)
|
||||
inline = await bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text="TELESRV_AIOGRAM_INLINE_STYLES_20260719",
|
||||
reply_markup=inline_keyboard(icon_id),
|
||||
)
|
||||
LOG.info(
|
||||
"sent styled buttons chat_id=%s reply_message_id=%s inline_message_id=%s",
|
||||
chat_id,
|
||||
reply.message_id,
|
||||
inline.message_id,
|
||||
)
|
||||
|
||||
|
||||
def build_dispatcher(args: argparse.Namespace) -> Dispatcher:
|
||||
router = Router(name="telesrv-aiogramecho")
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def start(message: Message) -> None:
|
||||
await message.answer("send /ping <text>, /buttons, or any private text")
|
||||
|
||||
@router.message(Command("buttons"))
|
||||
async def buttons(message: Message) -> None:
|
||||
await send_button_messages(message.bot, message.chat.id, args.button_icon_id)
|
||||
|
||||
@router.message(Command("ping"))
|
||||
async def ping(message: Message) -> None:
|
||||
await message.answer(args.prefix + (message.text or ""))
|
||||
|
||||
@router.callback_query(F.data.startswith("aiogram-"))
|
||||
async def callback(query: CallbackQuery) -> None:
|
||||
await query.answer(f"telesrv {query.data} callback OK")
|
||||
LOG.info("answered callback query_id=%s data=%r", query.id, query.data)
|
||||
|
||||
@router.callback_query()
|
||||
async def fallback_callback(query: CallbackQuery) -> None:
|
||||
"""Keep the echo demo responsive for buttons created by another demo."""
|
||||
await query.answer("telesrv callback OK")
|
||||
LOG.info("answered fallback callback query_id=%s data=%r", query.id, query.data)
|
||||
|
||||
@router.message(F.text)
|
||||
async def echo(message: Message) -> None:
|
||||
await message.answer(args.prefix + (message.text or ""))
|
||||
LOG.info("echoed chat_id=%s message_id=%s", message.chat.id, message.message_id)
|
||||
|
||||
dispatcher = Dispatcher()
|
||||
dispatcher.include_router(router)
|
||||
return dispatcher
|
||||
|
||||
|
||||
def build_bot(args: argparse.Namespace) -> Bot:
|
||||
session = AiohttpSession(api=TelegramAPIServer.from_base(args.base_url.rstrip("/")))
|
||||
return Bot(token=args.token, session=session)
|
||||
|
||||
|
||||
async def run(args: argparse.Namespace) -> None:
|
||||
bot = build_bot(args)
|
||||
runner: web.AppRunner | None = None
|
||||
try:
|
||||
me = await bot.get_me()
|
||||
LOG.info("authenticated as @%s (%s), bot_api=%s", me.username or me.id, me.id, args.base_url)
|
||||
if args.send_chat_id is not None and args.send_text:
|
||||
sent = await bot.send_message(chat_id=args.send_chat_id, text=args.send_text)
|
||||
LOG.info("sent proactive chat_id=%s message_id=%s", args.send_chat_id, sent.message_id)
|
||||
if args.buttons_chat_id is not None:
|
||||
await send_button_messages(bot, args.buttons_chat_id, args.button_icon_id)
|
||||
if args.send_only:
|
||||
return
|
||||
dispatcher = build_dispatcher(args)
|
||||
allowed_updates = ["message", "edited_message", "callback_query"]
|
||||
if args.mode == "polling":
|
||||
await bot.delete_webhook(drop_pending_updates=args.drop_pending)
|
||||
await dispatcher.start_polling(
|
||||
bot,
|
||||
allowed_updates=allowed_updates,
|
||||
polling_timeout=args.timeout,
|
||||
close_bot_session=False,
|
||||
)
|
||||
return
|
||||
|
||||
application = web.Application()
|
||||
SimpleRequestHandler(
|
||||
dispatcher=dispatcher,
|
||||
bot=bot,
|
||||
secret_token=args.webhook_secret,
|
||||
).register(application, path=args.webhook_path)
|
||||
setup_application(application, dispatcher, bot=bot)
|
||||
await bot.set_webhook(
|
||||
url=args.webhook_url,
|
||||
secret_token=args.webhook_secret,
|
||||
allowed_updates=allowed_updates,
|
||||
drop_pending_updates=args.drop_pending,
|
||||
)
|
||||
runner = web.AppRunner(application)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, host=args.listen_host, port=args.listen_port)
|
||||
await site.start()
|
||||
LOG.info(
|
||||
"webhook listening on http://%s:%s%s, public_url=%s",
|
||||
args.listen_host,
|
||||
args.listen_port,
|
||||
args.webhook_path,
|
||||
args.webhook_url,
|
||||
)
|
||||
await asyncio.Event().wait()
|
||||
finally:
|
||||
if args.mode == "webhook" and args.delete_webhook_on_exit:
|
||||
try:
|
||||
await bot.delete_webhook()
|
||||
except Exception: # pragma: no cover - best-effort shutdown logging
|
||||
LOG.exception("failed to delete webhook during shutdown")
|
||||
if runner is not None:
|
||||
await runner.cleanup()
|
||||
await bot.session.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, args.log_level.upper(), logging.INFO),
|
||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||
)
|
||||
asyncio.run(run(args))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
1
cmd/bots/aiogramecho/requirements.txt
Normal file
1
cmd/bots/aiogramecho/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
aiogram==3.30.0
|
||||
|
|
@ -34,11 +34,24 @@ python .\cmd\bots\ptbecho\echo.py `
|
|||
--send-text "ptbecho is online"
|
||||
```
|
||||
|
||||
发送 reply keyboard 与 inline callback 两条验证消息并保持 polling:
|
||||
|
||||
```powershell
|
||||
python .\cmd\bots\ptbecho\echo.py `
|
||||
--buttons-chat-id 1780243200
|
||||
```
|
||||
|
||||
reply keyboard 与 inline keyboard 都会各显示蓝/绿/红三种语义色;点击 reply
|
||||
button 会按普通文本消息进入 echo 链,点击 inline button 会由 `callback_query` handler 调用
|
||||
`answerCallbackQuery` 并显示 `telesrv inline callback OK`。也可以在私聊中发送
|
||||
`/buttons` 生成同样的两条消息。
|
||||
|
||||
可选参数:
|
||||
|
||||
- `--send-count N`:连续主动发送 N 条。
|
||||
- `--send-interval SEC`:连续发送之间的间隔。
|
||||
- `TELESRV_BOT_DEMO_CHAT_ID` / `TELESRV_BOT_DEMO_SEND_TEXT`:主动发送参数的环境变量形式。
|
||||
- `--buttons-chat-id` / `TELESRV_BOT_DEMO_BUTTONS_CHAT_ID`:发送两类键盘验证消息并监听 callback。
|
||||
|
||||
本地超级群 chat id 使用 Bot API 形式 `-100<channel_id>`;例如 channel id 为
|
||||
`2` 时是 `-1000000000002`。
|
||||
|
|
|
|||
|
|
@ -32,11 +32,18 @@ import os
|
|||
import signal
|
||||
from typing import Iterable
|
||||
|
||||
from telegram import Bot, Update
|
||||
from telegram.constants import ChatAction
|
||||
from telegram import (
|
||||
Bot,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
ReplyKeyboardMarkup,
|
||||
Update,
|
||||
)
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
ApplicationBuilder,
|
||||
CallbackQueryHandler,
|
||||
CommandHandler,
|
||||
ContextTypes,
|
||||
MessageHandler,
|
||||
|
|
@ -78,6 +85,12 @@ def parse_args() -> argparse.Namespace:
|
|||
)
|
||||
parser.add_argument("--send-count", type=int, default=1, help="Number of proactive messages to send")
|
||||
parser.add_argument("--send-interval", type=float, default=1.0, help="Seconds between proactive sends")
|
||||
parser.add_argument(
|
||||
"--buttons-chat-id",
|
||||
type=int,
|
||||
default=env_int("TELESRV_BOT_DEMO_BUTTONS_CHAT_ID"),
|
||||
help="Send reply/inline keyboard validation messages to this chat on startup",
|
||||
)
|
||||
parser.add_argument("--send-only", action="store_true", help="Send proactive messages and exit without polling")
|
||||
parser.add_argument("--log-level", default="INFO")
|
||||
args = parser.parse_args()
|
||||
|
|
@ -87,11 +100,10 @@ def parse_args() -> argparse.Namespace:
|
|||
parser.error("--send-count must be >= 1")
|
||||
if args.send_interval < 0:
|
||||
parser.error("--send-interval must be >= 0")
|
||||
wants_send = args.send_only or bool(args.send_text)
|
||||
if wants_send and args.send_chat_id is None:
|
||||
if args.send_text and args.send_chat_id is None:
|
||||
parser.error("--send-chat-id is required when --send-text or --send-only is used")
|
||||
if args.send_only and not args.send_text:
|
||||
parser.error("--send-only requires --send-text")
|
||||
if args.send_only and not args.send_text and args.buttons_chat_id is None:
|
||||
parser.error("--send-only requires --send-text or --buttons-chat-id")
|
||||
return args
|
||||
|
||||
|
||||
|
|
@ -112,7 +124,6 @@ async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||
if not text:
|
||||
return
|
||||
prefix = context.application.bot_data.get("prefix", "echo: ")
|
||||
await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=ChatAction.TYPING)
|
||||
sent = await update.effective_message.reply_text(prefix + text)
|
||||
LOG.info(
|
||||
"echoed update_id=%s chat_id=%s message_id=%s sent_message_id=%s text=%r",
|
||||
|
|
@ -124,6 +135,26 @@ async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||
)
|
||||
|
||||
|
||||
async def buttons(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if update.effective_chat is None:
|
||||
return
|
||||
await send_button_messages(context.bot, update.effective_chat.id)
|
||||
|
||||
|
||||
async def callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
if query is None:
|
||||
return
|
||||
await query.answer("telesrv inline callback OK")
|
||||
LOG.info(
|
||||
"answered callback query_id=%s chat_id=%s message_id=%s data=%r",
|
||||
query.id,
|
||||
query.message.chat_id if query.message else None,
|
||||
query.message.message_id if query.message else None,
|
||||
query.data,
|
||||
)
|
||||
|
||||
|
||||
async def send_active_messages(bot: Bot, chat_id: int, text: str, count: int, interval: float) -> None:
|
||||
for index in range(count):
|
||||
sent = await bot.send_message(chat_id=chat_id, text=text)
|
||||
|
|
@ -139,25 +170,63 @@ async def send_active_messages(bot: Bot, chat_id: int, text: str, count: int, in
|
|||
await asyncio.sleep(interval)
|
||||
|
||||
|
||||
async def send_button_messages(bot: Bot, chat_id: int) -> None:
|
||||
reply = await bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text="TELESRV_REPLY_KEYBOARD_20260719",
|
||||
reply_markup=ReplyKeyboardMarkup(
|
||||
[[
|
||||
KeyboardButton("Primary", api_kwargs={"style": "primary"}),
|
||||
KeyboardButton("Success", api_kwargs={"style": "success"}),
|
||||
KeyboardButton("Danger", api_kwargs={"style": "danger"}),
|
||||
]],
|
||||
resize_keyboard=True,
|
||||
one_time_keyboard=True,
|
||||
input_field_placeholder="Tap the reply button",
|
||||
),
|
||||
)
|
||||
inline = await bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text="TELESRV_INLINE_CALLBACK_20260719",
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
[[
|
||||
InlineKeyboardButton("Primary", callback_data="telesrv-primary", api_kwargs={"style": "primary"}),
|
||||
InlineKeyboardButton("Success", callback_data="telesrv-success", api_kwargs={"style": "success"}),
|
||||
InlineKeyboardButton("Danger", callback_data="telesrv-danger", api_kwargs={"style": "danger"}),
|
||||
]],
|
||||
),
|
||||
)
|
||||
LOG.info(
|
||||
"sent keyboard validation chat_id=%s reply_message_id=%s inline_message_id=%s",
|
||||
chat_id,
|
||||
reply.message_id,
|
||||
inline.message_id,
|
||||
)
|
||||
|
||||
|
||||
async def send_on_startup(app: Application) -> None:
|
||||
chat_id = app.bot_data.get("send_chat_id")
|
||||
text = app.bot_data.get("send_text")
|
||||
if chat_id is None or not text:
|
||||
return
|
||||
await send_active_messages(
|
||||
app.bot,
|
||||
chat_id=chat_id,
|
||||
text=text,
|
||||
count=int(app.bot_data.get("send_count", 1)),
|
||||
interval=float(app.bot_data.get("send_interval", 1.0)),
|
||||
)
|
||||
if chat_id is not None and text:
|
||||
await send_active_messages(
|
||||
app.bot,
|
||||
chat_id=chat_id,
|
||||
text=text,
|
||||
count=int(app.bot_data.get("send_count", 1)),
|
||||
interval=float(app.bot_data.get("send_interval", 1.0)),
|
||||
)
|
||||
buttons_chat_id = app.bot_data.get("buttons_chat_id")
|
||||
if buttons_chat_id is not None:
|
||||
await send_button_messages(app.bot, int(buttons_chat_id))
|
||||
|
||||
|
||||
async def post_init(app: Application) -> None:
|
||||
me = await app.bot.get_me()
|
||||
LOG.info("listening as @%s (%s), bot_api=%s", me.username or me.id, me.id, app.bot_data["base_url"])
|
||||
if app.bot_data.get("send_chat_id") is not None and app.bot_data.get("send_text"):
|
||||
app.create_task(send_on_startup(app), name="ptbecho-proactive-send")
|
||||
if (app.bot_data.get("send_chat_id") is not None and app.bot_data.get("send_text")) or app.bot_data.get(
|
||||
"buttons_chat_id"
|
||||
) is not None:
|
||||
await send_on_startup(app)
|
||||
|
||||
|
||||
def build_app(args: argparse.Namespace) -> Application:
|
||||
|
|
@ -175,8 +244,11 @@ def build_app(args: argparse.Namespace) -> Application:
|
|||
app.bot_data["send_text"] = args.send_text
|
||||
app.bot_data["send_count"] = args.send_count
|
||||
app.bot_data["send_interval"] = args.send_interval
|
||||
app.bot_data["buttons_chat_id"] = args.buttons_chat_id
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(CommandHandler("ping", ping))
|
||||
app.add_handler(CommandHandler("buttons", buttons))
|
||||
app.add_handler(CallbackQueryHandler(callback))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
|
||||
return app
|
||||
|
||||
|
|
@ -185,18 +257,21 @@ async def run_send_only(args: argparse.Namespace) -> None:
|
|||
bot = Bot(token=args.token, base_url=args.base_url, base_file_url=args.base_file_url)
|
||||
me = await bot.get_me()
|
||||
LOG.info("authenticated as @%s (%s), bot_api=%s", me.username or me.id, me.id, args.base_url)
|
||||
await send_active_messages(
|
||||
bot,
|
||||
chat_id=args.send_chat_id,
|
||||
text=args.send_text,
|
||||
count=args.send_count,
|
||||
interval=args.send_interval,
|
||||
)
|
||||
if args.send_chat_id is not None and args.send_text:
|
||||
await send_active_messages(
|
||||
bot,
|
||||
chat_id=args.send_chat_id,
|
||||
text=args.send_text,
|
||||
count=args.send_count,
|
||||
interval=args.send_interval,
|
||||
)
|
||||
if args.buttons_chat_id is not None:
|
||||
await send_button_messages(bot, args.buttons_chat_id)
|
||||
|
||||
|
||||
def stop_signals() -> Iterable[int]:
|
||||
def stop_signals() -> Iterable[int] | None:
|
||||
if os.name == "nt":
|
||||
return (signal.SIGINT, signal.SIGTERM)
|
||||
return None
|
||||
return (signal.SIGINT, signal.SIGTERM, signal.SIGHUP)
|
||||
|
||||
|
||||
|
|
@ -214,7 +289,7 @@ def main() -> int:
|
|||
|
||||
app = build_app(args)
|
||||
app.run_polling(
|
||||
allowed_updates=["message", "edited_message"],
|
||||
allowed_updates=["message", "edited_message", "callback_query"],
|
||||
drop_pending_updates=args.drop_pending,
|
||||
poll_interval=0.0,
|
||||
timeout=args.timeout,
|
||||
|
|
|
|||
|
|
@ -362,6 +362,7 @@ func run(logger *zap.Logger) error {
|
|||
dispatchOutboxStore := postgres.NewDispatchOutboxStore(pool, postgres.WithLeaseTimeout(cfg.OutboxLeaseTimeout))
|
||||
bootstrapUpdateStore := postgres.NewBootstrapUpdateJobStore(pool)
|
||||
botAPIUpdateStore := postgres.NewBotAPIUpdateStore(pool)
|
||||
botCallbackStore := redisstore.NewBotCallbackRegistryStore(rdb)
|
||||
boxIDAllocator := redisstore.NewBoxIDAllocator(rdb, postgres.NewMessageBoxCounterSource(pool))
|
||||
channelIDAllocator := redisstore.NewChannelIDAllocator(rdb, postgres.NewChannelIDCounterSource(pool))
|
||||
channelMessageIDAllocator := redisstore.NewChannelMessageIDAllocator(rdb, postgres.NewChannelMessageIDCounterSource(pool))
|
||||
|
|
@ -436,7 +437,7 @@ func run(logger *zap.Logger) error {
|
|||
return fmt.Errorf("seed appearance: %w", err)
|
||||
} else if !stats.Skipped {
|
||||
logger.Info("外观种子导入完成",
|
||||
zap.String("source", "orange-live"),
|
||||
zap.String("source", "default-seed"),
|
||||
zap.Int("wallpapers", stats.Wallpapers),
|
||||
zap.Int("documents", stats.Documents),
|
||||
zap.Int("blobs", stats.Blobs),
|
||||
|
|
@ -792,6 +793,7 @@ func run(logger *zap.Logger) error {
|
|||
Updates: updatesService,
|
||||
BootstrapUpdates: bootstrapUpdateStore,
|
||||
BotAPIUpdates: botAPIUpdateStore,
|
||||
BotCallbacks: botCallbackStore,
|
||||
Contacts: contactsService,
|
||||
Dialogs: dialogsService,
|
||||
Chatlists: chatlistsService,
|
||||
|
|
@ -900,6 +902,7 @@ func run(logger *zap.Logger) error {
|
|||
}
|
||||
}()
|
||||
go router.RunInlineBotPushSubscriber(ctx)
|
||||
go router.RunBotCallbackAnswerSubscriber(ctx)
|
||||
if _, err := botapi.Start(ctx, cfg.BotAPIAddr, botsService, usersService, router, router, logger.Named("botapi")); err != nil {
|
||||
return fmt.Errorf("start bot api: %w", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue