passerelle/passerelle/apps/ovh/models.py

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)