roomkit 0.1.0__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.
- roomkit/AGENTS.md +362 -0
- roomkit/__init__.py +372 -0
- roomkit/_version.py +1 -0
- roomkit/ai_docs.py +93 -0
- roomkit/channels/__init__.py +194 -0
- roomkit/channels/ai.py +238 -0
- roomkit/channels/base.py +66 -0
- roomkit/channels/transport.py +115 -0
- roomkit/channels/websocket.py +85 -0
- roomkit/core/__init__.py +0 -0
- roomkit/core/_channel_ops.py +252 -0
- roomkit/core/_helpers.py +296 -0
- roomkit/core/_inbound.py +435 -0
- roomkit/core/_room_lifecycle.py +275 -0
- roomkit/core/circuit_breaker.py +84 -0
- roomkit/core/event_router.py +401 -0
- roomkit/core/framework.py +793 -0
- roomkit/core/hooks.py +232 -0
- roomkit/core/inbound_router.py +57 -0
- roomkit/core/locks.py +66 -0
- roomkit/core/rate_limiter.py +67 -0
- roomkit/core/retry.py +49 -0
- roomkit/core/router.py +24 -0
- roomkit/core/transcoder.py +85 -0
- roomkit/identity/__init__.py +0 -0
- roomkit/identity/base.py +27 -0
- roomkit/identity/mock.py +49 -0
- roomkit/llms.txt +52 -0
- roomkit/models/__init__.py +104 -0
- roomkit/models/channel.py +99 -0
- roomkit/models/context.py +35 -0
- roomkit/models/delivery.py +76 -0
- roomkit/models/enums.py +170 -0
- roomkit/models/event.py +203 -0
- roomkit/models/framework_event.py +19 -0
- roomkit/models/hook.py +68 -0
- roomkit/models/identity.py +81 -0
- roomkit/models/participant.py +34 -0
- roomkit/models/room.py +33 -0
- roomkit/models/task.py +36 -0
- roomkit/providers/__init__.py +0 -0
- roomkit/providers/ai/__init__.py +0 -0
- roomkit/providers/ai/base.py +140 -0
- roomkit/providers/ai/mock.py +33 -0
- roomkit/providers/anthropic/__init__.py +6 -0
- roomkit/providers/anthropic/ai.py +145 -0
- roomkit/providers/anthropic/config.py +14 -0
- roomkit/providers/elasticemail/__init__.py +6 -0
- roomkit/providers/elasticemail/config.py +16 -0
- roomkit/providers/elasticemail/email.py +97 -0
- roomkit/providers/email/__init__.py +0 -0
- roomkit/providers/email/base.py +46 -0
- roomkit/providers/email/mock.py +34 -0
- roomkit/providers/gemini/__init__.py +6 -0
- roomkit/providers/gemini/ai.py +153 -0
- roomkit/providers/gemini/config.py +14 -0
- roomkit/providers/http/__init__.py +15 -0
- roomkit/providers/http/base.py +33 -0
- roomkit/providers/http/config.py +14 -0
- roomkit/providers/http/mock.py +21 -0
- roomkit/providers/http/provider.py +105 -0
- roomkit/providers/http/webhook.py +33 -0
- roomkit/providers/messenger/__init__.py +15 -0
- roomkit/providers/messenger/base.py +33 -0
- roomkit/providers/messenger/config.py +17 -0
- roomkit/providers/messenger/facebook.py +95 -0
- roomkit/providers/messenger/mock.py +21 -0
- roomkit/providers/messenger/webhook.py +42 -0
- roomkit/providers/openai/__init__.py +6 -0
- roomkit/providers/openai/ai.py +155 -0
- roomkit/providers/openai/config.py +24 -0
- roomkit/providers/pydantic_ai/__init__.py +5 -0
- roomkit/providers/pydantic_ai/config.py +14 -0
- roomkit/providers/rcs/__init__.py +9 -0
- roomkit/providers/rcs/base.py +95 -0
- roomkit/providers/rcs/mock.py +78 -0
- roomkit/providers/sendgrid/__init__.py +5 -0
- roomkit/providers/sendgrid/config.py +13 -0
- roomkit/providers/sinch/__init__.py +6 -0
- roomkit/providers/sinch/config.py +22 -0
- roomkit/providers/sinch/sms.py +192 -0
- roomkit/providers/sms/__init__.py +15 -0
- roomkit/providers/sms/base.py +67 -0
- roomkit/providers/sms/meta.py +401 -0
- roomkit/providers/sms/mock.py +24 -0
- roomkit/providers/sms/phone.py +77 -0
- roomkit/providers/telnyx/__init__.py +21 -0
- roomkit/providers/telnyx/config.py +14 -0
- roomkit/providers/telnyx/rcs.py +352 -0
- roomkit/providers/telnyx/sms.py +231 -0
- roomkit/providers/twilio/__init__.py +18 -0
- roomkit/providers/twilio/config.py +19 -0
- roomkit/providers/twilio/rcs.py +183 -0
- roomkit/providers/twilio/sms.py +200 -0
- roomkit/providers/voicemeup/__init__.py +15 -0
- roomkit/providers/voicemeup/config.py +21 -0
- roomkit/providers/voicemeup/sms.py +374 -0
- roomkit/providers/whatsapp/__init__.py +0 -0
- roomkit/providers/whatsapp/base.py +44 -0
- roomkit/providers/whatsapp/mock.py +21 -0
- roomkit/py.typed +0 -0
- roomkit/realtime/__init__.py +17 -0
- roomkit/realtime/base.py +111 -0
- roomkit/realtime/memory.py +158 -0
- roomkit/sources/__init__.py +35 -0
- roomkit/sources/base.py +207 -0
- roomkit/sources/websocket.py +260 -0
- roomkit/store/__init__.py +0 -0
- roomkit/store/base.py +230 -0
- roomkit/store/memory.py +293 -0
- roomkit-0.1.0.dist-info/METADATA +567 -0
- roomkit-0.1.0.dist-info/RECORD +114 -0
- roomkit-0.1.0.dist-info/WHEEL +4 -0
- roomkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Twilio RCS provider — sends RCS messages via the Twilio REST API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, SecretStr
|
|
8
|
+
|
|
9
|
+
from roomkit.models.delivery import InboundMessage
|
|
10
|
+
from roomkit.models.event import RoomEvent
|
|
11
|
+
from roomkit.providers.rcs.base import RCSDeliveryResult, RCSProvider
|
|
12
|
+
from roomkit.providers.sms.meta import (
|
|
13
|
+
build_inbound_content,
|
|
14
|
+
extract_media_urls,
|
|
15
|
+
extract_text_body,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TwilioRCSConfig(BaseModel):
|
|
23
|
+
"""Twilio RCS provider configuration."""
|
|
24
|
+
|
|
25
|
+
account_sid: str
|
|
26
|
+
auth_token: SecretStr
|
|
27
|
+
messaging_service_sid: str # Required for RCS (must be RCS-enabled)
|
|
28
|
+
timeout: float = 10.0
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def api_url(self) -> str:
|
|
32
|
+
return f"https://api.twilio.com/2010-04-01/Accounts/{self.account_sid}/Messages.json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TwilioRCSProvider(RCSProvider):
|
|
36
|
+
"""RCS provider using the Twilio REST API.
|
|
37
|
+
|
|
38
|
+
Twilio RCS uses the same Messages API as SMS, but requires an RCS-enabled
|
|
39
|
+
Messaging Service. When the recipient doesn't support RCS, Twilio can
|
|
40
|
+
automatically fall back to SMS (unless disabled via fallback=False).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, config: TwilioRCSConfig) -> None:
|
|
44
|
+
try:
|
|
45
|
+
import httpx as _httpx
|
|
46
|
+
except ImportError as exc:
|
|
47
|
+
raise ImportError(
|
|
48
|
+
"httpx is required for TwilioRCSProvider. "
|
|
49
|
+
"Install it with: pip install roomkit[httpx]"
|
|
50
|
+
) from exc
|
|
51
|
+
self._config = config
|
|
52
|
+
self._httpx = _httpx
|
|
53
|
+
self._client: httpx.AsyncClient = _httpx.AsyncClient(timeout=config.timeout)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def sender_id(self) -> str:
|
|
57
|
+
return self._config.messaging_service_sid
|
|
58
|
+
|
|
59
|
+
async def send(
|
|
60
|
+
self,
|
|
61
|
+
event: RoomEvent,
|
|
62
|
+
to: str,
|
|
63
|
+
*,
|
|
64
|
+
fallback: bool = True,
|
|
65
|
+
) -> RCSDeliveryResult:
|
|
66
|
+
"""Send an RCS message via Twilio.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
event: The room event containing the message content.
|
|
70
|
+
to: Recipient phone number (E.164 format).
|
|
71
|
+
fallback: If True, allow SMS fallback. If False, RCS only.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Result with delivery info including channel used.
|
|
75
|
+
"""
|
|
76
|
+
content = event.content
|
|
77
|
+
body = extract_text_body(content)
|
|
78
|
+
media_urls = extract_media_urls(content)
|
|
79
|
+
|
|
80
|
+
if not body and not media_urls:
|
|
81
|
+
return RCSDeliveryResult(success=False, error="empty_message")
|
|
82
|
+
|
|
83
|
+
auth = (self._config.account_sid, self._config.auth_token.get_secret_value())
|
|
84
|
+
|
|
85
|
+
# Twilio expects form-encoded data
|
|
86
|
+
data: dict[str, str] = {
|
|
87
|
+
"MessagingServiceSid": self._config.messaging_service_sid,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# For RCS-only (no fallback), prefix "to" with "rcs:"
|
|
91
|
+
if fallback:
|
|
92
|
+
data["To"] = to
|
|
93
|
+
else:
|
|
94
|
+
data["To"] = f"rcs:{to}"
|
|
95
|
+
|
|
96
|
+
if body:
|
|
97
|
+
data["Body"] = body
|
|
98
|
+
|
|
99
|
+
# Add media URLs (Twilio supports up to 10)
|
|
100
|
+
for i, url in enumerate(media_urls[:10]):
|
|
101
|
+
data[f"MediaUrl{i}"] = url
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
resp = await self._client.post(
|
|
105
|
+
self._config.api_url,
|
|
106
|
+
auth=auth,
|
|
107
|
+
data=data,
|
|
108
|
+
)
|
|
109
|
+
resp.raise_for_status()
|
|
110
|
+
response_data = resp.json()
|
|
111
|
+
except self._httpx.TimeoutException:
|
|
112
|
+
return RCSDeliveryResult(success=False, error="timeout")
|
|
113
|
+
except self._httpx.HTTPStatusError as exc:
|
|
114
|
+
status = exc.response.status_code
|
|
115
|
+
if status == 401:
|
|
116
|
+
return RCSDeliveryResult(success=False, error="auth_error")
|
|
117
|
+
if status == 429:
|
|
118
|
+
return RCSDeliveryResult(success=False, error="rate_limit")
|
|
119
|
+
if status == 400:
|
|
120
|
+
try:
|
|
121
|
+
error_data = exc.response.json()
|
|
122
|
+
error_code = error_data.get("code", "invalid_request")
|
|
123
|
+
return RCSDeliveryResult(
|
|
124
|
+
success=False,
|
|
125
|
+
error=f"twilio_{error_code}",
|
|
126
|
+
metadata={"message": error_data.get("message", "")},
|
|
127
|
+
)
|
|
128
|
+
except Exception:
|
|
129
|
+
return RCSDeliveryResult(success=False, error="invalid_request")
|
|
130
|
+
return RCSDeliveryResult(success=False, error=f"http_{status}")
|
|
131
|
+
except self._httpx.HTTPError as exc:
|
|
132
|
+
return RCSDeliveryResult(success=False, error=str(exc))
|
|
133
|
+
|
|
134
|
+
# Twilio returns the channel used in the response
|
|
135
|
+
# For now, we assume RCS unless we detect fallback from status callback
|
|
136
|
+
return RCSDeliveryResult(
|
|
137
|
+
success=True,
|
|
138
|
+
provider_message_id=response_data.get("sid"),
|
|
139
|
+
channel_used="rcs",
|
|
140
|
+
fallback=False,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
async def close(self) -> None:
|
|
144
|
+
await self._client.aclose()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parse_twilio_rcs_webhook(
|
|
148
|
+
payload: dict[str, Any],
|
|
149
|
+
channel_id: str,
|
|
150
|
+
) -> InboundMessage:
|
|
151
|
+
"""Convert a Twilio RCS webhook POST body into an InboundMessage.
|
|
152
|
+
|
|
153
|
+
Note: Twilio sends webhooks as form-encoded data. Convert to dict first:
|
|
154
|
+
payload = dict(await request.form())
|
|
155
|
+
"""
|
|
156
|
+
body = payload.get("Body", "")
|
|
157
|
+
|
|
158
|
+
media: list[dict[str, str | None]] = []
|
|
159
|
+
num_media = int(payload.get("NumMedia", "0"))
|
|
160
|
+
for i in range(num_media):
|
|
161
|
+
url = payload.get(f"MediaUrl{i}")
|
|
162
|
+
if url:
|
|
163
|
+
media.append(
|
|
164
|
+
{
|
|
165
|
+
"url": url,
|
|
166
|
+
"mime_type": payload.get(f"MediaContentType{i}"),
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return InboundMessage(
|
|
171
|
+
channel_id=channel_id,
|
|
172
|
+
sender_id=payload.get("From", ""),
|
|
173
|
+
content=build_inbound_content(body, media),
|
|
174
|
+
external_id=payload.get("MessageSid"),
|
|
175
|
+
idempotency_key=payload.get("MessageSid"),
|
|
176
|
+
metadata={
|
|
177
|
+
"to": payload.get("To", ""),
|
|
178
|
+
"account_sid": payload.get("AccountSid", ""),
|
|
179
|
+
"num_media": payload.get("NumMedia", "0"),
|
|
180
|
+
"channel": payload.get("Channel", "rcs"), # RCS-specific
|
|
181
|
+
"messaging_service_sid": payload.get("MessagingServiceSid", ""),
|
|
182
|
+
},
|
|
183
|
+
)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Twilio SMS provider — sends SMS via the Twilio REST API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from roomkit.models.delivery import InboundMessage, ProviderResult
|
|
11
|
+
from roomkit.models.event import RoomEvent
|
|
12
|
+
from roomkit.providers.sms.base import SMSProvider
|
|
13
|
+
from roomkit.providers.sms.meta import (
|
|
14
|
+
build_inbound_content,
|
|
15
|
+
extract_media_urls,
|
|
16
|
+
extract_text_body,
|
|
17
|
+
)
|
|
18
|
+
from roomkit.providers.twilio.config import TwilioConfig
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TwilioSMSProvider(SMSProvider):
|
|
25
|
+
"""SMS provider using the Twilio REST API."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: TwilioConfig) -> None:
|
|
28
|
+
try:
|
|
29
|
+
import httpx as _httpx
|
|
30
|
+
except ImportError as exc:
|
|
31
|
+
raise ImportError(
|
|
32
|
+
"httpx is required for TwilioSMSProvider. "
|
|
33
|
+
"Install it with: pip install roomkit[httpx]"
|
|
34
|
+
) from exc
|
|
35
|
+
self._config = config
|
|
36
|
+
self._httpx = _httpx
|
|
37
|
+
self._client: httpx.AsyncClient = _httpx.AsyncClient(timeout=config.timeout)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def from_number(self) -> str:
|
|
41
|
+
return self._config.from_number
|
|
42
|
+
|
|
43
|
+
async def send(self, event: RoomEvent, to: str, from_: str | None = None) -> ProviderResult:
|
|
44
|
+
content = event.content
|
|
45
|
+
body = extract_text_body(content)
|
|
46
|
+
media_urls = extract_media_urls(content)
|
|
47
|
+
|
|
48
|
+
if not body and not media_urls:
|
|
49
|
+
return ProviderResult(success=False, error="empty_message")
|
|
50
|
+
|
|
51
|
+
from_number = from_ or self._config.from_number
|
|
52
|
+
|
|
53
|
+
# Twilio uses HTTP Basic auth
|
|
54
|
+
auth = (self._config.account_sid, self._config.auth_token.get_secret_value())
|
|
55
|
+
|
|
56
|
+
# Twilio expects form-encoded data
|
|
57
|
+
data: dict[str, str] = {
|
|
58
|
+
"To": to,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if body:
|
|
62
|
+
data["Body"] = body
|
|
63
|
+
|
|
64
|
+
# Add media URLs (Twilio supports up to 10)
|
|
65
|
+
for i, url in enumerate(media_urls[:10]):
|
|
66
|
+
data[f"MediaUrl{i}"] = url
|
|
67
|
+
|
|
68
|
+
# Use MessagingServiceSid if provided, otherwise use From number
|
|
69
|
+
if self._config.messaging_service_sid:
|
|
70
|
+
data["MessagingServiceSid"] = self._config.messaging_service_sid
|
|
71
|
+
else:
|
|
72
|
+
data["From"] = from_number
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
resp = await self._client.post(
|
|
76
|
+
self._config.api_url,
|
|
77
|
+
auth=auth,
|
|
78
|
+
data=data,
|
|
79
|
+
)
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
response_data = resp.json()
|
|
82
|
+
except self._httpx.TimeoutException:
|
|
83
|
+
return ProviderResult(success=False, error="timeout")
|
|
84
|
+
except self._httpx.HTTPStatusError as exc:
|
|
85
|
+
status = exc.response.status_code
|
|
86
|
+
if status == 401:
|
|
87
|
+
return ProviderResult(success=False, error="auth_error")
|
|
88
|
+
if status == 429:
|
|
89
|
+
return ProviderResult(success=False, error="rate_limit")
|
|
90
|
+
if status == 400:
|
|
91
|
+
# Try to extract Twilio error code
|
|
92
|
+
try:
|
|
93
|
+
error_data = exc.response.json()
|
|
94
|
+
error_code = error_data.get("code", "invalid_request")
|
|
95
|
+
return ProviderResult(
|
|
96
|
+
success=False,
|
|
97
|
+
error=f"twilio_{error_code}",
|
|
98
|
+
metadata={"message": error_data.get("message", "")},
|
|
99
|
+
)
|
|
100
|
+
except Exception:
|
|
101
|
+
return ProviderResult(success=False, error="invalid_request")
|
|
102
|
+
return ProviderResult(success=False, error=f"http_{status}")
|
|
103
|
+
except self._httpx.HTTPError as exc:
|
|
104
|
+
return ProviderResult(success=False, error=str(exc))
|
|
105
|
+
|
|
106
|
+
return ProviderResult(
|
|
107
|
+
success=True,
|
|
108
|
+
provider_message_id=response_data.get("sid"),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def verify_signature(
|
|
112
|
+
self,
|
|
113
|
+
payload: bytes,
|
|
114
|
+
signature: str,
|
|
115
|
+
timestamp: str | None = None,
|
|
116
|
+
url: str | None = None,
|
|
117
|
+
) -> bool:
|
|
118
|
+
"""Verify a Twilio webhook signature using HMAC-SHA1.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
payload: Raw request body bytes (form-encoded).
|
|
122
|
+
signature: Value of the ``X-Twilio-Signature`` header.
|
|
123
|
+
timestamp: Not used by Twilio (included for interface compatibility).
|
|
124
|
+
url: The full URL that Twilio called (required for verification).
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if the signature is valid, False otherwise.
|
|
128
|
+
"""
|
|
129
|
+
if not url:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# Parse form-encoded payload and sort parameters
|
|
133
|
+
try:
|
|
134
|
+
from urllib.parse import unquote_plus
|
|
135
|
+
|
|
136
|
+
pairs = payload.decode().split("&")
|
|
137
|
+
params = dict(pair.split("=", 1) for pair in pairs if "=" in pair)
|
|
138
|
+
# URL decode the values
|
|
139
|
+
params = {k: unquote_plus(v) for k, v in params.items()}
|
|
140
|
+
except Exception:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
# Build the validation string: URL + sorted params
|
|
144
|
+
validation_string = url
|
|
145
|
+
for key in sorted(params.keys()):
|
|
146
|
+
validation_string += key + params[key]
|
|
147
|
+
|
|
148
|
+
# Compute HMAC-SHA1
|
|
149
|
+
expected_sig = base64.b64encode(
|
|
150
|
+
hmac.new(
|
|
151
|
+
self._config.auth_token.get_secret_value().encode(),
|
|
152
|
+
validation_string.encode(),
|
|
153
|
+
hashlib.sha1,
|
|
154
|
+
).digest()
|
|
155
|
+
).decode()
|
|
156
|
+
|
|
157
|
+
return hmac.compare_digest(expected_sig, signature)
|
|
158
|
+
|
|
159
|
+
async def close(self) -> None:
|
|
160
|
+
await self._client.aclose()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_twilio_webhook(
|
|
164
|
+
payload: dict[str, Any],
|
|
165
|
+
channel_id: str,
|
|
166
|
+
) -> InboundMessage:
|
|
167
|
+
"""Convert a Twilio webhook POST body into an InboundMessage.
|
|
168
|
+
|
|
169
|
+
Note: Twilio sends webhooks as form-encoded data. Convert to dict first:
|
|
170
|
+
payload = dict(await request.form())
|
|
171
|
+
"""
|
|
172
|
+
body = payload.get("Body", "")
|
|
173
|
+
|
|
174
|
+
media: list[dict[str, str | None]] = []
|
|
175
|
+
num_media = int(payload.get("NumMedia", "0"))
|
|
176
|
+
for i in range(num_media):
|
|
177
|
+
url = payload.get(f"MediaUrl{i}")
|
|
178
|
+
if url:
|
|
179
|
+
media.append(
|
|
180
|
+
{
|
|
181
|
+
"url": url,
|
|
182
|
+
"mime_type": payload.get(f"MediaContentType{i}"),
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return InboundMessage(
|
|
187
|
+
channel_id=channel_id,
|
|
188
|
+
sender_id=payload.get("From", ""),
|
|
189
|
+
content=build_inbound_content(body, media),
|
|
190
|
+
external_id=payload.get("MessageSid"),
|
|
191
|
+
idempotency_key=payload.get("MessageSid"),
|
|
192
|
+
metadata={
|
|
193
|
+
"to": payload.get("To", ""),
|
|
194
|
+
"account_sid": payload.get("AccountSid", ""),
|
|
195
|
+
"num_media": payload.get("NumMedia", "0"),
|
|
196
|
+
"from_city": payload.get("FromCity"),
|
|
197
|
+
"from_state": payload.get("FromState"),
|
|
198
|
+
"from_country": payload.get("FromCountry"),
|
|
199
|
+
},
|
|
200
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""VoiceMeUp provider."""
|
|
2
|
+
|
|
3
|
+
from roomkit.providers.voicemeup.config import VoiceMeUpConfig
|
|
4
|
+
from roomkit.providers.voicemeup.sms import (
|
|
5
|
+
VoiceMeUpSMSProvider,
|
|
6
|
+
configure_voicemeup_mms,
|
|
7
|
+
parse_voicemeup_webhook,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"VoiceMeUpConfig",
|
|
12
|
+
"VoiceMeUpSMSProvider",
|
|
13
|
+
"configure_voicemeup_mms",
|
|
14
|
+
"parse_voicemeup_webhook",
|
|
15
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""VoiceMeUp provider configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, SecretStr
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VoiceMeUpConfig(BaseModel):
|
|
9
|
+
"""VoiceMeUp SMS provider configuration."""
|
|
10
|
+
|
|
11
|
+
username: str
|
|
12
|
+
auth_token: SecretStr
|
|
13
|
+
from_number: str
|
|
14
|
+
environment: str = "production"
|
|
15
|
+
timeout: float = 10.0
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def base_url(self) -> str:
|
|
19
|
+
if self.environment == "sandbox":
|
|
20
|
+
return "https://dev-clients.voicemeup.com/api/v1.1/json/"
|
|
21
|
+
return "https://clients.voicemeup.com/api/v1.1/json/"
|