refactor: Consolidate and clean up imports in main.py

This commit is contained in:
anonim 2025-04-18 15:03:46 +03:00
parent 3faa4a006e
commit 0833e51f48

266
main.py
View file

@ -1,19 +1,27 @@
import os import os
import sys import sys
import time
from dotenv import load_dotenv
import asyncio
import nest_asyncio
from mcp.server.fastmcp import FastMCP
from telethon import TelegramClient
from telethon.sessions import StringSession
import sqlite3
from telethon.tl.types import User, Chat, Channel, ChatAdminRights, ChatBannedRights, ChannelParticipantsKicked, ChannelParticipantsAdmins, InputChatPhoto, InputChatUploadedPhoto, InputChatPhotoEmpty, InputPeerUser, InputPeerChat, InputPeerChannel
from datetime import datetime, timedelta
import json import json
from typing import List, Dict, Optional, Union, Any import time
import mimetypes import asyncio
import sqlite3
import logging import logging
import mimetypes
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Union, Any
# Third-party libraries
import nest_asyncio
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
from telethon import TelegramClient, functions, utils
from telethon.sessions import StringSession
from telethon.tl.types import (
User, Chat, Channel,
ChatAdminRights, ChatBannedRights,
ChannelParticipantsKicked, ChannelParticipantsAdmins,
InputChatPhoto, InputChatUploadedPhoto, InputChatPhotoEmpty,
InputPeerUser, InputPeerChat, InputPeerChannel
)
import telethon.errors.rpcerrorlist import telethon.errors.rpcerrorlist
def json_serializer(obj): def json_serializer(obj):
@ -60,7 +68,7 @@ try:
file_handler.setLevel(logging.ERROR) file_handler.setLevel(logging.ERROR)
# Create formatter and add to handlers # Create formatter and add to handlers
formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s') formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s - %(message)s - %(filename)s:%(lineno)d')
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
@ -73,6 +81,53 @@ except Exception as log_error:
print(f"WARNING: Error setting up log file: {log_error}") print(f"WARNING: Error setting up log file: {log_error}")
# Fallback to console-only logging # Fallback to console-only logging
logger.addHandler(console_handler) logger.addHandler(console_handler)
logger.error(f"Failed to set up log file handler: {log_error}")
# Error code prefix mapping for better error tracing
ERROR_PREFIXES = {
"chat": "CHAT",
"msg": "MSG",
"contact": "CONTACT",
"group": "GROUP",
"media": "MEDIA",
"profile": "PROFILE",
"auth": "AUTH",
"admin": "ADMIN"
}
def log_and_format_error(function_name: str, error: Exception, prefix: str = None, **kwargs) -> str:
"""
Centralized error handling function that logs the error and returns a formatted user-friendly message.
Args:
function_name: Name of the function where error occurred
error: The exception that was raised
prefix: Error code prefix (e.g., "CHAT", "MSG") - if None, will be derived from function_name
**kwargs: Additional context parameters to include in log
Returns:
A user-friendly error message with error code
"""
# Generate a consistent error code
if prefix is None:
# Try to derive prefix from function name
for key, value in ERROR_PREFIXES.items():
if key in function_name.lower():
prefix = value
break
if prefix is None:
prefix = "GEN" # Generic prefix if none matches
error_code = f"{prefix}-ERR-{abs(hash(function_name)) % 1000:03d}"
# Format the additional context parameters
context = ", ".join(f"{k}={v}" for k, v in kwargs.items())
# Log the full technical error
logger.exception(f"{function_name} failed ({context}): {error}")
# Return a user-friendly message
return f"An error occurred (code: {error_code}). Check mcp_errors.log for details."
def format_entity(entity) -> Dict[str, Any]: def format_entity(entity) -> Dict[str, Any]:
"""Helper function to format entity information consistently.""" """Helper function to format entity information consistently."""
@ -138,8 +193,7 @@ async def get_chats(page: int = 1, page_size: int = 20) -> str:
lines.append(f"Chat ID: {chat_id}, Title: {title}") lines.append(f"Chat ID: {chat_id}, Title: {title}")
return "\n".join(lines) return "\n".join(lines)
except Exception as e: except Exception as e:
logger.exception(f"get_chats failed (page={page}, page_size={page_size})") return log_and_format_error("get_chats", e)
return "An error occurred (code: GETCHATS-ERR-001). Check mcp_errors.log for details."
@mcp.tool() @mcp.tool()
@ -162,8 +216,7 @@ async def get_messages(chat_id: int, page: int = 1, page_size: int = 20) -> str:
lines.append(f"ID: {msg.id} | Date: {msg.date} | Message: {msg.message}") lines.append(f"ID: {msg.id} | Date: {msg.date} | Message: {msg.message}")
return "\n".join(lines) return "\n".join(lines)
except Exception as e: except Exception as e:
logger.exception(f"get_messages failed (chat_id={chat_id}, page={page}, page_size={page_size})") return log_and_format_error("get_messages", e, chat_id=chat_id, page=page, page_size=page_size)
return "An error occurred (code: GETMSGS-ERR-001). Check mcp_errors.log for details."
@mcp.tool() @mcp.tool()
@ -179,8 +232,7 @@ async def send_message(chat_id: int, message: str) -> str:
await client.send_message(entity, message) await client.send_message(entity, message)
return "Message sent successfully." return "Message sent successfully."
except Exception as e: except Exception as e:
logger.exception(f"send_message failed (chat_id={chat_id})") return log_and_format_error("send_message", e, chat_id=chat_id)
return "An error occurred (code: SENDMSG-ERR-001). Check mcp_errors.log for details."
@mcp.tool() @mcp.tool()
@ -206,7 +258,7 @@ async def list_contacts() -> str:
lines.append(contact_info) lines.append(contact_info)
return "\n".join(lines) return "\n".join(lines)
except Exception as e: except Exception as e:
return f"Error listing contacts: {e}" return log_and_format_error("list_contacts", e)
@mcp.tool() @mcp.tool()
@ -234,7 +286,7 @@ async def search_contacts(query: str) -> str:
lines.append(contact_info) lines.append(contact_info)
return "\n".join(lines) return "\n".join(lines)
except Exception as e: except Exception as e:
return f"Error searching contacts: {e}" return log_and_format_error("search_contacts", e, query=query)
@mcp.tool() @mcp.tool()
@ -248,7 +300,7 @@ async def get_contact_ids() -> str:
return "No contact IDs found." return "No contact IDs found."
return "Contact IDs: " + ", ".join(str(cid) for cid in result) return "Contact IDs: " + ", ".join(str(cid) for cid in result)
except Exception as e: except Exception as e:
return f"Error getting contact IDs: {e}" return log_and_format_error("get_contact_ids", e)
@mcp.tool() @mcp.tool()
@ -332,8 +384,7 @@ async def list_messages(chat_id: int, limit: int = 20, search_query: str = None,
return "\n".join(lines) return "\n".join(lines)
except Exception as e: except Exception as e:
logger.exception(f"list_messages failed (chat_id={chat_id})") return log_and_format_error("list_messages", e, chat_id=chat_id)
return f"Error retrieving messages: {e}"
@mcp.tool() @mcp.tool()
@ -394,7 +445,7 @@ async def list_chats(chat_type: str = None, limit: int = 20) -> str:
return "\n".join(results) return "\n".join(results)
except Exception as e: except Exception as e:
return f"Error listing chats: {e}" return log_and_format_error("list_chats", e, chat_type=chat_type, limit=limit)
@mcp.tool() @mcp.tool()
@ -470,8 +521,7 @@ async def get_chat(chat_id: int) -> str:
return "\n".join(result) return "\n".join(result)
except Exception as e: except Exception as e:
logger.exception(f"get_chat failed (chat_id={chat_id})") return log_and_format_error("get_chat", e, chat_id=chat_id)
return f"Error getting chat info: {e}"
@mcp.tool() @mcp.tool()
@ -518,7 +568,7 @@ async def get_direct_chat_by_contact(contact_query: str) -> str:
return f"Found contacts: {found_names}, but no direct chats were found with them." return f"Found contacts: {found_names}, but no direct chats were found with them."
return "\n".join(results) return "\n".join(results)
except Exception as e: except Exception as e:
return f"Error finding direct chat: {e}" return log_and_format_error("get_direct_chat_by_contact", e, contact_query=contact_query)
@mcp.tool() @mcp.tool()
@ -568,7 +618,7 @@ async def get_contact_chats(contact_id: int) -> str:
return f"Chats with {contact_name} (ID: {contact_id}):\n" + "\n".join(results) return f"Chats with {contact_name} (ID: {contact_id}):\n" + "\n".join(results)
except Exception as e: except Exception as e:
return f"Error retrieving contact chats: {e}" return log_and_format_error("get_contact_chats", e, contact_id=contact_id)
@mcp.tool() @mcp.tool()
@ -602,7 +652,7 @@ async def get_last_interaction(contact_id: int) -> str:
return "\n".join(results) return "\n".join(results)
except Exception as e: except Exception as e:
return f"Error retrieving last interaction: {e}" return log_and_format_error("get_last_interaction", e, contact_id=contact_id)
@mcp.tool() @mcp.tool()
@ -652,7 +702,7 @@ async def get_message_context(chat_id: int, message_id: int, context_size: int =
results.append(f"ID: {msg.id} | {sender_name} | {msg.date}{highlight}\n{msg.message or '[Media/No text]'}\n") results.append(f"ID: {msg.id} | {sender_name} | {msg.date}{highlight}\n{msg.message or '[Media/No text]'}\n")
return "\n".join(results) return "\n".join(results)
except Exception as e: except Exception as e:
return f"Error retrieving message context: {e}" return log_and_format_error("get_message_context", e, chat_id=chat_id, message_id=message_id, context_size=context_size)
@mcp.tool() @mcp.tool()
@ -699,10 +749,10 @@ async def add_contact(phone: str, first_name: str, last_name: str = "") -> str:
return f"Contact not added. Alternative method response: {str(result)}" return f"Contact not added. Alternative method response: {str(result)}"
except Exception as alt_e: except Exception as alt_e:
logger.exception(f"add_contact (alt method) failed (phone={phone})") logger.exception(f"add_contact (alt method) failed (phone={phone})")
return f"Error adding contact (alternative method): {alt_e}" return log_and_format_error("add_contact", alt_e, phone=phone)
except Exception as e: except Exception as e:
logger.exception(f"add_contact failed (phone={phone})") logger.exception(f"add_contact failed (phone={phone})")
return f"Error adding contact: {e}" return log_and_format_error("add_contact", e, phone=phone)
@mcp.tool() @mcp.tool()
@ -717,7 +767,7 @@ async def delete_contact(user_id: int) -> str:
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:
return f"Error deleting contact: {e}" return log_and_format_error("delete_contact", e, user_id=user_id)
@mcp.tool() @mcp.tool()
@ -732,7 +782,7 @@ async def block_user(user_id: int) -> str:
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:
return f"Error blocking user: {e}" return log_and_format_error("block_user", e, user_id=user_id)
@mcp.tool() @mcp.tool()
@ -747,7 +797,7 @@ async def unblock_user(user_id: int) -> str:
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:
return f"Error unblocking user: {e}" return log_and_format_error("unblock_user", e, user_id=user_id)
@mcp.tool() @mcp.tool()
@ -759,7 +809,7 @@ async def get_me() -> str:
me = await client.get_me() me = await client.get_me()
return json.dumps(format_entity(me), indent=2) return json.dumps(format_entity(me), indent=2)
except Exception as e: except Exception as e:
return f"Error getting your info: {e}" return log_and_format_error("get_me", e)
@mcp.tool() @mcp.tool()
@ -820,7 +870,7 @@ async def create_group(title: str, user_ids: list) -> str:
raise # Let the outer exception handler catch it raise # Let the outer exception handler catch it
except Exception as e: except Exception as e:
logger.exception(f"create_group failed (title={title}, user_ids={user_ids})") logger.exception(f"create_group failed (title={title}, user_ids={user_ids})")
return f"Error creating group: {e}" return log_and_format_error("create_group", e, title=title, user_ids=user_ids)
@mcp.tool() @mcp.tool()
@ -861,11 +911,11 @@ async def invite_to_group(group_id: int, user_ids: list) -> str:
except telethon.errors.rpcerrorlist.UserPrivacyRestrictedError: except telethon.errors.rpcerrorlist.UserPrivacyRestrictedError:
return "Error: One or more users have privacy settings that prevent you from adding them." return "Error: One or more users have privacy settings that prevent you from adding them."
except Exception as e: except Exception as e:
return f"Error inviting users: {e}" return log_and_format_error("invite_to_group", e, group_id=group_id, user_ids=user_ids)
except Exception as e: except Exception as e:
logger.error(f"telegram_mcp invite_to_group failed (group_id={group_id}, user_ids={user_ids})", exc_info=True) logger.error(f"telegram_mcp invite_to_group failed (group_id={group_id}, user_ids={user_ids})", exc_info=True)
return f"Error: {e}" return log_and_format_error("invite_to_group", e, group_id=group_id, user_ids=user_ids)
@mcp.tool() @mcp.tool()
@ -887,7 +937,7 @@ async def leave_chat(chat_id: int) -> str:
chat_name = getattr(entity, 'title', str(chat_id)) chat_name = getattr(entity, 'title', str(chat_id))
return f"Left channel/supergroup {chat_name} (ID: {chat_id})." return f"Left channel/supergroup {chat_name} (ID: {chat_id})."
except Exception as chan_err: except Exception as chan_err:
return f"Error leaving channel: {chan_err}" return log_and_format_error("leave_chat", chan_err, chat_id=chat_id)
elif isinstance(entity, Chat): elif isinstance(entity, Chat):
# Traditional basic groups (not supergroups) # Traditional basic groups (not supergroups)
@ -914,11 +964,11 @@ async def leave_chat(chat_id: int) -> str:
chat_name = getattr(entity, 'title', str(chat_id)) chat_name = getattr(entity, 'title', str(chat_id))
return f"Left basic group {chat_name} (ID: {chat_id})." return f"Left basic group {chat_name} (ID: {chat_id})."
except Exception as alt_err: except Exception as alt_err:
return f"Error leaving basic group: {alt_err}" return log_and_format_error("leave_chat", alt_err, chat_id=chat_id)
else: else:
# Cannot leave a user chat this way # Cannot leave a user chat this way
entity_type = type(entity).__name__ entity_type = type(entity).__name__
return f"Cannot leave chat ID {chat_id} of type {entity_type}. This function is for groups and channels only." return log_and_format_error("leave_chat", Exception(f"Cannot leave chat ID {chat_id} of type {entity_type}. This function is for groups and channels only."), chat_id=chat_id)
except Exception as e: except Exception as e:
logger.exception(f"leave_chat failed (chat_id={chat_id})") logger.exception(f"leave_chat failed (chat_id={chat_id})")
@ -926,9 +976,9 @@ async def leave_chat(chat_id: int) -> str:
# Provide helpful hint for common errors # Provide helpful hint for common errors
error_str = str(e).lower() error_str = str(e).lower()
if "invalid" in error_str and "chat" in error_str: if "invalid" in error_str and "chat" in error_str:
return "Error: This appears to be a channel/supergroup. Please check the chat ID and try again." return log_and_format_error("leave_chat", Exception(f"Error leaving chat: This appears to be a channel/supergroup. Please check the chat ID and try again."), chat_id=chat_id)
return f"Error leaving chat: {e}" return log_and_format_error("leave_chat", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -943,7 +993,7 @@ async def get_participants(chat_id: int) -> str:
lines = [f"ID: {p.id}, Name: {getattr(p, 'first_name', '')} {getattr(p, 'last_name', '')}" for p in participants] lines = [f"ID: {p.id}, Name: {getattr(p, 'first_name', '')} {getattr(p, 'last_name', '')}" for p in participants]
return "\n".join(lines) return "\n".join(lines)
except Exception as e: except Exception as e:
return f"Error getting participants: {e}" return log_and_format_error("get_participants", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -964,7 +1014,7 @@ async def send_file(chat_id: int, file_path: str, caption: str = None) -> str:
await client.send_file(entity, file_path, caption=caption) await client.send_file(entity, file_path, caption=caption)
return f"File sent to chat {chat_id}." return f"File sent to chat {chat_id}."
except Exception as e: except Exception as e:
return f"Error sending file: {e}" return log_and_format_error("send_file", e, chat_id=chat_id, file_path=file_path, caption=caption)
@mcp.tool() @mcp.tool()
@ -990,7 +1040,7 @@ 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"Download failed: file not created at {file_path}"
return f"Media downloaded to {file_path}." return f"Media downloaded to {file_path}."
except Exception as e: except Exception as e:
return f"Error downloading media: {e}" return log_and_format_error("download_media", e, chat_id=chat_id, message_id=message_id, file_path=file_path)
@mcp.tool() @mcp.tool()
@ -1006,7 +1056,7 @@ async def update_profile(first_name: str = None, last_name: str = None, about: s
)) ))
return "Profile updated." return "Profile updated."
except Exception as e: except Exception as e:
return f"Error updating profile: {e}" return log_and_format_error("update_profile", e, first_name=first_name, last_name=last_name, about=about)
@mcp.tool() @mcp.tool()
@ -1020,7 +1070,7 @@ async def set_profile_photo(file_path: str) -> str:
)) ))
return "Profile photo updated." return "Profile photo updated."
except Exception as e: except Exception as e:
return f"Error setting profile photo: {e}" return log_and_format_error("set_profile_photo", e, file_path=file_path)
@mcp.tool() @mcp.tool()
@ -1035,7 +1085,7 @@ async def delete_profile_photo() -> str:
await client(functions.photos.DeletePhotosRequest(id=[photos.photos[0].id])) await client(functions.photos.DeletePhotosRequest(id=[photos.photos[0].id]))
return "Profile photo deleted." return "Profile photo deleted."
except Exception as e: except Exception as e:
return f"Error deleting profile photo: {e}" return log_and_format_error("delete_profile_photo", e)
@mcp.tool() @mcp.tool()
@ -1059,7 +1109,7 @@ async def get_privacy_settings() -> str:
raise raise
except Exception as e: except Exception as e:
logger.exception("get_privacy_settings failed") logger.exception("get_privacy_settings failed")
return f"Error getting privacy settings: {e}" return log_and_format_error("get_privacy_settings", e)
@mcp.tool() @mcp.tool()
@ -1119,7 +1169,7 @@ async def set_privacy_settings(key: str, allow_users: list = None, disallow_user
rules.append(InputPrivacyValueAllowUsers(users=allow_entities)) rules.append(InputPrivacyValueAllowUsers(users=allow_entities))
except Exception as allow_err: except Exception as allow_err:
logger.error(f"Error processing allowed users: {allow_err}") logger.error(f"Error processing allowed users: {allow_err}")
return f"Error processing allowed users: {allow_err}" return log_and_format_error("set_privacy_settings", allow_err, key=key)
# Process disallow rules # Process disallow rules
if disallow_users and len(disallow_users) > 0: if disallow_users and len(disallow_users) > 0:
@ -1136,7 +1186,7 @@ async def set_privacy_settings(key: str, allow_users: list = None, disallow_user
rules.append(InputPrivacyValueDisallowUsers(users=disallow_entities)) rules.append(InputPrivacyValueDisallowUsers(users=disallow_entities))
except Exception as disallow_err: except Exception as disallow_err:
logger.error(f"Error processing disallowed users: {disallow_err}") logger.error(f"Error processing disallowed users: {disallow_err}")
return f"Error processing disallowed users: {disallow_err}" return log_and_format_error("set_privacy_settings", disallow_err, key=key)
# Apply the privacy settings # Apply the privacy settings
try: try:
@ -1152,7 +1202,7 @@ async def set_privacy_settings(key: str, allow_users: list = None, disallow_user
raise raise
except Exception as e: except Exception as e:
logger.exception(f"set_privacy_settings failed (key={key})") logger.exception(f"set_privacy_settings failed (key={key})")
return f"Error setting privacy settings: {e}" return log_and_format_error("set_privacy_settings", e, key=key)
@mcp.tool() @mcp.tool()
@ -1165,7 +1215,7 @@ async def import_contacts(contacts: list) -> str:
result = await client(functions.contacts.ImportContactsRequest(contacts=input_contacts)) result = await client(functions.contacts.ImportContactsRequest(contacts=input_contacts))
return f"Imported {len(result.imported)} contacts." return f"Imported {len(result.imported)} contacts."
except Exception as e: except Exception as e:
return f"Error importing contacts: {e}" return log_and_format_error("import_contacts", e, contacts=contacts)
@mcp.tool() @mcp.tool()
@ -1178,7 +1228,7 @@ async def export_contacts() -> str:
users = result.users users = result.users
return json.dumps([format_entity(u) for u in users], indent=2) return json.dumps([format_entity(u) for u in users], indent=2)
except Exception as e: except Exception as e:
return f"Error exporting contacts: {e}" return log_and_format_error("export_contacts", e)
@mcp.tool() @mcp.tool()
@ -1190,7 +1240,7 @@ async def get_blocked_users() -> str:
result = await client(functions.contacts.GetBlockedRequest(offset=0, limit=100)) result = await client(functions.contacts.GetBlockedRequest(offset=0, limit=100))
return json.dumps([format_entity(u) for u in result.users], indent=2) return json.dumps([format_entity(u) for u in result.users], indent=2)
except Exception as e: except Exception as e:
return f"Error getting blocked users: {e}" return log_and_format_error("get_blocked_users", e)
@mcp.tool() @mcp.tool()
@ -1206,7 +1256,7 @@ async def create_channel(title: str, about: str = "", megagroup: bool = False) -
)) ))
return f"Channel '{title}' created with ID: {result.chats[0].id}" return f"Channel '{title}' created with ID: {result.chats[0].id}"
except Exception as e: except Exception as e:
return f"Error creating channel: {e}" return log_and_format_error("create_channel", e, title=title, about=about, megagroup=megagroup)
@mcp.tool() @mcp.tool()
@ -1225,7 +1275,7 @@ async def edit_chat_title(chat_id: int, title: str) -> str:
return f"Chat {chat_id} title updated to '{title}'." return f"Chat {chat_id} title updated to '{title}'."
except Exception as e: except Exception as e:
logger.exception(f"edit_chat_title failed (chat_id={chat_id}, title='{title}')") logger.exception(f"edit_chat_title failed (chat_id={chat_id}, title='{title}')")
return f"Error editing chat title: {e}" return log_and_format_error("edit_chat_title", e, chat_id=chat_id, title=title)
@mcp.tool() @mcp.tool()
@ -1256,7 +1306,7 @@ async def edit_chat_photo(chat_id: int, file_path: str) -> str:
return f"Chat {chat_id} photo updated." return f"Chat {chat_id} photo updated."
except Exception as e: except Exception as e:
logger.exception(f"edit_chat_photo failed (chat_id={chat_id}, file_path='{file_path}')") logger.exception(f"edit_chat_photo failed (chat_id={chat_id}, file_path='{file_path}')")
return f"Error editing chat photo: {e}" return log_and_format_error("edit_chat_photo", e, chat_id=chat_id, file_path=file_path)
@mcp.tool() @mcp.tool()
@ -1278,7 +1328,7 @@ async def delete_chat_photo(chat_id: int) -> str:
return f"Chat {chat_id} photo deleted." return f"Chat {chat_id} photo deleted."
except Exception as e: except Exception as e:
logger.exception(f"delete_chat_photo failed (chat_id={chat_id})") logger.exception(f"delete_chat_photo failed (chat_id={chat_id})")
return f"Error deleting chat photo: {e}" return log_and_format_error("delete_chat_photo", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1336,11 +1386,11 @@ async def promote_admin(group_id: int, user_id: int, rights: dict = None) -> str
except telethon.errors.rpcerrorlist.UserNotMutualContactError: except telethon.errors.rpcerrorlist.UserNotMutualContactError:
return "Error: Cannot promote users who are not mutual contacts. Please ensure the user is in your contacts and has added you back." return "Error: Cannot promote users who are not mutual contacts. Please ensure the user is in your contacts and has added you back."
except Exception as e: except Exception as e:
return f"Error promoting user to admin: {e}" return log_and_format_error("promote_admin", e, group_id=group_id, user_id=user_id)
except Exception as e: except Exception as e:
logger.error(f"telegram_mcp promote_admin failed (group_id={group_id}, user_id={user_id})", exc_info=True) logger.error(f"telegram_mcp promote_admin failed (group_id={group_id}, user_id={user_id})", exc_info=True)
return f"Error: {str(e)}" return log_and_format_error("promote_admin", e, group_id=group_id, user_id=user_id)
@mcp.tool() @mcp.tool()
@ -1382,11 +1432,11 @@ async def demote_admin(group_id: int, user_id: int) -> str:
except telethon.errors.rpcerrorlist.UserNotMutualContactError: except telethon.errors.rpcerrorlist.UserNotMutualContactError:
return "Error: Cannot modify admin status of users who are not mutual contacts. Please ensure the user is in your contacts and has added you back." return "Error: Cannot modify admin status of users who are not mutual contacts. Please ensure the user is in your contacts and has added you back."
except Exception as e: except Exception as e:
return f"Error demoting admin: {e}" return log_and_format_error("demote_admin", e, group_id=group_id, user_id=user_id)
except Exception as e: except Exception as e:
logger.error(f"telegram_mcp demote_admin failed (group_id={group_id}, user_id={user_id})", exc_info=True) logger.error(f"telegram_mcp demote_admin failed (group_id={group_id}, user_id={user_id})", exc_info=True)
return f"Error: {str(e)}" return log_and_format_error("demote_admin", e, group_id=group_id, user_id=user_id)
@mcp.tool() @mcp.tool()
@ -1429,10 +1479,10 @@ async def ban_user(chat_id: int, user_id: int) -> str:
except telethon.errors.rpcerrorlist.UserNotMutualContactError: except telethon.errors.rpcerrorlist.UserNotMutualContactError:
return "Error: Cannot ban users who are not mutual contacts. Please ensure the user is in your contacts and has added you back." return "Error: Cannot ban users who are not mutual contacts. Please ensure the user is in your contacts and has added you back."
except Exception as e: except Exception as e:
return f"Error banning user: {e}" return log_and_format_error("ban_user", e, chat_id=chat_id, user_id=user_id)
except Exception as e: except Exception as e:
logger.exception(f"ban_user failed (chat_id={chat_id}, user_id={user_id})") logger.exception(f"ban_user failed (chat_id={chat_id}, user_id={user_id})")
return f"Error: {e}" return log_and_format_error("ban_user", e, chat_id=chat_id, user_id=user_id)
@mcp.tool() @mcp.tool()
@ -1475,10 +1525,10 @@ async def unban_user(chat_id: int, user_id: int) -> str:
except telethon.errors.rpcerrorlist.UserNotMutualContactError: except telethon.errors.rpcerrorlist.UserNotMutualContactError:
return "Error: Cannot modify status of users who are not mutual contacts. Please ensure the user is in your contacts and has added you back." return "Error: Cannot modify status of users who are not mutual contacts. Please ensure the user is in your contacts and has added you back."
except Exception as e: except Exception as e:
return f"Error unbanning user: {e}" return log_and_format_error("unban_user", e, chat_id=chat_id, user_id=user_id)
except Exception as e: except Exception as e:
logger.exception(f"unban_user failed (chat_id={chat_id}, user_id={user_id})") logger.exception(f"unban_user failed (chat_id={chat_id}, user_id={user_id})")
return f"Error: {e}" return log_and_format_error("unban_user", e, chat_id=chat_id, user_id=user_id)
@mcp.tool() @mcp.tool()
@ -1493,7 +1543,7 @@ async def get_admins(chat_id: int) -> str:
return "\n".join(lines) if lines else "No admins found." return "\n".join(lines) if lines else "No admins found."
except Exception as e: except Exception as e:
logger.exception(f"get_admins failed (chat_id={chat_id})") logger.exception(f"get_admins failed (chat_id={chat_id})")
return f"Error getting admins: {e}" return log_and_format_error("get_admins", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1508,7 +1558,7 @@ async def get_banned_users(chat_id: int) -> str:
return "\n".join(lines) if lines else "No banned users found." return "\n".join(lines) if lines else "No banned users found."
except Exception as e: except Exception as e:
logger.exception(f"get_banned_users failed (chat_id={chat_id})") logger.exception(f"get_banned_users failed (chat_id={chat_id})")
return f"Error getting banned users: {e}" return log_and_format_error("get_banned_users", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1554,7 +1604,7 @@ async def get_invite_link(chat_id: int) -> str:
return "Could not retrieve invite link for this chat." return "Could not retrieve invite link for this chat."
except Exception as e: except Exception as e:
logger.exception(f"get_invite_link failed (chat_id={chat_id})") logger.exception(f"get_invite_link failed (chat_id={chat_id})")
return f"Error getting invite link: {e}" return log_and_format_error("get_invite_link", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1610,7 +1660,7 @@ async def join_chat_by_link(link: str) -> str:
raise # Re-raise to be caught by the outer exception handler raise # Re-raise to be caught by the outer exception handler
except Exception as e: except Exception as e:
logger.exception(f"join_chat_by_link failed (link={link})") logger.exception(f"join_chat_by_link failed (link={link})")
return f"Error joining chat: {e}" return log_and_format_error("join_chat_by_link", e, link=link)
@mcp.tool() @mcp.tool()
@ -1641,10 +1691,10 @@ async def export_chat_invite(chat_id: int) -> str:
return invite_link return invite_link
except Exception as e2: except Exception as e2:
logger.warning(f"export_chat_invite_link failed: {e2}") logger.warning(f"export_chat_invite_link failed: {e2}")
return f"Could not export chat invite: {e2}" return log_and_format_error("export_chat_invite", e2, chat_id=chat_id)
except Exception as e: except Exception as e:
logger.exception(f"export_chat_invite failed (chat_id={chat_id})") logger.exception(f"export_chat_invite failed (chat_id={chat_id})")
return f"Error exporting chat invite: {e}" return log_and_format_error("export_chat_invite", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1696,7 +1746,7 @@ async def import_chat_invite(hash: str) -> str:
raise # Re-raise to be caught by the outer exception handler raise # Re-raise to be caught by the outer exception handler
except Exception as e: except Exception as e:
logger.exception(f"import_chat_invite failed (hash={hash})") logger.exception(f"import_chat_invite failed (hash={hash})")
return f"Error importing chat invite: {e}" return log_and_format_error("import_chat_invite", e, hash=hash)
@mcp.tool() @mcp.tool()
@ -1719,7 +1769,7 @@ async def send_voice(chat_id: int, file_path: str) -> str:
await client.send_file(entity, file_path, voice_note=True) await client.send_file(entity, file_path, voice_note=True)
return f"Voice message sent to chat {chat_id}." return f"Voice message sent to chat {chat_id}."
except Exception as e: except Exception as e:
return f"Error sending voice: {e}" return log_and_format_error("send_voice", e, chat_id=chat_id, file_path=file_path)
@mcp.tool() @mcp.tool()
@ -1733,7 +1783,7 @@ async def forward_message(from_chat_id: int, message_id: int, to_chat_id: int) -
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:
return f"Error forwarding message: {e}" return log_and_format_error("forward_message", e, from_chat_id=from_chat_id, message_id=message_id, to_chat_id=to_chat_id)
@mcp.tool() @mcp.tool()
@ -1746,7 +1796,7 @@ async def edit_message(chat_id: int, message_id: int, new_text: str) -> str:
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:
return f"Error editing message: {e}" return log_and_format_error("edit_message", e, chat_id=chat_id, message_id=message_id, new_text=new_text)
@mcp.tool() @mcp.tool()
@ -1759,7 +1809,7 @@ async def delete_message(chat_id: int, message_id: int) -> str:
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:
return f"Error deleting message: {e}" return log_and_format_error("delete_message", e, chat_id=chat_id, message_id=message_id)
@mcp.tool() @mcp.tool()
@ -1772,7 +1822,7 @@ async def pin_message(chat_id: int, message_id: int) -> str:
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:
return f"Error pinning message: {e}" return log_and_format_error("pin_message", e, chat_id=chat_id, message_id=message_id)
@mcp.tool() @mcp.tool()
@ -1785,7 +1835,7 @@ async def unpin_message(chat_id: int, message_id: int) -> str:
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:
return f"Error unpinning message: {e}" return log_and_format_error("unpin_message", e, chat_id=chat_id, message_id=message_id)
@mcp.tool() @mcp.tool()
@ -1798,7 +1848,7 @@ async def mark_as_read(chat_id: int) -> str:
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:
return f"Error marking as read: {e}" return log_and_format_error("mark_as_read", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1811,7 +1861,7 @@ async def reply_to_message(chat_id: int, message_id: int, text: str) -> str:
await client.send_message(entity, text, reply_to=message_id) await client.send_message(entity, text, reply_to=message_id)
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:
return f"Error replying to message: {e}" return log_and_format_error("reply_to_message", e, chat_id=chat_id, message_id=message_id, text=text)
@mcp.tool() @mcp.tool()
@ -1829,7 +1879,7 @@ async def upload_file(file_path: str) -> str:
file = await client.upload_file(file_path) file = await client.upload_file(file_path)
return str(file) return str(file)
except Exception as e: except Exception as e:
return f"Error uploading file: {e}" return log_and_format_error("upload_file", e, file_path=file_path)
@mcp.tool() @mcp.tool()
@ -1847,7 +1897,7 @@ async def get_media_info(chat_id: int, message_id: int) -> str:
return "No media found in the specified message." return "No media found in the specified message."
return str(msg.media) return str(msg.media)
except Exception as e: except Exception as e:
return f"Error getting media info: {e}" return log_and_format_error("get_media_info", e, chat_id=chat_id, message_id=message_id)
@mcp.tool() @mcp.tool()
@ -1859,7 +1909,7 @@ async def search_public_chats(query: str) -> str:
result = await client(functions.contacts.SearchRequest(q=query, limit=20)) result = await client(functions.contacts.SearchRequest(q=query, limit=20))
return json.dumps([format_entity(u) for u in result.users], indent=2) return json.dumps([format_entity(u) for u in result.users], indent=2)
except Exception as e: except Exception as e:
return f"Error searching public chats: {e}" return log_and_format_error("search_public_chats", e, query=query)
@mcp.tool() @mcp.tool()
@ -1872,7 +1922,7 @@ async def search_messages(chat_id: int, query: str, limit: int = 20) -> str:
messages = await client.get_messages(entity, limit=limit, search=query) messages = await client.get_messages(entity, limit=limit, search=query)
return "\n".join([f"ID: {m.id} | {m.date} | {m.message}" for m in messages]) return "\n".join([f"ID: {m.id} | {m.date} | {m.message}" for m in messages])
except Exception as e: except Exception as e:
return f"Error searching messages: {e}" return log_and_format_error("search_messages", e, chat_id=chat_id, query=query, limit=limit)
@mcp.tool() @mcp.tool()
@ -1884,7 +1934,7 @@ async def resolve_username(username: str) -> str:
result = await client(functions.contacts.ResolveUsernameRequest(username=username)) result = await client(functions.contacts.ResolveUsernameRequest(username=username))
return str(result) return str(result)
except Exception as e: except Exception as e:
return f"Error resolving username: {e}" return log_and_format_error("resolve_username", e, username=username)
@mcp.tool() @mcp.tool()
@ -1916,10 +1966,10 @@ async def mute_chat(chat_id: int) -> str:
return f"Chat {chat_id} muted (using alternative method)." return f"Chat {chat_id} muted (using alternative method)."
except Exception as alt_e: except Exception as alt_e:
logger.exception(f"mute_chat (alt method) failed (chat_id={chat_id})") logger.exception(f"mute_chat (alt method) failed (chat_id={chat_id})")
return f"Error muting chat (alternative method): {alt_e}" return log_and_format_error("mute_chat", alt_e, chat_id=chat_id)
except Exception as e: except Exception as e:
logger.exception(f"mute_chat failed (chat_id={chat_id})") logger.exception(f"mute_chat failed (chat_id={chat_id})")
return f"Error muting chat: {e}" return log_and_format_error("mute_chat", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1951,10 +2001,10 @@ async def unmute_chat(chat_id: int) -> str:
return f"Chat {chat_id} unmuted (using alternative method)." return f"Chat {chat_id} unmuted (using alternative method)."
except Exception as alt_e: except Exception as alt_e:
logger.exception(f"unmute_chat (alt method) failed (chat_id={chat_id})") logger.exception(f"unmute_chat (alt method) failed (chat_id={chat_id})")
return f"Error unmuting chat (alternative method): {alt_e}" return log_and_format_error("unmute_chat", alt_e, chat_id=chat_id)
except Exception as e: except Exception as e:
logger.exception(f"unmute_chat failed (chat_id={chat_id})") logger.exception(f"unmute_chat failed (chat_id={chat_id})")
return f"Error unmuting chat: {e}" return log_and_format_error("unmute_chat", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1969,7 +2019,7 @@ async def archive_chat(chat_id: int) -> str:
)) ))
return f"Chat {chat_id} archived." return f"Chat {chat_id} archived."
except Exception as e: except Exception as e:
return f"Error archiving chat: {e}" return log_and_format_error("archive_chat", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1984,7 +2034,7 @@ async def unarchive_chat(chat_id: int) -> str:
)) ))
return f"Chat {chat_id} unarchived." return f"Chat {chat_id} unarchived."
except Exception as e: except Exception as e:
return f"Error unarchiving chat: {e}" return log_and_format_error("unarchive_chat", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -1996,7 +2046,7 @@ async def get_sticker_sets() -> str:
result = await client(functions.messages.GetAllStickersRequest(hash=0)) result = await client(functions.messages.GetAllStickersRequest(hash=0))
return json.dumps([s.title for s in result.sets], indent=2) return json.dumps([s.title for s in result.sets], indent=2)
except Exception as e: except Exception as e:
return f"Error getting sticker sets: {e}" return log_and_format_error("get_sticker_sets", e)
@mcp.tool() @mcp.tool()
@ -2018,7 +2068,7 @@ async def send_sticker(chat_id: int, file_path: str) -> str:
await client.send_file(entity, file_path, force_document=False) await client.send_file(entity, file_path, force_document=False)
return f"Sticker sent to chat {chat_id}." return f"Sticker sent to chat {chat_id}."
except Exception as e: except Exception as e:
return f"Error sending sticker: {e}" return log_and_format_error("send_sticker", e, chat_id=chat_id, file_path=file_path)
@mcp.tool() @mcp.tool()
@ -2058,7 +2108,7 @@ async def get_gif_search(query: str, limit: int = 10) -> str:
return f"Could not search GIFs using available methods: {inner_e}" return f"Could not search GIFs using available methods: {inner_e}"
except Exception as e: except Exception as e:
logger.exception(f"get_gif_search failed (query={query}, limit={limit})") logger.exception(f"get_gif_search failed (query={query}, limit={limit})")
return f"Error searching GIFs: {e}" return log_and_format_error("get_gif_search", e, query=query, limit=limit)
@mcp.tool() @mcp.tool()
@ -2076,7 +2126,7 @@ async def send_gif(chat_id: int, gif_id: int) -> str:
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:
return f"Error sending GIF: {e}" return log_and_format_error("send_gif", e, chat_id=chat_id, gif_id=gif_id)
@mcp.tool() @mcp.tool()
@ -2113,7 +2163,7 @@ async def get_bot_info(bot_username: str) -> str:
return json.dumps(info, indent=2) return json.dumps(info, indent=2)
except Exception as e: except Exception as e:
logger.exception(f"get_bot_info failed (bot_username={bot_username})") logger.exception(f"get_bot_info failed (bot_username={bot_username})")
return f"Error getting bot info: {e}" return log_and_format_error("get_bot_info", e, bot_username=bot_username)
@mcp.tool() @mcp.tool()
@ -2156,10 +2206,10 @@ async def set_bot_commands(bot_username: str, commands: list) -> str:
return f"Bot commands set for {bot_username}." return f"Bot commands set for {bot_username}."
except ImportError as ie: except ImportError as ie:
logger.exception(f"set_bot_commands failed - ImportError: {ie}") logger.exception(f"set_bot_commands failed - ImportError: {ie}")
return f"Error: Your Telethon version doesn't support SetBotCommandsRequest. Please update Telethon." return log_and_format_error("set_bot_commands", ie)
except Exception as e: except Exception as e:
logger.exception(f"set_bot_commands failed (bot_username={bot_username})") logger.exception(f"set_bot_commands failed (bot_username={bot_username})")
return f"Error setting bot commands: {e}" return log_and_format_error("set_bot_commands", e, bot_username=bot_username)
@mcp.tool() @mcp.tool()
@ -2172,7 +2222,7 @@ async def get_history(chat_id: int, limit: int = 100) -> str:
messages = await client.get_messages(entity, limit=limit) messages = await client.get_messages(entity, limit=limit)
return "\n".join([f"ID: {m.id} | {m.date} | {m.message}" for m in messages]) return "\n".join([f"ID: {m.id} | {m.date} | {m.message}" for m in messages])
except Exception as e: except Exception as e:
return f"Error getting history: {e}" return log_and_format_error("get_history", e, chat_id=chat_id, limit=limit)
@mcp.tool() @mcp.tool()
@ -2185,7 +2235,7 @@ async def get_user_photos(user_id: int, limit: int = 10) -> str:
photos = await client(functions.photos.GetUserPhotosRequest(user_id=user, offset=0, max_id=0, limit=limit)) photos = await client(functions.photos.GetUserPhotosRequest(user_id=user, offset=0, max_id=0, limit=limit))
return json.dumps([p.id for p in photos.photos], indent=2) return json.dumps([p.id for p in photos.photos], indent=2)
except Exception as e: except Exception as e:
return f"Error getting user photos: {e}" return log_and_format_error("get_user_photos", e, user_id=user_id, limit=limit)
@mcp.tool() @mcp.tool()
@ -2197,7 +2247,7 @@ async def get_user_status(user_id: int) -> str:
user = await client.get_entity(user_id) user = await client.get_entity(user_id)
return str(user.status) return str(user.status)
except Exception as e: except Exception as e:
return f"Error getting user status: {e}" return log_and_format_error("get_user_status", e, user_id=user_id)
@mcp.tool() @mcp.tool()
@ -2223,7 +2273,7 @@ async def get_recent_actions(chat_id: int) -> str:
return json.dumps([e.to_dict() for e in result.events], indent=2, default=json_serializer) return json.dumps([e.to_dict() for e in result.events], indent=2, default=json_serializer)
except Exception as e: except Exception as e:
logger.exception(f"get_recent_actions failed (chat_id={chat_id})") logger.exception(f"get_recent_actions failed (chat_id={chat_id})")
return f"Error getting recent actions: {e}" return log_and_format_error("get_recent_actions", e, chat_id=chat_id)
@mcp.tool() @mcp.tool()
@ -2249,7 +2299,7 @@ async def get_pinned_messages(chat_id: int) -> str:
return "\n".join([f"ID: {m.id} | {m.date} | {m.message or '[Media/No text]'}" for m in messages]) return "\n".join([f"ID: {m.id} | {m.date} | {m.message or '[Media/No text]'}" for m in messages])
except Exception as e: except Exception as e:
logger.exception(f"get_pinned_messages failed (chat_id={chat_id})") logger.exception(f"get_pinned_messages failed (chat_id={chat_id})")
return f"Error getting pinned messages: {e}" return log_and_format_error("get_pinned_messages", e, chat_id=chat_id)
if __name__ == "__main__": if __name__ == "__main__":