Skip to content
Open
Show file tree
Hide file tree
Changes from all 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