Skip to content

Commit b3b5d34

Browse files
authored
Add verify2 (#254)
* starting verify implementation * using Literal from typing-extensions for 3.7 compatibility * add new_request and check_code verify2 methods, use new class structure, add tests * adding more test cases * adding custom verification code validation and testing * moving custom code validation from workflow object to the main request body * adding fraud_check parameter to verify v2 request * adding verify v2 to readme, adding verify2.cancel_verification * removing build on PR as we already build on push * fixing typo in link
1 parent 99b9635 commit b3b5d34

21 files changed

+740
-14
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: Build
2-
on: [push, pull_request]
2+
on: [push]
33
jobs:
44
test:
55
name: Test

README.md

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
1717
- [Messages API](#messages-api)
1818
- [Voice API](#voice-api)
1919
- [NCCO Builder](#ncco-builder)
20-
- [Verify API](#verify-api)
20+
- [Verify V2 API](#verify-v2-api)
21+
- [Verify V1 API](#verify-v1-api)
2122
- [Number Insight API](#number-insight-api)
2223
- [Account API](#account-api)
2324
- [Number Management API](#number-management-api)
@@ -406,8 +407,75 @@ pprint(response)
406407

407408
When using the `connect` action, use the parameter `from_` to specify the recipient (as `from` is a reserved keyword in Python!)
408409

410+
## Verify V2 API
409411

410-
## Verify API
412+
V2 of the Vonage Verify API lets you send verification codes via SMS, WhatsApp, Voice and Email
413+
414+
You can also verify a user by WhatsApp Interactive Message or by Silent Authentication on their mobile device.
415+
416+
### Send a verification code
417+
418+
```python
419+
params = {
420+
'brand': 'ACME, Inc',
421+
'workflow': [{'channel': 'sms', 'to': '447700900000'}]
422+
}
423+
verify_request = verify2.new_request(params)
424+
```
425+
426+
### Use silent authentication, with email as a fallback
427+
428+
```python
429+
params = {
430+
'brand': 'ACME, Inc',
431+
'workflow': [
432+
{'channel': 'silent_auth', 'to': '447700900000'},
433+
{'channel': 'email', 'to': 'customer@example.com', 'from': 'business@example.com'}
434+
]
435+
}
436+
verify_request = verify2.new_request(params)
437+
```
438+
439+
### Send a verification code with custom options, including a custom code
440+
441+
```python
442+
params = {
443+
'locale': 'en-gb',
444+
'channel_timeout': 120,
445+
'client_ref': 'my client reference',
446+
'code': 'asdf1234',
447+
'brand': 'ACME, Inc',
448+
'workflow': [{'channel': 'sms', 'to': '447700900000', 'app_hash': 'asdfghjklqw'}],
449+
}
450+
verify_request = verify2.new_request(params)
451+
```
452+
453+
### Send a verification request to a blocked network
454+
455+
This feature is only enabled if you have requested for it to be added to your account.
456+
457+
```python
458+
params = {
459+
'brand': 'ACME, Inc',
460+
'fraud_check': False,
461+
'workflow': [{'channel': 'sms', 'to': '447700900000'}]
462+
}
463+
verify_request = verify2.new_request(params)
464+
```
465+
466+
### Check a verification code
467+
468+
```python
469+
verify2.check_code(REQUEST_ID, CODE)
470+
```
471+
472+
### Cancel an ongoing verification
473+
474+
```python
475+
verify2.cancel_verification(REQUEST_ID)
476+
```
477+
478+
## Verify V1 API
411479

412480
### Search for a Verification request
413481

src/vonage/client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .ussd import Ussd
1313
from .voice import Voice
1414
from .verify import Verify
15+
from .verify2 import Verify2
1516

1617
import logging
1718
from platform import python_version
@@ -123,6 +124,7 @@ def __init__(
123124
self.sms = Sms(self)
124125
self.ussd = Ussd(self)
125126
self.verify = Verify(self)
127+
self.verify2 = Verify2(self)
126128
self.voice = Voice(self)
127129

128130
self.timeout = timeout
@@ -278,7 +280,11 @@ def parse(self, host, response: Response):
278280
return None
279281
elif 200 <= response.status_code < 300:
280282
# Strip off any encoding from the content-type header:
281-
content_mime = response.headers.get("content-type").split(";", 1)[0]
283+
try:
284+
content_mime = response.headers.get("content-type").split(";", 1)[0]
285+
except AttributeError:
286+
if response.json() is None:
287+
return None
282288
if content_mime == "application/json":
283289
return response.json()
284290
else:
@@ -295,7 +301,8 @@ def parse(self, host, response: Response):
295301
detail = error_data["detail"]
296302
type = error_data["type"]
297303
message = f"{title}: {detail} ({type})"
298-
304+
else:
305+
message = error_data
299306
except JSONDecodeError:
300307
pass
301308
raise ClientError(message)

src/vonage/errors.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@ class MessagesError(Error):
2727
class PricingTypeError(Error):
2828
"""A pricing type was specified that is not allowed."""
2929

30+
3031
class RedactError(Error):
3132
"""Error related to the Redact class or Redact API."""
3233

34+
3335
class InvalidAuthenticationTypeError(Error):
34-
"""An authentication method was specified that is not allowed"""
36+
"""An authentication method was specified that is not allowed."""
37+
38+
39+
class Verify2Error(ClientError):
40+
"""An error relating to the Verify (V2) API."""

src/vonage/verify.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ def __init__(self, client):
77

88
def start_verification(self, params=None, **kwargs):
99
return self._client.post(
10-
self._client.api_host(),
11-
"/verify/json",
10+
self._client.api_host(),
11+
"/verify/json",
1212
params or kwargs,
1313
**Verify.defaults,
1414
)
@@ -44,6 +44,8 @@ def trigger_next_event(self, request_id):
4444

4545
def psd2(self, params=None, **kwargs):
4646
return self._client.post(
47-
self._client.api_host(), "/verify/psd2/json", params or kwargs, **Verify.defaults,
47+
self._client.api_host(),
48+
"/verify/psd2/json",
49+
params or kwargs,
50+
**Verify.defaults,
4851
)
49-

src/vonage/verify2.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from pydantic import BaseModel, ValidationError, validator, conint, constr
2+
from typing import Optional, List
3+
4+
import copy
5+
import re
6+
7+
from .errors import Verify2Error
8+
9+
10+
class Verify2:
11+
valid_channels = [
12+
'sms',
13+
'whatsapp',
14+
'whatsapp_interactive',
15+
'voice',
16+
'email',
17+
'silent_auth',
18+
]
19+
20+
def __init__(self, client):
21+
self._client = client
22+
self._auth_type = 'jwt'
23+
24+
def new_request(self, params: dict):
25+
try:
26+
params_to_verify = copy.deepcopy(params)
27+
Verify2.VerifyRequest.parse_obj(params_to_verify)
28+
except (ValidationError, Verify2Error) as err:
29+
raise err
30+
31+
if not hasattr(self._client, '_application_id'):
32+
self._auth_type = 'header'
33+
34+
return self._client.post(
35+
self._client.api_host(),
36+
'/v2/verify',
37+
params,
38+
auth_type=self._auth_type,
39+
)
40+
41+
def check_code(self, request_id: str, code: str):
42+
params = {'code': str(code)}
43+
44+
if not hasattr(self._client, '_application_id'):
45+
self._auth_type = 'header'
46+
47+
return self._client.post(
48+
self._client.api_host(),
49+
f'/v2/verify/{request_id}',
50+
params,
51+
auth_type=self._auth_type,
52+
)
53+
54+
def cancel_verification(self, request_id: str):
55+
if not hasattr(self._client, '_application_id'):
56+
self._auth_type = 'header'
57+
58+
return self._client.delete(
59+
self._client.api_host(),
60+
f'/v2/verify/{request_id}',
61+
auth_type=self._auth_type,
62+
)
63+
64+
class VerifyRequest(BaseModel):
65+
brand: str
66+
workflow: List[dict]
67+
locale: Optional[str]
68+
channel_timeout: Optional[conint(ge=60, le=900)]
69+
client_ref: Optional[str]
70+
code_length: Optional[conint(ge=4, le=10)]
71+
fraud_check: Optional[bool]
72+
code: Optional[constr(min_length=4, max_length=10, regex='^(?=[a-zA-Z0-9]{4,10}$)[a-zA-Z0-9]*$')]
73+
74+
@validator('workflow')
75+
def check_valid_workflow(cls, v):
76+
for workflow in v:
77+
Verify2._check_valid_channel(workflow)
78+
Verify2._check_valid_recipient(workflow)
79+
Verify2._check_app_hash(workflow)
80+
if workflow['channel'] == 'whatsapp' and 'from' in workflow:
81+
Verify2._check_whatsapp_sender(workflow)
82+
83+
def _check_valid_channel(workflow):
84+
if 'channel' not in workflow or workflow['channel'] not in Verify2.valid_channels:
85+
raise Verify2Error(
86+
f'You must specify a valid verify channel inside the "workflow" object, one of: "{Verify2.valid_channels}"'
87+
)
88+
89+
def _check_valid_recipient(workflow):
90+
if 'to' not in workflow or (
91+
workflow['channel'] != 'email' and not re.search(r'^[1-9]\d{6,14}$', workflow['to'])
92+
):
93+
raise Verify2Error(f'You must specify a valid "to" value for channel "{workflow["channel"]}"')
94+
95+
def _check_app_hash(workflow):
96+
if workflow['channel'] == 'sms' and 'app_hash' in workflow:
97+
if type(workflow['app_hash']) != str or len(workflow['app_hash']) != 11:
98+
raise Verify2Error(
99+
'Invalid "app_hash" specified. If specifying app_hash, \
100+
it must be passed as a string and contain exactly 11 characters.'
101+
)
102+
elif workflow['channel'] != 'sms' and 'app_hash' in workflow:
103+
raise Verify2Error('Cannot specify a value for "app_hash" unless using SMS for authentication.')
104+
105+
def _check_whatsapp_sender(workflow):
106+
if not re.search(r'^[1-9]\d{6,14}$', workflow['from']):
107+
raise Verify2Error(f'You must specify a valid "from" value if included.')

tests/conftest.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,50 +69,58 @@ def verify(client):
6969

7070
return vonage.Verify(client)
7171

72+
7273
@pytest.fixture
7374
def number_insight(client):
7475
import vonage
7576

7677
return vonage.NumberInsight(client)
7778

79+
7880
@pytest.fixture
7981
def account(client):
8082
import vonage
8183

8284
return vonage.Account(client)
8385

86+
8487
@pytest.fixture
8588
def numbers(client):
8689
import vonage
87-
90+
8891
return vonage.Numbers(client)
8992

93+
9094
@pytest.fixture
9195
def ussd(client):
9296
import vonage
93-
97+
9498
return vonage.Ussd(client)
9599

100+
96101
@pytest.fixture
97102
def short_codes(client):
98103
import vonage
99-
104+
100105
return vonage.ShortCodes(client)
101106

107+
102108
@pytest.fixture
103109
def messages(client):
104110
import vonage
105111

106112
return vonage.Messages(client)
107113

114+
108115
@pytest.fixture
109116
def redact(client):
110117
import vonage
111118

112119
return vonage.Redact(client)
113120

121+
114122
@pytest.fixture
115123
def application_v2(client):
116124
import vonage
117125

118-
return vonage.ApplicationV2(client)
126+
return vonage.ApplicationV2(client)

tests/data/no_content.json

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "https://developer.nexmo.com/api-errors#not-found",
3+
"title": "Not Found",
4+
"detail": "Request '5fcc26ef-1e54-48a6-83ab-c47546a19824' was not found or it has been verified already.",
5+
"instance": "02cabfcc-2e09-4b5d-b098-1fa7ccef4607"
6+
}

tests/data/verify2/check_code.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"request_id": "e043d872-459b-4750-a20c-d33f91d6959f",
3+
"status": "completed"
4+
}

0 commit comments

Comments
 (0)