mirror of
https://github.com/jlengrand/whatsapp-mcp.git
synced 2026-03-10 00:41:24 +00:00
cleanup
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.db
|
||||
claude_desktop_config.json
|
||||
whatsapp.log
|
||||
135
README.md
Normal file
135
README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# WhatsApp MCP Server
|
||||
|
||||
This is a Model Context Protocol (MCP) server for WhatsApp.
|
||||
|
||||
With this you can search you personal Whatsapp messages, search your contacts and send messages.
|
||||
|
||||
It connects to your **personal WhatsApp account** directly via the Whatsapp web multidevice API (using the [whatsmeow](https://github.com/tulir/whatsmeow) 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).
|
||||
|
||||
Here's an example of what you can do when it's connected to Claude.
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go
|
||||
- Python 3.6+
|
||||
- Anthropic Claude Desktop app (or Cursor)
|
||||
- UV (Python package manager), install with `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
||||
|
||||
### Steps
|
||||
|
||||
1. **Clone this repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lharries/whatsapp-mcp.git
|
||||
cd whatsapp-mcp
|
||||
```
|
||||
|
||||
2. **Run the WhatsApp bridge**
|
||||
|
||||
Navigate to the whatsapp-bridge directory and run the Go application:
|
||||
|
||||
```bash
|
||||
cd whatsapp-bridge
|
||||
go run main.go
|
||||
```
|
||||
|
||||
The first time you run it, you will be prompted to scan a QR code. Scan the QR code with your WhatsApp mobile app to authenticate.
|
||||
|
||||
After approximately 20 days, you will might need to re-authenticate.
|
||||
|
||||
3. **Connect to the the MCP server**
|
||||
|
||||
Copy the below json with the appropriate {{PATH}} values:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"whatsapp": {
|
||||
"command": "{{PATH}}/.local/bin/uv", // Run `which uv` and place the output here
|
||||
"args": [
|
||||
"--directory",
|
||||
"{{PATH}}/whatsapp-mcp/whatsapp-mcp-server", // cd into the repo, run `pwd` and enter the output here + "/whatsapp-mcp-server"
|
||||
"run",
|
||||
"main.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
4. **Restart Claude Desktop / Cursor**
|
||||
|
||||
Open Claude Desktop and you should now see WhatsApp as an available integration.
|
||||
|
||||
Or restart Cursor.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This application consists of two main components:
|
||||
|
||||
1. **Go WhatsApp Bridge** (`whatsapp-bridge/`): A Go application that connects to WhatsApp's web API, handles authentication via QR code, and stores message history in SQLite. It serves as the bridge between WhatsApp and the MCP server.
|
||||
|
||||
2. **Python MCP Server** (`whatsapp-mcp-server/`): A Python server implementing the Model Context Protocol (MCP), which provides standardized tools for Claude to interact with WhatsApp data and send/receive messages.
|
||||
|
||||
### Data Storage
|
||||
|
||||
- All message history is stored in a SQLite database within the `whatsapp-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 WhatsApp contacts through Claude, leveraging Claude's AI capabilities in your WhatsApp conversations.
|
||||
|
||||
### MCP Tools
|
||||
|
||||
Claude can access the following tools to interact with WhatsApp:
|
||||
|
||||
- **search_contacts**: Search for contacts by name or phone number
|
||||
- **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 WhatsApp message to a specified phone number
|
||||
|
||||
## Technical Details
|
||||
|
||||
1. Claude sends requests to the Python MCP server
|
||||
2. The MCP server queries the Go bridge for WhatsApp data or directly to the SQLite database
|
||||
3. The Go accesses the WhatsApp 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 Go bridge and to WhatsApp
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If you encounter permission issues when running uv, you may need to add it to your PATH or use the full path to the executable.
|
||||
- Make sure both the Go application and the Python server are running for the integration to work properly.
|
||||
|
||||
### Authentication Issues
|
||||
|
||||
- **QR Code Not Displaying**: If the QR code doesn't appear, try restarting the authentication script. If issues persist, check if your terminal supports displaying QR codes.
|
||||
- **WhatsApp Already Logged In**: If your session is already active, the Go bridge will automatically reconnect without showing a QR code.
|
||||
- **Device Limit Reached**: WhatsApp limits the number of linked devices. If you reach this limit, you'll need to remove an existing device from WhatsApp on your phone (Settings > Linked Devices).
|
||||
- **No Messages Loading**: After initial authentication, it can take several minutes for your message history to load, especially if you have many chats.
|
||||
- **WhatsApp Out of Sync**: If your WhatsApp messages get out of sync with the bridge, delete both database files (`whatsapp-bridge/store/messages.db` and `whatsapp-bridge/store/whatsapp.db`) and restart the bridge to re-authenticate.
|
||||
|
||||
For additional Claude Desktop integration troubleshooting, see the [MCP documentation](https://modelcontextprotocol.io/quickstart/server#claude-for-desktop-integration-issues). The documentation includes helpful tips for checking logs and resolving common issues.
|
||||
BIN
example-use.png
Normal file
BIN
example-use.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
25
whatsapp-bridge/go.mod
Normal file
25
whatsapp-bridge/go.mod
Normal file
@@ -0,0 +1,25 @@
|
||||
module whatsapp-client
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mdp/qrterminal v1.0.1 // indirect
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
go.mau.fi/libsignal v0.1.2 // indirect
|
||||
go.mau.fi/util v0.8.6 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
52
whatsapp-bridge/go.sum
Normal file
52
whatsapp-bridge/go.sum
Normal file
@@ -0,0 +1,52 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c=
|
||||
github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0=
|
||||
go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE=
|
||||
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
|
||||
go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
|
||||
go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 h1:AZlDkXHgoQNW4gd2hnTCvPH7hYznmwc3gPaYqGZ5w8A=
|
||||
go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
603
whatsapp-bridge/main.go
Normal file
603
whatsapp-bridge/main.go
Normal file
@@ -0,0 +1,603 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/mdp/qrterminal"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
"google.golang.org/protobuf/proto"
|
||||
waProto "go.mau.fi/whatsmeow/binary/proto"
|
||||
)
|
||||
|
||||
// Message represents a chat message for our client
|
||||
type Message struct {
|
||||
Time time.Time
|
||||
Sender string
|
||||
Content string
|
||||
IsFromMe bool
|
||||
}
|
||||
|
||||
// Database handler for storing message history
|
||||
type MessageStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Initialize message store
|
||||
func NewMessageStore() (*MessageStore, error) {
|
||||
// Create directory for database if it doesn't exist
|
||||
if err := os.MkdirAll("store", 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create store directory: %v", err)
|
||||
}
|
||||
|
||||
// Open SQLite database for messages
|
||||
db, err := sql.Open("sqlite3", "file:store/messages.db?_foreign_keys=on")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open message database: %v", err)
|
||||
}
|
||||
|
||||
// Create tables if they don't exist
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS chats (
|
||||
jid TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
last_message_time TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT,
|
||||
chat_jid TEXT,
|
||||
sender TEXT,
|
||||
content TEXT,
|
||||
timestamp TIMESTAMP,
|
||||
is_from_me BOOLEAN,
|
||||
PRIMARY KEY (id, chat_jid),
|
||||
FOREIGN KEY (chat_jid) REFERENCES chats(jid)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to create tables: %v", err)
|
||||
}
|
||||
|
||||
return &MessageStore{db: db}, nil
|
||||
}
|
||||
|
||||
// Close the database connection
|
||||
func (store *MessageStore) Close() error {
|
||||
return store.db.Close()
|
||||
}
|
||||
|
||||
// Store a chat in the database
|
||||
func (store *MessageStore) StoreChat(jid, name string, lastMessageTime time.Time) error {
|
||||
_, err := store.db.Exec(
|
||||
"INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)",
|
||||
jid, name, lastMessageTime,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Store a message in the database
|
||||
func (store *MessageStore) StoreMessage(id, chatJID, sender, content string, timestamp time.Time, isFromMe bool) error {
|
||||
// Only store if there's actual content
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := store.db.Exec(
|
||||
"INSERT OR REPLACE INTO messages (id, chat_jid, sender, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
id, chatJID, sender, content, timestamp, isFromMe,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get messages from a chat
|
||||
func (store *MessageStore) GetMessages(chatJID string, limit int) ([]Message, error) {
|
||||
rows, err := store.db.Query(
|
||||
"SELECT sender, content, timestamp, is_from_me FROM messages WHERE chat_jid = ? ORDER BY timestamp DESC LIMIT ?",
|
||||
chatJID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var messages []Message
|
||||
for rows.Next() {
|
||||
var msg Message
|
||||
var timestamp time.Time
|
||||
err := rows.Scan(&msg.Sender, &msg.Content, ×tamp, &msg.IsFromMe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.Time = timestamp
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// Get all chats
|
||||
func (store *MessageStore) GetChats() (map[string]time.Time, error) {
|
||||
rows, err := store.db.Query("SELECT jid, last_message_time FROM chats ORDER BY last_message_time DESC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
chats := make(map[string]time.Time)
|
||||
for rows.Next() {
|
||||
var jid string
|
||||
var lastMessageTime time.Time
|
||||
err := rows.Scan(&jid, &lastMessageTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chats[jid] = lastMessageTime
|
||||
}
|
||||
|
||||
return chats, nil
|
||||
}
|
||||
|
||||
// Extract text content from a message
|
||||
func extractTextContent(msg *waProto.Message) string {
|
||||
if msg == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try to get text content
|
||||
if text := msg.GetConversation(); text != "" {
|
||||
return text
|
||||
} else if extendedText := msg.GetExtendedTextMessage(); extendedText != nil {
|
||||
return extendedText.GetText()
|
||||
}
|
||||
|
||||
// For now, we're ignoring non-text messages
|
||||
return ""
|
||||
}
|
||||
|
||||
// SendMessageResponse represents the response for the send message API
|
||||
type SendMessageResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SendMessageRequest represents the request body for the send message API
|
||||
type SendMessageRequest struct {
|
||||
Phone string `json:"phone"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Function to send a WhatsApp message
|
||||
func sendWhatsAppMessage(client *whatsmeow.Client, phone, message string) (bool, string) {
|
||||
if !client.IsConnected() {
|
||||
return false, "Not connected to WhatsApp"
|
||||
}
|
||||
|
||||
// Create JID for recipient
|
||||
recipientJID := types.JID{
|
||||
User: phone,
|
||||
Server: "s.whatsapp.net", // For personal chats
|
||||
}
|
||||
|
||||
// Send the message
|
||||
_, err := client.SendMessage(context.Background(), recipientJID, &waProto.Message{
|
||||
Conversation: proto.String(message),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("Error sending message: %v", err)
|
||||
}
|
||||
|
||||
return true, fmt.Sprintf("Message sent to %s", phone)
|
||||
}
|
||||
|
||||
// Start a REST API server to expose the WhatsApp client functionality
|
||||
func startRESTServer(client *whatsmeow.Client, port int) {
|
||||
// Handler for sending messages
|
||||
http.HandleFunc("/api/send", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow POST requests
|
||||
fmt.Println("Received request to send message")
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request body
|
||||
var req SendMessageRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if req.Phone == "" || req.Message == "" {
|
||||
http.Error(w, "Phone and message are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the message
|
||||
success, message := sendWhatsAppMessage(client, req.Phone, req.Message)
|
||||
fmt.Println("Message sent", success, message)
|
||||
// Set response headers
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Set appropriate status code
|
||||
if !success {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Send response
|
||||
json.NewEncoder(w).Encode(SendMessageResponse{
|
||||
Success: success,
|
||||
Message: message,
|
||||
})
|
||||
})
|
||||
|
||||
// Start the server
|
||||
serverAddr := fmt.Sprintf(":%d", port)
|
||||
fmt.Printf("Starting REST API server on %s...\n", serverAddr)
|
||||
|
||||
// Run server in a goroutine so it doesn't block
|
||||
go func() {
|
||||
if err := http.ListenAndServe(serverAddr, nil); err != nil {
|
||||
fmt.Printf("REST API server error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Set up logger
|
||||
logger := waLog.Stdout("Client", "INFO", true)
|
||||
logger.Infof("Starting WhatsApp client...")
|
||||
|
||||
// Create database connection for storing session data
|
||||
dbLog := waLog.Stdout("Database", "INFO", true)
|
||||
|
||||
// Create directory for database if it doesn't exist
|
||||
if err := os.MkdirAll("store", 0755); err != nil {
|
||||
logger.Errorf("Failed to create store directory: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
container, err := sqlstore.New("sqlite3", "file:store/whatsapp.db?_foreign_keys=on", dbLog)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to connect to database: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get device store - This contains session information
|
||||
deviceStore, err := container.GetFirstDevice()
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// No device exists, create one
|
||||
deviceStore = container.NewDevice()
|
||||
logger.Infof("Created new device")
|
||||
} else {
|
||||
logger.Errorf("Failed to get device: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create client instance
|
||||
client := whatsmeow.NewClient(deviceStore, logger)
|
||||
if client == nil {
|
||||
logger.Errorf("Failed to create WhatsApp client")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize message store
|
||||
messageStore, err := NewMessageStore()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to initialize message store: %v", err)
|
||||
return
|
||||
}
|
||||
defer messageStore.Close()
|
||||
|
||||
// Setup event handling for messages and history sync
|
||||
client.AddEventHandler(func(evt interface{}) {
|
||||
switch v := evt.(type) {
|
||||
case *events.Message:
|
||||
// Process regular messages
|
||||
handleMessage(client, messageStore, v, logger)
|
||||
|
||||
case *events.HistorySync:
|
||||
// Process history sync events
|
||||
handleHistorySync(client, messageStore, v, logger)
|
||||
|
||||
case *events.Connected:
|
||||
logger.Infof("Connected to WhatsApp")
|
||||
|
||||
case *events.LoggedOut:
|
||||
logger.Warnf("Device logged out, please scan QR code to log in again")
|
||||
}
|
||||
})
|
||||
|
||||
// Create channel to track connection success
|
||||
connected := make(chan bool, 1)
|
||||
|
||||
// Connect to WhatsApp
|
||||
if client.Store.ID == nil {
|
||||
// No ID stored, this is a new client, need to pair with phone
|
||||
qrChan, _ := client.GetQRChannel(context.Background())
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to connect: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Print QR code for pairing with phone
|
||||
for evt := range qrChan {
|
||||
if evt.Event == "code" {
|
||||
fmt.Println("\nScan this QR code with your WhatsApp app:")
|
||||
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
|
||||
} else if evt.Event == "success" {
|
||||
connected <- true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for connection
|
||||
select {
|
||||
case <-connected:
|
||||
fmt.Println("\nSuccessfully connected and authenticated!")
|
||||
case <-time.After(3 * time.Minute):
|
||||
logger.Errorf("Timeout waiting for QR code scan")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Already logged in, just connect
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to connect: %v", err)
|
||||
return
|
||||
}
|
||||
connected <- true
|
||||
}
|
||||
|
||||
// Wait a moment for connection to stabilize
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if !client.IsConnected() {
|
||||
logger.Errorf("Failed to establish stable connection")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n✓ Connected to WhatsApp! Type 'help' for commands.")
|
||||
|
||||
// Start REST API server
|
||||
startRESTServer(client, 8080)
|
||||
|
||||
// Create a channel to keep the main goroutine alive
|
||||
exitChan := make(chan os.Signal, 1)
|
||||
signal.Notify(exitChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
fmt.Println("REST server is running. Press Ctrl+C to disconnect and exit.")
|
||||
|
||||
// Wait for termination signal
|
||||
<-exitChan
|
||||
|
||||
fmt.Println("Disconnecting...")
|
||||
// Disconnect client
|
||||
client.Disconnect()
|
||||
}
|
||||
|
||||
// Handle regular incoming messages
|
||||
func handleMessage(client *whatsmeow.Client, messageStore *MessageStore, msg *events.Message, logger waLog.Logger) {
|
||||
// Extract text content
|
||||
content := extractTextContent(msg.Message)
|
||||
if content == "" {
|
||||
return // Skip non-text messages
|
||||
}
|
||||
|
||||
// Save message to database
|
||||
chatJID := msg.Info.Chat.String()
|
||||
sender := msg.Info.Sender.User
|
||||
|
||||
// Get contact name if possible
|
||||
name := sender
|
||||
contact, err := client.Store.Contacts.GetContact(msg.Info.Sender)
|
||||
if err == nil && contact.FullName != "" {
|
||||
name = contact.FullName
|
||||
}
|
||||
|
||||
// Update chat in database
|
||||
err = messageStore.StoreChat(chatJID, name, msg.Info.Timestamp)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to store chat: %v", err)
|
||||
}
|
||||
|
||||
// Store message in database
|
||||
err = messageStore.StoreMessage(
|
||||
msg.Info.ID,
|
||||
chatJID,
|
||||
sender,
|
||||
content,
|
||||
msg.Info.Timestamp,
|
||||
msg.Info.IsFromMe,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to store message: %v", err)
|
||||
} else {
|
||||
// Log message reception
|
||||
timestamp := msg.Info.Timestamp.Format("2006-01-02 15:04:05")
|
||||
direction := "←"
|
||||
if msg.Info.IsFromMe {
|
||||
direction = "→"
|
||||
}
|
||||
fmt.Printf("[%s] %s %s: %s\n", timestamp, direction, sender, content)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle history sync events
|
||||
func handleHistorySync(client *whatsmeow.Client, messageStore *MessageStore, historySync *events.HistorySync, logger waLog.Logger) {
|
||||
fmt.Printf("Received history sync event with %d conversations\n", len(historySync.Data.Conversations))
|
||||
|
||||
syncedCount := 0
|
||||
for _, conversation := range historySync.Data.Conversations {
|
||||
// Parse JID from the conversation
|
||||
if conversation.ID == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
chatJID := *conversation.ID
|
||||
|
||||
// Try to parse the JID
|
||||
jid, err := types.ParseJID(chatJID)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to parse JID %s: %v", chatJID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get contact name
|
||||
name := jid.User
|
||||
contact, err := client.Store.Contacts.GetContact(jid)
|
||||
if err == nil && contact.FullName != "" {
|
||||
name = contact.FullName
|
||||
}
|
||||
|
||||
// Process messages
|
||||
messages := conversation.Messages
|
||||
if len(messages) > 0 {
|
||||
// Update chat with latest message timestamp
|
||||
latestMsg := messages[0]
|
||||
if latestMsg == nil || latestMsg.Message == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get timestamp from message info
|
||||
timestamp := time.Time{}
|
||||
if ts := latestMsg.Message.GetMessageTimestamp(); ts != 0 {
|
||||
timestamp = time.Unix(int64(ts), 0)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
messageStore.StoreChat(chatJID, name, timestamp)
|
||||
|
||||
// Store messages
|
||||
for _, msg := range messages {
|
||||
if msg == nil || msg.Message == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract text content
|
||||
var content string
|
||||
if msg.Message.Message != nil {
|
||||
if conv := msg.Message.Message.GetConversation(); conv != "" {
|
||||
content = conv
|
||||
} else if ext := msg.Message.Message.GetExtendedTextMessage(); ext != nil {
|
||||
content = ext.GetText()
|
||||
}
|
||||
}
|
||||
|
||||
// Log the message content for debugging
|
||||
logger.Infof("Message content: %v", content)
|
||||
|
||||
// Skip non-text messages
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine sender
|
||||
var sender string
|
||||
isFromMe := false
|
||||
if msg.Message.Key != nil {
|
||||
if msg.Message.Key.FromMe != nil {
|
||||
isFromMe = *msg.Message.Key.FromMe
|
||||
}
|
||||
if !isFromMe && msg.Message.Key.Participant != nil && *msg.Message.Key.Participant != "" {
|
||||
sender = *msg.Message.Key.Participant
|
||||
} else if isFromMe {
|
||||
sender = client.Store.ID.User
|
||||
} else {
|
||||
sender = jid.User
|
||||
}
|
||||
} else {
|
||||
sender = jid.User
|
||||
}
|
||||
|
||||
// Store message
|
||||
msgID := ""
|
||||
if msg.Message.Key != nil && msg.Message.Key.ID != nil {
|
||||
msgID = *msg.Message.Key.ID
|
||||
}
|
||||
|
||||
// Get message timestamp
|
||||
timestamp := time.Time{}
|
||||
if ts := msg.Message.GetMessageTimestamp(); ts != 0 {
|
||||
timestamp = time.Unix(int64(ts), 0)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
err = messageStore.StoreMessage(
|
||||
msgID,
|
||||
chatJID,
|
||||
sender,
|
||||
content,
|
||||
timestamp,
|
||||
isFromMe,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to store history message: %v", err)
|
||||
} else {
|
||||
syncedCount++
|
||||
// Log successful message storage
|
||||
logger.Infof("Stored message: [%s] %s -> %s: %s", timestamp.Format("2006-01-02 15:04:05"), sender, chatJID, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("History sync complete. Stored %d text messages.\n", syncedCount)
|
||||
}
|
||||
|
||||
// Request history sync from the server
|
||||
func requestHistorySync(client *whatsmeow.Client) {
|
||||
if client == nil {
|
||||
fmt.Println("Client is not initialized. Cannot request history sync.")
|
||||
return
|
||||
}
|
||||
|
||||
if !client.IsConnected() {
|
||||
fmt.Println("Client is not connected. Please ensure you are connected to WhatsApp first.")
|
||||
return
|
||||
}
|
||||
|
||||
if client.Store.ID == nil {
|
||||
fmt.Println("Client is not logged in. Please scan the QR code first.")
|
||||
return
|
||||
}
|
||||
|
||||
// Build and send a history sync request
|
||||
historyMsg := client.BuildHistorySyncRequest(nil, 100)
|
||||
if historyMsg == nil {
|
||||
fmt.Println("Failed to build history sync request.")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := client.SendMessage(context.Background(), types.JID{
|
||||
Server: "s.whatsapp.net",
|
||||
User: "status",
|
||||
}, historyMsg)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to request history sync: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("History sync requested. Waiting for server response...")
|
||||
}
|
||||
}
|
||||
10
whatsapp-mcp-server/.gitignore
vendored
Normal file
10
whatsapp-mcp-server/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
whatsapp-mcp-server/.python-version
Normal file
1
whatsapp-mcp-server/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.11
|
||||
171
whatsapp-mcp-server/main.py
Normal file
171
whatsapp-mcp-server/main.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from datetime import datetime
|
||||
from whatsapp import (
|
||||
search_contacts as whatsapp_search_contacts,
|
||||
list_messages as whatsapp_list_messages,
|
||||
list_chats as whatsapp_list_chats,
|
||||
get_chat as whatsapp_get_chat,
|
||||
get_direct_chat_by_contact as whatsapp_get_direct_chat_by_contact,
|
||||
get_contact_chats as whatsapp_get_contact_chats,
|
||||
get_last_interaction as whatsapp_get_last_interaction,
|
||||
get_message_context as whatsapp_get_message_context,
|
||||
send_message as whatsapp_send_message
|
||||
)
|
||||
|
||||
# Initialize FastMCP server
|
||||
mcp = FastMCP("whatsapp")
|
||||
|
||||
@mcp.tool()
|
||||
def search_contacts(query: str) -> List[Dict[str, Any]]:
|
||||
"""Search WhatsApp contacts by name or phone number.
|
||||
|
||||
Args:
|
||||
query: Search term to match against contact names or phone numbers
|
||||
"""
|
||||
contacts = whatsapp_search_contacts(query)
|
||||
return contacts
|
||||
|
||||
@mcp.tool()
|
||||
def list_messages(
|
||||
date_range: Optional[Tuple[datetime, datetime]] = None,
|
||||
sender_phone_number: Optional[str] = None,
|
||||
chat_jid: Optional[str] = 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 WhatsApp messages matching specified criteria with optional context.
|
||||
|
||||
Args:
|
||||
date_range: Optional tuple of (start_date, end_date) to filter messages by date
|
||||
sender_phone_number: Optional phone number to filter messages by sender
|
||||
chat_jid: Optional chat JID 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 = whatsapp_list_messages(
|
||||
date_range=date_range,
|
||||
sender_phone_number=sender_phone_number,
|
||||
chat_jid=chat_jid,
|
||||
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,
|
||||
include_last_message: bool = True,
|
||||
sort_by: str = "last_active"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get WhatsApp chats matching specified criteria.
|
||||
|
||||
Args:
|
||||
query: Optional search term to filter chats by name or JID
|
||||
limit: Maximum number of chats to return (default 20)
|
||||
page: Page number for pagination (default 0)
|
||||
include_last_message: Whether to include the last message in each chat (default True)
|
||||
sort_by: Field to sort results by, either "last_active" or "name" (default "last_active")
|
||||
"""
|
||||
chats = whatsapp_list_chats(
|
||||
query=query,
|
||||
limit=limit,
|
||||
page=page,
|
||||
include_last_message=include_last_message,
|
||||
sort_by=sort_by
|
||||
)
|
||||
return chats
|
||||
|
||||
@mcp.tool()
|
||||
def get_chat(chat_jid: str, include_last_message: bool = True) -> Dict[str, Any]:
|
||||
"""Get WhatsApp chat metadata by JID.
|
||||
|
||||
Args:
|
||||
chat_jid: The JID of the chat to retrieve
|
||||
include_last_message: Whether to include the last message (default True)
|
||||
"""
|
||||
chat = whatsapp_get_chat(chat_jid, include_last_message)
|
||||
return chat
|
||||
|
||||
@mcp.tool()
|
||||
def get_direct_chat_by_contact(sender_phone_number: str) -> Dict[str, Any]:
|
||||
"""Get WhatsApp chat metadata by sender phone number.
|
||||
|
||||
Args:
|
||||
sender_phone_number: The phone number to search for
|
||||
"""
|
||||
chat = whatsapp_get_direct_chat_by_contact(sender_phone_number)
|
||||
return chat
|
||||
|
||||
@mcp.tool()
|
||||
def get_contact_chats(jid: str, limit: int = 20, page: int = 0) -> List[Dict[str, Any]]:
|
||||
"""Get all WhatsApp chats involving the contact.
|
||||
|
||||
Args:
|
||||
jid: The contact's JID to search for
|
||||
limit: Maximum number of chats to return (default 20)
|
||||
page: Page number for pagination (default 0)
|
||||
"""
|
||||
chats = whatsapp_get_contact_chats(jid, limit, page)
|
||||
return chats
|
||||
|
||||
@mcp.tool()
|
||||
def get_last_interaction(jid: str) -> Dict[str, Any]:
|
||||
"""Get most recent WhatsApp message involving the contact.
|
||||
|
||||
Args:
|
||||
jid: The JID of the contact to search for
|
||||
"""
|
||||
message = whatsapp_get_last_interaction(jid)
|
||||
return message
|
||||
|
||||
@mcp.tool()
|
||||
def get_message_context(
|
||||
message_id: str,
|
||||
before: int = 5,
|
||||
after: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
"""Get context around a specific WhatsApp message.
|
||||
|
||||
Args:
|
||||
message_id: The ID of the message to get context for
|
||||
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 = whatsapp_get_message_context(message_id, before, after)
|
||||
return context
|
||||
|
||||
@mcp.tool()
|
||||
def send_message(phone_number: str, message: str) -> Dict[str, Any]:
|
||||
"""Send a WhatsApp message to the specified phone number.
|
||||
|
||||
Args:
|
||||
phone_number: The recipient's phone number, with country code but no + or other symbols
|
||||
message: The message text to send
|
||||
|
||||
Returns:
|
||||
A dictionary containing success status and a status message
|
||||
"""
|
||||
success, status_message = whatsapp_send_message(phone_number, message)
|
||||
return {
|
||||
"success": success,
|
||||
"message": status_message
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Initialize and run the server
|
||||
mcp.run(transport='stdio')
|
||||
11
whatsapp-mcp-server/pyproject.toml
Normal file
11
whatsapp-mcp-server/pyproject.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "whatsapp-mcp-server"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"httpx>=0.28.1",
|
||||
"mcp[cli]>=1.6.0",
|
||||
"requests>=2.32.3",
|
||||
]
|
||||
462
whatsapp-mcp-server/uv.lock
generated
Normal file
462
whatsapp-mcp-server/uv.lock
generated
Normal file
@@ -0,0 +1,462 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cli = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/93/9e97af2619b4026596487a79133e425c7d3c374f0a7f100f3d76bcdf9c83/pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef", size = 2042784 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/b4/0bba8412fd242729feeb80e7152e24f0e1a1c19f4121ca3d4a307f4e6222/pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a", size = 1858179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/1f/c1c40305d929bd08af863df64b0a26203b70b352a1962d86f3bcd52950fe/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b", size = 1909396 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/99/d2e727375c329c1e652b5d450fbb9d56e8c3933a397e4bd46e67c68c2cd5/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd", size = 1998264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/2e/3119a33931278d96ecc2e9e1b9d50c240636cfeb0c49951746ae34e4de74/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b", size = 2140588 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/bd/9267bd1ba55f17c80ef6cb7e07b3890b4acbe8eb6014f3102092d53d9300/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d", size = 2746296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ed/ef37de6478a412ee627cbebd73e7b72a680f45bfacce9ff1199de6e17e88/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd", size = 2005555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/84/72c8d1439585d8ee7bc35eb8f88a04a4d302ee4018871f1f85ae1b0c6625/pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453", size = 2124452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/8f/cb13de30c6a3e303423751a529a3d1271c2effee4b98cf3e397a66ae8498/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b", size = 2087001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/d0/e93dc8884bf288a63fedeb8040ac8f29cb71ca52e755f48e5170bb63e55b/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915", size = 2261663 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ba/4b7739c95efa0b542ee45fd872c8f6b1884ab808cf04ce7ac6621b6df76e/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2", size = 2257786 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/98/73cbca1d2360c27752cfa2fcdcf14d96230e92d7d48ecd50499865c56bf7/pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86", size = 1925697 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/26/d85a40edeca5d8830ffc33667d6fef329fd0f4bc0c5181b8b0e206cfe488/pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b", size = 1949859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/0b/5a381605f0b9870465b805f2c86c06b0a7c191668ebe4117777306c2c1e5/pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a", size = 1907978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c4/c9381323cbdc1bb26d352bc184422ce77c4bc2f2312b782761093a59fafc/pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43", size = 2025127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/bd/af35278080716ecab8f57e84515c7dc535ed95d1c7f52c1c6f7b313a9dab/pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd", size = 1851687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e4/a01461225809c3533c23bd1916b1e8c2e21727f0fea60ab1acbffc4e2fca/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6", size = 1892232 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/17/3d53d62a328fb0a49911c2962036b9e7a4f781b7d15e9093c26299e5f76d/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6", size = 1977896 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/98/01f9d86e02ec4a38f4b02086acf067f2c776b845d43f901bd1ee1c21bc4b/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4", size = 2127717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/43/6f381575c61b7c58b0fd0b92134c5a1897deea4cdfc3d47567b3ff460a4e/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61", size = 2680287 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/42/c0d10d1451d161a9a0da9bbef023b8005aa26e9993a8cc24dc9e3aa96c93/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862", size = 2008276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/ca/e08df9dba546905c70bae44ced9f3bea25432e34448d95618d41968f40b7/pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a", size = 2115305 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/1f/9b01d990730a98833113581a78e595fd40ed4c20f9693f5a658fb5f91eff/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099", size = 2068999 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/18/fe752476a709191148e8b1e1139147841ea5d2b22adcde6ee6abb6c8e7cf/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6", size = 2241488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/22/14738ad0a0bf484b928c9e52004f5e0b81dd8dabbdf23b843717b37a71d1/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3", size = 2248430 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/27/be7571e215ac8d321712f2433c445b03dbcd645366a18f67b334df8912bc/pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2", size = 1908353 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/3a/be78f28732f93128bd0e3944bdd4b3970b389a1fbd44907c97291c8dcdec/pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48", size = 1955956 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/26/b8911ac74faa994694b76ee6a22875cc7a4abea3c381fdba4edc6c6bef84/pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6", size = 1903259 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/553e42762e7b08771fca41c0230c1ac276f9e79e78f57628e1b7d328551d/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c", size = 2041207 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/81/a91a57bbf3efe53525ab75f65944b8950e6ef84fe3b9a26c1ec173363263/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db", size = 1873736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d2/5ab52e9f551cdcbc1ee99a0b3ef595f56d031f66f88e5ca6726c49f9ce65/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b", size = 1903794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/5f/a81742d3f3821b16f1265f057d6e0b68a3ab13a814fe4bffac536a1f26fd/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9", size = 2083457 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2f/e872005bc0fc47f9c036b67b12349a8522d32e3bda928e82d676e2a594d1/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c", size = 2119537 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/13/183f13ce647202eaf3dada9e42cdfc59cbb95faedd44d25f22b931115c7f/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976", size = 2080069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/8b/b6be91243da44a26558d9c3a9007043b3750334136c6550551e8092d6d96/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c", size = 2251618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c5/fbcf1977035b834f63eb542e74cd6c807177f383386175b468f0865bcac4/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936", size = 2255374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/f8/66f328e411f1c9574b13c2c28ab01f308b53688bbbe6ca8fb981e6cabc42/pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8", size = 2082099 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.46.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whatsapp-mcp-server"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "mcp", extra = ["cli"] },
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "mcp", extras = ["cli"], specifier = ">=1.6.0" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
]
|
||||
696
whatsapp-mcp-server/whatsapp.py
Normal file
696
whatsapp-mcp-server/whatsapp.py
Normal file
@@ -0,0 +1,696 @@
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Tuple
|
||||
import os.path
|
||||
import requests
|
||||
import json
|
||||
|
||||
MESSAGES_DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'whatsapp-bridge', 'store', 'messages.db')
|
||||
WHATSAPP_API_BASE_URL = "http://localhost:8080/api"
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
timestamp: datetime
|
||||
sender: str
|
||||
content: str
|
||||
is_from_me: bool
|
||||
chat_jid: str
|
||||
id: str
|
||||
chat_name: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class Chat:
|
||||
jid: str
|
||||
name: Optional[str]
|
||||
last_message_time: Optional[datetime]
|
||||
last_message: Optional[str] = None
|
||||
last_sender: Optional[str] = None
|
||||
last_is_from_me: Optional[bool] = None
|
||||
|
||||
@property
|
||||
def is_group(self) -> bool:
|
||||
"""Determine if chat is a group based on JID pattern."""
|
||||
return self.jid.endswith("@g.us")
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
phone_number: str
|
||||
name: Optional[str]
|
||||
jid: 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 and message.chat_name:
|
||||
print(f"[{message.timestamp:%Y-%m-%d %H:%M:%S}] {direction} Chat: {message.chat_name} ({message.chat_jid})")
|
||||
else:
|
||||
print(f"[{message.timestamp:%Y-%m-%d %H:%M:%S}] {direction}")
|
||||
|
||||
print(f"From: {'Me' if message.is_from_me else message.sender}")
|
||||
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.name} ({chat.jid})")
|
||||
if chat.last_message_time:
|
||||
print(f"Last active: {chat.last_message_time:%Y-%m-%d %H:%M:%S}")
|
||||
direction = "→" if chat.last_is_from_me else "←"
|
||||
sender = "Me" if chat.last_is_from_me else chat.last_sender
|
||||
print(f"Last message: {direction} {sender}: {chat.last_message}")
|
||||
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 print_paginated_messages(messages: List[Message], page: int, total_pages: int, chat_name: str) -> None:
|
||||
"""Print a paginated list of messages with navigation hints."""
|
||||
print(f"\nMessages for chat: {chat_name}")
|
||||
print(f"Page {page} of {total_pages}")
|
||||
print("-" * 100)
|
||||
|
||||
print_messages_list(messages, show_chat_info=False)
|
||||
|
||||
# Print pagination info
|
||||
if page > 1:
|
||||
print(f"Use page={page-1} to see newer messages")
|
||||
if page < total_pages:
|
||||
print(f"Use page={page+1} to see older messages")
|
||||
|
||||
"""
|
||||
CREATE TABLE messages (
|
||||
id TEXT,
|
||||
chat_jid TEXT,
|
||||
sender TEXT,
|
||||
content TEXT,
|
||||
timestamp TIMESTAMP,
|
||||
is_from_me BOOLEAN,
|
||||
PRIMARY KEY (id, chat_jid),
|
||||
FOREIGN KEY (chat_jid) REFERENCES chats(jid)
|
||||
)
|
||||
|
||||
CREATE TABLE chats (
|
||||
jid TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
last_message_time TIMESTAMP
|
||||
)
|
||||
"""
|
||||
|
||||
def print_recent_messages(limit=10) -> List[Message]:
|
||||
try:
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Query recent messages with chat info
|
||||
query = """
|
||||
SELECT
|
||||
m.timestamp,
|
||||
m.sender,
|
||||
c.name,
|
||||
m.content,
|
||||
m.is_from_me,
|
||||
c.jid,
|
||||
m.id
|
||||
FROM messages m
|
||||
JOIN chats c ON m.chat_jid = c.jid
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
|
||||
cursor.execute(query, (limit,))
|
||||
messages = cursor.fetchall()
|
||||
|
||||
if not messages:
|
||||
print("No messages found in the database.")
|
||||
return []
|
||||
|
||||
result = []
|
||||
|
||||
# Convert to Message objects
|
||||
for msg in messages:
|
||||
message = Message(
|
||||
timestamp=datetime.fromisoformat(msg[0]),
|
||||
sender=msg[1],
|
||||
chat_name=msg[2] or "Unknown Chat",
|
||||
content=msg[3],
|
||||
is_from_me=msg[4],
|
||||
chat_jid=msg[5],
|
||||
id=msg[6]
|
||||
)
|
||||
result.append(message)
|
||||
|
||||
# Print messages using helper function
|
||||
print_messages_list(result, title=f"Last {limit} messages:")
|
||||
return result
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error accessing database: {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def list_messages(
|
||||
date_range: Optional[Tuple[datetime, datetime]] = None,
|
||||
sender_phone_number: Optional[str] = None,
|
||||
chat_jid: Optional[str] = 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 messages.timestamp, messages.sender, chats.name, messages.content, messages.is_from_me, chats.jid, messages.id FROM messages"]
|
||||
query_parts.append("JOIN chats ON messages.chat_jid = chats.jid")
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
# Add filters
|
||||
if date_range:
|
||||
where_clauses.append("messages.timestamp BETWEEN ? AND ?")
|
||||
params.extend([date_range[0].isoformat(), date_range[1].isoformat()])
|
||||
|
||||
if sender_phone_number:
|
||||
where_clauses.append("messages.sender = ?")
|
||||
params.append(sender_phone_number)
|
||||
|
||||
if chat_jid:
|
||||
where_clauses.append("messages.chat_jid = ?")
|
||||
params.append(chat_jid)
|
||||
|
||||
if query:
|
||||
where_clauses.append("LOWER(messages.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 messages.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(
|
||||
timestamp=datetime.fromisoformat(msg[0]),
|
||||
sender=msg[1],
|
||||
chat_name=msg[2],
|
||||
content=msg[3],
|
||||
is_from_me=msg[4],
|
||||
chat_jid=msg[5],
|
||||
id=msg[6]
|
||||
)
|
||||
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, 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: str,
|
||||
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 messages.timestamp, messages.sender, chats.name, messages.content, messages.is_from_me, chats.jid, messages.id, messages.chat_jid
|
||||
FROM messages
|
||||
JOIN chats ON messages.chat_jid = chats.jid
|
||||
WHERE messages.id = ?
|
||||
""", (message_id,))
|
||||
msg_data = cursor.fetchone()
|
||||
|
||||
if not msg_data:
|
||||
raise ValueError(f"Message with ID {message_id} not found")
|
||||
|
||||
target_message = Message(
|
||||
timestamp=datetime.fromisoformat(msg_data[0]),
|
||||
sender=msg_data[1],
|
||||
chat_name=msg_data[2],
|
||||
content=msg_data[3],
|
||||
is_from_me=msg_data[4],
|
||||
chat_jid=msg_data[5],
|
||||
id=msg_data[6]
|
||||
)
|
||||
|
||||
# Get messages before
|
||||
cursor.execute("""
|
||||
SELECT messages.timestamp, messages.sender, chats.name, messages.content, messages.is_from_me, chats.jid, messages.id
|
||||
FROM messages
|
||||
JOIN chats ON messages.chat_jid = chats.jid
|
||||
WHERE messages.chat_jid = ? AND messages.timestamp < ?
|
||||
ORDER BY messages.timestamp DESC
|
||||
LIMIT ?
|
||||
""", (msg_data[7], msg_data[0], before))
|
||||
|
||||
before_messages = []
|
||||
for msg in cursor.fetchall():
|
||||
before_messages.append(Message(
|
||||
timestamp=datetime.fromisoformat(msg[0]),
|
||||
sender=msg[1],
|
||||
chat_name=msg[2],
|
||||
content=msg[3],
|
||||
is_from_me=msg[4],
|
||||
chat_jid=msg[5],
|
||||
id=msg[6]
|
||||
))
|
||||
|
||||
# Get messages after
|
||||
cursor.execute("""
|
||||
SELECT messages.timestamp, messages.sender, chats.name, messages.content, messages.is_from_me, chats.jid, messages.id
|
||||
FROM messages
|
||||
JOIN chats ON messages.chat_jid = chats.jid
|
||||
WHERE messages.chat_jid = ? AND messages.timestamp > ?
|
||||
ORDER BY messages.timestamp ASC
|
||||
LIMIT ?
|
||||
""", (msg_data[7], msg_data[0], after))
|
||||
|
||||
after_messages = []
|
||||
for msg in cursor.fetchall():
|
||||
after_messages.append(Message(
|
||||
timestamp=datetime.fromisoformat(msg[0]),
|
||||
sender=msg[1],
|
||||
chat_name=msg[2],
|
||||
content=msg[3],
|
||||
is_from_me=msg[4],
|
||||
chat_jid=msg[5],
|
||||
id=msg[6]
|
||||
))
|
||||
|
||||
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,
|
||||
include_last_message: bool = True,
|
||||
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
|
||||
chats.jid,
|
||||
chats.name,
|
||||
chats.last_message_time,
|
||||
messages.content as last_message,
|
||||
messages.sender as last_sender,
|
||||
messages.is_from_me as last_is_from_me
|
||||
FROM chats
|
||||
"""]
|
||||
|
||||
if include_last_message:
|
||||
query_parts.append("""
|
||||
LEFT JOIN messages ON chats.jid = messages.chat_jid
|
||||
AND chats.last_message_time = messages.timestamp
|
||||
""")
|
||||
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if query:
|
||||
where_clauses.append("(LOWER(chats.name) LIKE LOWER(?) OR chats.jid LIKE ?)")
|
||||
params.extend([f"%{query}%", f"%{query}%"])
|
||||
|
||||
if where_clauses:
|
||||
query_parts.append("WHERE " + " AND ".join(where_clauses))
|
||||
|
||||
# Add sorting
|
||||
order_by = "chats.last_message_time DESC" if sort_by == "last_active" else "chats.name"
|
||||
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:
|
||||
chat = Chat(
|
||||
jid=chat_data[0],
|
||||
name=chat_data[1],
|
||||
last_message_time=datetime.fromisoformat(chat_data[2]) if chat_data[2] else None,
|
||||
last_message=chat_data[3],
|
||||
last_sender=chat_data[4],
|
||||
last_is_from_me=chat_data[5]
|
||||
)
|
||||
result.append(chat)
|
||||
|
||||
return result
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def search_contacts(query: str) -> List[Contact]:
|
||||
"""Search contacts by name or phone number."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Split query into characters to support partial matching
|
||||
search_pattern = '%' +query + '%'
|
||||
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT
|
||||
jid,
|
||||
name
|
||||
FROM chats
|
||||
WHERE
|
||||
(LOWER(name) LIKE LOWER(?) OR LOWER(jid) LIKE LOWER(?))
|
||||
AND jid NOT LIKE '%@g.us'
|
||||
ORDER BY name, jid
|
||||
LIMIT 50
|
||||
""", (search_pattern, search_pattern))
|
||||
|
||||
contacts = cursor.fetchall()
|
||||
|
||||
result = []
|
||||
for contact_data in contacts:
|
||||
contact = Contact(
|
||||
phone_number=contact_data[0].split('@')[0],
|
||||
name=contact_data[1],
|
||||
jid=contact_data[0]
|
||||
)
|
||||
result.append(contact)
|
||||
|
||||
return result
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_contact_chats(jid: str, limit: int = 20, page: int = 0) -> List[Chat]:
|
||||
"""Get all chats involving the contact.
|
||||
|
||||
Args:
|
||||
jid: The contact's JID 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.jid,
|
||||
c.name,
|
||||
c.last_message_time,
|
||||
m.content as last_message,
|
||||
m.sender as last_sender,
|
||||
m.is_from_me as last_is_from_me
|
||||
FROM chats c
|
||||
JOIN messages m ON c.jid = m.chat_jid
|
||||
WHERE m.sender = ? OR c.jid = ?
|
||||
ORDER BY c.last_message_time DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""", (jid, jid, limit, page * limit))
|
||||
|
||||
chats = cursor.fetchall()
|
||||
|
||||
result = []
|
||||
for chat_data in chats:
|
||||
chat = Chat(
|
||||
jid=chat_data[0],
|
||||
name=chat_data[1],
|
||||
last_message_time=datetime.fromisoformat(chat_data[2]) if chat_data[2] else None,
|
||||
last_message=chat_data[3],
|
||||
last_sender=chat_data[4],
|
||||
last_is_from_me=chat_data[5]
|
||||
)
|
||||
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(jid: str) -> Optional[Message]:
|
||||
"""Get most recent message involving the contact."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
m.timestamp,
|
||||
m.sender,
|
||||
c.name,
|
||||
m.content,
|
||||
m.is_from_me,
|
||||
c.jid,
|
||||
m.id
|
||||
FROM messages m
|
||||
JOIN chats c ON m.chat_jid = c.jid
|
||||
WHERE m.sender = ? OR c.jid = ?
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 1
|
||||
""", (jid, jid))
|
||||
|
||||
msg_data = cursor.fetchone()
|
||||
|
||||
if not msg_data:
|
||||
return None
|
||||
|
||||
return Message(
|
||||
timestamp=datetime.fromisoformat(msg_data[0]),
|
||||
sender=msg_data[1],
|
||||
chat_name=msg_data[2],
|
||||
content=msg_data[3],
|
||||
is_from_me=msg_data[4],
|
||||
chat_jid=msg_data[5],
|
||||
id=msg_data[6]
|
||||
)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return None
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_chat(chat_jid: str, include_last_message: bool = True) -> Optional[Chat]:
|
||||
"""Get chat metadata by JID."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
c.jid,
|
||||
c.name,
|
||||
c.last_message_time,
|
||||
m.content as last_message,
|
||||
m.sender as last_sender,
|
||||
m.is_from_me as last_is_from_me
|
||||
FROM chats c
|
||||
"""
|
||||
|
||||
if include_last_message:
|
||||
query += """
|
||||
LEFT JOIN messages m ON c.jid = m.chat_jid
|
||||
AND c.last_message_time = m.timestamp
|
||||
"""
|
||||
|
||||
query += " WHERE c.jid = ?"
|
||||
|
||||
cursor.execute(query, (chat_jid,))
|
||||
chat_data = cursor.fetchone()
|
||||
|
||||
if not chat_data:
|
||||
return None
|
||||
|
||||
return Chat(
|
||||
jid=chat_data[0],
|
||||
name=chat_data[1],
|
||||
last_message_time=datetime.fromisoformat(chat_data[2]) if chat_data[2] else None,
|
||||
last_message=chat_data[3],
|
||||
last_sender=chat_data[4],
|
||||
last_is_from_me=chat_data[5]
|
||||
)
|
||||
|
||||
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(sender_phone_number: str) -> Optional[Chat]:
|
||||
"""Get chat metadata by sender phone number."""
|
||||
try:
|
||||
conn = sqlite3.connect(MESSAGES_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
c.jid,
|
||||
c.name,
|
||||
c.last_message_time,
|
||||
m.content as last_message,
|
||||
m.sender as last_sender,
|
||||
m.is_from_me as last_is_from_me
|
||||
FROM chats c
|
||||
LEFT JOIN messages m ON c.jid = m.chat_jid
|
||||
AND c.last_message_time = m.timestamp
|
||||
WHERE c.jid LIKE ? AND c.jid NOT LIKE '%@g.us'
|
||||
LIMIT 1
|
||||
""", (f"%{sender_phone_number}%",))
|
||||
|
||||
chat_data = cursor.fetchone()
|
||||
|
||||
if not chat_data:
|
||||
return None
|
||||
|
||||
return Chat(
|
||||
jid=chat_data[0],
|
||||
name=chat_data[1],
|
||||
last_message_time=datetime.fromisoformat(chat_data[2]) if chat_data[2] else None,
|
||||
last_message=chat_data[3],
|
||||
last_sender=chat_data[4],
|
||||
last_is_from_me=chat_data[5]
|
||||
)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return None
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def send_message(phone_number: str, message: str) -> Tuple[bool, str]:
|
||||
"""Send a WhatsApp message to the specified phone number.
|
||||
|
||||
Args:
|
||||
phone_number (str): The recipient's phone number, with country code but no + or other symbols
|
||||
message (str): The message text to send
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: A tuple containing success status and a status message
|
||||
"""
|
||||
try:
|
||||
url = f"{WHATSAPP_API_BASE_URL}/send"
|
||||
payload = {
|
||||
"phone": phone_number,
|
||||
"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: {response.text}"
|
||||
except Exception as e:
|
||||
return False, f"Unexpected error: {str(e)}"
|
||||
Reference in New Issue
Block a user