passerelle/passerelle/apps/smsfactor/models.py

209 lines
7.5 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 not kwargs.get('stop') 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 check_status(self):
self.update_credit_left()
self.send_credit_alert_if_needed()