Skip to content

Commit

Permalink
merging main into 4.x
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkahan committed May 16, 2023
2 parents ec49508 + c0f1a5b commit 73502df
Show file tree
Hide file tree
Showing 27 changed files with 798 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Build
on: [push, pull_request]
on: [push]
jobs:
test:
name: Test
Expand Down
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 3.5.0
- Adding support for V2 of the Vonage Verify API
- Multiple authentication channels are supported (sms, voice, email, whatsapp, whatsapp interactive messages and silent authentication)
- Using fallback channels is now possible in case verification methods fail
- You can now customise the verification code that is sent, or even specify your own custom code
- Adding `advancedMachineDetection` functionality to the NCCO builder for the Vonage Voice API

# 3.4.0
- Internal refactoring changes
- Using header authentication for the Numbers API
Expand Down
72 changes: 70 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
- [Messages API](#messages-api)
- [Voice API](#voice-api)
- [NCCO Builder](#ncco-builder)
- [Verify API](#verify-api)
- [Verify V2 API](#verify-v2-api)
- [Verify V1 API](#verify-v1-api)
- [Number Insight API](#number-insight-api)
- [Account API](#account-api)
- [Number Management API](#number-management-api)
Expand Down Expand Up @@ -406,8 +407,75 @@ pprint(response)

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

## Verify V2 API

## Verify API
V2 of the Vonage Verify API lets you send verification codes via SMS, WhatsApp, Voice and Email

You can also verify a user by WhatsApp Interactive Message or by Silent Authentication on their mobile device.

### Send a verification code

```python
params = {
'brand': 'ACME, Inc',
'workflow': [{'channel': 'sms', 'to': '447700900000'}]
}
verify_request = verify2.new_request(params)
```

### Use silent authentication, with email as a fallback

```python
params = {
'brand': 'ACME, Inc',
'workflow': [
{'channel': 'silent_auth', 'to': '447700900000'},
{'channel': 'email', 'to': 'customer@example.com', 'from': 'business@example.com'}
]
}
verify_request = verify2.new_request(params)
```

### Send a verification code with custom options, including a custom code

```python
params = {
'locale': 'en-gb',
'channel_timeout': 120,
'client_ref': 'my client reference',
'code': 'asdf1234',
'brand': 'ACME, Inc',
'workflow': [{'channel': 'sms', 'to': '447700900000', 'app_hash': 'asdfghjklqw'}],
}
verify_request = verify2.new_request(params)
```

### Send a verification request to a blocked network

This feature is only enabled if you have requested for it to be added to your account.

```python
params = {
'brand': 'ACME, Inc',
'fraud_check': False,
'workflow': [{'channel': 'sms', 'to': '447700900000'}]
}
verify_request = verify2.new_request(params)
```

### Check a verification code

```python
verify2.check_code(REQUEST_ID, CODE)
```

### Cancel an ongoing verification

```python
verify2.cancel_verification(REQUEST_ID)
```

## Verify V1 API

### Search for a Verification request

Expand Down
8 changes: 7 additions & 1 deletion src/vonage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .video import Video
from .voice import Voice
from .verify import Verify
from .verify2 import Verify2

import logging
from platform import python_version
Expand Down Expand Up @@ -126,6 +127,7 @@ def __init__(
self.ussd = Ussd(self)
self.video = Video(self)
self.verify = Verify(self)
self.verify2 = Verify2(self)
self.voice = Voice(self)

self.timeout = timeout
Expand Down Expand Up @@ -301,7 +303,11 @@ def parse(self, host, response: Response):
return None
elif 200 <= response.status_code < 300:
# Strip off any encoding from the content-type header:
content_mime = response.headers.get("content-type").split(";", 1)[0]
try:
content_mime = response.headers.get("content-type").split(";", 1)[0]
except AttributeError:
if response.json() is None:
return None
if content_mime == "application/json":
return response.json()
else:
Expand Down
8 changes: 8 additions & 0 deletions src/vonage/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,11 @@ class SipError(ClientError):

class InvalidInputError(ClientError):
"""The input that was provided was invalid."""


class InvalidAuthenticationTypeError(ClientError):
"""An authentication method was specified that is not allowed."""


class Verify2Error(ClientError):
"""An error relating to the Verify (V2) API."""
9 changes: 9 additions & 0 deletions src/vonage/ncco_builder/ncco.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class Connect(Action):
timeout: Optional[int]
limit: Optional[conint(le=7200)]
machineDetection: Optional[Literal['continue', 'hangup']]
advancedMachineDetection: Optional[dict]
eventUrl: Optional[Union[List[str], str]]
eventMethod: Optional[constr(to_upper=True)]
ringbackTone: Optional[str]
Expand Down Expand Up @@ -104,6 +105,14 @@ def check_from_not_set(cls, v, values):
def ensure_url_in_list(cls, v):
return Ncco._ensure_object_in_list(v)

@validator('advancedMachineDetection')
def validate_advancedMachineDetection(cls, v):
if 'behavior' in v and v['behavior'] not in ('continue', 'hangup'):
raise ValueError('advancedMachineDetection["behavior"] must be one of: "continue", "hangup".')
if 'mode' in v and v['mode'] not in ('detect, detect_beep'):
raise ValueError('advancedMachineDetection["mode"] must be one of: "detect", "detect_beep".')
return v

class Config:
smart_union = True

Expand Down
13 changes: 9 additions & 4 deletions src/vonage/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ def __init__(self, client):

def start_verification(self, params=None, **kwargs):
response = self._client.post(
self._client.api_host(),
"/verify/json",
self._client.api_host(),
"/verify/json",
params or kwargs,
**Verify.defaults,
)
Expand Down Expand Up @@ -69,7 +69,10 @@ def trigger_next_event(self, request_id):

def psd2(self, params=None, **kwargs):
response = self._client.post(
self._client.api_host(), "/verify/psd2/json", params or kwargs, **Verify.defaults,
self._client.api_host(),
"/verify/psd2/json",
params or kwargs,
**Verify.defaults,
)

self.check_for_error(response)
Expand All @@ -79,4 +82,6 @@ def check_for_error(self, response):
if response['status'] == '7':
raise BlockedNumberError('Error code 7: The number you are trying to verify is blocked for verification.')
elif 'error_text' in response:
raise VerifyError(f'Verify API method failed with status: {response["status"]} and error: {response["error_text"]}')
raise VerifyError(
f'Verify API method failed with status: {response["status"]} and error: {response["error_text"]}'
)
107 changes: 107 additions & 0 deletions src/vonage/verify2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from pydantic import BaseModel, ValidationError, validator, conint, constr
from typing import Optional, List

import copy
import re

from .errors import Verify2Error


class Verify2:
valid_channels = [
'sms',
'whatsapp',
'whatsapp_interactive',
'voice',
'email',
'silent_auth',
]

def __init__(self, client):
self._client = client
self._auth_type = 'jwt'

def new_request(self, params: dict):
try:
params_to_verify = copy.deepcopy(params)
Verify2.VerifyRequest.parse_obj(params_to_verify)
except (ValidationError, Verify2Error) as err:
raise err

if not hasattr(self._client, '_application_id'):
self._auth_type = 'header'

return self._client.post(
self._client.api_host(),
'/v2/verify',
params,
auth_type=self._auth_type,
)

def check_code(self, request_id: str, code: str):
params = {'code': str(code)}

if not hasattr(self._client, '_application_id'):
self._auth_type = 'header'

return self._client.post(
self._client.api_host(),
f'/v2/verify/{request_id}',
params,
auth_type=self._auth_type,
)

def cancel_verification(self, request_id: str):
if not hasattr(self._client, '_application_id'):
self._auth_type = 'header'

return self._client.delete(
self._client.api_host(),
f'/v2/verify/{request_id}',
auth_type=self._auth_type,
)

class VerifyRequest(BaseModel):
brand: str
workflow: List[dict]
locale: Optional[str]
channel_timeout: Optional[conint(ge=60, le=900)]
client_ref: Optional[str]
code_length: Optional[conint(ge=4, le=10)]
fraud_check: Optional[bool]
code: Optional[constr(min_length=4, max_length=10, regex='^(?=[a-zA-Z0-9]{4,10}$)[a-zA-Z0-9]*$')]

@validator('workflow')
def check_valid_workflow(cls, v):
for workflow in v:
Verify2._check_valid_channel(workflow)
Verify2._check_valid_recipient(workflow)
Verify2._check_app_hash(workflow)
if workflow['channel'] == 'whatsapp' and 'from' in workflow:
Verify2._check_whatsapp_sender(workflow)

def _check_valid_channel(workflow):
if 'channel' not in workflow or workflow['channel'] not in Verify2.valid_channels:
raise Verify2Error(
f'You must specify a valid verify channel inside the "workflow" object, one of: "{Verify2.valid_channels}"'
)

def _check_valid_recipient(workflow):
if 'to' not in workflow or (
workflow['channel'] != 'email' and not re.search(r'^[1-9]\d{6,14}$', workflow['to'])
):
raise Verify2Error(f'You must specify a valid "to" value for channel "{workflow["channel"]}"')

def _check_app_hash(workflow):
if workflow['channel'] == 'sms' and 'app_hash' in workflow:
if type(workflow['app_hash']) != str or len(workflow['app_hash']) != 11:
raise Verify2Error(
'Invalid "app_hash" specified. If specifying app_hash, \
it must be passed as a string and contain exactly 11 characters.'
)
elif workflow['channel'] != 'sms' and 'app_hash' in workflow:
raise Verify2Error('Cannot specify a value for "app_hash" unless using SMS for authentication.')

def _check_whatsapp_sender(workflow):
if not re.search(r'^[1-9]\d{6,14}$', workflow['from']):
raise Verify2Error(f'You must specify a valid "from" value if included.')
16 changes: 16 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,34 +59,50 @@ def sms(client):
def verify(client):
return vonage.Verify(client)


@pytest.fixture
def number_insight(client):
return vonage.NumberInsight(client)


@pytest.fixture
def account(client):
return vonage.Account(client)


@pytest.fixture
def numbers(client):
import vonage

return vonage.Numbers(client)


@pytest.fixture
def ussd(client):
import vonage

return vonage.Ussd(client)


@pytest.fixture
def short_codes(client):
import vonage

return vonage.ShortCodes(client)


@pytest.fixture
def messages(client):
return vonage.Messages(client)


@pytest.fixture
def redact(client):
return vonage.Redact(client)


@pytest.fixture
def application_v2(client):
import vonage

return vonage.ApplicationV2(client)
Empty file added tests/data/no_content.json
Empty file.
6 changes: 6 additions & 0 deletions tests/data/verify2/already_verified.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "https://developer.nexmo.com/api-errors#not-found",
"title": "Not Found",
"detail": "Request '5fcc26ef-1e54-48a6-83ab-c47546a19824' was not found or it has been verified already.",
"instance": "02cabfcc-2e09-4b5d-b098-1fa7ccef4607"
}
4 changes: 4 additions & 0 deletions tests/data/verify2/check_code.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"request_id": "e043d872-459b-4750-a20c-d33f91d6959f",
"status": "completed"
}
6 changes: 6 additions & 0 deletions tests/data/verify2/code_not_supported.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"title": "Conflict",
"detail": "The current Verify workflow step does not support a code.",
"instance": "690c48de-c5d1-49f2-8712-b3b0a840f911",
"type": "https://developer.nexmo.com/api-errors#conflict"
}
3 changes: 3 additions & 0 deletions tests/data/verify2/create_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"request_id": "c11236f4-00bf-4b89-84ba-88b25df97315"
}
7 changes: 7 additions & 0 deletions tests/data/verify2/error_conflict.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"title": "Conflict",
"type": "https://www.developer.vonage.com/api-errors/verify#conflict",
"detail": "Concurrent verifications to the same number are not allowed.",
"instance": "738f9313-418a-4259-9b0d-6670f06fa82d",
"request_id": "575a2054-aaaf-4405-994e-290be7b9a91f"
}
Loading

0 comments on commit 73502df

Please sign in to comment.