fix(telegram): accept @username chat_id in delivery paths (#13206)

TELEGRAM_HOME_CHANNEL set to an @username (not a numeric chat ID) crashed
all webhook/cron->Telegram home-channel delivery with 'ValueError: invalid
literal for int()'. The Telegram Bot API accepts both a numeric chat_id and
an @username string; Hermes was force-coercing every chat_id with int().

Add normalize_telegram_chat_id() (returns int for numeric values, passes
@username strings through) and apply it at the Bot API send/edit sites in
the Telegram adapter and the send_message tool. Username targets are now
recognized as explicit targets in _parse_target_ref.

Reapplies the approach from #13274 (season179), whose branch predated the
gateway/platforms/telegram.py -> plugins/platforms/telegram/adapter.py
relocation. Dupes: #13535 (Tranquil-Flow), #37572 (chewkaah).

Co-authored-by: season179 <season.saw@gmail.com>
This commit is contained in:
teknium1 2026-06-27 03:47:46 -07:00 committed by Teknium
parent f2ca3e3d84
commit ab1f9b94c5
4 changed files with 321 additions and 39 deletions

View file

@ -83,6 +83,9 @@ from gateway.platforms.base import (
_TEXT_INJECT_EXTENSIONS,
utf16_len,
)
from plugins.platforms.telegram.telegram_ids import (
normalize_telegram_chat_id,
)
from plugins.platforms.telegram.telegram_network import (
TelegramFallbackTransport,
discover_fallback_ips,
@ -1243,7 +1246,7 @@ class TelegramAdapter(BasePlatformAdapter):
reply_to_id, thread_kwargs = routing
payload: Dict[str, Any] = {
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"rich_message": self._rich_message_payload(content),
}
# Only forward non-None routing keys: when direct_messages_topic_id is
@ -1350,7 +1353,7 @@ class TelegramAdapter(BasePlatformAdapter):
semantics (the message may already be edited; do NOT legacy-resend)
"""
payload: Dict[str, Any] = {
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"message_id": int(message_id),
"rich_message": self._rich_message_payload(content),
}
@ -1443,7 +1446,7 @@ class TelegramAdapter(BasePlatformAdapter):
latches ``_rich_draft_disabled`` so later frames skip the rich attempt.
"""
payload: Dict[str, Any] = {
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"draft_id": int(draft_id),
"rich_message": self._rich_message_payload(content),
}
@ -2079,7 +2082,7 @@ class TelegramAdapter(BasePlatformAdapter):
icon_emoji = topic_conf.get("icon_custom_emoji_id")
thread_id = await self._create_dm_topic(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
name=topic_name,
icon_color=icon_color,
icon_custom_emoji_id=icon_emoji,
@ -2098,7 +2101,7 @@ class TelegramAdapter(BasePlatformAdapter):
# Empty topics are hidden by the client UI until they contain a message.
try:
await self._bot.send_message(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_thread_id=thread_id,
text=f"\U0001f4cc {topic_name}",
)
@ -2720,7 +2723,7 @@ class TelegramAdapter(BasePlatformAdapter):
# Try Markdown first, fall back to plain text if it fails
try:
msg = await self._bot.send_message(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
text=chunk,
parse_mode=ParseMode.MARKDOWN_V2,
reply_to_message_id=reply_to_id,
@ -2734,7 +2737,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.warning("[%s] MarkdownV2 parse failed, falling back to plain text: %s", self.name, md_error)
plain_chunk = _strip_mdv2(chunk)
msg = await self._bot.send_message(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
text=plain_chunk,
parse_mode=None,
reply_to_message_id=reply_to_id,
@ -3002,7 +3005,7 @@ class TelegramAdapter(BasePlatformAdapter):
try:
if not finalize:
await self._bot.edit_message_text(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
text=content,
)
@ -3011,7 +3014,7 @@ class TelegramAdapter(BasePlatformAdapter):
formatted = self.format_message(content)
try:
await self._bot.edit_message_text(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
text=formatted,
parse_mode=ParseMode.MARKDOWN_V2,
@ -3028,7 +3031,7 @@ class TelegramAdapter(BasePlatformAdapter):
)
_plain = _strip_mdv2(content) if content else content
await self._bot.edit_message_text(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
text=_plain,
)
@ -3053,7 +3056,7 @@ class TelegramAdapter(BasePlatformAdapter):
# Mid-stream: truncate and retry instead of splitting (#48648).
truncated = self._truncate_stream_overflow_preview(content)
await self._bot.edit_message_text(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
text=truncated,
)
@ -3073,7 +3076,7 @@ class TelegramAdapter(BasePlatformAdapter):
await asyncio.sleep(wait)
try:
await self._bot.edit_message_text(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
text=content,
)
@ -3177,7 +3180,7 @@ class TelegramAdapter(BasePlatformAdapter):
)
try:
await self._bot.edit_message_text(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
text=formatted,
parse_mode=ParseMode.MARKDOWN_V2,
@ -3190,13 +3193,13 @@ class TelegramAdapter(BasePlatformAdapter):
self.name, fmt_err,
)
await self._bot.edit_message_text(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
text=_strip_mdv2(first_chunk),
)
else:
await self._bot.edit_message_text(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
text=first_chunk,
)
@ -3245,7 +3248,7 @@ class TelegramAdapter(BasePlatformAdapter):
# literally); streaming previews stay raw.
text = _strip_mdv2(chunk) if finalize else chunk
sent_msg = await self._bot.send_message(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
text=text,
parse_mode=ParseMode.MARKDOWN_V2 if use_markdown else None,
reply_to_message_id=reply_to_id,
@ -3268,7 +3271,7 @@ class TelegramAdapter(BasePlatformAdapter):
)
try:
sent_msg = await self._bot.send_message(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
text=_strip_mdv2(chunk) if finalize else chunk,
**retry_thread_kwargs,
**self._link_preview_kwargs(),
@ -3351,7 +3354,7 @@ class TelegramAdapter(BasePlatformAdapter):
return False
try:
await self._bot.delete_message(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
)
return True
@ -3433,7 +3436,7 @@ class TelegramAdapter(BasePlatformAdapter):
# kills draft streaming for the whole response.
for use_markdown in (True, False):
kwargs: Dict[str, Any] = {
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"draft_id": int(draft_id),
"text": self.format_message(text) if use_markdown else text,
}
@ -3533,7 +3536,7 @@ class TelegramAdapter(BasePlatformAdapter):
thread_id = self._metadata_thread_id(metadata)
reply_to_id = self._reply_to_message_id_for_send(None, metadata, reply_to_mode=self._reply_to_mode)
msg = await self._send_message_with_thread_fallback(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
text=text,
parse_mode=ParseMode.MARKDOWN_V2,
reply_markup=keyboard,
@ -3596,7 +3599,7 @@ class TelegramAdapter(BasePlatformAdapter):
])
kwargs: Dict[str, Any] = {
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"text": text,
"parse_mode": ParseMode.HTML,
"reply_markup": keyboard,
@ -3647,7 +3650,7 @@ class TelegramAdapter(BasePlatformAdapter):
thread_id = self._metadata_thread_id(metadata)
kwargs: Dict[str, Any] = {
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"text": preview,
"parse_mode": ParseMode.MARKDOWN_V2,
"reply_markup": keyboard,
@ -3711,7 +3714,7 @@ class TelegramAdapter(BasePlatformAdapter):
text += f"\n\n{option_lines}"
kwargs: Dict[str, Any] = {
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"text": text,
"parse_mode": ParseMode.HTML,
**self._link_preview_kwargs(),
@ -3795,7 +3798,7 @@ class TelegramAdapter(BasePlatformAdapter):
thread_id = metadata.get("thread_id") if metadata else None
reply_to_id = self._reply_to_message_id_for_send(None, metadata, reply_to_mode=self._reply_to_mode)
msg = await self._send_message_with_thread_fallback(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
text=text,
parse_mode=ParseMode.MARKDOWN_V2,
reply_markup=keyboard,
@ -4755,7 +4758,7 @@ class TelegramAdapter(BasePlatformAdapter):
msg = await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_voice,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"voice": audio_file,
"caption": caption[:1024] if caption else None,
"reply_to_message_id": reply_to_id,
@ -4781,7 +4784,7 @@ class TelegramAdapter(BasePlatformAdapter):
msg = await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_audio,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"audio": audio_file,
"caption": caption[:1024] if caption else None,
"reply_to_message_id": reply_to_id,
@ -4920,7 +4923,7 @@ class TelegramAdapter(BasePlatformAdapter):
await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_media_group,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"media": media,
"reply_to_message_id": reply_to_id,
**thread_kwargs,
@ -4978,7 +4981,7 @@ class TelegramAdapter(BasePlatformAdapter):
msg = await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_photo,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"photo": image_file,
"caption": caption[:1024] if caption else None,
"reply_to_message_id": reply_to_id,
@ -5074,7 +5077,7 @@ class TelegramAdapter(BasePlatformAdapter):
msg = await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_document,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"document": f,
"filename": display_name,
"caption": caption[:1024] if caption else None,
@ -5122,7 +5125,7 @@ class TelegramAdapter(BasePlatformAdapter):
msg = await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_video,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"video": f,
"caption": caption[:1024] if caption else None,
"reply_to_message_id": reply_to_id,
@ -5174,7 +5177,7 @@ class TelegramAdapter(BasePlatformAdapter):
msg = await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_photo,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"photo": image_url,
"caption": caption[:1024] if caption else None,
"reply_to_message_id": reply_to_id,
@ -5211,7 +5214,7 @@ class TelegramAdapter(BasePlatformAdapter):
msg = await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_photo,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"photo": image_data,
"caption": caption[:1024] if caption else None,
"reply_to_message_id": reply_to_id,
@ -5258,7 +5261,7 @@ class TelegramAdapter(BasePlatformAdapter):
msg = await self._send_with_dm_topic_reply_anchor_retry(
self._bot.send_animation,
{
"chat_id": int(chat_id),
"chat_id": normalize_telegram_chat_id(chat_id),
"animation": animation_url,
"caption": caption[:1024] if caption else None,
"reply_to_message_id": reply_to_id,
@ -5290,7 +5293,7 @@ class TelegramAdapter(BasePlatformAdapter):
_is_dm_topic = bool(metadata and metadata.get("telegram_dm_topic_reply_fallback"))
message_thread_id = self._message_thread_id_for_typing(_typing_thread)
await self._bot.send_chat_action(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
action="typing",
message_thread_id=message_thread_id,
)
@ -5301,7 +5304,7 @@ class TelegramAdapter(BasePlatformAdapter):
if _is_dm_topic and message_thread_id is not None:
try:
await self._bot.send_chat_action(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
action="typing",
)
return
@ -5321,7 +5324,7 @@ class TelegramAdapter(BasePlatformAdapter):
return {"name": "Unknown", "type": "dm"}
try:
chat = await self._bot.get_chat(int(chat_id))
chat = await self._bot.get_chat(normalize_telegram_chat_id(chat_id))
chat_type = "dm"
if chat.type == ChatType.GROUP:
@ -7198,7 +7201,7 @@ class TelegramAdapter(BasePlatformAdapter):
return False
try:
await self._bot.set_message_reaction(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
reaction=emoji,
)
@ -7219,7 +7222,7 @@ class TelegramAdapter(BasePlatformAdapter):
return False
try:
await self._bot.set_message_reaction(
chat_id=int(chat_id),
chat_id=normalize_telegram_chat_id(chat_id),
message_id=int(message_id),
reaction=None,
)

View file

@ -0,0 +1,51 @@
"""Helpers for Telegram Bot API chat identifiers.
Telegram's Bot API accepts a ``chat_id`` in two forms: a numeric ID (an int,
e.g. ``123456789`` for a DM or ``-1001234567890`` for a channel/supergroup) or
an ``@username`` string for public channels and groups. Hermes historically
coerced every ``chat_id`` with ``int()``, which crashes on the username form
(``ValueError: invalid literal for int()``). Normalizing here lets numeric IDs
pass through as ints while usernames pass through unchanged both are valid
values for the Bot API.
"""
from __future__ import annotations
import re
from typing import Any, Union
# Telegram usernames are 5-32 chars: letters, digits, underscores, with a
# leading "@". (Telegram also permits 4-char usernames for some legacy/official
# accounts, but the 5-32 public rule is the safe lower bound for routing.)
_TELEGRAM_USERNAME_RE = re.compile(r"@[A-Za-z0-9_]{4,32}")
def normalize_telegram_chat_id(chat_id: Any) -> Union[int, str]:
"""Return a Bot API-compatible chat_id.
Numeric values (incl. negative channel IDs) are returned as ``int``; any
non-numeric value (e.g. an ``@username``) is returned as a stripped string.
Telegram's Bot API accepts both, so this never raises on a username the way
a bare ``int(chat_id)`` would.
"""
chat_id_str = str(chat_id).strip()
try:
return int(chat_id_str)
except (TypeError, ValueError):
return chat_id_str
def telegram_chat_id_key(chat_id: Any) -> str:
"""Stable string key for a chat_id (for dict keys / persisted state)."""
return str(normalize_telegram_chat_id(chat_id))
def looks_like_telegram_username(chat_id: Any) -> bool:
"""True when the value is an ``@username``-format Telegram chat identifier."""
return bool(_TELEGRAM_USERNAME_RE.fullmatch(str(chat_id).strip()))
def parse_telegram_username_target(target_ref: Any) -> Union[str, None]:
"""Return the value when it is an ``@username`` target, else ``None``."""
value = str(target_ref).strip()
return value if looks_like_telegram_username(value) else None

View file

@ -0,0 +1,215 @@
"""Tests for Telegram username (non-numeric) chat_id handling (#13206).
When ``TELEGRAM_HOME_CHANNEL`` is an ``@username`` rather than a numeric chat
ID, webhook/cron deliveries that fall back to the home channel used to crash
with ``ValueError: invalid literal for int()`` because the adapter coerced
every chat_id with ``int()``. Telegram's Bot API accepts both forms, so the
adapter now normalizes instead of force-casting.
"""
import sys
import types
from types import SimpleNamespace
import pytest
from gateway.config import PlatformConfig, Platform
from plugins.platforms.telegram.telegram_ids import (
looks_like_telegram_username,
normalize_telegram_chat_id,
parse_telegram_username_target,
telegram_chat_id_key,
)
# ---------------------------------------------------------------------------
# Helper-level behavior (no telegram import needed)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"value,expected",
[
("123456789", 123456789), # positive numeric DM id
("-1001234567890", -1001234567890), # negative channel/supergroup id
(123456789, 123456789), # already int
(" 42 ", 42), # surrounding whitespace
("@some_user", "@some_user"), # username passes through as str
("@a_channel", "@a_channel"),
("not_numeric", "not_numeric"), # any other non-numeric string
],
)
def test_normalize_returns_int_or_passthrough_string(value, expected):
assert normalize_telegram_chat_id(value) == expected
def test_normalize_never_raises_on_username():
# A bare int() here would raise ValueError; normalize must not.
assert normalize_telegram_chat_id("@some_user") == "@some_user"
def test_numeric_normalizes_to_int_type():
assert isinstance(normalize_telegram_chat_id("123"), int)
def test_username_normalizes_to_str_type():
assert isinstance(normalize_telegram_chat_id("@some_user"), str)
@pytest.mark.parametrize(
"value,expected",
[
("@some_user", True),
("@a_chan", True),
("@abcd", True), # 4-char minimum
("@abc", False), # too short
("123456", False), # numeric
("-100123", False),
("@with space", False),
("plain", False),
],
)
def test_looks_like_username(value, expected):
assert looks_like_telegram_username(value) is expected
def test_parse_username_target():
assert parse_telegram_username_target("@some_user") == "@some_user"
assert parse_telegram_username_target(" @some_user ") == "@some_user"
assert parse_telegram_username_target("123456") is None
assert parse_telegram_username_target("-1001234567890") is None
def test_chat_id_key_is_stable_string():
assert telegram_chat_id_key("123") == "123"
assert telegram_chat_id_key(123) == "123"
assert telegram_chat_id_key("@some_user") == "@some_user"
# ---------------------------------------------------------------------------
# Fake telegram module tree (mirrors test_telegram_thread_fallback.py)
# ---------------------------------------------------------------------------
class FakeNetworkError(Exception):
pass
class FakeBadRequest(FakeNetworkError):
pass
class FakeTimedOut(FakeNetworkError):
pass
class _FakeInlineKeyboardButton:
def __init__(self, text, callback_data=None, **kwargs):
self.text = text
self.callback_data = callback_data
class _FakeInlineKeyboardMarkup:
def __init__(self, inline_keyboard):
self.inline_keyboard = inline_keyboard
_fake_telegram = types.ModuleType("telegram")
_fake_telegram.Update = object
_fake_telegram.Bot = object
_fake_telegram.Message = object
_fake_telegram.InlineKeyboardButton = _FakeInlineKeyboardButton
_fake_telegram.InlineKeyboardMarkup = _FakeInlineKeyboardMarkup
_fake_telegram.InputMediaPhoto = object
_fake_telegram_error = types.ModuleType("telegram.error")
_fake_telegram_error.NetworkError = FakeNetworkError
_fake_telegram_error.BadRequest = FakeBadRequest
_fake_telegram_error.TimedOut = FakeTimedOut
_fake_telegram.error = _fake_telegram_error
_fake_telegram_constants = types.ModuleType("telegram.constants")
_fake_telegram_constants.ParseMode = SimpleNamespace(
MARKDOWN_V2="MarkdownV2", MARKDOWN="Markdown", HTML="HTML",
)
_fake_telegram_constants.ChatType = SimpleNamespace(
GROUP="group", SUPERGROUP="supergroup", CHANNEL="channel", PRIVATE="private",
)
_fake_telegram.constants = _fake_telegram_constants
_fake_telegram_ext = types.ModuleType("telegram.ext")
for _attr in (
"Application", "CommandHandler", "CallbackQueryHandler",
"MessageHandler", "TypeHandler",
):
setattr(_fake_telegram_ext, _attr, object)
_fake_telegram_ext.ContextTypes = SimpleNamespace(DEFAULT_TYPE=object)
_fake_telegram_ext.filters = object
_fake_telegram_request = types.ModuleType("telegram.request")
_fake_telegram_request.HTTPXRequest = object
@pytest.fixture(autouse=True)
def _inject_fake_telegram(monkeypatch):
monkeypatch.setitem(sys.modules, "telegram", _fake_telegram)
monkeypatch.setitem(sys.modules, "telegram.error", _fake_telegram_error)
monkeypatch.setitem(sys.modules, "telegram.constants", _fake_telegram_constants)
monkeypatch.setitem(sys.modules, "telegram.ext", _fake_telegram_ext)
monkeypatch.setitem(sys.modules, "telegram.request", _fake_telegram_request)
def _make_adapter():
from plugins.platforms.telegram.adapter import TelegramAdapter
config = PlatformConfig(enabled=True, token="fake-token")
adapter = object.__new__(TelegramAdapter)
adapter.config = config
adapter._config = config
adapter._platform = Platform.TELEGRAM
adapter._connected = True
adapter._dm_topics = {}
adapter._dm_topics_config = []
adapter._reply_to_mode = "first"
adapter._fallback_ips = []
adapter._polling_conflict_count = 0
adapter._polling_network_error_count = 0
adapter._polling_error_callback_ref = None
adapter.platform = Platform.TELEGRAM
return adapter
# ---------------------------------------------------------------------------
# Adapter send path: username chat_id reaches the Bot API without int() crash
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_send_passes_username_chat_id_through_unchanged():
"""adapter.send(@username) calls the Bot API with the username string
rather than crashing on int() coercion (the #13206 regression)."""
adapter = _make_adapter()
call_log = []
async def mock_send_message(**kwargs):
call_log.append(dict(kwargs))
return SimpleNamespace(message_id=99)
adapter._bot = SimpleNamespace(send_message=mock_send_message)
result = await adapter.send(chat_id="@some_user", content="hello world")
assert result.success is True
assert call_log, "send_message was never called"
assert call_log[0]["chat_id"] == "@some_user"
@pytest.mark.asyncio
async def test_send_passes_numeric_chat_id_as_int():
adapter = _make_adapter()
call_log = []
async def mock_send_message(**kwargs):
call_log.append(dict(kwargs))
return SimpleNamespace(message_id=1)
adapter._bot = SimpleNamespace(send_message=mock_send_message)
result = await adapter.send(chat_id="123456789", content="hi")
assert result.success is True
assert call_log[0]["chat_id"] == 123456789
assert isinstance(call_log[0]["chat_id"], int)

View file

@ -478,6 +478,13 @@ def _parse_target_ref(platform_name: str, target_ref: str):
match = _TELEGRAM_TOPIC_TARGET_RE.fullmatch(target_ref)
if match:
return match.group(1), match.group(2), True
from plugins.platforms.telegram.telegram_ids import (
parse_telegram_username_target,
)
username = parse_telegram_username_target(target_ref)
if username:
return username, None, True
if platform_name == "feishu":
match = _FEISHU_TARGET_RE.fullmatch(target_ref)
if match:
@ -1034,7 +1041,13 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No
bot = Bot(token=token)
else:
bot = Bot(token=token)
int_chat_id = int(chat_id)
from plugins.platforms.telegram.telegram_ids import (
normalize_telegram_chat_id,
)
# Telegram accepts a numeric chat_id OR an @username string; normalize
# rather than force-int so username home channels don't crash (#13206).
int_chat_id = normalize_telegram_chat_id(chat_id)
media_files = media_files or []
thread_kwargs = {}
if thread_id is not None: