Merge pull request #77 from nathanschram/fix/entity-resolution-stringsession

fix: resolve entity cache miss for StringSession users
This commit is contained in:
Eugene Evstafev 2026-03-18 08:49:18 +00:00 committed by GitHub
commit 71cdefa55c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

174
main.py
View file

@ -341,6 +341,29 @@ def format_entity(entity) -> Dict[str, Any]:
return result return result
async def resolve_entity(identifier: Union[int, str]) -> Any:
"""Resolve entity with automatic cache warming on miss.
StringSession has no persistent entity cache. If get_entity() fails
because the cache is cold (ValueError on PeerUser lookup for group IDs),
warm the cache via get_dialogs() and retry.
"""
try:
return await client.get_entity(identifier)
except ValueError:
await client.get_dialogs()
return await client.get_entity(identifier)
async def resolve_input_entity(identifier: Union[int, str]) -> Any:
"""Like resolve_entity() but returns an InputPeer."""
try:
return await client.get_input_entity(identifier)
except ValueError:
await client.get_dialogs()
return await client.get_input_entity(identifier)
def format_message(message) -> Dict[str, Any]: def format_message(message) -> Dict[str, Any]:
"""Helper function to format message information consistently.""" """Helper function to format message information consistently."""
result = { result = {
@ -699,7 +722,7 @@ async def get_messages(chat_id: Union[int, str], page: int = 1, page_size: int =
page_size: Number of messages per page. page_size: Number of messages per page.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
offset = (page - 1) * page_size offset = (page - 1) * page_size
messages = await client.get_messages(entity, limit=page_size, add_offset=offset) messages = await client.get_messages(entity, limit=page_size, add_offset=offset)
if not messages: if not messages:
@ -740,7 +763,7 @@ async def send_message(
```pre```), or omit for plain text (no formatting). ```pre```), or omit for plain text (no formatting).
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.send_message(entity, message, parse_mode=parse_mode) await client.send_message(entity, message, parse_mode=parse_mode)
return "Message sent successfully." return "Message sent successfully."
except Exception as e: except Exception as e:
@ -761,7 +784,7 @@ async def subscribe_public_channel(channel: Union[int, str]) -> str:
Subscribe (join) to a public channel or supergroup by username or ID. Subscribe (join) to a public channel or supergroup by username or ID.
""" """
try: try:
entity = await client.get_entity(channel) entity = await resolve_entity(channel)
await client(functions.channels.JoinChannelRequest(channel=entity)) await client(functions.channels.JoinChannelRequest(channel=entity))
title = getattr(entity, "title", getattr(entity, "username", "Unknown channel")) title = getattr(entity, "title", getattr(entity, "username", "Unknown channel"))
return f"Subscribed to {title}." return f"Subscribed to {title}."
@ -791,7 +814,7 @@ async def list_inline_buttons(
else: else:
return "message_id must be an integer." return "message_id must be an integer."
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
target_message = None target_message = None
if message_id is not None: if message_id is not None:
@ -878,7 +901,7 @@ async def press_inline_button(
else: else:
return "button_index must be an integer." return "button_index must be an integer."
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
target_message = None target_message = None
if message_id is not None: if message_id is not None:
@ -1055,7 +1078,7 @@ async def list_messages(
to_date: Filter messages until this date (format: YYYY-MM-DD). to_date: Filter messages until this date (format: YYYY-MM-DD).
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
# Parse date filters if provided # Parse date filters if provided
from_date_obj = None from_date_obj = None
@ -1180,7 +1203,7 @@ async def list_topics(
search_query: Optional query to filter topics by title. search_query: Optional query to filter topics by title.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
if not isinstance(entity, Channel) or not getattr(entity, "megagroup", False): if not isinstance(entity, Channel) or not getattr(entity, "megagroup", False):
return "The specified chat is not a supergroup." return "The specified chat is not a supergroup."
@ -1320,7 +1343,7 @@ async def get_chat(chat_id: Union[int, str]) -> str:
chat_id: The ID or username of the chat. chat_id: The ID or username of the chat.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
result = [] result = []
result.append(f"ID: {entity.id}") result.append(f"ID: {entity.id}")
@ -1454,7 +1477,7 @@ async def get_contact_chats(contact_id: Union[int, str]) -> str:
""" """
try: try:
# Get contact info # Get contact info
contact = await client.get_entity(contact_id) contact = await resolve_entity(contact_id)
if not isinstance(contact, User): if not isinstance(contact, User):
return f"ID {contact_id} is not a user/contact." return f"ID {contact_id} is not a user/contact."
@ -1511,7 +1534,7 @@ async def get_last_interaction(contact_id: Union[int, str]) -> str:
""" """
try: try:
# Get contact info # Get contact info
contact = await client.get_entity(contact_id) contact = await resolve_entity(contact_id)
if not isinstance(contact, User): if not isinstance(contact, User):
return f"ID {contact_id} is not a user/contact." return f"ID {contact_id} is not a user/contact."
@ -1553,7 +1576,7 @@ async def get_message_context(
context_size: Number of messages before and after to include. context_size: Number of messages before and after to include.
""" """
try: try:
chat = await client.get_entity(chat_id) chat = await resolve_entity(chat_id)
# Get messages around the specified message # Get messages around the specified message
messages_before = await client.get_messages(chat, limit=context_size, max_id=message_id) messages_before = await client.get_messages(chat, limit=context_size, max_id=message_id)
central_message = await client.get_messages(chat, ids=message_id) central_message = await client.get_messages(chat, ids=message_id)
@ -1752,7 +1775,7 @@ async def delete_contact(user_id: Union[int, str]) -> str:
user_id: The Telegram user ID or username of the contact to delete. user_id: The Telegram user ID or username of the contact to delete.
""" """
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
await client(functions.contacts.DeleteContactsRequest(id=[user])) await client(functions.contacts.DeleteContactsRequest(id=[user]))
return f"Contact with user ID {user_id} deleted." return f"Contact with user ID {user_id} deleted."
except Exception as e: except Exception as e:
@ -1772,7 +1795,7 @@ async def block_user(user_id: Union[int, str]) -> str:
user_id: The Telegram user ID or username to block. user_id: The Telegram user ID or username to block.
""" """
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
await client(functions.contacts.BlockRequest(id=user)) await client(functions.contacts.BlockRequest(id=user))
return f"User {user_id} blocked." return f"User {user_id} blocked."
except Exception as e: except Exception as e:
@ -1792,7 +1815,7 @@ async def unblock_user(user_id: Union[int, str]) -> str:
user_id: The Telegram user ID or username to unblock. user_id: The Telegram user ID or username to unblock.
""" """
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
await client(functions.contacts.UnblockRequest(id=user)) await client(functions.contacts.UnblockRequest(id=user))
return f"User {user_id} unblocked." return f"User {user_id} unblocked."
except Exception as e: except Exception as e:
@ -1828,7 +1851,7 @@ async def create_group(title: str, user_ids: List[Union[int, str]]) -> str:
users = [] users = []
for user_id in user_ids: for user_id in user_ids:
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
users.append(user) users.append(user)
except Exception as e: except Exception as e:
logger.error(f"Failed to get entity for user ID {user_id}: {e}") logger.error(f"Failed to get entity for user ID {user_id}: {e}")
@ -1887,12 +1910,12 @@ async def invite_to_group(group_id: Union[int, str], user_ids: List[Union[int, s
user_ids: List of user IDs or usernames to invite. user_ids: List of user IDs or usernames to invite.
""" """
try: try:
entity = await client.get_entity(group_id) entity = await resolve_entity(group_id)
users_to_add = [] users_to_add = []
for user_id in user_ids: for user_id in user_ids:
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
users_to_add.append(user) users_to_add.append(user)
except ValueError as e: except ValueError as e:
return f"Error: User with ID {user_id} could not be found. {e}" return f"Error: User with ID {user_id} could not be found. {e}"
@ -1940,7 +1963,7 @@ async def leave_chat(chat_id: Union[int, str]) -> str:
chat_id: The chat ID or username to leave. chat_id: The chat ID or username to leave.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
# Check the entity type carefully # Check the entity type carefully
if isinstance(entity, Channel): if isinstance(entity, Channel):
@ -2055,7 +2078,7 @@ async def send_file(
) )
if path_error: if path_error:
return path_error return path_error
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.send_file(entity, str(safe_path), caption=caption) await client.send_file(entity, str(safe_path), caption=caption)
return f"File sent to chat {chat_id} from {safe_path}." return f"File sent to chat {chat_id} from {safe_path}."
except Exception as e: except Exception as e:
@ -2083,7 +2106,7 @@ async def download_media(
If omitted, saves into `<first_root>/downloads/`. If omitted, saves into `<first_root>/downloads/`.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
msg = await client.get_messages(entity, ids=message_id) msg = await client.get_messages(entity, ids=message_id)
if not msg or not msg.media: if not msg or not msg.media:
return "No media found in the specified message." return "No media found in the specified message."
@ -2275,7 +2298,7 @@ async def set_privacy_settings(
allow_entities = [] allow_entities = []
for user_id in allow_users: for user_id in allow_users:
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
allow_entities.append(user) allow_entities.append(user)
except Exception as user_err: except Exception as user_err:
logger.warning(f"Could not get entity for user ID {user_id}: {user_err}") logger.warning(f"Could not get entity for user ID {user_id}: {user_err}")
@ -2292,7 +2315,7 @@ async def set_privacy_settings(
disallow_entities = [] disallow_entities = []
for user_id in disallow_users: for user_id in disallow_users:
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
disallow_entities.append(user) disallow_entities.append(user)
except Exception as user_err: except Exception as user_err:
logger.warning(f"Could not get entity for user ID {user_id}: {user_err}") logger.warning(f"Could not get entity for user ID {user_id}: {user_err}")
@ -2400,7 +2423,7 @@ async def edit_chat_title(chat_id: Union[int, str], title: str) -> str:
Edit the title of a chat, group, or channel. Edit the title of a chat, group, or channel.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
if isinstance(entity, Channel): if isinstance(entity, Channel):
await client(functions.channels.EditTitleRequest(channel=entity, title=title)) await client(functions.channels.EditTitleRequest(channel=entity, title=title))
elif isinstance(entity, Chat): elif isinstance(entity, Chat):
@ -2436,7 +2459,7 @@ async def edit_chat_photo(
if path_error: if path_error:
return path_error return path_error
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
uploaded_file = await client.upload_file(str(safe_path)) uploaded_file = await client.upload_file(str(safe_path))
if isinstance(entity, Channel): if isinstance(entity, Channel):
@ -2469,7 +2492,7 @@ async def delete_chat_photo(chat_id: Union[int, str]) -> str:
Delete the photo of a chat, group, or channel. Delete the photo of a chat, group, or channel.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
if isinstance(entity, Channel): if isinstance(entity, Channel):
# Use InputChatPhotoEmpty for channels/supergroups # Use InputChatPhotoEmpty for channels/supergroups
await client( await client(
@ -2509,8 +2532,8 @@ async def promote_admin(
rights: Admin rights to give (optional) rights: Admin rights to give (optional)
""" """
try: try:
chat = await client.get_entity(group_id) chat = await resolve_entity(group_id)
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
# Set default admin rights if not provided # Set default admin rights if not provided
if not rights: if not rights:
@ -2577,8 +2600,8 @@ async def demote_admin(group_id: Union[int, str], user_id: Union[int, str]) -> s
user_id: User ID or username to demote user_id: User ID or username to demote
""" """
try: try:
chat = await client.get_entity(group_id) chat = await resolve_entity(group_id)
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
# Create empty admin rights (regular user) # Create empty admin rights (regular user)
admin_rights = ChatAdminRights( admin_rights = ChatAdminRights(
@ -2630,8 +2653,8 @@ async def ban_user(chat_id: Union[int, str], user_id: Union[int, str]) -> str:
user_id: User ID or username to ban user_id: User ID or username to ban
""" """
try: try:
chat = await client.get_entity(chat_id) chat = await resolve_entity(chat_id)
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
# Create banned rights (all restrictions enabled) # Create banned rights (all restrictions enabled)
banned_rights = ChatBannedRights( banned_rights = ChatBannedRights(
@ -2681,8 +2704,8 @@ async def unban_user(chat_id: Union[int, str], user_id: Union[int, str]) -> str:
user_id: User ID or username to unban user_id: User ID or username to unban
""" """
try: try:
chat = await client.get_entity(chat_id) chat = await resolve_entity(chat_id)
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
# Create unbanned rights (no restrictions) # Create unbanned rights (no restrictions)
unbanned_rights = ChatBannedRights( unbanned_rights = ChatBannedRights(
@ -2768,7 +2791,7 @@ async def get_invite_link(chat_id: Union[int, str]) -> str:
Get the invite link for a group or channel. Get the invite link for a group or channel.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
# Try using ExportChatInviteRequest first # Try using ExportChatInviteRequest first
try: try:
@ -2862,7 +2885,7 @@ async def export_chat_invite(chat_id: Union[int, str]) -> str:
Export a chat invite link. Export a chat invite link.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
# Try using ExportChatInviteRequest first # Try using ExportChatInviteRequest first
try: try:
@ -2987,7 +3010,7 @@ async def send_voice(
): ):
return "Voice file must be .ogg or .opus format." return "Voice file must be .ogg or .opus format."
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.send_file(entity, str(safe_path), voice_note=True) await client.send_file(entity, str(safe_path), voice_note=True)
return f"Voice message sent to chat {chat_id} from {safe_path}." return f"Voice message sent to chat {chat_id} from {safe_path}."
except Exception as e: except Exception as e:
@ -3036,8 +3059,8 @@ async def forward_message(
Forward a message from one chat to another. Forward a message from one chat to another.
""" """
try: try:
from_entity = await client.get_entity(from_chat_id) from_entity = await resolve_entity(from_chat_id)
to_entity = await client.get_entity(to_chat_id) to_entity = await resolve_entity(to_chat_id)
await client.forward_messages(to_entity, message_id, from_entity) await client.forward_messages(to_entity, message_id, from_entity)
return f"Message {message_id} forwarded from {from_chat_id} to {to_chat_id}." return f"Message {message_id} forwarded from {from_chat_id} to {to_chat_id}."
except Exception as e: except Exception as e:
@ -3061,7 +3084,7 @@ async def edit_message(chat_id: Union[int, str], message_id: int, new_text: str)
Edit a message you sent. Edit a message you sent.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.edit_message(entity, message_id, new_text) await client.edit_message(entity, message_id, new_text)
return f"Message {message_id} edited." return f"Message {message_id} edited."
except Exception as e: except Exception as e:
@ -3081,7 +3104,7 @@ async def delete_message(chat_id: Union[int, str], message_id: int) -> str:
Delete a message by ID. Delete a message by ID.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.delete_messages(entity, message_id) await client.delete_messages(entity, message_id)
return f"Message {message_id} deleted." return f"Message {message_id} deleted."
except Exception as e: except Exception as e:
@ -3099,7 +3122,7 @@ async def pin_message(chat_id: Union[int, str], message_id: int) -> str:
Pin a message in a chat. Pin a message in a chat.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.pin_message(entity, message_id) await client.pin_message(entity, message_id)
return f"Message {message_id} pinned in chat {chat_id}." return f"Message {message_id} pinned in chat {chat_id}."
except Exception as e: except Exception as e:
@ -3117,7 +3140,7 @@ async def unpin_message(chat_id: Union[int, str], message_id: int) -> str:
Unpin a message in a chat. Unpin a message in a chat.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.unpin_message(entity, message_id) await client.unpin_message(entity, message_id)
return f"Message {message_id} unpinned in chat {chat_id}." return f"Message {message_id} unpinned in chat {chat_id}."
except Exception as e: except Exception as e:
@ -3135,7 +3158,7 @@ async def mark_as_read(chat_id: Union[int, str]) -> str:
Mark all messages as read in a chat. Mark all messages as read in a chat.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.send_read_acknowledge(entity) await client.send_read_acknowledge(entity)
return f"Marked all messages as read in chat {chat_id}." return f"Marked all messages as read in chat {chat_id}."
except Exception as e: except Exception as e:
@ -3160,7 +3183,7 @@ async def reply_to_message(
```pre```), or omit for plain text (no formatting). ```pre```), or omit for plain text (no formatting).
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.send_message(entity, text, reply_to=message_id, parse_mode=parse_mode) await client.send_message(entity, text, reply_to=message_id, parse_mode=parse_mode)
return f"Replied to message {message_id} in chat {chat_id}." return f"Replied to message {message_id} in chat {chat_id}."
except Exception as e: except Exception as e:
@ -3182,7 +3205,7 @@ async def get_media_info(chat_id: Union[int, str], message_id: int) -> str:
message_id: The message ID. message_id: The message ID.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
msg = await client.get_messages(entity, ids=message_id) msg = await client.get_messages(entity, ids=message_id)
if not msg or not msg.media: if not msg or not msg.media:
@ -3217,7 +3240,7 @@ async def search_messages(chat_id: Union[int, str], query: str, limit: int = 20)
Search for messages in a chat by text. Search for messages in a chat by text.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
messages = await client.get_messages(entity, limit=limit, search=query) messages = await client.get_messages(entity, limit=limit, search=query)
lines = [] lines = []
@ -3302,7 +3325,7 @@ async def mute_chat(chat_id: Union[int, str]) -> str:
try: try:
from telethon.tl.types import InputPeerNotifySettings from telethon.tl.types import InputPeerNotifySettings
peer = await client.get_entity(chat_id) peer = await resolve_entity(chat_id)
await client( await client(
functions.account.UpdateNotifySettingsRequest( functions.account.UpdateNotifySettingsRequest(
peer=peer, settings=InputPeerNotifySettings(mute_until=2**31 - 1) peer=peer, settings=InputPeerNotifySettings(mute_until=2**31 - 1)
@ -3312,7 +3335,7 @@ async def mute_chat(chat_id: Union[int, str]) -> str:
except (ImportError, AttributeError) as type_err: except (ImportError, AttributeError) as type_err:
try: try:
# Alternative approach directly using raw API # Alternative approach directly using raw API
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
await client( await client(
functions.account.UpdateNotifySettingsRequest( functions.account.UpdateNotifySettingsRequest(
peer=peer, peer=peer,
@ -3345,7 +3368,7 @@ async def unmute_chat(chat_id: Union[int, str]) -> str:
try: try:
from telethon.tl.types import InputPeerNotifySettings from telethon.tl.types import InputPeerNotifySettings
peer = await client.get_entity(chat_id) peer = await resolve_entity(chat_id)
await client( await client(
functions.account.UpdateNotifySettingsRequest( functions.account.UpdateNotifySettingsRequest(
peer=peer, settings=InputPeerNotifySettings(mute_until=0) peer=peer, settings=InputPeerNotifySettings(mute_until=0)
@ -3355,7 +3378,7 @@ async def unmute_chat(chat_id: Union[int, str]) -> str:
except (ImportError, AttributeError) as type_err: except (ImportError, AttributeError) as type_err:
try: try:
# Alternative approach directly using raw API # Alternative approach directly using raw API
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
await client( await client(
functions.account.UpdateNotifySettingsRequest( functions.account.UpdateNotifySettingsRequest(
peer=peer, peer=peer,
@ -3386,7 +3409,7 @@ async def archive_chat(chat_id: Union[int, str]) -> str:
Archive a chat. Archive a chat.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
peer = utils.get_input_peer(entity) peer = utils.get_input_peer(entity)
await client( await client(
functions.folders.EditPeerFoldersRequest( functions.folders.EditPeerFoldersRequest(
@ -3409,7 +3432,7 @@ async def unarchive_chat(chat_id: Union[int, str]) -> str:
Unarchive a chat. Unarchive a chat.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
peer = utils.get_input_peer(entity) peer = utils.get_input_peer(entity)
await client( await client(
functions.folders.EditPeerFoldersRequest( functions.folders.EditPeerFoldersRequest(
@ -3460,7 +3483,7 @@ async def send_sticker(
if path_error: if path_error:
return path_error return path_error
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.send_file(entity, str(safe_path), force_document=False) await client.send_file(entity, str(safe_path), force_document=False)
return f"Sticker sent to chat {chat_id} from {safe_path}." return f"Sticker sent to chat {chat_id} from {safe_path}."
except Exception as e: except Exception as e:
@ -3538,7 +3561,7 @@ async def send_gif(chat_id: Union[int, str], gif_id: int) -> str:
try: try:
if not isinstance(gif_id, int): if not isinstance(gif_id, int):
return "gif_id must be a Telegram document ID (integer), not a file path. Use get_gif_search to find IDs." return "gif_id must be a Telegram document ID (integer), not a file path. Use get_gif_search to find IDs."
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
await client.send_file(entity, gif_id) await client.send_file(entity, gif_id)
return f"GIF sent to chat {chat_id}." return f"GIF sent to chat {chat_id}."
except Exception as e: except Exception as e:
@ -3551,7 +3574,7 @@ async def get_bot_info(bot_username: str) -> str:
Get information about a bot by username. Get information about a bot by username.
""" """
try: try:
entity = await client.get_entity(bot_username) entity = await resolve_entity(bot_username)
if not entity: if not entity:
return f"Bot with username {bot_username} not found." return f"Bot with username {bot_username} not found."
@ -3612,7 +3635,7 @@ async def set_bot_commands(bot_username: str, commands: list) -> str:
] ]
# Get the bot entity # Get the bot entity
bot = await client.get_entity(bot_username) bot = await resolve_entity(bot_username)
# Set the commands with proper scope # Set the commands with proper scope
await client( await client(
@ -3639,7 +3662,7 @@ async def get_history(chat_id: Union[int, str], limit: int = 100) -> str:
Get full chat history (up to limit). Get full chat history (up to limit).
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
messages = await client.get_messages(entity, limit=limit) messages = await client.get_messages(entity, limit=limit)
lines = [] lines = []
@ -3665,7 +3688,7 @@ async def get_user_photos(user_id: Union[int, str], limit: int = 10) -> str:
Get profile photos of a user. Get profile photos of a user.
""" """
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
photos = await client( photos = await client(
functions.photos.GetUserPhotosRequest(user_id=user, offset=0, max_id=0, limit=limit) functions.photos.GetUserPhotosRequest(user_id=user, offset=0, max_id=0, limit=limit)
) )
@ -3683,7 +3706,7 @@ async def get_user_status(user_id: Union[int, str]) -> str:
Get the online status of a user. Get the online status of a user.
""" """
try: try:
user = await client.get_entity(user_id) user = await resolve_entity(user_id)
return str(user.status) return str(user.status)
except Exception as e: except Exception as e:
return log_and_format_error("get_user_status", e, user_id=user_id) return log_and_format_error("get_user_status", e, user_id=user_id)
@ -3729,7 +3752,7 @@ async def get_pinned_messages(chat_id: Union[int, str]) -> str:
Get all pinned messages in a chat. Get all pinned messages in a chat.
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
# Use correct filter based on Telethon version # Use correct filter based on Telethon version
try: try:
@ -3786,7 +3809,7 @@ async def create_poll(
close_date: Optional close date in ISO format (YYYY-MM-DD HH:MM:SS) close_date: Optional close date in ISO format (YYYY-MM-DD HH:MM:SS)
""" """
try: try:
entity = await client.get_entity(chat_id) entity = await resolve_entity(chat_id)
# Validate options # Validate options
if len(options) < 2: if len(options) < 2:
@ -3860,7 +3883,7 @@ async def send_reaction(
try: try:
from telethon.tl.types import ReactionEmoji from telethon.tl.types import ReactionEmoji
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
await client( await client(
functions.messages.SendReactionRequest( functions.messages.SendReactionRequest(
peer=peer, peer=peer,
@ -3897,7 +3920,7 @@ async def remove_reaction(
message_id: The message ID to remove reaction from message_id: The message ID to remove reaction from
""" """
try: try:
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
await client( await client(
functions.messages.SendReactionRequest( functions.messages.SendReactionRequest(
peer=peer, peer=peer,
@ -3933,7 +3956,7 @@ async def get_message_reactions(
try: try:
from telethon.tl.types import ReactionEmoji, ReactionCustomEmoji from telethon.tl.types import ReactionEmoji, ReactionCustomEmoji
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
result = await client( result = await client(
functions.messages.GetMessageReactionsListRequest( functions.messages.GetMessageReactionsListRequest(
@ -4010,7 +4033,7 @@ async def save_draft(
no_webpage: If True, disable link preview in the draft no_webpage: If True, disable link preview in the draft
""" """
try: try:
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
# Build reply_to parameter if provided # Build reply_to parameter if provided
reply_to = None reply_to = None
@ -4105,7 +4128,7 @@ async def clear_draft(chat_id: Union[int, str]) -> str:
chat_id: The chat ID or username to clear the draft from chat_id: The chat ID or username to clear the draft from
""" """
try: try:
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
# Saving an empty message clears the draft # Saving an empty message clears the draft
await client( await client(
@ -4216,7 +4239,7 @@ async def get_folder(folder_id: int) -> str:
included_chats = [] included_chats = []
for peer in getattr(target_folder, "include_peers", []): for peer in getattr(target_folder, "include_peers", []):
try: try:
entity = await client.get_entity(peer) entity = await resolve_entity(peer)
chat_info = { chat_info = {
"id": entity.id, "id": entity.id,
"name": getattr(entity, "title", None) "name": getattr(entity, "title", None)
@ -4233,7 +4256,7 @@ async def get_folder(folder_id: int) -> str:
excluded_chats = [] excluded_chats = []
for peer in getattr(target_folder, "exclude_peers", []): for peer in getattr(target_folder, "exclude_peers", []):
try: try:
entity = await client.get_entity(peer) entity = await resolve_entity(peer)
chat_info = { chat_info = {
"id": entity.id, "id": entity.id,
"name": getattr(entity, "title", None) "name": getattr(entity, "title", None)
@ -4248,7 +4271,7 @@ async def get_folder(folder_id: int) -> str:
pinned_chats = [] pinned_chats = []
for peer in getattr(target_folder, "pinned_peers", []): for peer in getattr(target_folder, "pinned_peers", []):
try: try:
entity = await client.get_entity(peer) entity = await resolve_entity(peer)
chat_info = { chat_info = {
"id": entity.id, "id": entity.id,
"name": getattr(entity, "title", None) "name": getattr(entity, "title", None)
@ -4352,7 +4375,7 @@ async def create_folder(
if chat_ids: if chat_ids:
for chat_id in chat_ids: for chat_id in chat_ids:
try: try:
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
include_peers.append(peer) include_peers.append(peer)
except Exception as e: except Exception as e:
return f"Failed to resolve chat '{chat_id}': {str(e)}" return f"Failed to resolve chat '{chat_id}': {str(e)}"
@ -4427,7 +4450,7 @@ async def add_chat_to_folder(
# Resolve chat to input peer # Resolve chat to input peer
try: try:
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
except Exception as e: except Exception as e:
return f"Failed to resolve chat '{chat_id}': {str(e)}" return f"Failed to resolve chat '{chat_id}': {str(e)}"
@ -4528,7 +4551,7 @@ async def remove_chat_from_folder(folder_id: int, chat_id: Union[int, str]) -> s
# Resolve chat to get peer ID # Resolve chat to get peer ID
try: try:
peer = await client.get_input_entity(chat_id) peer = await resolve_input_entity(chat_id)
peer_id = utils.get_peer_id(peer) peer_id = utils.get_peer_id(peer)
except Exception as e: except Exception as e:
return f"Failed to resolve chat '{chat_id}': {str(e)}" return f"Failed to resolve chat '{chat_id}': {str(e)}"
@ -4696,7 +4719,12 @@ async def _main() -> None:
print("Starting Telegram client...", file=sys.stderr) print("Starting Telegram client...", file=sys.stderr)
await client.start() await client.start()
print("Telegram client started. Running MCP server...", file=sys.stderr) # Warm entity cache — StringSession has no persistent cache,
# so fetch all dialogs once to populate it
print("Warming entity cache...")
await client.get_dialogs()
print("Telegram client started. Running MCP server...")
# Use the asynchronous entrypoint instead of mcp.run() # Use the asynchronous entrypoint instead of mcp.run()
await mcp.run_stdio_async() await mcp.run_stdio_async()
except Exception as e: except Exception as e: