Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@
"telegram_command_auto_refresh": True,
"telegram_command_register_interval": 300,
"telegram_polling_restart_delay": 5.0,
"telegram_reply_to_message": "off",
},
"Discord": {
"id": "discord",
Expand Down Expand Up @@ -774,6 +775,13 @@
"type": "float",
"hint": "当轮询意外结束尝试自动重启时的延迟时间,理论上越短恢复越快,但过短(<0.1s)可能导致死循环针对 API 服务器的请求阻断。单位为秒。默认为 5s。",
},
"telegram_reply_to_message": {
"description": "Telegram 回复时引用消息",
"type": "string",
"options": ["off", "private", "group", "all"],
"labels": ["关闭", "仅私聊", "仅群聊", "私聊和群聊"],
"hint": "机器人回复时是否引用(reply to)触发该回复的原消息。off:关闭;private:仅私聊;group:仅群聊;all:私聊和群聊都引用。默认 off。",
},
"id": {
"description": "机器人名称",
"type": "string",
Expand Down
13 changes: 12 additions & 1 deletion astrbot/core/platform/sources/telegram/tg_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def __init__(
True,
)
self.last_command_hash = None
# 机器人回复时引用原消息的范围:off / private / group / all
self.reply_to_message = self.config.get("telegram_reply_to_message", "off")

self.scheduler = AsyncIOScheduler()
self.scheduler.add_listener(
Expand Down Expand Up @@ -484,9 +486,17 @@ def _apply_caption() -> None:
if not _from_user:
logger.warning("[Telegram] Received a message without a from_user.")
return None
# 同时读取显示名称和用户名,都不存在时回退为 "Unknown"
display_name = " ".join(
part for part in (_from_user.first_name, _from_user.last_name) if part
).strip()
if display_name and _from_user.username:
nickname = f"{display_name} (@{_from_user.username})"
else:
nickname = display_name or _from_user.username or "Unknown"
Comment on lines +490 to +496

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在单元测试中,_from_user 经常会被 Mock 为 MagicMock 对象。如果未显式为 Mock 对象设置 first_namelast_nameusername,直接访问它们会返回新的 MagicMock 实例(其布尔值为 True)。这会导致 " ".join(...) 尝试拼接 MagicMock 对象而抛出 TypeError,或者在 nickname 中泄露 Mock 对象的字符串表示。

建议使用 isinstance(part, str) 进行防御性类型检查,以确保代码在测试和各种边界情况下都能鲁棒运行。

        display_name = " ".join(
            part for part in (_from_user.first_name, _from_user.last_name) if isinstance(part, str)
        ).strip()
        username = _from_user.username if isinstance(_from_user.username, str) else None
        if display_name and username:
            nickname = f"{display_name} (@{username})"
        else:
            nickname = display_name or username or "Unknown"

message.sender = MessageMember(
str(_from_user.id),
_from_user.username or "Unknown",
nickname,
)
message.self_id = str(context.bot.username)
message.raw_message = update
Expand Down Expand Up @@ -751,6 +761,7 @@ def create_event(self, message: AstrBotMessage) -> TelegramPlatformEvent:
platform_meta=self.meta(),
session_id=message.session_id,
client=self.client,
reply_to_message=self.reply_to_message,
)

async def handle_msg(self, message: AstrBotMessage) -> None:
Expand Down
37 changes: 34 additions & 3 deletions astrbot/core/platform/sources/telegram/tg_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ def __init__(
platform_meta: PlatformMetadata,
session_id: str,
client: ExtBot,
reply_to_message: str = "off",
) -> None:
super().__init__(message_str, message_obj, platform_meta, session_id)
self.client = client
# 机器人回复时引用原消息的范围:off / private / group / all
self.reply_to_message = reply_to_message

@classmethod
def _split_message(cls, text: str) -> list[str]:
Expand Down Expand Up @@ -271,6 +274,7 @@ async def send_with_client(
client: ExtBot,
message: MessageChain,
user_name: str,
default_reply_to_message_id: str | None = None,
) -> None:
image_path = None

Expand All @@ -284,6 +288,11 @@ async def send_with_client(
if isinstance(i, At):
at_user_id = i.name

# 消息链未显式包含引用时,按配置默认引用触发该回复的原消息
if not has_reply and default_reply_to_message_id:
has_reply = True
reply_message_id = default_reply_to_message_id

at_flag = False
message_thread_id = None
if "#" in user_name:
Expand Down Expand Up @@ -341,10 +350,32 @@ async def send_with_client(
)

async def send(self, message: MessageChain) -> None:
if self.get_message_type() == MessageType.GROUP_MESSAGE:
await self.send_with_client(self.client, message, self.message_obj.group_id)
is_group = self.get_message_type() == MessageType.GROUP_MESSAGE
# 根据配置范围决定是否默认引用触发该回复的原消息。
# 当配置为 off 时,清除消息链中已有的 Reply 组件(来自全局管道
# 「回复时引用发送人消息」等设置),确保 Telegram 平台完全不引用。
should_reply = self.reply_to_message == "all" or self.reply_to_message == (
"group" if is_group else "private"
)
if not should_reply:
message.chain = [c for c in message.chain if not isinstance(c, Reply)]
default_reply_to_message_id = (
self.message_obj.message_id if should_reply else None
)
if is_group:
await self.send_with_client(
self.client,
message,
self.message_obj.group_id,
default_reply_to_message_id,
)
else:
await self.send_with_client(self.client, message, self.get_sender_id())
await self.send_with_client(
self.client,
message,
self.get_sender_id(),
default_reply_to_message_id,
)
await super().send(message)

async def react(self, emoji: str | None, big: bool = False) -> None:
Expand Down
10 changes: 10 additions & 0 deletions dashboard/src/i18n/locales/en-US/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,16 @@
"description": "Telegram Polling Restart Delay",
"hint": "Waiting time in seconds when the polling loop needs to restart after unexpected exits. Defaults to 5s."
},
"telegram_reply_to_message": {
"description": "Telegram Reply To Message",
"labels": [
"Disabled",
"Direct messages only",
"Group chats only",
"Direct and group chats"
],
"hint": "Whether the bot quotes (replies to) the message that triggered its response. off: disabled; private: direct messages only; group: group chats only; all: both direct messages and group chats. Defaults to off."
},
"telegram_token": {
"description": "Bot Token",
"hint": "If you are in mainland China, set a proxy or change api_base in Other Settings."
Expand Down
10 changes: 10 additions & 0 deletions dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,16 @@
"description": "Интервал автообновления команд Telegram",
"hint": "Интервал автоматического обновления команд Telegram в секундах."
},
"telegram_reply_to_message": {
"description": "Цитировать сообщение при ответе в Telegram",
"labels": [
"Отключено",
"Только личные сообщения",
"Только групповые чаты",
"Личные и групповые чаты"
],
"hint": "Цитирует ли бот (отвечает на) сообщение, вызвавшее его ответ. off: отключено; private: только личные сообщения; group: только групповые чаты; all: и личные сообщения, и групповые чаты. По умолчанию off."
},
"telegram_token": {
"description": "Токен бота",
"hint": "Если вы находитесь в материковом Китае, установите прокси или измените api_base в разделе «Другие настройки»."
Expand Down
10 changes: 10 additions & 0 deletions dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,16 @@
"description": "Telegram 轮询重启延迟",
"hint": "当轮询意外结束尝试自动重启时的延迟时间,单位为秒。默认为 5s。"
},
"telegram_reply_to_message": {
"description": "Telegram 回复时引用消息",
"labels": [
"关闭",
"仅私聊",
"仅群聊",
"私聊和群聊"
],
"hint": "机器人回复时是否引用触发该回复的原消息。off:关闭;private:仅私聊;group:仅群聊;all:私聊和群聊都引用。默认 off。"
},
"telegram_token": {
"description": "Bot Token",
"hint": "如果你的网络环境为中国大陆,请在 `其他配置` 处设置代理或更改 api_base。"
Expand Down