blob: f251c93a6e6fad64e5cdac750666cd742450d13b [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import unicode_literals
from __future__ import absolute_import
import logging
from six.moves.urllib.parse import urljoin
import cgi
import json
import requests
import jinja2
from allura.lib.phone import PhoneService
from allura.lib.utils import phone_number_hash
log = logging.getLogger(__name__)
class NexmoPhoneService(PhoneService):
"""
Implementation of :class:`allura.lib.phone.PhoneService` interface
for Nexmo Verify
"""
BASE_URL = 'https://api.nexmo.com/'
def __init__(self, config):
self.config = config
self.api_key = config.get('phone.api_key')
self.api_secret = config.get('phone.api_secret')
def add_common_params(self, params):
common = {
'api_key': self.api_key,
'api_secret': self.api_secret,
}
if self.config.get('phone.lang'):
common['lg'] = self.config['phone.lang']
return dict(params, **common)
def error(self, code=None, msg=None, number=''):
allowed_codes = ['3', '10', '15', '16', '17'] # https://docs.nexmo.com/index.php/verify/search#verify_return_code
if code is None or str(code) not in allowed_codes:
msg = 'Failed sending request to Nexmo'
if str(code) == '3' and msg.endswith(' number'):
msg = jinja2.Markup(
'{}{}{}'.format(
cgi.escape(msg), # escape it just in case Nexmo sent some HTML we don't want through
'<br>Make sure you include the country code (see examples above)',
'. For US numbers, you must include <code>1-</code> before the area code.' if len(number) == 10 else '',
))
return {'status': 'error', 'error': msg}
def ok(self, **params):
return dict({'status': 'ok'}, **params)
def post(self, url, **params):
if url[-1] != '/':
url += '/'
url = urljoin(url, 'json')
headers = {'Content-Type': 'application/json'}
params = self.add_common_params(params)
log_params = dict(params, api_key='...', api_secret='...')
if 'number' in log_params:
log_params['number'] = phone_number_hash(log_params['number'])
post_params = json.dumps(params, sort_keys=True)
log.info('PhoneService (nexmo) request: %s %s', url, log_params)
try:
resp = requests.post(url, data=post_params, headers=headers)
log.info('PhoneService (nexmo) response: %s', resp.content)
resp = resp.json()
except Exception:
log.exception('Failed sending request to Nexmo')
return self.error()
if resp.get('status') == '0':
return self.ok(request_id=resp.get('request_id'))
return self.error(code=resp.get('status'), msg=resp.get('error_text'), number=params.get('number',''))
def verify(self, number):
url = urljoin(self.BASE_URL, 'verify')
# Required. Brand or name of your app, service the verification is
# for. This alphanumeric (maximum length 18 characters) will be
# used inside the body of all SMS and TTS messages sent (e.g. "Your
# <brand> PIN code is ..")
brand = self.config.get('site_name')[:18]
return self.post(url, number=number, brand=brand)
def check(self, request_id, pin):
url = urljoin(self.BASE_URL, 'verify/check')
return self.post(url, request_id=request_id, code=pin)