mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-21 16:05:19 +08:00
fix(telegram): resolve real-client event and media delivery regressions
This commit is contained in:
@@ -335,6 +335,8 @@ class QueryEntryAdapter:
|
||||
|
||||
# Handle launcher_id
|
||||
launcher_id = getattr(query, 'launcher_id', None)
|
||||
if launcher_id is not None:
|
||||
launcher_id = str(launcher_id)
|
||||
|
||||
# Build session_id from launcher info if available
|
||||
session_id = None
|
||||
|
||||
@@ -149,7 +149,7 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
application.add_handler(
|
||||
MessageReactionHandler(
|
||||
telegram_callback,
|
||||
MessageReactionHandler.MESSAGE_REACTION,
|
||||
message_reaction_types=MessageReactionHandler.MESSAGE_REACTION,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -213,6 +213,7 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
chat_id: int | str = int(chat_id_str) if chat_id_str.lstrip('-').isdigit() else chat_id_str
|
||||
message_thread_id = int(thread_id_str) if thread_id_str and thread_id_str.isdigit() else None
|
||||
|
||||
sent_messages = []
|
||||
for component in components:
|
||||
component_type = component.get('type')
|
||||
args = {'chat_id': chat_id}
|
||||
@@ -225,20 +226,24 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
text = telegramify_markdown.markdownify(content=text)
|
||||
args['parse_mode'] = 'MarkdownV2'
|
||||
args['text'] = text
|
||||
await self.bot.send_message(**args)
|
||||
sent_messages.append(await self.bot.send_message(**args))
|
||||
elif component_type == 'photo':
|
||||
photo = component.get('photo')
|
||||
if photo is None:
|
||||
continue
|
||||
args['photo'] = telegram.InputFile(photo)
|
||||
await self.bot.send_photo(**args)
|
||||
sent_messages.append(await self._send_photo_or_document(photo, args))
|
||||
elif component_type == 'document':
|
||||
doc = component.get('document')
|
||||
if doc is None:
|
||||
continue
|
||||
filename = component.get('filename', 'file')
|
||||
args['document'] = telegram.InputFile(doc, filename=filename)
|
||||
await self.bot.send_document(**args)
|
||||
sent_messages.append(await self.bot.send_document(**args))
|
||||
|
||||
return platform_events.MessageResult(
|
||||
message_id=sent_messages[-1].message_id if sent_messages else None,
|
||||
raw={'message_ids': [sent.message_id for sent in sent_messages]},
|
||||
)
|
||||
|
||||
async def reply_message(
|
||||
self,
|
||||
@@ -249,6 +254,7 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
assert isinstance(message_source.source_platform_object, Update)
|
||||
components = await TelegramMessageConverter.yiri2target(message, self.bot)
|
||||
|
||||
sent_messages = []
|
||||
for component in components:
|
||||
component_type = component.get('type')
|
||||
args = {
|
||||
@@ -271,20 +277,34 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
if self.config['markdown_card'] is True:
|
||||
args['parse_mode'] = 'MarkdownV2'
|
||||
args['text'] = content
|
||||
await self.bot.send_message(**args)
|
||||
sent_messages.append(await self.bot.send_message(**args))
|
||||
elif component_type == 'photo':
|
||||
photo = component.get('photo')
|
||||
if photo is None:
|
||||
continue
|
||||
args['photo'] = telegram.InputFile(photo)
|
||||
await self.bot.send_photo(**args)
|
||||
sent_messages.append(await self._send_photo_or_document(photo, args))
|
||||
elif component_type == 'document':
|
||||
doc = component.get('document')
|
||||
if doc is None:
|
||||
continue
|
||||
filename = component.get('filename', 'file')
|
||||
args['document'] = telegram.InputFile(doc, filename=filename)
|
||||
await self.bot.send_document(**args)
|
||||
sent_messages.append(await self.bot.send_document(**args))
|
||||
|
||||
return platform_events.MessageResult(
|
||||
message_id=sent_messages[-1].message_id if sent_messages else None,
|
||||
raw={'message_ids': [sent.message_id for sent in sent_messages]},
|
||||
)
|
||||
|
||||
async def _send_photo_or_document(self, photo: bytes, args: dict):
|
||||
try:
|
||||
return await self.bot.send_photo(photo=telegram.InputFile(photo), **args)
|
||||
except telegram.error.BadRequest as exc:
|
||||
if 'image_process_failed' not in str(exc).lower() and 'photo_invalid_dimensions' not in str(exc).lower():
|
||||
raise
|
||||
# Preserve rejected image bytes without retrying already-sent parts.
|
||||
await self.logger.warning('Telegram rejected photo encoding; sending the image as a document')
|
||||
return await self.bot.send_document(document=telegram.InputFile(photo, filename='image'), **args)
|
||||
|
||||
# ---- Streaming Output (preserving original logic) ----
|
||||
|
||||
@@ -303,6 +323,14 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
|
||||
_MAX_STREAM_STATES: typing.ClassVar[int] = 1000
|
||||
|
||||
async def _delete_group_stream_message(self, chat_mode: str, chat_id: int, stream_id: int | None):
|
||||
if chat_mode == 'private' or stream_id is None:
|
||||
return
|
||||
try:
|
||||
await self.bot.delete_message(chat_id=chat_id, message_id=stream_id)
|
||||
except telegram.error.TelegramError:
|
||||
pass
|
||||
|
||||
def _cap_stream_states(self) -> None:
|
||||
while len(self.msg_stream_id) > self._MAX_STREAM_STATES:
|
||||
self.msg_stream_id.pop(next(iter(self.msg_stream_id)), None)
|
||||
@@ -350,6 +378,15 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
if message_id not in self.msg_stream_id:
|
||||
return
|
||||
|
||||
# Media cannot be edited into a text stream. Deliver terminal attachments
|
||||
# through the regular reply path, including attachment-only completions.
|
||||
if is_final and not bot_message.tool_calls:
|
||||
attachments = platform_message.MessageChain(
|
||||
[part for part in message if isinstance(part, (platform_message.Image, platform_message.File))]
|
||||
)
|
||||
if attachments:
|
||||
await self.reply_message(message_source, attachments, quote_origin=quote_origin)
|
||||
|
||||
stream_state = self.msg_stream_id[message_id]
|
||||
chat_mode, stream_id = stream_state[:2]
|
||||
has_visible_content = len(stream_state) > 2 and stream_state[2]
|
||||
@@ -357,6 +394,8 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
|
||||
if not components or components[0]['type'] != 'text':
|
||||
if is_final and bot_message.tool_calls is None:
|
||||
if not has_visible_content:
|
||||
await self._delete_group_stream_message(chat_mode, chat_id, stream_id)
|
||||
self.msg_stream_id.pop(message_id)
|
||||
return
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Converts all Telegram Update types to unified EBA events, not just messages.
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
import telegram
|
||||
from telegram import Update
|
||||
@@ -17,6 +18,13 @@ import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platf
|
||||
from langbot.pkg.platform.adapters.telegram.message_converter import TelegramMessageConverter
|
||||
|
||||
|
||||
def _restriction_duration(until_date: datetime.datetime | None, event_date: datetime.datetime) -> int | None:
|
||||
"""Telegram uses Unix epoch zero for an indefinite restriction."""
|
||||
if until_date is None or until_date.timestamp() <= 0:
|
||||
return None
|
||||
return max(0, int(until_date.timestamp() - event_date.timestamp()))
|
||||
|
||||
|
||||
def _make_user(tg_user: telegram.User) -> platform_entities.User:
|
||||
"""Convert a Telegram User to a unified User entity."""
|
||||
return platform_entities.User(
|
||||
@@ -229,9 +237,7 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
if new_status == 'restricted' and cm.new_chat_member:
|
||||
restricted = cm.new_chat_member
|
||||
if hasattr(restricted, 'can_send_messages') and not restricted.can_send_messages:
|
||||
duration = None
|
||||
if hasattr(restricted, 'until_date') and restricted.until_date:
|
||||
duration = int(restricted.until_date.timestamp() - time.time())
|
||||
duration = _restriction_duration(restricted.until_date, cm.date)
|
||||
return platform_events.MemberBannedEvent(
|
||||
type='group.member_banned',
|
||||
timestamp=cm.date.timestamp() if cm.date else time.time(),
|
||||
@@ -283,7 +289,7 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
)
|
||||
|
||||
# Bot removed from group
|
||||
if old_status in ('member', 'administrator', 'creator') and new_status in ('left', 'kicked'):
|
||||
if old_status in ('member', 'administrator', 'creator', 'restricted') and new_status in ('left', 'kicked'):
|
||||
return platform_events.BotRemovedFromGroupEvent(
|
||||
type='bot.removed_from_group',
|
||||
timestamp=mcm.date.timestamp() if mcm.date else time.time(),
|
||||
@@ -297,9 +303,7 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
if new_status == 'restricted' and mcm.new_chat_member:
|
||||
restricted = mcm.new_chat_member
|
||||
if hasattr(restricted, 'can_send_messages') and not restricted.can_send_messages:
|
||||
duration = None
|
||||
if hasattr(restricted, 'until_date') and restricted.until_date:
|
||||
duration = int(restricted.until_date.timestamp() - time.time())
|
||||
duration = _restriction_duration(restricted.until_date, mcm.date)
|
||||
return platform_events.BotMutedEvent(
|
||||
type='bot.muted',
|
||||
timestamp=mcm.date.timestamp() if mcm.date else time.time(),
|
||||
@@ -310,7 +314,11 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
source_platform_object=update,
|
||||
)
|
||||
|
||||
if old_status == 'restricted' and new_status in ('member', 'administrator') and mcm.new_chat_member:
|
||||
was_muted = old_status == 'restricted' and not mcm.old_chat_member.can_send_messages
|
||||
can_send = new_status in ('member', 'administrator') or (
|
||||
new_status == 'restricted' and mcm.new_chat_member.can_send_messages
|
||||
)
|
||||
if was_muted and can_send:
|
||||
return platform_events.BotUnmutedEvent(
|
||||
type='bot.unmuted',
|
||||
timestamp=mcm.date.timestamp() if mcm.date else time.time(),
|
||||
@@ -341,14 +349,18 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
reaction = update.message_reaction
|
||||
chat = reaction.chat
|
||||
|
||||
# Extract newly added emojis
|
||||
new_emojis = []
|
||||
if reaction.new_reaction:
|
||||
for r in reaction.new_reaction:
|
||||
if hasattr(r, 'emoji'):
|
||||
new_emojis.append(r.emoji)
|
||||
elif hasattr(r, 'custom_emoji_id'):
|
||||
new_emojis.append(str(r.custom_emoji_id))
|
||||
def emojis(items):
|
||||
return [
|
||||
str(r.emoji if hasattr(r, 'emoji') else r.custom_emoji_id)
|
||||
for r in items
|
||||
if hasattr(r, 'emoji') or hasattr(r, 'custom_emoji_id')
|
||||
]
|
||||
|
||||
old_emojis = emojis(reaction.old_reaction or [])
|
||||
new_emojis = emojis(reaction.new_reaction or [])
|
||||
added = [emoji for emoji in new_emojis if emoji not in old_emojis]
|
||||
removed = [emoji for emoji in old_emojis if emoji not in new_emojis]
|
||||
changed = added or removed
|
||||
|
||||
user = platform_entities.User(id='')
|
||||
if reaction.user:
|
||||
@@ -363,8 +375,8 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
adapter_name='telegram-omni',
|
||||
message_id=reaction.message_id,
|
||||
user=user,
|
||||
reaction=new_emojis[0] if new_emojis else '',
|
||||
is_add=len(new_emojis) > 0,
|
||||
reaction=changed[0] if changed else '',
|
||||
is_add=bool(added),
|
||||
chat_type=ct,
|
||||
chat_id=chat.id,
|
||||
group=group,
|
||||
|
||||
Reference in New Issue
Block a user