passerelle/passerelle/apps/smsfactor/models.py

210 lines
7.6 KiB
Python

# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import logging
import urllib.parse
import requests
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
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.translation import gettext_lazy as _
from passerelle.sms.models import SMSResource
from passerelle.utils.jsonresponse import APIError
class SMSFactorSMSGateway(SMSResource):
auth_token = models.CharField(verbose_name=_('Auth Token'), max_length=255)
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 list to send credit alerts to, separated by comma'),
)
credit_alert_timestamp = models.DateTimeField(null=True, editable=False)
# unecessary field
allow_premium_rate = None
class Meta:
verbose_name = 'SMS Factor'
db_table = 'sms_factor'
TEST_DEFAULTS = {
'create_kwargs': {
'auth_token': 'yyy',
'credit_threshold_alert': 1000,
},
'test_vectors': [
{
'status_code': 200,
'response': {
"status": -7,
"message": "Erreur de données",
"details": "Texte du message introuvable",
},
'result': {
'err': 1,
'err_desc': 'SMS Factor error: some destinations failed',
'data': [
['33688888888', "Texte du message introuvable"],
['33677777777', "Texte du message introuvable"],
],
},
},
{
'status_code': 200,
'response': {
"status": 1,
"message": "OK",
"ticket": "14672468",
"cost": 2,
"credits": 642,
"total": 2,
"sent": 2,
"blacklisted": 0,
"duplicated": 0,
"invalid": 0,
"npai": 0,
},
'result': {
'err': 0,
'data': {
"status": 1,
"message": "OK",
"ticket": "14672468",
"cost": 2,
"credits": 642,
"total": 2,
"sent": 2,
"blacklisted": 0,
"duplicated": 0,
"invalid": 0,
"npai": 0,
},
},
},
],
}
URL = 'https://api.smsfactor.com'
def request(self, method, endpoint, **kwargs):
url = urllib.parse.urljoin(self.URL, endpoint)
headers = {
"Authorization": f"Bearer {self.auth_token}",
"Accept": "application/json",
}
try:
response = self.requests.request(method, url, headers=headers, **kwargs)
except requests.RequestException as e:
raise APIError('SMS Factor: request failed, %s' % e)
else:
try:
result = response.json()
except ValueError:
raise APIError('SMS Factor: bad JSON response')
try:
response.raise_for_status()
except requests.RequestException as e:
raise APIError('SMS Factor: %s "%s"' % (e, result))
return result
def send_msg(self, text, sender, destinations, **kwargs):
"""Send a SMS using the SMS Factor provider"""
# from https://dev.smsfactor.com/en/api/sms/send/send-single
# and https://dev.smsfactor.com/en/api/sms/send/send-simulate
# set destinations phone number in E.164 format (without the + prefix)
# [country code][phone number including area code]
destinations = [dest[2:] for dest in destinations]
results = []
for dest in destinations:
params = {
'sender': sender,
'text': text,
'to': dest,
'pushtype': 'alert' if kwargs.get('stop', False) else 'marketing',
}
data = self.request('get', 'send', params=params)
logging.info('SMS Factor answered with %s', data)
results.append(data)
errors = [f'SMS Factor error: {r["status"]}: {r["message"]}' for r in results if r['status'] != 1]
consumed_credits = None
try:
self.credit_left = results[-1]['credits']
consumed_credits = sum(r['cost'] for r in results)
except KeyError:
# no credits key, there was probably an error with the request
pass
else:
self.save(update_fields=['credit_left'])
if any(errors):
raise APIError('SMS Factor error: some destinations failed', data=errors)
return consumed_credits
def update_credit_left(self):
result = self.request('get', endpoint='credits')
try:
# SMS Factor returns this as a string, for an unknown reason
self.credit_left = int(result['credits'])
except KeyError:
self.logger.warning('Cannot retrieve credits for sms-factor connector: %s', result)
else:
self.save(update_fields=['credit_left'])
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() - datetime.timedelta(
days=1
):
return # alerts are sent daily
ctx = {
'connector': self,
'connector_url': urllib.parse.urljoin(settings.SITE_BASE_URL, self.get_absolute_url()),
}
subject = render_to_string('smsfactor/credit_alert_subject.txt', ctx).strip()
body = render_to_string('smsfactor/credit_alert_body.txt', ctx)
html_body = render_to_string('smsfactor/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()
self.update_credit_left()
self.send_credit_alert_if_needed()