Initial commit: Telegram MCP server and bridge
This commit is contained in:
commit
f6a7d983e5
8 changed files with 1632 additions and 0 deletions
58
.gitignore
vendored
Normal file
58
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
myenv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.env
|
||||
|
||||
# Telegram session files
|
||||
telegram-bridge/store/*.session
|
||||
telegram-bridge/store/*.session-journal
|
||||
|
||||
# Database files with messages
|
||||
telegram-bridge/store/*.db
|
||||
telegram-bridge/store/*.db-shm
|
||||
telegram-bridge/store/*.db-wal
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Sensitive information
|
||||
telegram-bridge/.env
|
||||
*.key
|
||||
*.pem
|
||||
*.crt
|
||||
|
||||
# User-specific files
|
||||
claude_desktop_config.json
|
||||
mcp.json
|
||||
177
README.md
Normal file
177
README.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Telegram MCP Server
|
||||
|
||||
This is a Model Context Protocol (MCP) server for Telegram.
|
||||
|
||||
With this you can search your personal Telegram messages, search your contacts, and send messages to either individuals or groups.
|
||||
|
||||
It connects to your **personal Telegram account** directly via the Telegram API (using the [Telethon](https://github.com/LonamiWebs/Telethon) library). All your messages are stored locally in a SQLite database and only sent to an LLM (such as Claude) when the agent accesses them through tools (which you control).
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.6+
|
||||
- Anthropic Claude Desktop app (or Cursor)
|
||||
|
||||
### Steps
|
||||
|
||||
1. **Clone this repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/telegram-mcp.git
|
||||
cd telegram-mcp
|
||||
```
|
||||
Create a Python virtual environment and activate it:
|
||||
```bash
|
||||
python3 -m venv myenv
|
||||
source myenv/bin/activate
|
||||
```
|
||||
Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
This is the environment that can be used both for running the **Telegram bridge** and the **MCP server**.
|
||||
|
||||
|
||||
2. **Get your Telegram API credentials**
|
||||
|
||||
- Go to https://my.telegram.org/auth
|
||||
- Log in and go to "API development tools"
|
||||
- Create a new application
|
||||
- Note your API ID and API hash
|
||||
|
||||
3. **Run the Telegram bridge**
|
||||
|
||||
Navigate to the telegram-bridge directory:
|
||||
|
||||
```bash
|
||||
cd telegram-bridge
|
||||
```
|
||||
|
||||
Set up your API credentials as environment variables either by exporting them:
|
||||
|
||||
```bash
|
||||
export TELEGRAM_API_ID=your_api_id
|
||||
export TELEGRAM_API_HASH=your_api_hash
|
||||
```
|
||||
|
||||
Or by creating a `.env` file based on the provided `.env.example`:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # or use your preferred text editor
|
||||
```
|
||||
|
||||
Then update the values in the `.env` file:
|
||||
```
|
||||
TELEGRAM_API_ID=your_api_id
|
||||
TELEGRAM_API_HASH=your_api_hash
|
||||
```
|
||||
|
||||
Run the Python application:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
The first time you run it, you will be prompted to enter your phone number and the verification code sent to your Telegram account.
|
||||
|
||||
4. **Connect to the MCP server**
|
||||
|
||||
First, update the `run_telegram_server.sh` script with your absolute repository path:
|
||||
|
||||
```bash
|
||||
# Open the script in your preferred editor
|
||||
nano run_telegram_server.sh
|
||||
|
||||
# Update line 4 with your absolute path to the repository
|
||||
# Change this:
|
||||
BASE_DIR="/Users/muhammadabdullah/Desktop/mcp/telegram-mcp"
|
||||
# To your actual BASE_DIR path (get it by running `pwd` in the telegram-mcp directory)
|
||||
```
|
||||
|
||||
Then, configure the MCP server by creating a JSON configuration file with the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"telegram": {
|
||||
"command": "/bin/bash",
|
||||
"args": [
|
||||
"{{BASE_DIR}}/run_telegram_server.sh" // BASE_DIR is the same as above
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For **Claude**, save this as `claude_desktop_config.json` in your Claude Desktop configuration directory at:
|
||||
|
||||
```
|
||||
~/Library/Application\ Support/Claude/claude_desktop_config.json
|
||||
```
|
||||
|
||||
For **Cursor**, save this as `mcp.json` in your Cursor configuration directory at:
|
||||
|
||||
```
|
||||
~/.cursor/mcp.json
|
||||
```
|
||||
|
||||
5. **Restart Claude Desktop / Cursor**
|
||||
|
||||
Open Claude Desktop and you should now see Telegram as an available integration.
|
||||
|
||||
Or restart Cursor.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This application consists of two main components:
|
||||
|
||||
1. **Python Telegram Bridge** (`telegram-bridge/`): A Python application that connects to Telegram's API, handles authentication, and stores message history in SQLite. It serves as the bridge between Telegram and the MCP server. Can real-time sync for latest messages.
|
||||
|
||||
2. **Python MCP Server** (`telegram-mcp-server/`): A Python server implementing the Model Context Protocol (MCP), which provides standardized tools for Claude to interact with Telegram data and send/receive messages.
|
||||
|
||||
### Data Storage
|
||||
|
||||
- All message history is stored in a SQLite database within the `telegram-bridge/store/` directory
|
||||
- The database maintains tables for chats and messages
|
||||
- Messages are indexed for efficient searching and retrieval
|
||||
|
||||
## Usage
|
||||
|
||||
Once connected, you can interact with your Telegram contacts through Claude, leveraging Claude's AI capabilities in your Telegram conversations.
|
||||
|
||||
### MCP Tools
|
||||
|
||||
Claude can access the following tools to interact with Telegram:
|
||||
|
||||
- **search_contacts**: Search for contacts by name or username
|
||||
- **list_messages**: Retrieve messages with optional filters and context
|
||||
- **list_chats**: List available chats with metadata
|
||||
- **get_chat**: Get information about a specific chat
|
||||
- **get_direct_chat_by_contact**: Find a direct chat with a specific contact
|
||||
- **get_contact_chats**: List all chats involving a specific contact
|
||||
- **get_last_interaction**: Get the most recent message with a contact
|
||||
- **get_message_context**: Retrieve context around a specific message
|
||||
- **send_message**: Send a Telegram message to a specified username or chat ID
|
||||
|
||||
## Technical Details
|
||||
|
||||
1. Claude sends requests to the Python MCP server
|
||||
2. The MCP server queries the Telegram bridge or directly the SQLite database
|
||||
3. The bridge accesses the Telegram API and keeps the SQLite database up to date
|
||||
4. Data flows back through the chain to Claude
|
||||
5. When sending messages, the request flows from Claude through the MCP server to the Telegram bridge and to Telegram
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Make sure both the Telegram bridge and the Python server are running for the integration to work properly.
|
||||
|
||||
### Authentication Issues
|
||||
|
||||
- **Session expired**: If your session expires, you might need to re-authenticate. Delete the `telegram-bridge/store/telegram_session.session` file and restart the bridge.
|
||||
- **Two-factor authentication**: If you have 2FA enabled on your Telegram account, you'll be prompted for your password during authentication.
|
||||
- **No Messages Loading**: After initial authentication, it can take several minutes for your message history to load, especially if you have many chats.
|
||||
|
||||
For additional Claude Desktop integration troubleshooting, see the [MCP documentation](https://modelcontextprotocol.io/quickstart/server#claude-for-desktop-integration-issues).
|
||||
9
requirements.txt
Normal file
9
requirements.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Telegram Bridge dependencies
|
||||
telethon
|
||||
cryptg
|
||||
python-dotenv
|
||||
requests
|
||||
|
||||
# MCP Server dependencies
|
||||
fastmcp
|
||||
python-dateutil
|
||||
17
run_telegram_server.sh
Executable file
17
run_telegram_server.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Define the base directory and environment path
|
||||
BASE_DIR="/Users/muhammadabdullah/Desktop/mcp/telegram-mcp"
|
||||
ENV_PATH="$BASE_DIR/myenv"
|
||||
|
||||
# Check if the virtual environment exists
|
||||
if [ ! -d "$ENV_PATH" ]; then
|
||||
python3 -m venv "$ENV_PATH"
|
||||
source "$ENV_PATH/bin/activate"
|
||||
pip install -r "$BASE_DIR/requirements.txt" > /dev/null 2>&1
|
||||
else
|
||||
source "$ENV_PATH/bin/activate"
|
||||
fi
|
||||
|
||||
# Run the MCP server
|
||||
python "$BASE_DIR/telegram-mcp-server/main.py"
|
||||
4
telegram-bridge/.env.example
Normal file
4
telegram-bridge/.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Telegram API credentials
|
||||
# Get these from https://my.telegram.org/auth
|
||||
TELEGRAM_API_ID=your_app_id
|
||||
TELEGRAM_API_HASH=your_hash
|
||||
610
telegram-bridge/main.py
Normal file
610
telegram-bridge/main.py
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
"""
|
||||
Telegram Bridge Module
|
||||
|
||||
This module connects to the Telegram API using the Telethon library, enabling
|
||||
interaction with Telegram chats and messages. It provides an HTTP server for
|
||||
sending messages to Telegram users or groups, and stores message history in
|
||||
a SQLite database.
|
||||
|
||||
Key Features:
|
||||
- Connects to Telegram using API credentials.
|
||||
- Stores chat and message data in a database.
|
||||
- Provides an HTTP API for sending messages.
|
||||
- Synchronizes message history and processes new messages.
|
||||
|
||||
Usage:
|
||||
- Ensure TELEGRAM_API_ID and TELEGRAM_API_HASH are set in environment variables.
|
||||
- Run the script to start the Telegram bridge and HTTP server.
|
||||
- Use the '/api/send' endpoint to send messages via HTTP requests.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
import threading
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
from telethon.tl.types import (
|
||||
User,
|
||||
Chat,
|
||||
Channel,
|
||||
Message,
|
||||
Dialog,
|
||||
)
|
||||
from telethon.utils import get_display_name
|
||||
|
||||
# Global variable to store the main event loop
|
||||
main_loop = None
|
||||
|
||||
# Load environment variables from .env file if it exists
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[logging.StreamHandler(sys.stdout)],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Directory for storing data
|
||||
STORE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "store")
|
||||
os.makedirs(STORE_DIR, exist_ok=True)
|
||||
|
||||
# Database path
|
||||
DB_PATH = os.path.join(STORE_DIR, "messages.db")
|
||||
|
||||
# API credentials from environment variables
|
||||
API_ID = os.getenv("TELEGRAM_API_ID")
|
||||
API_HASH = os.getenv("TELEGRAM_API_HASH")
|
||||
|
||||
if not API_ID or not API_HASH:
|
||||
logger.error(
|
||||
"TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables must be set"
|
||||
)
|
||||
logger.error("Get them from https://my.telegram.org/auth")
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize the Telegram client
|
||||
SESSION_FILE = os.path.join(STORE_DIR, "telegram_session")
|
||||
client = TelegramClient(SESSION_FILE, API_ID, API_HASH)
|
||||
|
||||
|
||||
class MessageStore:
|
||||
"""Handles storage and retrieval of Telegram messages in SQLite."""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
"""Initialize the message store with the given database path."""
|
||||
self.db_path = db_path
|
||||
self.init_db()
|
||||
|
||||
def init_db(self):
|
||||
"""Initialize the database with necessary tables."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create tables if they don't exist
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS chats (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT,
|
||||
username TEXT,
|
||||
type TEXT,
|
||||
last_message_time TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER,
|
||||
chat_id INTEGER,
|
||||
sender_id INTEGER,
|
||||
sender_name TEXT,
|
||||
content TEXT,
|
||||
timestamp TIMESTAMP,
|
||||
is_from_me BOOLEAN,
|
||||
PRIMARY KEY (id, chat_id),
|
||||
FOREIGN KEY (chat_id) REFERENCES chats(id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# Create indexes for efficient queries
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id)"
|
||||
)
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)"
|
||||
)
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_content ON messages(content)"
|
||||
)
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_sender_id ON messages(sender_id)"
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def store_chat(
|
||||
self,
|
||||
chat_id: int,
|
||||
title: str,
|
||||
username: Optional[str],
|
||||
chat_type: str,
|
||||
last_message_time: datetime,
|
||||
) -> None:
|
||||
"""Store a chat in the database."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO chats (id, title, username, type, last_message_time) VALUES (?, ?, ?, ?, ?)",
|
||||
(chat_id, title, username, chat_type, last_message_time.isoformat()),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def store_message(
|
||||
self,
|
||||
message_id: int,
|
||||
chat_id: int,
|
||||
sender_id: int,
|
||||
sender_name: str,
|
||||
content: str,
|
||||
timestamp: datetime,
|
||||
is_from_me: bool,
|
||||
) -> None:
|
||||
"""Store a message in the database."""
|
||||
if not content: # Skip empty messages
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO messages
|
||||
(id, chat_id, sender_id, sender_name, content, timestamp, is_from_me)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
message_id,
|
||||
chat_id,
|
||||
sender_id,
|
||||
sender_name,
|
||||
content,
|
||||
timestamp.isoformat(),
|
||||
is_from_me,
|
||||
),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_messages(
|
||||
self,
|
||||
chat_id: Optional[int] = None,
|
||||
limit: int = 50,
|
||||
query: Optional[str] = None,
|
||||
offset: int = 0,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get messages from the database."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
query_parts = [
|
||||
"SELECT m.id, m.chat_id, c.title, m.sender_name, m.content, m.timestamp, m.is_from_me, m.sender_id FROM messages m"
|
||||
]
|
||||
query_parts.append("JOIN chats c ON m.chat_id = c.id")
|
||||
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
if chat_id:
|
||||
conditions.append("m.chat_id = ?")
|
||||
params.append(chat_id)
|
||||
|
||||
if query:
|
||||
conditions.append("m.content LIKE ?")
|
||||
params.append(f"%{query}%")
|
||||
|
||||
if conditions:
|
||||
query_parts.append("WHERE " + " AND ".join(conditions))
|
||||
|
||||
query_parts.append("ORDER BY m.timestamp DESC")
|
||||
query_parts.append("LIMIT ? OFFSET ?")
|
||||
params.extend([limit, offset])
|
||||
|
||||
cursor.execute(" ".join(query_parts), tuple(params))
|
||||
messages = cursor.fetchall()
|
||||
|
||||
results = []
|
||||
for msg in messages:
|
||||
timestamp = datetime.fromisoformat(msg[5])
|
||||
results.append(
|
||||
{
|
||||
"id": msg[0],
|
||||
"chat_id": msg[1],
|
||||
"chat_title": msg[2],
|
||||
"sender_name": msg[3],
|
||||
"content": msg[4],
|
||||
"timestamp": timestamp,
|
||||
"is_from_me": msg[6],
|
||||
"sender_id": msg[7],
|
||||
}
|
||||
)
|
||||
|
||||
conn.close()
|
||||
return results
|
||||
|
||||
def get_chats(
|
||||
self, limit: int = 50, query: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get chats from the database."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
query_parts = ["SELECT id, title, username, type, last_message_time FROM chats"]
|
||||
params = []
|
||||
|
||||
if query:
|
||||
query_parts.append("WHERE title LIKE ? OR username LIKE ?")
|
||||
params.extend([f"%{query}%", f"%{query}%"])
|
||||
|
||||
query_parts.append("ORDER BY last_message_time DESC")
|
||||
query_parts.append("LIMIT ?")
|
||||
params.append(limit)
|
||||
|
||||
cursor.execute(" ".join(query_parts), tuple(params))
|
||||
chats = cursor.fetchall()
|
||||
|
||||
results = []
|
||||
for chat in chats:
|
||||
last_message_time = datetime.fromisoformat(chat[4]) if chat[4] else None
|
||||
results.append(
|
||||
{
|
||||
"id": chat[0],
|
||||
"title": chat[1],
|
||||
"username": chat[2],
|
||||
"type": chat[3],
|
||||
"last_message_time": last_message_time,
|
||||
}
|
||||
)
|
||||
|
||||
conn.close()
|
||||
return results
|
||||
|
||||
|
||||
# Create message store
|
||||
message_store = MessageStore(DB_PATH)
|
||||
|
||||
|
||||
async def process_message(message: Message) -> None:
|
||||
"""Process and store a message."""
|
||||
if not message.text:
|
||||
return # Skip non-text messages
|
||||
|
||||
# Get the chat
|
||||
chat = message.chat
|
||||
if not chat:
|
||||
return
|
||||
|
||||
chat_id = message.chat_id
|
||||
|
||||
# Determine chat type and name
|
||||
if isinstance(chat, User):
|
||||
chat_type = "user"
|
||||
title = get_display_name(chat)
|
||||
username = chat.username
|
||||
elif isinstance(chat, Chat):
|
||||
chat_type = "group"
|
||||
title = chat.title
|
||||
username = None
|
||||
elif isinstance(chat, Channel):
|
||||
chat_type = "channel" if chat.broadcast else "supergroup"
|
||||
title = chat.title
|
||||
username = chat.username
|
||||
else:
|
||||
logger.warning(f"Unknown chat type: {type(chat)}")
|
||||
return
|
||||
|
||||
# Store chat information
|
||||
message_store.store_chat(
|
||||
chat_id=chat_id,
|
||||
title=title,
|
||||
username=username,
|
||||
chat_type=chat_type,
|
||||
last_message_time=message.date,
|
||||
)
|
||||
|
||||
# Get sender information
|
||||
sender = await message.get_sender()
|
||||
sender_id = sender.id if sender else 0
|
||||
sender_name = get_display_name(sender) if sender else "Unknown"
|
||||
|
||||
# Check if the message is from the current user
|
||||
my_id = (await client.get_me()).id
|
||||
is_from_me = sender_id == my_id
|
||||
|
||||
# Store the message
|
||||
message_store.store_message(
|
||||
message_id=message.id,
|
||||
chat_id=chat_id,
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
content=message.text,
|
||||
timestamp=message.date,
|
||||
is_from_me=is_from_me,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Stored message: [{message.date}] {sender_name} in {title}: {message.text[:30]}..."
|
||||
)
|
||||
|
||||
|
||||
async def sync_dialog_history(dialog: Dialog, limit: int = 100) -> None:
|
||||
"""Sync message history for a specific dialog."""
|
||||
chat_entity = dialog.entity
|
||||
|
||||
# Extract chat info
|
||||
if isinstance(chat_entity, User):
|
||||
chat_type = "user"
|
||||
title = get_display_name(chat_entity)
|
||||
username = chat_entity.username
|
||||
elif isinstance(chat_entity, Chat):
|
||||
chat_type = "group"
|
||||
title = chat_entity.title
|
||||
username = None
|
||||
elif isinstance(chat_entity, Channel):
|
||||
chat_type = "channel" if chat_entity.broadcast else "supergroup"
|
||||
title = chat_entity.title
|
||||
username = chat_entity.username
|
||||
else:
|
||||
logger.warning(f"Unknown chat type: {type(chat_entity)}")
|
||||
return
|
||||
|
||||
# Store chat info with last message time
|
||||
message_store.store_chat(
|
||||
chat_id=dialog.id,
|
||||
title=title,
|
||||
username=username,
|
||||
chat_type=chat_type,
|
||||
last_message_time=dialog.date,
|
||||
)
|
||||
|
||||
# Get messages
|
||||
messages = await client.get_messages(dialog.entity, limit=limit)
|
||||
|
||||
# Get current user ID
|
||||
my_id = (await client.get_me()).id
|
||||
|
||||
# Process each message
|
||||
for message in messages:
|
||||
if not message.text:
|
||||
continue # Skip non-text messages
|
||||
|
||||
# Get sender information
|
||||
try:
|
||||
sender = await message.get_sender()
|
||||
sender_id = sender.id if sender else 0
|
||||
sender_name = get_display_name(sender) if sender else "Unknown"
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting sender: {e}")
|
||||
sender_id = 0
|
||||
sender_name = "Unknown"
|
||||
|
||||
is_from_me = sender_id == my_id
|
||||
|
||||
# Store the message
|
||||
message_store.store_message(
|
||||
message_id=message.id,
|
||||
chat_id=dialog.id,
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
content=message.text,
|
||||
timestamp=message.date,
|
||||
is_from_me=is_from_me,
|
||||
)
|
||||
|
||||
logger.info(f"Synced {len(messages)} messages from {title}")
|
||||
|
||||
|
||||
async def sync_all_dialogs() -> None:
|
||||
"""Sync message history for all dialogs."""
|
||||
logger.info("Starting synchronization of all dialogs")
|
||||
|
||||
# Get all dialogs (chats)
|
||||
dialogs = await client.get_dialogs(limit=100)
|
||||
|
||||
for dialog in dialogs:
|
||||
try:
|
||||
await sync_dialog_history(dialog)
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing dialog {dialog.name}: {e}")
|
||||
|
||||
logger.info(f"Completed synchronization of {len(dialogs)} dialogs")
|
||||
|
||||
|
||||
# HTTP server for API endpoints
|
||||
class TelegramAPIHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers["Content-Length"])
|
||||
post_data = self.rfile.read(content_length)
|
||||
request = json.loads(post_data.decode("utf-8"))
|
||||
|
||||
if self.path == "/api/send":
|
||||
self._handle_send_message(request)
|
||||
else:
|
||||
self.send_error(404, "Endpoint not found")
|
||||
|
||||
def _handle_send_message(self, request):
|
||||
recipient = request.get("recipient")
|
||||
message_text = request.get("message")
|
||||
|
||||
if not recipient or not message_text:
|
||||
self._send_json_response(
|
||||
400, {"success": False, "message": "Recipient and message are required"}
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
# Instead of creating a new event loop, use a shared queue to communicate with the main thread
|
||||
# Create a Future to hold the result
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
send_message(recipient, message_text), main_loop
|
||||
)
|
||||
|
||||
# Wait for the result (with timeout)
|
||||
try:
|
||||
success, message = future.result(10) # Wait up to 10 seconds
|
||||
self._send_json_response(
|
||||
200 if success else 500, {"success": success, "message": message}
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
self._send_json_response(
|
||||
504,
|
||||
{
|
||||
"success": False,
|
||||
"message": "Request timed out while sending message",
|
||||
},
|
||||
)
|
||||
except Exception as inner_e:
|
||||
logger.error(f"Error in Future: {inner_e}")
|
||||
self._send_json_response(
|
||||
500,
|
||||
{"success": False, "message": f"Error in Future: {str(inner_e)}"},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message: {e}")
|
||||
self._send_json_response(
|
||||
500, {"success": False, "message": f"Error: {str(e)}"}
|
||||
)
|
||||
|
||||
def _send_json_response(self, status_code, data):
|
||||
self.send_response(status_code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode("utf-8"))
|
||||
|
||||
|
||||
async def send_message(recipient: str, message: str) -> Tuple[bool, str]:
|
||||
"""Send a message to a Telegram recipient."""
|
||||
if not client.is_connected():
|
||||
return False, "Not connected to Telegram"
|
||||
|
||||
try:
|
||||
# Try to parse recipient as an integer (chat ID)
|
||||
try:
|
||||
chat_id = int(recipient)
|
||||
entity = await client.get_entity(chat_id)
|
||||
except ValueError:
|
||||
# Not an integer, try as username
|
||||
if recipient.startswith("@"):
|
||||
recipient = recipient[1:] # Remove @ if present
|
||||
try:
|
||||
entity = await client.get_entity(recipient)
|
||||
except Exception:
|
||||
# Try to find in database
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT id FROM chats WHERE title LIKE ? OR username = ?",
|
||||
(f"%{recipient}%", recipient),
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if result:
|
||||
entity = await client.get_entity(result[0])
|
||||
else:
|
||||
return False, f"Recipient not found: {recipient}"
|
||||
|
||||
# Send the message
|
||||
await client.send_message(entity, message)
|
||||
return True, f"Message sent to {recipient}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message: {e}")
|
||||
return False, f"Error sending message: {str(e)}"
|
||||
|
||||
|
||||
# Start HTTP server in a separate thread
|
||||
def start_http_server(port: int = 8081):
|
||||
server_address = ("", port)
|
||||
httpd = HTTPServer(server_address, TelegramAPIHandler)
|
||||
logger.info(f"Starting HTTP server on port {port}")
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
async def main():
|
||||
global main_loop
|
||||
|
||||
# Store the current event loop
|
||||
main_loop = asyncio.get_event_loop()
|
||||
|
||||
logger.info("Starting Telegram bridge")
|
||||
|
||||
# Connect to Telegram
|
||||
await client.connect()
|
||||
|
||||
# Check if we're already authorized
|
||||
if not await client.is_user_authorized():
|
||||
logger.info("Need to log in. Please enter your phone number:")
|
||||
phone = input("Phone number: ")
|
||||
await client.send_code_request(phone)
|
||||
logger.info("Code sent. Please enter the code you received:")
|
||||
code = input("Code: ")
|
||||
try:
|
||||
await client.sign_in(phone, code)
|
||||
except Exception as e:
|
||||
logger.error(f"Error signing in: {e}")
|
||||
logger.info(
|
||||
"If you have two-factor authentication enabled, please enter your password:"
|
||||
)
|
||||
password = input("Password: ")
|
||||
await client.sign_in(password=password)
|
||||
|
||||
logger.info("Successfully logged in to Telegram")
|
||||
|
||||
# Start HTTP server in a separate thread
|
||||
server_thread = threading.Thread(target=start_http_server)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Register event handler for new messages
|
||||
@client.on(events.NewMessage)
|
||||
async def handle_new_message(event):
|
||||
await process_message(event.message)
|
||||
|
||||
# Initial sync of message history
|
||||
await sync_all_dialogs()
|
||||
|
||||
# Keep the script running
|
||||
logger.info("Telegram bridge is running. Press Ctrl+C to exit.")
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the main function
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Shutting down Telegram bridge")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
193
telegram-mcp-server/main.py
Normal file
193
telegram-mcp-server/main.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from datetime import datetime
|
||||
from telegram import (
|
||||
search_contacts as telegram_search_contacts,
|
||||
list_messages as telegram_list_messages,
|
||||
list_chats as telegram_list_chats,
|
||||
get_chat as telegram_get_chat,
|
||||
get_direct_chat_by_contact as telegram_get_direct_chat_by_contact,
|
||||
get_contact_chats as telegram_get_contact_chats,
|
||||
get_last_interaction as telegram_get_last_interaction,
|
||||
get_message_context as telegram_get_message_context,
|
||||
send_message as telegram_send_message
|
||||
)
|
||||
|
||||
# Initialize FastMCP server
|
||||
mcp = FastMCP("telegram")
|
||||
|
||||
@mcp.tool()
|
||||
def search_contacts(query: str) -> List[Dict[str, Any]]:
|
||||
"""Search Telegram contacts by name or username.
|
||||
|
||||
Args:
|
||||
query: Search term to match against contact names or usernames
|
||||
"""
|
||||
contacts = telegram_search_contacts(query)
|
||||
return contacts
|
||||
|
||||
@mcp.tool()
|
||||
def list_messages(
|
||||
date_range: Optional[Tuple[datetime, datetime]] = None,
|
||||
sender_id: Optional[int] = None,
|
||||
chat_id: Optional[int] = None,
|
||||
query: Optional[str] = None,
|
||||
limit: int = 20,
|
||||
page: int = 0,
|
||||
include_context: bool = True,
|
||||
context_before: int = 1,
|
||||
context_after: int = 1
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get Telegram messages matching specified criteria with optional context.
|
||||
|
||||
Args:
|
||||
date_range: Optional tuple of (start_date, end_date) to filter messages by date
|
||||
sender_id: Optional sender ID to filter messages by sender
|
||||
chat_id: Optional chat ID to filter messages by chat
|
||||
query: Optional search term to filter messages by content
|
||||
limit: Maximum number of messages to return (default 20)
|
||||
page: Page number for pagination (default 0)
|
||||
include_context: Whether to include messages before and after matches (default True)
|
||||
context_before: Number of messages to include before each match (default 1)
|
||||
context_after: Number of messages to include after each match (default 1)
|
||||
"""
|
||||
messages = telegram_list_messages(
|
||||
date_range=date_range,
|
||||
sender_id=sender_id,
|
||||
chat_id=chat_id,
|
||||
query=query,
|
||||
limit=limit,
|
||||
page=page,
|
||||
include_context=include_context,
|
||||
context_before=context_before,
|
||||
context_after=context_after
|
||||
)
|
||||
return messages
|
||||
|
||||
@mcp.tool()
|
||||
def list_chats(
|
||||
query: Optional[str] = None,
|
||||
limit: int = 20,
|
||||
page: int = 0,
|
||||
chat_type: Optional[str] = None,
|
||||
sort_by: str = "last_active"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get Telegram chats matching specified criteria.
|
||||
|
||||
Args:
|
||||
query: Optional search term to filter chats by name or username
|
||||
limit: Maximum number of chats to return (default 20)
|
||||
page: Page number for pagination (default 0)
|
||||
chat_type: Optional chat type filter ("user", "group", "channel", or "supergroup")
|
||||
sort_by: Field to sort results by, either "last_active" or "title" (default "last_active")
|
||||
"""
|
||||
chats = telegram_list_chats(
|
||||
query=query,
|
||||
limit=limit,
|
||||
page=page,
|
||||
chat_type=chat_type,
|
||||
sort_by=sort_by
|
||||
)
|
||||
return chats
|
||||
|
||||
@mcp.tool()
|
||||
def get_chat(chat_id: int) -> Dict[str, Any]:
|
||||
"""Get Telegram chat metadata by ID.
|
||||
|
||||
Args:
|
||||
chat_id: The ID of the chat to retrieve
|
||||
"""
|
||||
chat = telegram_get_chat(chat_id)
|
||||
return chat
|
||||
|
||||
@mcp.tool()
|
||||
def get_direct_chat_by_contact(contact_id: int) -> Dict[str, Any]:
|
||||
"""Get Telegram chat metadata by contact ID.
|
||||
|
||||
Args:
|
||||
contact_id: The contact ID to search for
|
||||
"""
|
||||
chat = telegram_get_direct_chat_by_contact(contact_id)
|
||||
return chat
|
||||
|
||||
@mcp.tool()
|
||||
def get_contact_chats(contact_id: int, limit: int = 20, page: int = 0) -> List[Dict[str, Any]]:
|
||||
"""Get all Telegram chats involving the contact.
|
||||
|
||||
Args:
|
||||
contact_id: The contact's ID to search for
|
||||
limit: Maximum number of chats to return (default 20)
|
||||
page: Page number for pagination (default 0)
|
||||
"""
|
||||
chats = telegram_get_contact_chats(contact_id, limit, page)
|
||||
return chats
|
||||
|
||||
@mcp.tool()
|
||||
def get_last_interaction(contact_id: int) -> Dict[str, Any]:
|
||||
"""Get most recent Telegram message involving the contact.
|
||||
|
||||
Args:
|
||||
contact_id: The ID of the contact to search for
|
||||
"""
|
||||
message = telegram_get_last_interaction(contact_id)
|
||||
return message
|
||||
|
||||
@mcp.tool()
|
||||
def get_message_context(
|
||||
message_id: int,
|
||||
chat_id: int,
|
||||
before: int = 5,
|
||||
after: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
"""Get context around a specific Telegram message.
|
||||
|
||||
Args:
|
||||
message_id: The ID of the message to get context for
|
||||
chat_id: The ID of the chat containing the message
|
||||
before: Number of messages to include before the target message (default 5)
|
||||
after: Number of messages to include after the target message (default 5)
|
||||
"""
|
||||
context = telegram_get_message_context(message_id, chat_id, before, after)
|
||||
return context
|
||||
|
||||
@mcp.tool()
|
||||
def send_message(
|
||||
recipient: str,
|
||||
message: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Send a Telegram message to a person or group.
|
||||
|
||||
Args:
|
||||
recipient: The recipient - either a username (with or without @) or a chat ID
|
||||
message: The message text to send
|
||||
|
||||
Returns:
|
||||
A dictionary containing success status and a status message
|
||||
"""
|
||||
# Validate input
|
||||
if not recipient:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Recipient must be provided"
|
||||
}
|
||||
|
||||
# Call the telegram_send_message function
|
||||
success, status_message = telegram_send_message(recipient, message)
|
||||
return {
|
||||
"success": success,
|
||||
"message": status_message
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Redirect stdout/stderr to suppress initial output that might confuse Claude
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Create logs directory
|
||||
os.makedirs(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs'), exist_ok=True)
|
||||
|
||||
# Redirect stderr to log file
|
||||
sys.stderr = open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs', 'mcp_error.log'), 'w')
|
||||
|
||||
# Initialize and run the server
|
||||
mcp.run(transport='stdio')
|
||||
564
telegram-mcp-server/telegram.py
Normal file
564
telegram-mcp-server/telegram.py
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
import sqlite3
|
||||
import requests
|
||||
import json
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Tuple, Dict, Any
|
||||
|
||||
# Database path
|
||||
MESSAGES_DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'telegram-bridge', 'store', 'messages.db')
|
||||
TELEGRAM_API_BASE_URL = "http://localhost:8081/api"
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
id: int
|
||||
chat_id: int
|
||||
chat_title: str
|
||||
sender_name: str
|
||||
content: str
|
||||
timestamp: datetime
|
||||
is_from_me: bool
|
||||
sender_id: int
|
||||
|
||||
@dataclass
|
||||
class Chat:
|
||||
id: int
|
||||
title: str
|
||||
username: Optional[str]
|
||||
type: str
|
||||
last_message_time: Optional[datetime]
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
id: int
|
||||
username: Optional[str]
|
||||
name: str
|
||||
|
||||
@dataclass
|
||||
class MessageContext:
|
||||
message: Message
|
||||
before: List[Message]
|
||||
after: List[Message]
|
||||
|
||||
def print_message(message: Message, show_chat_info: bool = True) -> None:
|
||||
"""Print a single message with consistent formatting."""
|
||||
direction = "→" if message.is_from_me else "←"
|
||||
|
||||
if show_chat_info:
|
||||
print(f"[{message.timestamp:%Y-%m-%d %H:%M:%S}] {direction} Chat: {message.chat_title} (ID: {message.chat_id})")
|
||||
else:
|
||||
print(f"[{message.timestamp:%Y-%m-%d %H:%M:%S}] {direction}")
|
||||
|
||||
print(f"From: {'Me' if message.is_from_me else message.sender_name}")
|
||||
print(f"Message: {message.content}")
|
||||
print("-" * 100)
|
||||
|
||||
def print_messages_list(messages: List[Message], title: str = "", show_chat_info: bool = True) -> None:
|
||||
"""Print a list of messages with a title and consistent formatting."""
|
||||
if not messages:
|
||||
print("No messages to display.")
|
||||
return
|
||||
|
||||
if title:
|
||||
print(f"\n{title}")
|
||||
print("-" * 100)
|
||||
|
||||
for message in messages:
|
||||
print_message(message, show_chat_info)
|
||||
|
||||
def print_chat(chat: Chat) -> None:
|
||||
"""Print a single chat with consistent formatting."""
|
||||
print(f"Chat: {chat.title} (ID: {chat.id})")
|
||||
print(f"Type: {chat.type}")
|
||||
if chat.username:
|
||||
print(f"Username: @{chat.username}")
|
||||
if chat.last_message_time:
|
||||
print(f"Last active: {chat.last_message_time:%Y-%m-%d %H:%M:%S}")
|
||||
print("-" * 100)
|
||||
|
||||
def print_chats_list(chats: List[Chat], title: str = "") -> None:
|
||||
"""Print a list of chats with a title and consistent formatting."""
|
||||
if not chats:
|
||||
print("No chats to display.")
|
||||
return
|
||||
|
||||
if title:
|
||||
print(f"\n{title}")
|
||||
print("-" * 100)
|
||||
|
||||
for chat in chats:
|
||||
print_chat(chat)
|
||||
|
||||
def search_contacts(query: str) -> List[Contact]:
|
||||
"""Search contacts by name or username."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Search in chats where type is 'user'
|
||||
cursor.execute("""
|
||||
SELECT id, username, title
|
||||
FROM chats
|
||||
WHERE type = 'user' AND (title LIKE ? OR username LIKE ?)
|
||||
ORDER BY title
|
||||
LIMIT 50
|
||||
""", (f"%{query}%", f"%{query}%"))
|
||||
|
||||
contacts = cursor.fetchall()
|
||||
|
||||
result = []
|
||||
for contact_data in contacts:
|
||||
contact = Contact(
|
||||
id=contact_data[0],
|
||||
username=contact_data[1],
|
||||
name=contact_data[2]
|
||||
)
|
||||
result.append(contact)
|
||||
|
||||
return result
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def list_messages(
|
||||
date_range: Optional[Tuple[datetime, datetime]] = None,
|
||||
sender_id: Optional[int] = None,
|
||||
chat_id: Optional[int] = None,
|
||||
query: Optional[str] = None,
|
||||
limit: int = 20,
|
||||
page: int = 0,
|
||||
include_context: bool = True,
|
||||
context_before: int = 1,
|
||||
context_after: int = 1
|
||||
) -> List[Message]:
|
||||
"""Get messages matching the specified criteria with optional context."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build base query
|
||||
query_parts = ["""
|
||||
SELECT
|
||||
m.id,
|
||||
m.chat_id,
|
||||
c.title,
|
||||
m.sender_name,
|
||||
m.content,
|
||||
m.timestamp,
|
||||
m.is_from_me,
|
||||
m.sender_id
|
||||
FROM messages m
|
||||
"""]
|
||||
query_parts.append("JOIN chats c ON m.chat_id = c.id")
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
# Add filters
|
||||
if date_range:
|
||||
where_clauses.append("m.timestamp BETWEEN ? AND ?")
|
||||
params.extend([date_range[0].isoformat(), date_range[1].isoformat()])
|
||||
|
||||
if sender_id:
|
||||
where_clauses.append("m.sender_id = ?")
|
||||
params.append(sender_id)
|
||||
|
||||
if chat_id:
|
||||
where_clauses.append("m.chat_id = ?")
|
||||
params.append(chat_id)
|
||||
|
||||
if query:
|
||||
where_clauses.append("LOWER(m.content) LIKE LOWER(?)")
|
||||
params.append(f"%{query}%")
|
||||
|
||||
if where_clauses:
|
||||
query_parts.append("WHERE " + " AND ".join(where_clauses))
|
||||
|
||||
# Add pagination
|
||||
offset = page * limit
|
||||
query_parts.append("ORDER BY m.timestamp DESC")
|
||||
query_parts.append("LIMIT ? OFFSET ?")
|
||||
params.extend([limit, offset])
|
||||
|
||||
cursor.execute(" ".join(query_parts), tuple(params))
|
||||
messages = cursor.fetchall()
|
||||
|
||||
result = []
|
||||
for msg in messages:
|
||||
message = Message(
|
||||
id=msg[0],
|
||||
chat_id=msg[1],
|
||||
chat_title=msg[2],
|
||||
sender_name=msg[3],
|
||||
content=msg[4],
|
||||
timestamp=datetime.fromisoformat(msg[5]),
|
||||
is_from_me=bool(msg[6]),
|
||||
sender_id=msg[7]
|
||||
)
|
||||
result.append(message)
|
||||
|
||||
if include_context and result:
|
||||
# Add context for each message
|
||||
messages_with_context = []
|
||||
for msg in result:
|
||||
context = get_message_context(msg.id, msg.chat_id, context_before, context_after)
|
||||
messages_with_context.extend(context.before)
|
||||
messages_with_context.append(context.message)
|
||||
messages_with_context.extend(context.after)
|
||||
return messages_with_context
|
||||
|
||||
return result
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def get_message_context(
|
||||
message_id: int,
|
||||
chat_id: int,
|
||||
before: int = 5,
|
||||
after: int = 5
|
||||
) -> MessageContext:
|
||||
"""Get context around a specific message."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get the target message first
|
||||
cursor.execute("""
|
||||
SELECT m.id, m.chat_id, c.title, m.sender_name, m.content, m.timestamp, m.is_from_me, m.sender_id
|
||||
FROM messages m
|
||||
JOIN chats c ON m.chat_id = c.id
|
||||
WHERE m.id = ? AND m.chat_id = ?
|
||||
""", (message_id, chat_id))
|
||||
msg_data = cursor.fetchone()
|
||||
|
||||
if not msg_data:
|
||||
raise ValueError(f"Message with ID {message_id} in chat {chat_id} not found")
|
||||
|
||||
target_message = Message(
|
||||
id=msg_data[0],
|
||||
chat_id=msg_data[1],
|
||||
chat_title=msg_data[2],
|
||||
sender_name=msg_data[3],
|
||||
content=msg_data[4],
|
||||
timestamp=datetime.fromisoformat(msg_data[5]),
|
||||
is_from_me=bool(msg_data[6]),
|
||||
sender_id=msg_data[7]
|
||||
)
|
||||
|
||||
# Get messages before
|
||||
cursor.execute("""
|
||||
SELECT m.id, m.chat_id, c.title, m.sender_name, m.content, m.timestamp, m.is_from_me, m.sender_id
|
||||
FROM messages m
|
||||
JOIN chats c ON m.chat_id = c.id
|
||||
WHERE m.chat_id = ? AND m.timestamp < ?
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT ?
|
||||
""", (chat_id, target_message.timestamp.isoformat(), before))
|
||||
|
||||
before_messages = []
|
||||
for msg in cursor.fetchall():
|
||||
before_messages.append(Message(
|
||||
id=msg[0],
|
||||
chat_id=msg[1],
|
||||
chat_title=msg[2],
|
||||
sender_name=msg[3],
|
||||
content=msg[4],
|
||||
timestamp=datetime.fromisoformat(msg[5]),
|
||||
is_from_me=bool(msg[6]),
|
||||
sender_id=msg[7]
|
||||
))
|
||||
|
||||
# Get messages after
|
||||
cursor.execute("""
|
||||
SELECT m.id, m.chat_id, c.title, m.sender_name, m.content, m.timestamp, m.is_from_me, m.sender_id
|
||||
FROM messages m
|
||||
JOIN chats c ON m.chat_id = c.id
|
||||
WHERE m.chat_id = ? AND m.timestamp > ?
|
||||
ORDER BY m.timestamp ASC
|
||||
LIMIT ?
|
||||
""", (chat_id, target_message.timestamp.isoformat(), after))
|
||||
|
||||
after_messages = []
|
||||
for msg in cursor.fetchall():
|
||||
after_messages.append(Message(
|
||||
id=msg[0],
|
||||
chat_id=msg[1],
|
||||
chat_title=msg[2],
|
||||
sender_name=msg[3],
|
||||
content=msg[4],
|
||||
timestamp=datetime.fromisoformat(msg[5]),
|
||||
is_from_me=bool(msg[6]),
|
||||
sender_id=msg[7]
|
||||
))
|
||||
|
||||
return MessageContext(
|
||||
message=target_message,
|
||||
before=before_messages,
|
||||
after=after_messages
|
||||
)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
raise
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def list_chats(
|
||||
query: Optional[str] = None,
|
||||
limit: int = 20,
|
||||
page: int = 0,
|
||||
chat_type: Optional[str] = None,
|
||||
sort_by: str = "last_active"
|
||||
) -> List[Chat]:
|
||||
"""Get chats matching the specified criteria."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build base query
|
||||
query_parts = ["SELECT id, title, username, type, last_message_time FROM chats"]
|
||||
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if query:
|
||||
where_clauses.append("(LOWER(title) LIKE LOWER(?) OR LOWER(username) LIKE LOWER(?))")
|
||||
params.extend([f"%{query}%", f"%{query}%"])
|
||||
|
||||
if chat_type:
|
||||
where_clauses.append("type = ?")
|
||||
params.append(chat_type)
|
||||
|
||||
if where_clauses:
|
||||
query_parts.append("WHERE " + " AND ".join(where_clauses))
|
||||
|
||||
# Add sorting
|
||||
order_by = "last_message_time DESC" if sort_by == "last_active" else "title"
|
||||
query_parts.append(f"ORDER BY {order_by}")
|
||||
|
||||
# Add pagination
|
||||
offset = (page) * limit
|
||||
query_parts.append("LIMIT ? OFFSET ?")
|
||||
params.extend([limit, offset])
|
||||
|
||||
cursor.execute(" ".join(query_parts), tuple(params))
|
||||
chats = cursor.fetchall()
|
||||
|
||||
result = []
|
||||
for chat_data in chats:
|
||||
last_message_time = datetime.fromisoformat(chat_data[4]) if chat_data[4] else None
|
||||
chat = Chat(
|
||||
id=chat_data[0],
|
||||
title=chat_data[1],
|
||||
username=chat_data[2],
|
||||
type=chat_data[3],
|
||||
last_message_time=last_message_time
|
||||
)
|
||||
result.append(chat)
|
||||
|
||||
return result
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def get_chat(chat_id: int) -> Optional[Chat]:
|
||||
"""Get chat metadata by ID."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT id, title, username, type, last_message_time
|
||||
FROM chats
|
||||
WHERE id = ?
|
||||
""", (chat_id,))
|
||||
|
||||
chat_data = cursor.fetchone()
|
||||
|
||||
if not chat_data:
|
||||
return None
|
||||
|
||||
last_message_time = datetime.fromisoformat(chat_data[4]) if chat_data[4] else None
|
||||
return Chat(
|
||||
id=chat_data[0],
|
||||
title=chat_data[1],
|
||||
username=chat_data[2],
|
||||
type=chat_data[3],
|
||||
last_message_time=last_message_time
|
||||
)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return None
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def get_direct_chat_by_contact(contact_id: int) -> Optional[Chat]:
|
||||
"""Get direct chat metadata by contact ID."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT id, title, username, type, last_message_time
|
||||
FROM chats
|
||||
WHERE id = ? AND type = 'user'
|
||||
""", (contact_id,))
|
||||
|
||||
chat_data = cursor.fetchone()
|
||||
|
||||
if not chat_data:
|
||||
return None
|
||||
|
||||
last_message_time = datetime.fromisoformat(chat_data[4]) if chat_data[4] else None
|
||||
return Chat(
|
||||
id=chat_data[0],
|
||||
title=chat_data[1],
|
||||
username=chat_data[2],
|
||||
type=chat_data[3],
|
||||
last_message_time=last_message_time
|
||||
)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return None
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def get_contact_chats(contact_id: int, limit: int = 20, page: int = 0) -> List[Chat]:
|
||||
"""Get all chats involving the contact.
|
||||
|
||||
Args:
|
||||
contact_id: The contact's ID to search for
|
||||
limit: Maximum number of chats to return (default 20)
|
||||
page: Page number for pagination (default 0)
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT
|
||||
c.id, c.title, c.username, c.type, c.last_message_time
|
||||
FROM chats c
|
||||
JOIN messages m ON c.id = m.chat_id
|
||||
WHERE m.sender_id = ? OR c.id = ?
|
||||
ORDER BY c.last_message_time DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""", (contact_id, contact_id, limit, page * limit))
|
||||
|
||||
chats = cursor.fetchall()
|
||||
|
||||
result = []
|
||||
for chat_data in chats:
|
||||
last_message_time = datetime.fromisoformat(chat_data[4]) if chat_data[4] else None
|
||||
chat = Chat(
|
||||
id=chat_data[0],
|
||||
title=chat_data[1],
|
||||
username=chat_data[2],
|
||||
type=chat_data[3],
|
||||
last_message_time=last_message_time
|
||||
)
|
||||
result.append(chat)
|
||||
|
||||
return result
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def get_last_interaction(contact_id: int) -> Optional[Message]:
|
||||
"""Get most recent message involving the contact."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
m.id, m.chat_id, c.title, m.sender_name, m.content, m.timestamp, m.is_from_me, m.sender_id
|
||||
FROM messages m
|
||||
JOIN chats c ON m.chat_id = c.id
|
||||
WHERE m.sender_id = ? OR c.id = ?
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 1
|
||||
""", (contact_id, contact_id))
|
||||
|
||||
msg_data = cursor.fetchone()
|
||||
|
||||
if not msg_data:
|
||||
return None
|
||||
|
||||
return Message(
|
||||
id=msg_data[0],
|
||||
chat_id=msg_data[1],
|
||||
chat_title=msg_data[2],
|
||||
sender_name=msg_data[3],
|
||||
content=msg_data[4],
|
||||
timestamp=datetime.fromisoformat(msg_data[5]),
|
||||
is_from_me=bool(msg_data[6]),
|
||||
sender_id=msg_data[7]
|
||||
)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return None
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def send_message(recipient: str, message: str) -> Tuple[bool, str]:
|
||||
"""Send a Telegram message to the specified recipient.
|
||||
|
||||
Args:
|
||||
recipient: The recipient - either a username (with or without @),
|
||||
or a chat ID as a string or integer
|
||||
message: The message text to send
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: A tuple containing success status and a status message
|
||||
"""
|
||||
try:
|
||||
# Validate input
|
||||
if not recipient:
|
||||
return False, "Recipient must be provided"
|
||||
|
||||
url = f"{TELEGRAM_API_BASE_URL}/send"
|
||||
payload = {
|
||||
"recipient": recipient,
|
||||
"message": message
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload)
|
||||
|
||||
# Check if the request was successful
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return result.get("success", False), result.get("message", "Unknown response")
|
||||
else:
|
||||
return False, f"Error: HTTP {response.status_code} - {response.text}"
|
||||
|
||||
except requests.RequestException as e:
|
||||
return False, f"Request error: {str(e)}"
|
||||
except json.JSONDecodeError:
|
||||
return False, f"Error parsing response: Unknown"
|
||||
except Exception as e:
|
||||
return False, f"Unexpected error: {str(e)}"
|
||||
Loading…
Add table
Reference in a new issue