feat: sync ephemeral bot demo commands
Sync telesrv 172a86e (feat(bot-demo): echo ephemeral group commands). Skipped telesrv docs changes per public sync rules.
This commit is contained in:
parent
f49c817def
commit
7de1766941
6 changed files with 258 additions and 4 deletions
|
|
@ -10,6 +10,21 @@ $env:TELESRV_BOT_TOKEN = "<bot_id>:<secret>"
|
|||
python .\cmd\bots\aiogramecho\echo.py --drop-pending
|
||||
```
|
||||
|
||||
## Ephemeral echo(Bot API 10.2)
|
||||
|
||||
先通过 `setMyCommands` 把 `private` 注册为 `is_ephemeral=true`,再在 TDesktop Layer 228
|
||||
的群组中发送:
|
||||
|
||||
```text
|
||||
/private@你的Bot用户名
|
||||
```
|
||||
|
||||
aiogram 3.30.0 原生解析 `ephemeral_message_id`。示例在 15 秒 action 窗口内携带
|
||||
`receiver_user_id` 和 `ReplyParameters(ephemeral_message_id=...)` 回复
|
||||
`ephemeral echo: ...`;失败不会降级成普通消息。Alice 应看到两条带
|
||||
可见性提示的消息:自己发出的命令显示“Only visible to @Bot”,bot 回复显示
|
||||
“Only visible to you”;Bob 不应看到其中任何一条。
|
||||
|
||||
发送三种语义色的 reply keyboard 与 inline callback 按钮:
|
||||
|
||||
```powershell
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from aiogram.types import (
|
|||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
Message,
|
||||
ReplyParameters,
|
||||
ReplyKeyboardMarkup,
|
||||
)
|
||||
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
||||
|
|
@ -45,6 +46,7 @@ def parse_args() -> argparse.Namespace:
|
|||
help="API server origin; aiogram adds /bot<TOKEN> and /file/bot<TOKEN>",
|
||||
)
|
||||
parser.add_argument("--prefix", default="aiogram echo: ")
|
||||
parser.add_argument("--ephemeral-prefix", default="ephemeral 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(
|
||||
|
|
@ -157,6 +159,35 @@ async def send_button_messages(bot: Bot, chat_id: int, icon_id: str | None) -> N
|
|||
)
|
||||
|
||||
|
||||
def ephemeral_message_id(message: object) -> int | None:
|
||||
"""Read the native aiogram field, retaining an extra-field fallback."""
|
||||
raw = getattr(message, "ephemeral_message_id", None)
|
||||
if raw is None:
|
||||
raw = (getattr(message, "model_extra", None) or {}).get("ephemeral_message_id")
|
||||
if isinstance(raw, int) and not isinstance(raw, bool) and raw > 0:
|
||||
return raw
|
||||
return None
|
||||
|
||||
|
||||
async def send_echo(message: Message, prefix: str, ephemeral_prefix: str):
|
||||
text = message.text or message.caption or ""
|
||||
if not text:
|
||||
return None
|
||||
transient_id = ephemeral_message_id(message)
|
||||
if transient_id is None:
|
||||
return await message.answer(prefix + text)
|
||||
|
||||
if message.from_user is None:
|
||||
LOG.warning("ignored ephemeral message without from_user ephemeral_message_id=%s", transient_id)
|
||||
return None
|
||||
return await message.bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=ephemeral_prefix + text,
|
||||
receiver_user_id=message.from_user.id,
|
||||
reply_parameters=ReplyParameters(ephemeral_message_id=transient_id),
|
||||
)
|
||||
|
||||
|
||||
def build_dispatcher(args: argparse.Namespace) -> Dispatcher:
|
||||
router = Router(name="telesrv-aiogramecho")
|
||||
|
||||
|
|
@ -170,7 +201,17 @@ def build_dispatcher(args: argparse.Namespace) -> Dispatcher:
|
|||
|
||||
@router.message(Command("ping"))
|
||||
async def ping(message: Message) -> None:
|
||||
await message.answer(args.prefix + (message.text or ""))
|
||||
await send_echo(message, args.prefix, args.ephemeral_prefix)
|
||||
|
||||
@router.message(Command("private"))
|
||||
async def private(message: Message) -> None:
|
||||
sent = await send_echo(message, args.prefix, args.ephemeral_prefix)
|
||||
LOG.info(
|
||||
"echoed ephemeral chat_id=%s ephemeral_message_id=%s sent_ephemeral_message_id=%s",
|
||||
message.chat.id,
|
||||
ephemeral_message_id(message),
|
||||
ephemeral_message_id(sent) if sent is not None else None,
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("aiogram-"))
|
||||
async def callback(query: CallbackQuery) -> None:
|
||||
|
|
@ -185,7 +226,7 @@ def build_dispatcher(args: argparse.Namespace) -> Dispatcher:
|
|||
|
||||
@router.message(F.text)
|
||||
async def echo(message: Message) -> None:
|
||||
await message.answer(args.prefix + (message.text or ""))
|
||||
await send_echo(message, args.prefix, args.ephemeral_prefix)
|
||||
LOG.info("echoed chat_id=%s message_id=%s", message.chat.id, message.message_id)
|
||||
|
||||
dispatcher = Dispatcher()
|
||||
|
|
|
|||
63
cmd/bots/aiogramecho/test_echo.py
Normal file
63
cmd/bots/aiogramecho/test_echo.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import echo
|
||||
|
||||
|
||||
class FakeBot:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def send_message(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
return SimpleNamespace(message_id=0, ephemeral_message_id=88)
|
||||
|
||||
|
||||
class FakeMessage:
|
||||
def __init__(self, *, text="hello", ephemeral_id=None):
|
||||
self.text = text
|
||||
self.caption = None
|
||||
self.chat = SimpleNamespace(id=-1000000000002)
|
||||
self.from_user = SimpleNamespace(id=1780243200)
|
||||
self.ephemeral_message_id = ephemeral_id
|
||||
self.model_extra = {}
|
||||
self.bot = FakeBot()
|
||||
self.answers = []
|
||||
|
||||
async def answer(self, text):
|
||||
self.answers.append(text)
|
||||
return SimpleNamespace(message_id=1, ephemeral_message_id=None)
|
||||
|
||||
|
||||
class EchoTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_ephemeral_echo_uses_receiver_and_transient_reply(self):
|
||||
message = FakeMessage(text="/private@TetrisBot", ephemeral_id=77)
|
||||
|
||||
await echo.send_echo(message, "aiogram echo: ", "ephemeral echo: ")
|
||||
|
||||
self.assertEqual(message.answers, [])
|
||||
self.assertEqual(len(message.bot.calls), 1)
|
||||
call = message.bot.calls[0]
|
||||
self.assertEqual(call["chat_id"], -1000000000002)
|
||||
self.assertEqual(call["text"], "ephemeral echo: /private@TetrisBot")
|
||||
self.assertEqual(call["receiver_user_id"], 1780243200)
|
||||
self.assertEqual(call["reply_parameters"].ephemeral_message_id, 77)
|
||||
|
||||
async def test_normal_echo_stays_on_standard_answer_path(self):
|
||||
message = FakeMessage()
|
||||
|
||||
await echo.send_echo(message, "aiogram echo: ", "ephemeral echo: ")
|
||||
|
||||
self.assertEqual(message.answers, ["aiogram echo: hello"])
|
||||
self.assertEqual(message.bot.calls, [])
|
||||
|
||||
def test_extra_field_fallback_and_invalid_values(self):
|
||||
message = FakeMessage()
|
||||
message.model_extra = {"ephemeral_message_id": 66}
|
||||
self.assertEqual(echo.ephemeral_message_id(message), 66)
|
||||
message.model_extra = {"ephemeral_message_id": True}
|
||||
self.assertIsNone(echo.ephemeral_message_id(message))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -17,6 +17,21 @@ In a group with BotFather privacy enabled, send a command such as:
|
|||
/ping hello from group
|
||||
```
|
||||
|
||||
## Ephemeral echo(Bot API 10.2)
|
||||
|
||||
先通过 `setMyCommands` 把 `private` 注册为 `is_ephemeral=true`,然后保持本示例轮询。
|
||||
在 TDesktop Layer 228 的群组里发送:
|
||||
|
||||
```text
|
||||
/private@你的Bot用户名
|
||||
```
|
||||
|
||||
本示例从 PTB 的 `Message.api_kwargs` 读取 `ephemeral_message_id`,并在 15 秒 action
|
||||
窗口内用 `receiver_user_id` 与 `reply_parameters.ephemeral_message_id` 回复
|
||||
`ephemeral echo: ...`。请求失败时不会降级成普通群消息。Alice 应看到两条带
|
||||
可见性提示的消息:自己发出的命令显示“Only visible to @Bot”,bot 回复显示
|
||||
“Only visible to you”;Bob 不应看到其中任何一条。
|
||||
|
||||
主动发送一条消息并退出:
|
||||
|
||||
```powershell
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ to the bot, mentions, or messages otherwise visible to bots. In a group, send:
|
|||
|
||||
/ping hello
|
||||
|
||||
For a command registered with ``is_ephemeral=true``, send:
|
||||
|
||||
/private@YourBotUsername
|
||||
|
||||
The incoming Bot API message has ``message_id=0`` and carries the transient
|
||||
identifier in ``api_kwargs`` until python-telegram-bot exposes the Bot API 10.2
|
||||
fields directly. The demo replies through the same ephemeral action window.
|
||||
|
||||
The same program can also send proactive messages:
|
||||
|
||||
python cmd/bots/ptbecho/echo.py \
|
||||
|
|
@ -70,6 +78,7 @@ def parse_args() -> argparse.Namespace:
|
|||
parser.add_argument("--base-url", default=os.getenv("TELESRV_BOT_API_BASE_URL", "http://127.0.0.1:8081/bot"))
|
||||
parser.add_argument("--base-file-url", default=os.getenv("TELESRV_BOT_API_BASE_FILE_URL", "http://127.0.0.1:8081/file/bot"))
|
||||
parser.add_argument("--prefix", default="echo: ")
|
||||
parser.add_argument("--ephemeral-prefix", default="ephemeral echo: ")
|
||||
parser.add_argument("--drop-pending", action="store_true", help="Drop pending updates before polling")
|
||||
parser.add_argument("--timeout", type=int, default=30, help="getUpdates long-poll timeout seconds")
|
||||
parser.add_argument(
|
||||
|
|
@ -117,6 +126,38 @@ async def ping(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||
await echo(update, context)
|
||||
|
||||
|
||||
def ephemeral_message_id(message: object) -> int | None:
|
||||
"""Read a Bot API 10.2 field without depending on a PTB release cycle."""
|
||||
raw = getattr(message, "ephemeral_message_id", None)
|
||||
if raw is None:
|
||||
raw = (getattr(message, "api_kwargs", None) or {}).get("ephemeral_message_id")
|
||||
if isinstance(raw, int) and not isinstance(raw, bool) and raw > 0:
|
||||
return raw
|
||||
return None
|
||||
|
||||
|
||||
async def send_echo(message: object, bot: Bot, prefix: str, ephemeral_prefix: str):
|
||||
text = getattr(message, "text", None) or getattr(message, "caption", None) or ""
|
||||
if not text:
|
||||
return None
|
||||
transient_id = ephemeral_message_id(message)
|
||||
if transient_id is None:
|
||||
return await message.reply_text(prefix + text)
|
||||
|
||||
sender = getattr(message, "from_user", None)
|
||||
if sender is None:
|
||||
LOG.warning("ignored ephemeral message without from_user ephemeral_message_id=%s", transient_id)
|
||||
return None
|
||||
return await bot.send_message(
|
||||
chat_id=message.chat_id,
|
||||
text=ephemeral_prefix + text,
|
||||
api_kwargs={
|
||||
"receiver_user_id": sender.id,
|
||||
"reply_parameters": {"ephemeral_message_id": transient_id},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if update.effective_message is None or update.effective_chat is None:
|
||||
return
|
||||
|
|
@ -124,13 +165,20 @@ async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||
if not text:
|
||||
return
|
||||
prefix = context.application.bot_data.get("prefix", "echo: ")
|
||||
sent = await update.effective_message.reply_text(prefix + text)
|
||||
ephemeral_prefix = context.application.bot_data.get("ephemeral_prefix", "ephemeral echo: ")
|
||||
transient_id = ephemeral_message_id(update.effective_message)
|
||||
sent = await send_echo(update.effective_message, context.bot, prefix, ephemeral_prefix)
|
||||
if sent is None:
|
||||
return
|
||||
LOG.info(
|
||||
"echoed update_id=%s chat_id=%s message_id=%s sent_message_id=%s text=%r",
|
||||
"echoed update_id=%s chat_id=%s message_id=%s ephemeral_message_id=%s "
|
||||
"sent_message_id=%s sent_ephemeral_message_id=%s text=%r",
|
||||
update.update_id,
|
||||
update.effective_chat.id,
|
||||
update.effective_message.message_id,
|
||||
transient_id,
|
||||
sent.message_id,
|
||||
ephemeral_message_id(sent),
|
||||
text,
|
||||
)
|
||||
|
||||
|
|
@ -239,6 +287,7 @@ def build_app(args: argparse.Namespace) -> Application:
|
|||
.build()
|
||||
)
|
||||
app.bot_data["prefix"] = args.prefix
|
||||
app.bot_data["ephemeral_prefix"] = args.ephemeral_prefix
|
||||
app.bot_data["base_url"] = args.base_url
|
||||
app.bot_data["send_chat_id"] = args.send_chat_id
|
||||
app.bot_data["send_text"] = args.send_text
|
||||
|
|
@ -247,6 +296,7 @@ def build_app(args: argparse.Namespace) -> Application:
|
|||
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("private", echo))
|
||||
app.add_handler(CommandHandler("buttons", buttons))
|
||||
app.add_handler(CallbackQueryHandler(callback))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
|
||||
|
|
|
|||
70
cmd/bots/ptbecho/test_echo.py
Normal file
70
cmd/bots/ptbecho/test_echo.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import echo
|
||||
|
||||
|
||||
class FakeMessage:
|
||||
def __init__(self, *, text="hello", ephemeral_id=None):
|
||||
self.text = text
|
||||
self.caption = None
|
||||
self.chat_id = -1000000000002
|
||||
self.from_user = SimpleNamespace(id=1780243200)
|
||||
self.api_kwargs = {}
|
||||
if ephemeral_id is not None:
|
||||
self.api_kwargs["ephemeral_message_id"] = ephemeral_id
|
||||
self.replies = []
|
||||
|
||||
async def reply_text(self, text):
|
||||
self.replies.append(text)
|
||||
return SimpleNamespace(message_id=1, api_kwargs={})
|
||||
|
||||
|
||||
class FakeBot:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def send_message(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
return SimpleNamespace(
|
||||
message_id=0,
|
||||
api_kwargs={"ephemeral_message_id": 88},
|
||||
)
|
||||
|
||||
|
||||
class EchoTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_ephemeral_echo_uses_receiver_and_transient_reply(self):
|
||||
message = FakeMessage(text="/private@TetrisBot", ephemeral_id=77)
|
||||
bot = FakeBot()
|
||||
|
||||
await echo.send_echo(message, bot, "echo: ", "ephemeral echo: ")
|
||||
|
||||
self.assertEqual(message.replies, [])
|
||||
self.assertEqual(
|
||||
bot.calls,
|
||||
[{
|
||||
"chat_id": -1000000000002,
|
||||
"text": "ephemeral echo: /private@TetrisBot",
|
||||
"api_kwargs": {
|
||||
"receiver_user_id": 1780243200,
|
||||
"reply_parameters": {"ephemeral_message_id": 77},
|
||||
},
|
||||
}],
|
||||
)
|
||||
|
||||
async def test_normal_echo_stays_on_standard_reply_path(self):
|
||||
message = FakeMessage()
|
||||
bot = FakeBot()
|
||||
|
||||
await echo.send_echo(message, bot, "echo: ", "ephemeral echo: ")
|
||||
|
||||
self.assertEqual(message.replies, ["echo: hello"])
|
||||
self.assertEqual(bot.calls, [])
|
||||
|
||||
def test_invalid_ephemeral_id_is_not_accepted(self):
|
||||
self.assertIsNone(echo.ephemeral_message_id(FakeMessage(ephemeral_id=0)))
|
||||
self.assertIsNone(echo.ephemeral_message_id(FakeMessage(ephemeral_id=True)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue