Skip to content

Commit f7db2f6

Browse files
authored
Merge pull request #215 from maxkahan/2.x
Adding Messages API support
2 parents c8a99a7 + d695c4c commit f7db2f6

File tree

10 files changed

+373
-24
lines changed

10 files changed

+373
-24
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 2.8.0
2+
- Added Messages API v1.0 support. Messages API can now be used by calling the client.messages.send_message() method.
3+
14
# 2.7.0
25
- Moved some client methods into their own classes: `account.py, application.py,
36
message_search.py, number_insight.py, numbers.py, short_codes.py, ussd.py`

README.md

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
1414
- [Installation](#installation)
1515
- [Usage](#usage)
1616
- [SMS API](#sms-api)
17+
- [Messages API](#messages-api)
1718
- [Voice API](#voice-api)
1819
- [Verify API](#verify-api)
1920
- [Number Insight API](#number-insight-api)
@@ -68,6 +69,8 @@ To check signatures for incoming webhook requests, you'll also need
6869
to specify the `signature_secret` argument (or the `VONAGE_SIGNATURE_SECRET`
6970
environment variable).
7071

72+
To use the SDK to call Vonage APIs, pass in dicts with the required options to methods like `Sms.send_message()`. Examples of this are given below.
73+
7174
## Simplified structure for calling API Methods
7275

7376
The client now instantiates a class object for each API when it is created, e.g. `vonage.Client(key="mykey", secret="mysecret")`
@@ -93,6 +96,7 @@ client.CLASS_NAME.CLASS_METHOD
9396

9497
## SMS API
9598

99+
Although the Messages API adds more messaging channels, the SMS API is still supported.
96100
### Send an SMS
97101

98102
```python
@@ -138,6 +142,82 @@ response = client.sms.send_message({
138142
client.sms.submit_sms_conversion(response['message-id'])
139143
```
140144

145+
## Messages API
146+
147+
The Messages API is an API that allows you to send messages via SMS, MMS, WhatsApp, Messenger and Viber. Call the API from your Python code by
148+
passing a dict of parameters into the `client.messages.send_message()` method.
149+
150+
It accepts JWT or API key/secret authentication.
151+
152+
Some basic samples are below. For more detailed information and code snippets, please visit the [Vonage Developer Documentation](https://developer.vonage.com).
153+
154+
### Send an SMS
155+
```python
156+
responseData = client.messages.send_message({
157+
'channel': 'sms',
158+
'message_type': 'text',
159+
'to': '447123456789',
160+
'from': 'Vonage',
161+
'text': 'Hello from Vonage'
162+
})
163+
```
164+
165+
### Send an MMS
166+
Note: only available in the US. You will need a 10DLC number to send an MMS message.
167+
168+
```python
169+
client.messages.send_message({
170+
'channel': 'mms',
171+
'message_type': 'image',
172+
'to': '11112223333',
173+
'from': '1223345567',
174+
'image': {'url': 'https://example.com/image.jpg', 'caption': 'Test Image'}
175+
})
176+
```
177+
178+
### Send an audio file via WhatsApp
179+
180+
You will need a WhatsApp Business Account to use WhatsApp messaging. WhatsApp restrictions mean that you
181+
must send a template message to a user if they have not previously messaged you, but you can send any message
182+
type to a user if they have messaged your business number in the last 24 hours.
183+
184+
```python
185+
client.messages.send_message({
186+
'channel': 'whatsapp',
187+
'message_type': 'audio',
188+
'to': '447123456789',
189+
'from': '440123456789',
190+
'audio': {'url': 'https://example.com/audio.mp3'}
191+
})
192+
```
193+
194+
### Send a video file via Facebook Messenger
195+
196+
You will need to link your Facebook business page to your Vonage account in the Vonage developer dashboard. (Click on the sidebar
197+
"External Accounts" option to do this.)
198+
199+
```python
200+
client.messages.send_message({
201+
'channel': 'messenger',
202+
'message_type': 'video',
203+
'to': '594123123123123',
204+
'from': '1012312312312',
205+
'video': {'url': 'https://example.com/video.mp4'}
206+
})
207+
```
208+
209+
### Send a text message with Viber
210+
211+
```python
212+
client.messages.send_message({
213+
'channel': 'viber_service',
214+
'message_type': 'text',
215+
'to': '447123456789',
216+
'from': '440123456789',
217+
'text': 'Hello from Vonage!'
218+
})
219+
```
220+
141221
## Voice API
142222

143223
### Make a call
@@ -247,6 +327,7 @@ client.voice.send_dtmf(response['uuid'], digits='1234')
247327
response = client.get_recording(RECORDING_URL)
248328
```
249329

330+
250331
## Verify API
251332

252333
### Search for a Verification request
@@ -509,12 +590,6 @@ client.api_host('myapi.vonage.com') # rewrite the value of api_host
509590

510591
## Frequently Asked Questions
511592

512-
### Dropping support for Python 2.7
513-
514-
Back in 2014 when Guido van Rossum, Python's creator and principal author, made the announcement, January 1, 2020 seemed pretty far away. Python 2.7’s sunset has happened, after which there’ll be absolutely no more support from the core Python team. Many utilized projects pledge to drop Python 2 support in or before 2020. [(Official statement here)](https://www.python.org/doc/sunset-python-2/).
515-
516-
Just because 2.7 isn’t going to be maintained past 2020 doesn’t mean your applications or libraries suddenly stop working but as of this moment we won't give official support for upcoming releases. Please read the official ["Porting Python 2 Code to Python 3" guide](https://docs.python.org/3/howto/pyporting.html). Please also read the [Python 3 Statement Practicalities](https://python3statement.org/practicalities/) for advice on sunsetting your Python 2 code.
517-
518593
### Supported APIs
519594

520595
The following is a list of Vonage APIs and whether the Python SDK provides support for them:
@@ -529,7 +604,7 @@ The following is a list of Vonage APIs and whether the Python SDK provides suppo
529604
| Dispatch API | Beta ||
530605
| External Accounts API | Beta ||
531606
| Media API | Beta ||
532-
| Messages API | Beta | |
607+
| Messages API | General Availability | |
533608
| Number Insight API | General Availability ||
534609
| Number Management API | General Availability ||
535610
| Pricing API | General Availability ||

src/vonage/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
from .client import *
2-
from .errors import *
3-
from .voice import *
4-
from .sms import *
5-
from .verify import *
62

73
__version__ = "2.7.0"

src/vonage/client.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
import vonage
2+
13
from ._internal import _format_date_param
2-
from .account import *
4+
from .account import Account
35
from .application import ApplicationV2, BasicAuthenticatedServer
46
from .errors import *
5-
from .message_search import *
7+
from .message_search import MessageSearch
8+
from .messages import Messages
69
from .number_insight import NumberInsight
7-
from .numbers import *
8-
from .short_codes import *
9-
from .sms import *
10-
from .ussd import *
11-
from .voice import *
12-
from .verify import *
10+
from .numbers import Numbers
11+
from .short_codes import ShortCodes
12+
from .sms import Sms
13+
from .ussd import Ussd
14+
from .voice import Voice
15+
from .verify import Verify
1316

1417
import logging
1518
from datetime import datetime
@@ -129,9 +132,10 @@ def __init__(
129132
api_secret=self.api_secret,
130133
)
131134
self.application_v2 = ApplicationV2(api_server)
132-
135+
133136
self.account = Account(self)
134137
self.message_search = MessageSearch(self)
138+
self.messages = Messages(self)
135139
self.number_insight = NumberInsight(self)
136140
self.numbers = Numbers(self)
137141
self.short_codes = ShortCodes(self)
@@ -234,6 +238,7 @@ def post(
234238
params,
235239
supports_signature_auth=False,
236240
header_auth=False,
241+
additional_headers=None
237242
):
238243
"""
239244
Low-level method to make a post request to a Vonage API server, which may have a Nexmo url.
@@ -245,7 +250,12 @@ def post(
245250
:param bool header_auth: Use basic authentication instead of adding api_key and api_secret to the request params.
246251
"""
247252
uri = f"https://{host}{request_uri}"
248-
headers = self.headers
253+
254+
if not additional_headers:
255+
headers = {**self.headers}
256+
else:
257+
headers = {**self.headers, **additional_headers}
258+
249259
if supports_signature_auth and self.signature_secret:
250260
params["api_key"] = self.api_key
251261
params["sig"] = self.signature(params)

src/vonage/errors.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ class AuthenticationError(ClientError):
1717
class CallbackRequiredError(Error):
1818
"""
1919
Indicates a callback is required but was not present.
20-
"""
20+
"""
21+
22+
class MessagesError(Error):
23+
"""
24+
Indicates an error related to the Messages class which calls the Vonage Messages API.
25+
"""

src/vonage/messages.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from .errors import MessagesError
2+
3+
import re
4+
import json
5+
6+
class Messages:
7+
valid_message_channels = {'sms', 'mms', 'whatsapp', 'messenger', 'viber_service'}
8+
valid_message_types = {
9+
'sms': {'text'},
10+
'mms': {'image', 'vcard', 'audio', 'video'},
11+
'whatsapp': {'text', 'image', 'audio', 'video', 'file', 'template', 'custom'},
12+
'messenger': {'text', 'image', 'audio', 'video', 'file'},
13+
'viber_service': {'text', 'image'}
14+
}
15+
16+
def __init__(self, client):
17+
self._client = client
18+
19+
def send_message(self, params: dict, header_auth=False):
20+
self.validate_send_message_input(params)
21+
22+
json_formatted_params = json.dumps(params)
23+
if header_auth: # Using base64 encoded API key/secret pair
24+
return self._client.post(
25+
self._client.api_host(),
26+
"/v1/messages",
27+
json_formatted_params,
28+
header_auth=header_auth,
29+
additional_headers={'Content-Type': 'application/json'})
30+
else: # If using jwt auth
31+
return self._client._jwt_signed_post(
32+
"/v1/messages",
33+
params)
34+
35+
def validate_send_message_input(self, params):
36+
self._check_input_is_dict(params)
37+
self._check_valid_message_channel(params)
38+
self._check_valid_message_type(params)
39+
self._check_valid_recipient(params)
40+
self._check_valid_sender(params)
41+
self._channel_specific_checks(params)
42+
self._check_valid_client_ref(params)
43+
44+
def _check_input_is_dict(self, params):
45+
if type(params) is not dict:
46+
raise MessagesError(f'Parameters to the send_message method must be specified as a dictionary.')
47+
48+
def _check_valid_message_channel(self, params):
49+
if params['channel'] not in Messages.valid_message_channels:
50+
raise MessagesError(f"""
51+
'{params['channel']}' is an invalid message channel.
52+
Must be one of the following types: {self.valid_message_channels}'
53+
""")
54+
55+
def _check_valid_message_type(self, params):
56+
if params['message_type'] not in self.valid_message_types[params['channel']]:
57+
raise MessagesError(f"""
58+
"{params['message_type']}" is not a valid message type for channel "{params["channel"]}".
59+
Must be one of the following types: {self.valid_message_types[params["channel"]]}
60+
""")
61+
62+
def _check_valid_recipient(self, params):
63+
if not isinstance(params['to'], str):
64+
raise MessagesError(f'Message recipient ("to={params["to"]}") not in a valid format.')
65+
elif params['channel'] != 'messenger' and not re.search(r'^[1-9]\d{6,14}$', params['to']):
66+
raise MessagesError(f'Message recipient number ("to={params["to"]}") not in a valid format.')
67+
elif params['channel'] == 'messenger' and not 0 < len(params['to']) < 50:
68+
raise MessagesError(f'Message recipient ID ("to={params["to"]}") not in a valid format.')
69+
70+
def _check_valid_sender(self, params):
71+
if not isinstance(params['from'], str) or params['from'] == "":
72+
raise MessagesError(f'Message sender ("frm={params["from"]}") set incorrectly. Set a valid name or number for the sender.')
73+
74+
def _channel_specific_checks(self, params):
75+
try:
76+
if params['channel'] == 'whatsapp' and params['message_type'] == 'template':
77+
params['whatsapp']
78+
if params['channel'] == 'viber_service':
79+
params['viber_service']
80+
except (KeyError, TypeError):
81+
raise MessagesError(f'''You must specify all required properties for message channel "{params["channel"]}".''')
82+
83+
def _check_valid_client_ref(self, params):
84+
if 'client_ref' in params:
85+
if len(params['client_ref']) <= 40:
86+
self._client_ref = params['client_ref']
87+
else:
88+
raise MessagesError('client_ref can be a maximum of 40 characters.')

src/vonage/voice.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import vonage
22

3-
class Voice():
3+
class Voice:
44
#application_id and private_key are needed for the calling methods
55
#Passing a Vonage Client is also possible
66
def __init__(

tests/conftest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,10 @@ def ussd(client):
103103
def short_codes(client):
104104
import vonage
105105

106-
return vonage.ShortCodes(client)
106+
return vonage.ShortCodes(client)
107+
108+
@pytest.fixture
109+
def messages(client):
110+
import vonage
111+
112+
return vonage.Messages(client)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from util import *
2+
3+
@responses.activate
4+
def test_send_sms_with_messages_api(messages, dummy_data):
5+
stub(responses.POST, 'https://api.nexmo.com/v1/messages')
6+
7+
params = {
8+
'channel': 'sms',
9+
'message_type': 'text',
10+
'to': '447123456789',
11+
'from': 'Vonage',
12+
'text': 'Hello from Vonage'
13+
}
14+
15+
assert isinstance(messages.send_message(params), dict)
16+
assert request_user_agent() == dummy_data.user_agent
17+
assert b'"from": "Vonage"' in request_body()
18+
assert b'"to": "447123456789"' in request_body()
19+
assert b'"text": "Hello from Vonage"' in request_body()
20+
21+
@responses.activate
22+
def test_send_whatsapp_image_with_messages_api(messages, dummy_data):
23+
stub(responses.POST, 'https://api.nexmo.com/v1/messages')
24+
25+
params = {
26+
'channel': 'whatsapp',
27+
'message_type': 'image',
28+
'to': '447123456789',
29+
'from': '440123456789',
30+
'image': {'url': 'https://example.com/image.jpg', 'caption': 'fake test image'}
31+
}
32+
33+
assert isinstance(messages.send_message(params), dict)
34+
assert request_user_agent() == dummy_data.user_agent
35+
assert b'"from": "440123456789"' in request_body()
36+
assert b'"to": "447123456789"' in request_body()
37+
assert b'"image": {"url": "https://example.com/image.jpg", "caption": "fake test image"}' in request_body()
38+

0 commit comments

Comments
 (0)