From d562cf61fbca7f8ba8a8794765faa68048970446 Mon Sep 17 00:00:00 2001 From: anonim <70073044+l1v0n1@users.noreply.github.com> Date: Tue, 22 Apr 2025 18:24:46 +0300 Subject: [PATCH 1/2] refactor: Remove file handling functions from main.py and update README.md Removed several functions that required direct file path access, including `send_file`, `download_media`, `set_profile_photo`, `edit_chat_photo`, `send_voice`, `send_sticker`, and `upload_file`, due to limitations in the current MCP environment. Updated the README to reflect these changes and added a section on removed functionality. --- README.md | 17 ++--- main.py | 213 ------------------------------------------------------ 2 files changed, 7 insertions(+), 223 deletions(-) diff --git a/README.md b/README.md index f09885b..dbdd26f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ This MCP server exposes a huge suite of Telegram tools. **Every major Telegram/T - **create_group(title, user_ids)**: Create a new group - **create_channel(title, about, megagroup)**: Create a channel or supergroup - **edit_chat_title(chat_id, title)**: Change chat/group/channel title -- **edit_chat_photo(chat_id, file_path)**: Set chat/group/channel photo - **delete_chat_photo(chat_id)**: Remove chat/group/channel photo - **leave_chat(chat_id)**: Leave a group or channel - **get_participants(chat_id)**: List all participants @@ -70,6 +69,7 @@ This MCP server exposes a huge suite of Telegram tools. **Every major Telegram/T - **get_message_context(chat_id, message_id, context_size)**: Context around a message - **get_history(chat_id, limit)**: Full chat history - **get_pinned_messages(chat_id)**: List pinned messages +- **get_last_interaction(contact_id)**: Most recent message with a contact ### Contact Management - **list_contacts()**: List all contacts @@ -84,21 +84,15 @@ This MCP server exposes a huge suite of Telegram tools. **Every major Telegram/T - **get_contact_ids()**: List all contact IDs - **get_direct_chat_by_contact(contact_query)**: Find direct chat with a contact - **get_contact_chats(contact_id)**: List all chats with a contact -- **get_last_interaction(contact_id)**: Most recent message with a contact ### User & Profile - **get_me()**: Get your user info - **update_profile(first_name, last_name, about)**: Update your profile -- **set_profile_photo(file_path)**: Set your profile photo - **delete_profile_photo()**: Remove your profile photo - **get_user_photos(user_id, limit)**: Get a user's profile photos - **get_user_status(user_id)**: Get a user's online status ### Media -- **send_file(chat_id, file_path, caption)**: Send a file -- **send_voice(chat_id, file_path)**: Send a voice message -- **download_media(chat_id, message_id, file_path)**: Download media -- **upload_file(file_path)**: Upload a file to Telegram servers - **get_media_info(chat_id, message_id)**: Get info about media in a message ### Search & Discovery @@ -108,9 +102,6 @@ This MCP server exposes a huge suite of Telegram tools. **Every major Telegram/T ### Stickers, GIFs, Bots - **get_sticker_sets()**: List sticker sets -- **send_sticker(chat_id, file_path)**: Send a sticker -- **get_gif_search(query, limit)**: Search for GIFs -- **send_gif(chat_id, gif_id)**: Send a GIF - **get_bot_info(bot_username)**: Get info about a bot - **set_bot_commands(bot_username, commands)**: Set bot commands (bot accounts only) @@ -123,6 +114,12 @@ This MCP server exposes a huge suite of Telegram tools. **Every major Telegram/T - **unarchive_chat(chat_id)**: Unarchive a chat - **get_recent_actions(chat_id)**: Get recent admin actions +## Removed Functionality + +Please note that tools requiring direct file path access on the server (`send_file`, `download_media`, `set_profile_photo`, `edit_chat_photo`, `send_voice`, `send_sticker`, `upload_file`) have been removed from `main.py`. This is due to limitations in the current MCP environment regarding handling file attachments and local file system paths. + +Additionally, GIF-related tools (`get_gif_search`, `get_saved_gifs`, `send_gif`) have been removed due to ongoing issues with reliability in the Telethon library or Telegram API interactions. + --- ## 📋 Requirements diff --git a/main.py b/main.py index c7899df..71500a9 100644 --- a/main.py +++ b/main.py @@ -996,53 +996,6 @@ async def get_participants(chat_id: int) -> str: return log_and_format_error("get_participants", e, chat_id=chat_id) -@mcp.tool() -async def send_file(chat_id: int, file_path: str, caption: str = None) -> str: - """ - Send a file to a chat. - Args: - chat_id: The chat ID. - file_path: Absolute path to the file to send (must exist and be readable). - caption: Optional caption for the file. - """ - try: - if not os.path.isfile(file_path): - return f"File not found: {file_path}" - if not os.access(file_path, os.R_OK): - return f"File is not readable: {file_path}" - entity = await client.get_entity(chat_id) - await client.send_file(entity, file_path, caption=caption) - return f"File sent to chat {chat_id}." - except Exception as e: - return log_and_format_error("send_file", e, chat_id=chat_id, file_path=file_path, caption=caption) - - -@mcp.tool() -async def download_media(chat_id: int, message_id: int, file_path: str) -> str: - """ - Download media from a message in a chat. - Args: - chat_id: The chat ID. - message_id: The message ID containing the media. - file_path: Absolute path to save the downloaded file (must be writable). - """ - try: - entity = await client.get_entity(chat_id) - msg = await client.get_messages(entity, ids=message_id) - if not msg or not msg.media: - return "No media found in the specified message." - # Check if directory is writable - dir_path = os.path.dirname(file_path) or '.' - if not os.access(dir_path, os.W_OK): - return f"Directory not writable: {dir_path}" - await client.download_media(msg, file=file_path) - if not os.path.isfile(file_path): - return f"Download failed: file not created at {file_path}" - return f"Media downloaded to {file_path}." - except Exception as e: - return log_and_format_error("download_media", e, chat_id=chat_id, message_id=message_id, file_path=file_path) - - @mcp.tool() async def update_profile(first_name: str = None, last_name: str = None, about: str = None) -> str: """ @@ -1059,20 +1012,6 @@ async def update_profile(first_name: str = None, last_name: str = None, about: s return log_and_format_error("update_profile", e, first_name=first_name, last_name=last_name, about=about) -@mcp.tool() -async def set_profile_photo(file_path: str) -> str: - """ - Set a new profile photo. - """ - try: - await client(functions.photos.UploadProfilePhotoRequest( - file=await client.upload_file(file_path) - )) - return "Profile photo updated." - except Exception as e: - return log_and_format_error("set_profile_photo", e, file_path=file_path) - - @mcp.tool() async def delete_profile_photo() -> str: """ @@ -1278,37 +1217,6 @@ async def edit_chat_title(chat_id: int, title: str) -> str: return log_and_format_error("edit_chat_title", e, chat_id=chat_id, title=title) -@mcp.tool() -async def edit_chat_photo(chat_id: int, file_path: str) -> str: - """ - Edit the photo of a chat, group, or channel. Requires a file path to an image. - """ - try: - if not os.path.isfile(file_path): - return f"Photo file not found: {file_path}" - if not os.access(file_path, os.R_OK): - return f"Photo file not readable: {file_path}" - - entity = await client.get_entity(chat_id) - uploaded_file = await client.upload_file(file_path) - - if isinstance(entity, Channel): - # For channels/supergroups, use EditPhotoRequest with InputChatUploadedPhoto - input_photo = InputChatUploadedPhoto(file=uploaded_file) - await client(functions.channels.EditPhotoRequest(channel=entity, photo=input_photo)) - elif isinstance(entity, Chat): - # For basic groups, use EditChatPhotoRequest with InputChatUploadedPhoto - input_photo = InputChatUploadedPhoto(file=uploaded_file) - await client(functions.messages.EditChatPhotoRequest(chat_id=chat_id, photo=input_photo)) - else: - return f"Cannot edit photo for this entity type ({type(entity)})." - - return f"Chat {chat_id} photo updated." - except Exception as e: - logger.exception(f"edit_chat_photo failed (chat_id={chat_id}, file_path='{file_path}')") - return log_and_format_error("edit_chat_photo", e, chat_id=chat_id, file_path=file_path) - - @mcp.tool() async def delete_chat_photo(chat_id: int) -> str: """ @@ -1749,29 +1657,6 @@ async def import_chat_invite(hash: str) -> str: return log_and_format_error("import_chat_invite", e, hash=hash) -@mcp.tool() -async def send_voice(chat_id: int, file_path: str) -> str: - """ - Send a voice message to a chat. File must be an OGG/OPUS voice note. - Args: - chat_id: The chat ID. - file_path: Absolute path to the OGG/OPUS file. - """ - try: - if not os.path.isfile(file_path): - return f"File not found: {file_path}" - if not os.access(file_path, os.R_OK): - return f"File is not readable: {file_path}" - mime, _ = mimetypes.guess_type(file_path) - if not (mime and (mime == 'audio/ogg' or file_path.lower().endswith('.ogg') or file_path.lower().endswith('.opus'))): - return "Voice file must be .ogg or .opus format." - entity = await client.get_entity(chat_id) - await client.send_file(entity, file_path, voice_note=True) - return f"Voice message sent to chat {chat_id}." - except Exception as e: - return log_and_format_error("send_voice", e, chat_id=chat_id, file_path=file_path) - - @mcp.tool() async def forward_message(from_chat_id: int, message_id: int, to_chat_id: int) -> str: """ @@ -1864,24 +1749,6 @@ async def reply_to_message(chat_id: int, message_id: int, text: str) -> str: return log_and_format_error("reply_to_message", e, chat_id=chat_id, message_id=message_id, text=text) -@mcp.tool() -async def upload_file(file_path: str) -> str: - """ - Upload a file to Telegram servers (returns file handle as string, not a file path). - Args: - file_path: Absolute path to the file to upload (must exist and be readable). - """ - try: - if not os.path.isfile(file_path): - return f"File not found: {file_path}" - if not os.access(file_path, os.R_OK): - return f"File is not readable: {file_path}" - file = await client.upload_file(file_path) - return str(file) - except Exception as e: - return log_and_format_error("upload_file", e, file_path=file_path) - - @mcp.tool() async def get_media_info(chat_id: int, message_id: int) -> str: """ @@ -2049,86 +1916,6 @@ async def get_sticker_sets() -> str: return log_and_format_error("get_sticker_sets", e) -@mcp.tool() -async def send_sticker(chat_id: int, file_path: str) -> str: - """ - Send a sticker to a chat. File must be a valid .webp sticker file. - Args: - chat_id: The chat ID. - file_path: Absolute path to the .webp sticker file. - """ - try: - if not os.path.isfile(file_path): - return f"Sticker file not found: {file_path}" - if not os.access(file_path, os.R_OK): - return f"Sticker file is not readable: {file_path}" - if not file_path.lower().endswith('.webp'): - return "Sticker file must be a .webp file." - entity = await client.get_entity(chat_id) - await client.send_file(entity, file_path, force_document=False) - return f"Sticker sent to chat {chat_id}." - except Exception as e: - return log_and_format_error("send_sticker", e, chat_id=chat_id, file_path=file_path) - - -@mcp.tool() -async def get_gif_search(query: str, limit: int = 10) -> str: - """ - Search for GIFs by query. Returns a list of Telegram document IDs (not file paths). - Args: - query: Search term for GIFs. - limit: Max number of GIFs to return. - """ - try: - # Try approach 1: SearchGifsRequest - try: - result = await client(functions.messages.SearchGifsRequest(q=query, offset_id=0, limit=limit)) - if not result.gifs: - return "[]" - return json.dumps([g.document.id for g in result.gifs], indent=2, default=json_serializer) - except (AttributeError, ImportError): - # Fallback approach: Use SearchRequest with GIF filter - try: - from telethon.tl.types import InputMessagesFilterGif - result = await client(functions.messages.SearchRequest( - peer="gif", q=query, filter=InputMessagesFilterGif(), - min_date=None, max_date=None, offset_id=0, add_offset=0, - limit=limit, max_id=0, min_id=0, hash=0 - )) - if not result or not hasattr(result, 'messages') or not result.messages: - return "[]" - # Extract document IDs from any messages with media - gif_ids = [] - for msg in result.messages: - if hasattr(msg, 'media') and msg.media and hasattr(msg.media, 'document'): - gif_ids.append(msg.media.document.id) - return json.dumps(gif_ids, default=json_serializer) - except Exception as inner_e: - # Last resort: Try to fetch from a public bot - return f"Could not search GIFs using available methods: {inner_e}" - except Exception as e: - logger.exception(f"get_gif_search failed (query={query}, limit={limit})") - return log_and_format_error("get_gif_search", e, query=query, limit=limit) - - -@mcp.tool() -async def send_gif(chat_id: int, gif_id: int) -> str: - """ - Send a GIF to a chat by Telegram GIF document ID (not a file path). - Args: - chat_id: The chat ID. - gif_id: Telegram document ID for the GIF (from get_gif_search). - """ - try: - 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." - entity = await client.get_entity(chat_id) - await client.send_file(entity, gif_id) - return f"GIF sent to chat {chat_id}." - except Exception as e: - return log_and_format_error("send_gif", e, chat_id=chat_id, gif_id=gif_id) - - @mcp.tool() async def get_bot_info(bot_username: str) -> str: """ From 214bd6c6e59a0010a41114b619a746587857bd42 Mon Sep 17 00:00:00 2001 From: anonim <70073044+l1v0n1@users.noreply.github.com> Date: Tue, 22 Apr 2025 18:39:58 +0300 Subject: [PATCH 2/2] style: improve code formatting and consistency in main.py Refactored several sections of main.py to enhance readability by adjusting line breaks and indentation. This includes formatting changes in error handling and function calls across various asynchronous functions. --- main.py | 82 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/main.py b/main.py index d3582fe..4cbbc5b 100644 --- a/main.py +++ b/main.py @@ -1082,7 +1082,9 @@ async def send_file(chat_id: int, file_path: str, caption: str = None) -> str: await client.send_file(entity, file_path, caption=caption) return f"File sent to chat {chat_id}." except Exception as e: - return log_and_format_error("send_file", e, chat_id=chat_id, file_path=file_path, caption=caption) + return log_and_format_error( + "send_file", e, chat_id=chat_id, file_path=file_path, caption=caption + ) @mcp.tool() @@ -1100,7 +1102,7 @@ async def download_media(chat_id: int, message_id: int, file_path: str) -> str: if not msg or not msg.media: return "No media found in the specified message." # Check if directory is writable - dir_path = os.path.dirname(file_path) or '.' + dir_path = os.path.dirname(file_path) or "." if not os.access(dir_path, os.W_OK): return f"Directory not writable: {dir_path}" await client.download_media(msg, file=file_path) @@ -1108,7 +1110,9 @@ async def download_media(chat_id: int, message_id: int, file_path: str) -> str: return f"Download failed: file not created at {file_path}" return f"Media downloaded to {file_path}." except Exception as e: - return log_and_format_error("download_media", e, chat_id=chat_id, message_id=message_id, file_path=file_path) + return log_and_format_error( + "download_media", e, chat_id=chat_id, message_id=message_id, file_path=file_path + ) @mcp.tool() @@ -1135,9 +1139,9 @@ async def set_profile_photo(file_path: str) -> str: Set a new profile photo. """ try: - await client(functions.photos.UploadProfilePhotoRequest( - file=await client.upload_file(file_path) - )) + await client( + functions.photos.UploadProfilePhotoRequest(file=await client.upload_file(file_path)) + ) return "Profile photo updated." except Exception as e: return log_and_format_error("set_profile_photo", e, file_path=file_path) @@ -1366,24 +1370,26 @@ async def edit_chat_photo(chat_id: int, file_path: str) -> str: """ try: if not os.path.isfile(file_path): - return f"Photo file not found: {file_path}" + return f"Photo file not found: {file_path}" if not os.access(file_path, os.R_OK): - return f"Photo file not readable: {file_path}" + return f"Photo file not readable: {file_path}" entity = await client.get_entity(chat_id) uploaded_file = await client.upload_file(file_path) if isinstance(entity, Channel): - # For channels/supergroups, use EditPhotoRequest with InputChatUploadedPhoto - input_photo = InputChatUploadedPhoto(file=uploaded_file) - await client(functions.channels.EditPhotoRequest(channel=entity, photo=input_photo)) + # For channels/supergroups, use EditPhotoRequest with InputChatUploadedPhoto + input_photo = InputChatUploadedPhoto(file=uploaded_file) + await client(functions.channels.EditPhotoRequest(channel=entity, photo=input_photo)) elif isinstance(entity, Chat): - # For basic groups, use EditChatPhotoRequest with InputChatUploadedPhoto - input_photo = InputChatUploadedPhoto(file=uploaded_file) - await client(functions.messages.EditChatPhotoRequest(chat_id=chat_id, photo=input_photo)) + # For basic groups, use EditChatPhotoRequest with InputChatUploadedPhoto + input_photo = InputChatUploadedPhoto(file=uploaded_file) + await client( + functions.messages.EditChatPhotoRequest(chat_id=chat_id, photo=input_photo) + ) else: - return f"Cannot edit photo for this entity type ({type(entity)})." - + return f"Cannot edit photo for this entity type ({type(entity)})." + return f"Chat {chat_id} photo updated." except Exception as e: logger.exception(f"edit_chat_photo failed (chat_id={chat_id}, file_path='{file_path}')") @@ -1866,7 +1872,14 @@ async def send_voice(chat_id: int, file_path: str) -> str: if not os.access(file_path, os.R_OK): return f"File is not readable: {file_path}" mime, _ = mimetypes.guess_type(file_path) - if not (mime and (mime == 'audio/ogg' or file_path.lower().endswith('.ogg') or file_path.lower().endswith('.opus'))): + if not ( + mime + and ( + mime == "audio/ogg" + or file_path.lower().endswith(".ogg") + or file_path.lower().endswith(".opus") + ) + ): return "Voice file must be .ogg or .opus format." entity = await client.get_entity(chat_id) await client.send_file(entity, file_path, voice_note=True) @@ -2167,7 +2180,7 @@ async def send_sticker(chat_id: int, file_path: str) -> str: return f"Sticker file not found: {file_path}" if not os.access(file_path, os.R_OK): return f"Sticker file is not readable: {file_path}" - if not file_path.lower().endswith('.webp'): + if not file_path.lower().endswith(".webp"): return "Sticker file must be a .webp file." entity = await client.get_entity(chat_id) await client.send_file(entity, file_path, force_document=False) @@ -2187,25 +2200,40 @@ async def get_gif_search(query: str, limit: int = 10) -> str: try: # Try approach 1: SearchGifsRequest try: - result = await client(functions.messages.SearchGifsRequest(q=query, offset_id=0, limit=limit)) + result = await client( + functions.messages.SearchGifsRequest(q=query, offset_id=0, limit=limit) + ) if not result.gifs: return "[]" - return json.dumps([g.document.id for g in result.gifs], indent=2, default=json_serializer) + return json.dumps( + [g.document.id for g in result.gifs], indent=2, default=json_serializer + ) except (AttributeError, ImportError): # Fallback approach: Use SearchRequest with GIF filter try: from telethon.tl.types import InputMessagesFilterGif - result = await client(functions.messages.SearchRequest( - peer="gif", q=query, filter=InputMessagesFilterGif(), - min_date=None, max_date=None, offset_id=0, add_offset=0, - limit=limit, max_id=0, min_id=0, hash=0 - )) - if not result or not hasattr(result, 'messages') or not result.messages: + + result = await client( + functions.messages.SearchRequest( + peer="gif", + q=query, + filter=InputMessagesFilterGif(), + min_date=None, + max_date=None, + offset_id=0, + add_offset=0, + limit=limit, + max_id=0, + min_id=0, + hash=0, + ) + ) + if not result or not hasattr(result, "messages") or not result.messages: return "[]" # Extract document IDs from any messages with media gif_ids = [] for msg in result.messages: - if hasattr(msg, 'media') and msg.media and hasattr(msg.media, 'document'): + if hasattr(msg, "media") and msg.media and hasattr(msg.media, "document"): gif_ids.append(msg.media.document.id) return json.dumps(gif_ids, default=json_serializer) except Exception as inner_e: