282 lines
10 KiB
Python
282 lines
10 KiB
Python
import hashlib
|
|
import json
|
|
import requests
|
|
import time
|
|
from datetime import timedelta
|
|
from urllib.parse import urljoin
|
|
|
|
from django.contrib.postgres.fields import ArrayField
|
|
from django.conf import settings
|
|
from django.core.mail import send_mail
|
|
from django.db import models
|
|
from django.template.loader import render_to_string
|
|
from django.utils import timezone
|
|
from django.utils.encoding import force_text
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from passerelle.sms.models import SMSResource
|
|
from passerelle.utils.jsonresponse import APIError
|
|
|
|
|
|
class OVHSMSGateway(SMSResource):
|
|
documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
|
|
hide_description_fields = ['account', 'credit_left']
|
|
API_URL = 'https://eu.api.ovh.com/1.0/sms/%(serviceName)s/'
|
|
URL = 'https://www.ovh.com/cgi-bin/sms/http2sms.cgi'
|
|
MESSAGES_CLASSES = (
|
|
(0, _('Message are directly shown to users on phone screen '
|
|
'at reception. The message is never stored, neither in the '
|
|
'phone memory nor in the SIM card. It is deleted as '
|
|
'soon as the user validate the display.')),
|
|
(1, _('Messages are stored in the phone memory, or in the '
|
|
'SIM card if the memory is full. ')),
|
|
(2, _('Messages are stored in the SIM card.')),
|
|
(3, _('Messages are stored in external storage like a PDA or '
|
|
'a PC.')),
|
|
)
|
|
NEW_MESSAGES_CLASSES = ['flash', 'phoneDisplay', 'sim', 'toolkit']
|
|
|
|
account = models.CharField(
|
|
verbose_name=_('Account'), max_length=64, help_text=_('Account identifier, such as sms-XXXXXX-1.')
|
|
)
|
|
|
|
application_key = models.CharField(
|
|
verbose_name=_('Application key'),
|
|
max_length=16,
|
|
blank=True,
|
|
)
|
|
application_secret = models.CharField(
|
|
verbose_name=_('Application secret'),
|
|
max_length=32,
|
|
blank=True,
|
|
help_text=_('Obtained at the same time as "Application key".'),
|
|
)
|
|
consumer_key = models.CharField(
|
|
verbose_name=_('Consumer key'),
|
|
max_length=32,
|
|
blank=True,
|
|
help_text=_('Automatically obtained from OVH, should not be filled manually.'),
|
|
)
|
|
|
|
username = models.CharField(
|
|
verbose_name=_('Username (deprecated)'),
|
|
max_length=64,
|
|
blank=True,
|
|
help_text=_('API user created on the SMS account. This field is obsolete once keys and secret '
|
|
'fields above are filled.'),
|
|
)
|
|
password = models.CharField(
|
|
verbose_name=_('Password (deprecated)'),
|
|
max_length=64,
|
|
blank=True,
|
|
help_text=_(
|
|
'Password for legacy API. This field is obsolete once keys and secret fields above are filled.'
|
|
),
|
|
)
|
|
msg_class = models.IntegerField(choices=MESSAGES_CLASSES, default=1,
|
|
verbose_name=_('Message class'))
|
|
credit_threshold_alert = models.PositiveIntegerField(verbose_name=_('Credit alert threshold'),
|
|
default=500)
|
|
credit_left = models.PositiveIntegerField(verbose_name=_('Credit left'), default=0, editable=False)
|
|
alert_emails = ArrayField(
|
|
models.EmailField(blank=True),
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_('Email addresses to send credit alerts to'),
|
|
)
|
|
credit_alert_timestamp = models.DateTimeField(null=True, editable=False)
|
|
|
|
TEST_DEFAULTS = {
|
|
'create_kwargs': {
|
|
'account': '1234',
|
|
'username': 'john',
|
|
'password': 'doe',
|
|
},
|
|
'test_vectors': [
|
|
{
|
|
'response': '',
|
|
'result': {
|
|
'err': 1,
|
|
'err_desc': 'OVH error: bad JSON response',
|
|
}
|
|
},
|
|
{
|
|
'response': {
|
|
'status': 100,
|
|
'creditLeft': 47,
|
|
'SmsIds': [1234],
|
|
},
|
|
'result': {
|
|
'err': 0,
|
|
'data': {
|
|
'credit_left': 47.0,
|
|
'ovh_result': {
|
|
'SmsIds': [1234],
|
|
'creditLeft': 47,
|
|
'status': 100
|
|
},
|
|
'sms_ids': [1234],
|
|
'warning': 'credit level too low for ovhsmsgateway: 47.0 (threshold 100)',
|
|
}
|
|
}
|
|
}
|
|
|
|
],
|
|
|
|
}
|
|
|
|
class Meta:
|
|
verbose_name = 'OVH'
|
|
db_table = 'sms_ovh'
|
|
|
|
@property
|
|
def uses_new_api(self):
|
|
return self.application_key and self.application_secret
|
|
|
|
def request(self, method, endpoint, **kwargs):
|
|
url = self.API_URL % {'serviceName': self.account, 'login': self.username}
|
|
url = urljoin(url, endpoint)
|
|
|
|
# sign request
|
|
body = json.dumps(kwargs['json']) if 'json' in kwargs else ''
|
|
now = str(int(time.time()))
|
|
signature = hashlib.sha1()
|
|
to_sign = "+".join((self.application_secret, self.consumer_key, method.upper(), url, body, now))
|
|
signature.update(to_sign.encode())
|
|
|
|
headers = {
|
|
'X-Ovh-Application': self.application_key,
|
|
'X-Ovh-Consumer': self.consumer_key,
|
|
'X-Ovh-Timestamp': now,
|
|
'X-Ovh-Signature': "$1$" + signature.hexdigest(),
|
|
}
|
|
|
|
try:
|
|
response = self.requests.request(method, url, headers=headers, **kwargs)
|
|
except requests.RequestException as e:
|
|
raise APIError('OVH error: POST failed, %s' % e)
|
|
else:
|
|
try:
|
|
result = response.json()
|
|
except ValueError as e:
|
|
raise APIError('OVH error: bad JSON response')
|
|
try:
|
|
response.raise_for_status()
|
|
except requests.RequestException as e:
|
|
raise APIError('OVH error: %s "%s"' % (e, result))
|
|
return result
|
|
|
|
def send_msg(self, text, sender, destinations, **kwargs):
|
|
if not self.uses_new_api:
|
|
return self.send_msg_legacy(text, sender, destinations, **kwargs)
|
|
|
|
body = {
|
|
'sender': sender,
|
|
'receivers': destinations,
|
|
'message': text,
|
|
'class': self.NEW_MESSAGES_CLASSES[self.msg_class],
|
|
}
|
|
if not kwargs['stop']:
|
|
body.update({'noStopClause': True})
|
|
|
|
result = self.request('post', 'jobs/', json=body)
|
|
|
|
ret = {}
|
|
credits_removed = result['totalCreditsRemoved']
|
|
# update credit left
|
|
self.credit_left -= credits_removed
|
|
if self.credit_left < 0:
|
|
self.credit_left = 0
|
|
self.save(update_credit=False)
|
|
ret['credit_left'] = self.credit_left
|
|
ret['ovh_result'] = result
|
|
ret['sms_ids'] = result.get('ids', [])
|
|
|
|
return ret
|
|
|
|
def update_credit_left(self):
|
|
result = self.request('get', endpoint='')
|
|
self.credit_left = result['creditsLeft']
|
|
self.save(update_credit=False)
|
|
|
|
def send_credit_alert_if_needed(self):
|
|
if self.credit_left >= self.credit_threshold_alert:
|
|
return
|
|
if self.credit_alert_timestamp and self.credit_alert_timestamp > timezone.now() - timedelta(days=1):
|
|
return # alerts are sent daily
|
|
ctx = {
|
|
'connector': self,
|
|
'connector_url': urljoin(settings.SITE_BASE_URL, self.get_absolute_url()),
|
|
}
|
|
subject = render_to_string('ovh/credit_alert_subject.txt', ctx).strip()
|
|
body = render_to_string('ovh/credit_alert_body.txt', ctx)
|
|
html_body = render_to_string('ovh/credit_alert_body.html', ctx)
|
|
send_mail(
|
|
subject,
|
|
body,
|
|
settings.DEFAULT_FROM_EMAIL,
|
|
self.alert_emails,
|
|
html_message=html_body,
|
|
)
|
|
self.credit_alert_timestamp = timezone.now()
|
|
self.save()
|
|
self.logger.warning('credit is too low, alerts were sent to %s', self.alert_emails)
|
|
|
|
def hourly(self):
|
|
super().hourly()
|
|
if self.uses_new_api:
|
|
self.update_credit_left()
|
|
self.send_credit_alert_if_needed()
|
|
|
|
def save(self, *args, update_credit=True, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
if update_credit and self.uses_new_api:
|
|
self.add_job('update_credit_left')
|
|
|
|
def send_msg_legacy(self, text, sender, destinations, **kwargs):
|
|
"""Send a SMS using the HTTP2 endpoint"""
|
|
if not self.password:
|
|
raise APIError('Improperly configured, empty keys or password fields.')
|
|
|
|
text = force_text(text).encode('utf-8')
|
|
to = ','.join(destinations)
|
|
params = {
|
|
'account': self.account.encode('utf-8'),
|
|
'login': self.username.encode('utf-8'),
|
|
'password': self.password.encode('utf-8'),
|
|
'from': sender.encode('utf-8'),
|
|
'to': to,
|
|
'message': text,
|
|
'contentType': 'text/json',
|
|
'class': self.msg_class,
|
|
}
|
|
if not kwargs['stop']:
|
|
params.update({'noStop': 1})
|
|
try:
|
|
response = self.requests.post(self.URL, data=params)
|
|
except requests.RequestException as e:
|
|
raise APIError('OVH error: POST failed, %s' % e)
|
|
else:
|
|
try:
|
|
result = response.json()
|
|
except ValueError as e:
|
|
raise APIError('OVH error: bad JSON response')
|
|
else:
|
|
if not isinstance(result, dict):
|
|
raise APIError('OVH error: bad JSON response %r, it should be a dictionnary' %
|
|
result)
|
|
if 100 <= result['status'] < 200:
|
|
ret = {}
|
|
credit_left = float(result['creditLeft'])
|
|
# update credit left
|
|
OVHSMSGateway.objects.filter(id=self.id).update(credit_left=credit_left)
|
|
if credit_left < self.credit_threshold_alert:
|
|
ret['warning'] = ('credit level too low for %s: %s (threshold %s)' %
|
|
(self.slug, credit_left, self.credit_threshold_alert))
|
|
ret['credit_left'] = credit_left
|
|
ret['ovh_result'] = result
|
|
ret['sms_ids'] = result.get('SmsIds', [])
|
|
return ret
|
|
else:
|
|
raise APIError('OVH error: %r' % result)
|