universal-mcp-applications 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- universal_mcp/applications/airtable/app.py +1 -0
- universal_mcp/applications/apollo/app.py +1 -0
- universal_mcp/applications/{aws-s3 → aws_s3}/app.py +4 -5
- universal_mcp/applications/bill/app.py +3 -3
- universal_mcp/applications/box/app.py +2 -6
- universal_mcp/applications/braze/app.py +2 -6
- universal_mcp/applications/cal_com_v2/__init__.py +1 -0
- universal_mcp/applications/{cal-com-v2 → cal_com_v2}/app.py +138 -182
- universal_mcp/applications/clickup/app.py +2 -2
- universal_mcp/applications/confluence/app.py +1 -0
- universal_mcp/applications/contentful/app.py +8 -19
- universal_mcp/applications/digitalocean/app.py +9 -27
- universal_mcp/applications/{domain-checker → domain_checker}/app.py +2 -1
- universal_mcp/applications/elevenlabs/app.py +98 -3188
- universal_mcp/applications/falai/app.py +1 -0
- universal_mcp/applications/file_system/__init__.py +1 -0
- universal_mcp/applications/file_system/app.py +96 -0
- universal_mcp/applications/fireflies/app.py +4 -3
- universal_mcp/applications/fpl/app.py +1 -0
- universal_mcp/applications/fpl/utils/fixtures.py +1 -1
- universal_mcp/applications/fpl/utils/helper.py +1 -1
- universal_mcp/applications/fpl/utils/position_utils.py +0 -1
- universal_mcp/applications/{ghost-content → ghost_content}/app.py +2 -1
- universal_mcp/applications/github/app.py +4 -3
- universal_mcp/applications/{google-calendar → google_calendar}/app.py +2 -1
- universal_mcp/applications/{google-docs → google_docs}/app.py +1 -1
- universal_mcp/applications/{google-drive → google_drive}/app.py +2 -1
- universal_mcp/applications/google_gemini/app.py +183 -0
- universal_mcp/applications/{google-mail → google_mail}/app.py +2 -1
- universal_mcp/applications/{google-searchconsole → google_searchconsole}/app.py +1 -1
- universal_mcp/applications/{google-sheet → google_sheet}/app.py +3 -2
- universal_mcp/applications/google_sheet/helper.py +385 -0
- universal_mcp/applications/hashnode/app.py +2 -1
- universal_mcp/applications/{http-tools → http_tools}/app.py +2 -1
- universal_mcp/applications/hubspot/app.py +16 -2
- universal_mcp/applications/jira/app.py +7 -18
- universal_mcp/applications/markitdown/app.py +2 -3
- universal_mcp/applications/{ms-teams → ms_teams}/app.py +1 -1
- universal_mcp/applications/openai/app.py +2 -3
- universal_mcp/applications/outlook/app.py +1 -3
- universal_mcp/applications/pipedrive/app.py +2 -6
- universal_mcp/applications/reddit/app.py +1 -0
- universal_mcp/applications/replicate/app.py +3 -3
- universal_mcp/applications/resend/app.py +1 -2
- universal_mcp/applications/rocketlane/app.py +1 -0
- universal_mcp/applications/semrush/app.py +478 -1467
- universal_mcp/applications/sentry/README.md +20 -20
- universal_mcp/applications/sentry/app.py +40 -40
- universal_mcp/applications/serpapi/app.py +2 -2
- universal_mcp/applications/sharepoint/app.py +2 -1
- universal_mcp/applications/shopify/app.py +1 -0
- universal_mcp/applications/slack/app.py +3 -3
- universal_mcp/applications/trello/app.py +9 -27
- universal_mcp/applications/twilio/__init__.py +1 -0
- universal_mcp/applications/{twillo → twilio}/app.py +2 -2
- universal_mcp/applications/twitter/README.md +1 -1
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +2 -2
- universal_mcp/applications/twitter/api_segments/lists_api.py +1 -1
- universal_mcp/applications/unipile/app.py +5 -1
- universal_mcp/applications/whatsapp/app.py +18 -17
- universal_mcp/applications/whatsapp/audio.py +110 -0
- universal_mcp/applications/whatsapp/whatsapp.py +398 -0
- universal_mcp/applications/{whatsapp-business → whatsapp_business}/app.py +1 -1
- universal_mcp/applications/youtube/app.py +195 -191
- universal_mcp/applications/zenquotes/app.py +1 -1
- {universal_mcp_applications-0.1.1.dist-info → universal_mcp_applications-0.1.3.dist-info}/METADATA +4 -2
- {universal_mcp_applications-0.1.1.dist-info → universal_mcp_applications-0.1.3.dist-info}/RECORD +97 -95
- universal_mcp/applications/cal-com-v2/__init__.py +0 -1
- universal_mcp/applications/google-ads/__init__.py +0 -1
- universal_mcp/applications/google-ads/app.py +0 -23
- universal_mcp/applications/google-gemini/app.py +0 -663
- universal_mcp/applications/twillo/README.md +0 -0
- universal_mcp/applications/twillo/__init__.py +0 -1
- /universal_mcp/applications/{aws-s3 → aws_s3}/README.md +0 -0
- /universal_mcp/applications/{aws-s3 → aws_s3}/__init__.py +0 -0
- /universal_mcp/applications/{cal-com-v2 → cal_com_v2}/README.md +0 -0
- /universal_mcp/applications/{domain-checker → domain_checker}/README.md +0 -0
- /universal_mcp/applications/{domain-checker → domain_checker}/__init__.py +0 -0
- /universal_mcp/applications/{ghost-content → ghost_content}/README.md +0 -0
- /universal_mcp/applications/{ghost-content → ghost_content}/__init__.py +0 -0
- /universal_mcp/applications/{google-calendar → google_calendar}/README.md +0 -0
- /universal_mcp/applications/{google-calendar → google_calendar}/__init__.py +0 -0
- /universal_mcp/applications/{google-docs → google_docs}/README.md +0 -0
- /universal_mcp/applications/{google-docs → google_docs}/__init__.py +0 -0
- /universal_mcp/applications/{google-drive → google_drive}/README.md +0 -0
- /universal_mcp/applications/{google-drive → google_drive}/__init__.py +0 -0
- /universal_mcp/applications/{google-gemini → google_gemini}/README.md +0 -0
- /universal_mcp/applications/{google-gemini → google_gemini}/__init__.py +0 -0
- /universal_mcp/applications/{google-mail → google_mail}/README.md +0 -0
- /universal_mcp/applications/{google-mail → google_mail}/__init__.py +0 -0
- /universal_mcp/applications/{google-searchconsole → google_searchconsole}/README.md +0 -0
- /universal_mcp/applications/{google-searchconsole → google_searchconsole}/__init__.py +0 -0
- /universal_mcp/applications/{google-sheet → google_sheet}/README.md +0 -0
- /universal_mcp/applications/{google-sheet → google_sheet}/__init__.py +0 -0
- /universal_mcp/applications/{http-tools → http_tools}/README.md +0 -0
- /universal_mcp/applications/{http-tools → http_tools}/__init__.py +0 -0
- /universal_mcp/applications/{ms-teams → ms_teams}/README.md +0 -0
- /universal_mcp/applications/{ms-teams → ms_teams}/__init__.py +0 -0
- /universal_mcp/applications/{google-ads → twilio}/README.md +0 -0
- /universal_mcp/applications/{whatsapp-business → whatsapp_business}/README.md +0 -0
- /universal_mcp/applications/{whatsapp-business → whatsapp_business}/__init__.py +0 -0
- {universal_mcp_applications-0.1.1.dist-info → universal_mcp_applications-0.1.3.dist-info}/WHEEL +0 -0
- {universal_mcp_applications-0.1.1.dist-info → universal_mcp_applications-0.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,7 @@ from collections.abc import Callable
|
|
|
3
3
|
from typing import Any, Literal
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
|
+
|
|
6
7
|
from universal_mcp.applications.application import APIApplication
|
|
7
8
|
from universal_mcp.integrations import Integration
|
|
8
9
|
|
|
@@ -666,7 +667,10 @@ class UnipileApp(APIApplication):
|
|
|
666
667
|
keywords: str | None = None,
|
|
667
668
|
sort_by: Literal["relevance", "date"] | None = None,
|
|
668
669
|
date_posted: Literal["past_day", "past_week", "past_month"] | None = None,
|
|
669
|
-
content_type: Literal[
|
|
670
|
+
content_type: Literal[
|
|
671
|
+
"videos", "images", "live_videos", "collaborative_articles", "documents"
|
|
672
|
+
]
|
|
673
|
+
| None = None,
|
|
670
674
|
posted_by: dict[str, Any] | None = None,
|
|
671
675
|
mentioning: dict[str, Any] | None = None,
|
|
672
676
|
author: dict[str, Any] | None = None,
|
|
@@ -1,56 +1,57 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
import requests
|
|
4
|
-
from
|
|
5
|
-
from universal_mcp.exceptions import NotAuthorizedError
|
|
6
|
-
from universal_mcp.integrations import AgentRIntegration
|
|
7
|
-
from universal_mcp_whatsapp.whatsapp import (
|
|
4
|
+
from .whatsapp import (
|
|
8
5
|
WHATSAPP_API_BASE_URL,
|
|
9
6
|
)
|
|
10
|
-
from
|
|
7
|
+
from .whatsapp import (
|
|
11
8
|
download_media as whatsapp_download_media,
|
|
12
9
|
)
|
|
13
|
-
from
|
|
10
|
+
from .whatsapp import (
|
|
14
11
|
get_chat as whatsapp_get_chat,
|
|
15
12
|
)
|
|
16
|
-
from
|
|
13
|
+
from .whatsapp import (
|
|
17
14
|
get_contact_chats as whatsapp_get_contact_chats,
|
|
18
15
|
)
|
|
19
|
-
from
|
|
16
|
+
from .whatsapp import (
|
|
20
17
|
get_direct_chat_by_contact as whatsapp_get_direct_chat_by_contact,
|
|
21
18
|
)
|
|
22
|
-
from
|
|
19
|
+
from .whatsapp import (
|
|
23
20
|
get_last_interaction as whatsapp_get_last_interaction,
|
|
24
21
|
)
|
|
25
|
-
from
|
|
22
|
+
from .whatsapp import (
|
|
26
23
|
get_message_context as whatsapp_get_message_context,
|
|
27
24
|
)
|
|
28
|
-
from
|
|
25
|
+
from .whatsapp import (
|
|
29
26
|
list_chats as whatsapp_list_chats,
|
|
30
27
|
)
|
|
31
|
-
from
|
|
28
|
+
from .whatsapp import (
|
|
32
29
|
list_messages as whatsapp_list_messages,
|
|
33
30
|
)
|
|
34
|
-
from
|
|
31
|
+
from .whatsapp import (
|
|
35
32
|
search_contacts as whatsapp_search_contacts,
|
|
36
33
|
)
|
|
37
|
-
from
|
|
34
|
+
from .whatsapp import (
|
|
38
35
|
send_audio_message as whatsapp_audio_voice_message,
|
|
39
36
|
)
|
|
40
|
-
from
|
|
37
|
+
from .whatsapp import (
|
|
41
38
|
send_file as whatsapp_send_file,
|
|
42
39
|
)
|
|
43
|
-
from
|
|
40
|
+
from .whatsapp import (
|
|
44
41
|
send_message as whatsapp_send_message,
|
|
45
42
|
)
|
|
46
43
|
|
|
44
|
+
from universal_mcp.applications.application import BaseApplication
|
|
45
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
|
46
|
+
from universal_mcp.agentr.integration import AgentrIntegration
|
|
47
|
+
|
|
47
48
|
|
|
48
49
|
class WhatsappApp(BaseApplication):
|
|
49
50
|
"""
|
|
50
51
|
Base class for Universal MCP Applications.
|
|
51
52
|
"""
|
|
52
53
|
|
|
53
|
-
def __init__(self, integration:
|
|
54
|
+
def __init__(self, integration: AgentrIntegration | None = None, **kwargs) -> None:
|
|
54
55
|
super().__init__(name="whatsapp", integration=integration, **kwargs)
|
|
55
56
|
self.base_url = WHATSAPP_API_BASE_URL
|
|
56
57
|
self._api_key: str = integration.client.api_key if integration else None
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
def convert_to_opus_ogg(input_file, output_file=None, bitrate="32k", sample_rate=24000):
|
|
6
|
+
"""
|
|
7
|
+
Convert an audio file to Opus format in an Ogg container.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
input_file (str): Path to the input audio file
|
|
11
|
+
output_file (str, optional): Path to save the output file. If None, replaces the
|
|
12
|
+
extension of input_file with .ogg
|
|
13
|
+
bitrate (str, optional): Target bitrate for Opus encoding (default: "32k")
|
|
14
|
+
sample_rate (int, optional): Sample rate for output (default: 24000)
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
str: Path to the converted file
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
FileNotFoundError: If the input file doesn't exist
|
|
21
|
+
RuntimeError: If the ffmpeg conversion fails
|
|
22
|
+
"""
|
|
23
|
+
if not os.path.isfile(input_file):
|
|
24
|
+
raise FileNotFoundError(f"Input file not found: {input_file}")
|
|
25
|
+
|
|
26
|
+
# If no output file is specified, replace the extension with .ogg
|
|
27
|
+
if output_file is None:
|
|
28
|
+
output_file = os.path.splitext(input_file)[0] + ".ogg"
|
|
29
|
+
|
|
30
|
+
# Ensure the output directory exists
|
|
31
|
+
output_dir = os.path.dirname(output_file)
|
|
32
|
+
if output_dir and not os.path.exists(output_dir):
|
|
33
|
+
os.makedirs(output_dir)
|
|
34
|
+
|
|
35
|
+
# Build the ffmpeg command
|
|
36
|
+
cmd = [
|
|
37
|
+
"ffmpeg",
|
|
38
|
+
"-i", input_file,
|
|
39
|
+
"-c:a", "libopus",
|
|
40
|
+
"-b:a", bitrate,
|
|
41
|
+
"-ar", str(sample_rate),
|
|
42
|
+
"-application", "voip", # Optimize for voice
|
|
43
|
+
"-vbr", "on", # Variable bitrate
|
|
44
|
+
"-compression_level", "10", # Maximum compression
|
|
45
|
+
"-frame_duration", "60", # 60ms frames (good for voice)
|
|
46
|
+
"-y", # Overwrite output file if it exists
|
|
47
|
+
output_file
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Run the ffmpeg command and capture output
|
|
52
|
+
process = subprocess.run(
|
|
53
|
+
cmd,
|
|
54
|
+
stdout=subprocess.PIPE,
|
|
55
|
+
stderr=subprocess.PIPE,
|
|
56
|
+
text=True,
|
|
57
|
+
check=True
|
|
58
|
+
)
|
|
59
|
+
return output_file
|
|
60
|
+
except subprocess.CalledProcessError as e:
|
|
61
|
+
raise RuntimeError(f"Failed to convert audio. You likely need to install ffmpeg {e.stderr}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def convert_to_opus_ogg_temp(input_file, bitrate="32k", sample_rate=24000):
|
|
65
|
+
"""
|
|
66
|
+
Convert an audio file to Opus format in an Ogg container and store in a temporary file.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
input_file (str): Path to the input audio file
|
|
70
|
+
bitrate (str, optional): Target bitrate for Opus encoding (default: "32k")
|
|
71
|
+
sample_rate (int, optional): Sample rate for output (default: 24000)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: Path to the temporary file with the converted audio
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
FileNotFoundError: If the input file doesn't exist
|
|
78
|
+
RuntimeError: If the ffmpeg conversion fails
|
|
79
|
+
"""
|
|
80
|
+
# Create a temporary file with .ogg extension
|
|
81
|
+
temp_file = tempfile.NamedTemporaryFile(suffix=".ogg", delete=False)
|
|
82
|
+
temp_file.close()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Convert the audio
|
|
86
|
+
convert_to_opus_ogg(input_file, temp_file.name, bitrate, sample_rate)
|
|
87
|
+
return temp_file.name
|
|
88
|
+
except Exception as e:
|
|
89
|
+
# Clean up the temporary file if conversion fails
|
|
90
|
+
if os.path.exists(temp_file.name):
|
|
91
|
+
os.unlink(temp_file.name)
|
|
92
|
+
raise e
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
# Example usage
|
|
97
|
+
import sys
|
|
98
|
+
|
|
99
|
+
if len(sys.argv) < 2:
|
|
100
|
+
print("Usage: python audio.py input_file [output_file]")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
input_file = sys.argv[1]
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
result = convert_to_opus_ogg_temp(input_file)
|
|
107
|
+
print(f"Successfully converted to: {result}")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"Error: {e}")
|
|
110
|
+
sys.exit(1)
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional, List, Tuple
|
|
6
|
+
import os.path
|
|
7
|
+
from . import audio
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
WHATSAPP_API_BASE_URL = os.getenv('WHATSAPP_API_BASE_URL', "http://134.209.144.43:8080")
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Message:
|
|
16
|
+
timestamp: datetime
|
|
17
|
+
sender: str
|
|
18
|
+
content: str
|
|
19
|
+
is_from_me: bool
|
|
20
|
+
chat_jid: str
|
|
21
|
+
id: str
|
|
22
|
+
chat_name: Optional[str] = None
|
|
23
|
+
media_type: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Chat:
|
|
27
|
+
jid: str
|
|
28
|
+
name: Optional[str]
|
|
29
|
+
last_message_time: Optional[datetime]
|
|
30
|
+
last_message: Optional[str] = None
|
|
31
|
+
last_sender: Optional[str] = None
|
|
32
|
+
last_is_from_me: Optional[bool] = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_group(self) -> bool:
|
|
36
|
+
"""Determine if chat is a group based on JID pattern."""
|
|
37
|
+
return self.jid.endswith("@g.us")
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Contact:
|
|
41
|
+
phone_number: str
|
|
42
|
+
name: Optional[str]
|
|
43
|
+
jid: str
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class MessageContext:
|
|
47
|
+
message: Message
|
|
48
|
+
before: List[Message]
|
|
49
|
+
after: List[Message]
|
|
50
|
+
|
|
51
|
+
def _make_api_request(endpoint: str, method: str = "GET", data: dict = None, user_id: str = "default_user") -> dict:
|
|
52
|
+
"""Make HTTP request to WhatsApp Bridge API."""
|
|
53
|
+
url = f"{WHATSAPP_API_BASE_URL}/api/{endpoint}"
|
|
54
|
+
|
|
55
|
+
# Add user_id to query parameters for GET requests
|
|
56
|
+
if method.upper() == "GET" and data:
|
|
57
|
+
data["user_id"] = user_id
|
|
58
|
+
elif method.upper() == "GET":
|
|
59
|
+
data = {"user_id": user_id}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
if method.upper() == "GET":
|
|
63
|
+
response = requests.get(url, params=data, timeout=30)
|
|
64
|
+
elif method.upper() == "POST":
|
|
65
|
+
response = requests.post(url, json=data, timeout=30)
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
68
|
+
|
|
69
|
+
if response.status_code == 200:
|
|
70
|
+
return response.json()
|
|
71
|
+
else:
|
|
72
|
+
return {"error": f"HTTP {response.status_code}: {response.text}"}
|
|
73
|
+
|
|
74
|
+
except requests.RequestException as e:
|
|
75
|
+
return {"error": f"Request failed: {str(e)}"}
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
return {"error": f"Invalid JSON response: {response.text}"}
|
|
78
|
+
|
|
79
|
+
def get_sender_name(sender_jid: str, user_id: str = "default_user") -> str:
|
|
80
|
+
"""Get sender name via API call."""
|
|
81
|
+
result = _make_api_request("sender_name", data={"sender_jid": sender_jid}, user_id=user_id)
|
|
82
|
+
|
|
83
|
+
if "error" in result:
|
|
84
|
+
return sender_jid
|
|
85
|
+
|
|
86
|
+
return result.get("name", sender_jid)
|
|
87
|
+
|
|
88
|
+
def format_message(message: Message, show_chat_info: bool = True, user_id: str = "default_user") -> str:
|
|
89
|
+
"""Print a single message with consistent formatting."""
|
|
90
|
+
output = ""
|
|
91
|
+
|
|
92
|
+
if show_chat_info and message.chat_name:
|
|
93
|
+
output += f"[{message.timestamp:%Y-%m-%d %H:%M:%S}] Chat: {message.chat_name} "
|
|
94
|
+
else:
|
|
95
|
+
output += f"[{message.timestamp:%Y-%m-%d %H:%M:%S}] "
|
|
96
|
+
|
|
97
|
+
content_prefix = ""
|
|
98
|
+
if hasattr(message, 'media_type') and message.media_type:
|
|
99
|
+
content_prefix = f"[{message.media_type} - Message ID: {message.id} - Chat JID: {message.chat_jid}] "
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
sender_name = get_sender_name(message.sender, user_id) if not message.is_from_me else "Me"
|
|
103
|
+
output += f"From: {sender_name}: {content_prefix}{message.content}\n"
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(f"Error formatting message: {e}")
|
|
106
|
+
return output
|
|
107
|
+
|
|
108
|
+
def format_messages_list(messages: List[Message], show_chat_info: bool = True, user_id: str = "default_user") -> str:
|
|
109
|
+
output = ""
|
|
110
|
+
if not messages:
|
|
111
|
+
output += "No messages to display."
|
|
112
|
+
return output
|
|
113
|
+
|
|
114
|
+
for message in messages:
|
|
115
|
+
output += format_message(message, show_chat_info, user_id)
|
|
116
|
+
return output
|
|
117
|
+
|
|
118
|
+
def list_messages(
|
|
119
|
+
after: Optional[str] = None,
|
|
120
|
+
before: Optional[str] = None,
|
|
121
|
+
sender_phone_number: Optional[str] = None,
|
|
122
|
+
chat_jid: Optional[str] = None,
|
|
123
|
+
query: Optional[str] = None,
|
|
124
|
+
limit: int = 20,
|
|
125
|
+
page: int = 0,
|
|
126
|
+
include_context: bool = True,
|
|
127
|
+
context_before: int = 1,
|
|
128
|
+
context_after: int = 1,
|
|
129
|
+
user_id: str = "default_user"
|
|
130
|
+
) -> str:
|
|
131
|
+
"""Get messages matching the specified criteria with optional context via API."""
|
|
132
|
+
params = {
|
|
133
|
+
"after": after,
|
|
134
|
+
"before": before,
|
|
135
|
+
"sender_phone_number": sender_phone_number,
|
|
136
|
+
"chat_jid": chat_jid,
|
|
137
|
+
"query": query,
|
|
138
|
+
"limit": limit,
|
|
139
|
+
"page": page,
|
|
140
|
+
"include_context": include_context,
|
|
141
|
+
"context_before": context_before,
|
|
142
|
+
"context_after": context_after
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Remove None values
|
|
146
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
147
|
+
|
|
148
|
+
result = _make_api_request("messages", data=params, user_id=user_id)
|
|
149
|
+
|
|
150
|
+
if "error" in result:
|
|
151
|
+
return f"Error retrieving messages: {result['error']}"
|
|
152
|
+
|
|
153
|
+
return result.get("messages", "No messages found")
|
|
154
|
+
|
|
155
|
+
def get_message_context(
|
|
156
|
+
message_id: str,
|
|
157
|
+
before: int = 5,
|
|
158
|
+
after: int = 5,
|
|
159
|
+
user_id: str = "default_user"
|
|
160
|
+
) -> MessageContext:
|
|
161
|
+
"""Get context around a specific message via API."""
|
|
162
|
+
params = {
|
|
163
|
+
"message_id": message_id,
|
|
164
|
+
"before": before,
|
|
165
|
+
"after": after
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
result = _make_api_request("message_context", data=params, user_id=user_id)
|
|
169
|
+
|
|
170
|
+
if "error" in result:
|
|
171
|
+
raise ValueError(f"Error getting message context: {result['error']}")
|
|
172
|
+
|
|
173
|
+
# Parse the response into MessageContext object
|
|
174
|
+
# This would need to be implemented based on the API response structure
|
|
175
|
+
# For now, returning a simple error message
|
|
176
|
+
raise NotImplementedError("Message context API not yet implemented in bridge")
|
|
177
|
+
|
|
178
|
+
def list_chats(
|
|
179
|
+
query: Optional[str] = None,
|
|
180
|
+
limit: int = 20,
|
|
181
|
+
page: int = 0,
|
|
182
|
+
include_last_message: bool = True,
|
|
183
|
+
sort_by: str = "last_active",
|
|
184
|
+
user_id: str = "default_user"
|
|
185
|
+
) -> List[Chat]:
|
|
186
|
+
"""Get chats matching the specified criteria via API."""
|
|
187
|
+
params = {
|
|
188
|
+
"query": query,
|
|
189
|
+
"limit": limit,
|
|
190
|
+
"page": page,
|
|
191
|
+
"include_last_message": include_last_message,
|
|
192
|
+
"sort_by": sort_by
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Remove None values
|
|
196
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
197
|
+
|
|
198
|
+
result = _make_api_request("chats", data=params, user_id=user_id)
|
|
199
|
+
|
|
200
|
+
if "error" in result:
|
|
201
|
+
print(f"Error retrieving chats: {result['error']}")
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
chats_data = result.get("chats", [])
|
|
205
|
+
chats = []
|
|
206
|
+
|
|
207
|
+
for chat_data in chats_data:
|
|
208
|
+
chat = Chat(
|
|
209
|
+
jid=chat_data["jid"],
|
|
210
|
+
name=chat_data.get("name"),
|
|
211
|
+
last_message_time=datetime.fromisoformat(chat_data["last_message_time"]) if chat_data.get("last_message_time") else None,
|
|
212
|
+
last_message=chat_data.get("last_message"),
|
|
213
|
+
last_sender=chat_data.get("last_sender"),
|
|
214
|
+
last_is_from_me=chat_data.get("last_is_from_me")
|
|
215
|
+
)
|
|
216
|
+
chats.append(chat)
|
|
217
|
+
|
|
218
|
+
return chats
|
|
219
|
+
|
|
220
|
+
def search_contacts(query: str, user_id: str = "default_user") -> List[Contact]:
|
|
221
|
+
"""Search contacts by name or phone number via API."""
|
|
222
|
+
result = _make_api_request("contacts", data={"query": query}, user_id=user_id)
|
|
223
|
+
|
|
224
|
+
if "error" in result:
|
|
225
|
+
print(f"Error searching contacts: {result['error']}")
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
contacts_data = result.get("contacts", [])
|
|
229
|
+
contacts = []
|
|
230
|
+
|
|
231
|
+
for contact_data in contacts_data:
|
|
232
|
+
contact = Contact(
|
|
233
|
+
phone_number=contact_data["phone_number"],
|
|
234
|
+
name=contact_data.get("name"),
|
|
235
|
+
jid=contact_data["jid"]
|
|
236
|
+
)
|
|
237
|
+
contacts.append(contact)
|
|
238
|
+
|
|
239
|
+
return contacts
|
|
240
|
+
|
|
241
|
+
def get_contact_chats(jid: str, limit: int = 20, page: int = 0, user_id: str = "default_user") -> List[Chat]:
|
|
242
|
+
"""Get all chats involving the contact via API."""
|
|
243
|
+
params = {
|
|
244
|
+
"jid": jid,
|
|
245
|
+
"limit": limit,
|
|
246
|
+
"page": page
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
result = _make_api_request("contact_chats", data=params, user_id=user_id)
|
|
250
|
+
|
|
251
|
+
if "error" in result:
|
|
252
|
+
print(f"Error getting contact chats: {result['error']}")
|
|
253
|
+
return []
|
|
254
|
+
|
|
255
|
+
chats_data = result.get("chats", [])
|
|
256
|
+
chats = []
|
|
257
|
+
|
|
258
|
+
for chat_data in chats_data:
|
|
259
|
+
chat = Chat(
|
|
260
|
+
jid=chat_data["jid"],
|
|
261
|
+
name=chat_data.get("name"),
|
|
262
|
+
last_message_time=datetime.fromisoformat(chat_data["last_message_time"]) if chat_data.get("last_message_time") else None,
|
|
263
|
+
last_message=chat_data.get("last_message"),
|
|
264
|
+
last_sender=chat_data.get("last_sender"),
|
|
265
|
+
last_is_from_me=chat_data.get("last_is_from_me")
|
|
266
|
+
)
|
|
267
|
+
chats.append(chat)
|
|
268
|
+
|
|
269
|
+
return chats
|
|
270
|
+
|
|
271
|
+
def get_last_interaction(jid: str, user_id: str = "default_user") -> str:
|
|
272
|
+
"""Get most recent message involving the contact via API."""
|
|
273
|
+
result = _make_api_request("last_interaction", data={"jid": jid}, user_id=user_id)
|
|
274
|
+
|
|
275
|
+
if "error" in result:
|
|
276
|
+
return f"Error getting last interaction: {result['error']}"
|
|
277
|
+
|
|
278
|
+
return result.get("message", "No interaction found")
|
|
279
|
+
|
|
280
|
+
def get_chat(chat_jid: str, include_last_message: bool = True, user_id: str = "default_user") -> Optional[Chat]:
|
|
281
|
+
"""Get chat metadata by JID via API."""
|
|
282
|
+
params = {
|
|
283
|
+
"chat_jid": chat_jid,
|
|
284
|
+
"include_last_message": include_last_message
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
result = _make_api_request("chat", data=params, user_id=user_id)
|
|
288
|
+
|
|
289
|
+
if "error" in result:
|
|
290
|
+
print(f"Error getting chat: {result['error']}")
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
chat_data = result.get("chat")
|
|
294
|
+
if not chat_data:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
return Chat(
|
|
298
|
+
jid=chat_data["jid"],
|
|
299
|
+
name=chat_data.get("name"),
|
|
300
|
+
last_message_time=datetime.fromisoformat(chat_data["last_message_time"]) if chat_data.get("last_message_time") else None,
|
|
301
|
+
last_message=chat_data.get("last_message"),
|
|
302
|
+
last_sender=chat_data.get("last_sender"),
|
|
303
|
+
last_is_from_me=chat_data.get("last_is_from_me")
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def get_direct_chat_by_contact(sender_phone_number: str, user_id: str = "default_user") -> Optional[Chat]:
|
|
307
|
+
"""Get chat metadata by sender phone number via API."""
|
|
308
|
+
result = _make_api_request("direct_chat", data={"sender_phone_number": sender_phone_number}, user_id=user_id)
|
|
309
|
+
|
|
310
|
+
if "error" in result:
|
|
311
|
+
print(f"Error getting direct chat: {result['error']}")
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
chat_data = result.get("chat")
|
|
315
|
+
if not chat_data:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
return Chat(
|
|
319
|
+
jid=chat_data["jid"],
|
|
320
|
+
name=chat_data.get("name"),
|
|
321
|
+
last_message_time=datetime.fromisoformat(chat_data["last_message_time"]) if chat_data.get("last_message_time") else None,
|
|
322
|
+
last_message=chat_data.get("last_message"),
|
|
323
|
+
last_sender=chat_data.get("last_sender"),
|
|
324
|
+
last_is_from_me=chat_data.get("last_is_from_me")
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def send_message(recipient: str, message: str, user_id: str = "default_user") -> Tuple[bool, str]:
|
|
328
|
+
"""Send message via API."""
|
|
329
|
+
payload = {
|
|
330
|
+
"user_id": user_id,
|
|
331
|
+
"recipient": recipient,
|
|
332
|
+
"message": message
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
result = _make_api_request("send", method="POST", data=payload, user_id=user_id)
|
|
336
|
+
|
|
337
|
+
if "error" in result:
|
|
338
|
+
return False, result["error"]
|
|
339
|
+
|
|
340
|
+
return result.get("success", False), result.get("message", "Unknown response")
|
|
341
|
+
|
|
342
|
+
def send_file(recipient: str, media_path: str, user_id: str = "default_user") -> Tuple[bool, str]:
|
|
343
|
+
"""Send file via API."""
|
|
344
|
+
payload = {
|
|
345
|
+
"user_id": user_id,
|
|
346
|
+
"recipient": recipient,
|
|
347
|
+
"media_path": media_path
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
result = _make_api_request("send", method="POST", data=payload, user_id=user_id)
|
|
351
|
+
|
|
352
|
+
if "error" in result:
|
|
353
|
+
return False, result["error"]
|
|
354
|
+
|
|
355
|
+
return result.get("success", False), result.get("message", "Unknown response")
|
|
356
|
+
|
|
357
|
+
def send_audio_message(recipient: str, media_path: str, user_id: str = "default_user") -> Tuple[bool, str]:
|
|
358
|
+
"""Send audio message via API."""
|
|
359
|
+
if not media_path.endswith(".ogg"):
|
|
360
|
+
try:
|
|
361
|
+
media_path = audio.convert_to_opus_ogg_temp(media_path)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
return False, f"Error converting file to opus ogg. You likely need to install ffmpeg: {str(e)}"
|
|
364
|
+
|
|
365
|
+
payload = {
|
|
366
|
+
"user_id": user_id,
|
|
367
|
+
"recipient": recipient,
|
|
368
|
+
"media_path": media_path
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
result = _make_api_request("send", method="POST", data=payload, user_id=user_id)
|
|
372
|
+
|
|
373
|
+
if "error" in result:
|
|
374
|
+
return False, result["error"]
|
|
375
|
+
|
|
376
|
+
return result.get("success", False), result.get("message", "Unknown response")
|
|
377
|
+
|
|
378
|
+
def download_media(message_id: str, chat_jid: str, user_id: str = "default_user") -> Optional[str]:
|
|
379
|
+
"""Download media from a message via API."""
|
|
380
|
+
payload = {
|
|
381
|
+
"user_id": user_id,
|
|
382
|
+
"message_id": message_id,
|
|
383
|
+
"chat_jid": chat_jid
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
result = _make_api_request("download", method="POST", data=payload, user_id=user_id)
|
|
387
|
+
|
|
388
|
+
if "error" in result:
|
|
389
|
+
print(f"Download failed: {result['error']}")
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
if result.get("success", False):
|
|
393
|
+
path = result.get("path")
|
|
394
|
+
print(f"Media downloaded successfully: {path}")
|
|
395
|
+
return path
|
|
396
|
+
else:
|
|
397
|
+
print(f"Download failed: {result.get('message', 'Unknown error')}")
|
|
398
|
+
return None
|
|
@@ -6,7 +6,7 @@ from universal_mcp.integrations import Integration
|
|
|
6
6
|
|
|
7
7
|
class WhatsappBusinessApp(APIApplication):
|
|
8
8
|
def __init__(self, integration: Integration = None, **kwargs) -> None:
|
|
9
|
-
super().__init__(name="
|
|
9
|
+
super().__init__(name="whatsapp_business", integration=integration, **kwargs)
|
|
10
10
|
self.base_url = "https://graph.facebook.com"
|
|
11
11
|
|
|
12
12
|
def get_analytics(
|