From d9adb7b3e38b05385e305ed48575c6fc9933d147 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Sat, 2 May 2020 19:42:40 +0200
Subject: [PATCH] sms: move sms code into a dedicated directory (#42426)

---
 passerelle/apps/choosit/models.py         |  4 +-
 passerelle/apps/mobyt/models.py           |  4 +-
 passerelle/apps/orange/models.py          |  3 +-
 passerelle/apps/ovh/models.py             |  4 +-
 passerelle/apps/oxyd/models.py            |  4 +-
 passerelle/base/models.py                 |  5 +-
 passerelle/settings.py                    |  1 +
 passerelle/sms/__init__.py                |  0
 passerelle/sms/migrations/0001_initial.py | 25 ++++++
 passerelle/sms/migrations/__init__.py     |  0
 passerelle/sms/models.py                  | 97 +++++++++++++++++++++++
 tests/test_sms.py                         |  3 +-
 12 files changed, 136 insertions(+), 14 deletions(-)
 create mode 100644 passerelle/sms/__init__.py
 create mode 100644 passerelle/sms/migrations/0001_initial.py
 create mode 100644 passerelle/sms/migrations/__init__.py
 create mode 100644 passerelle/sms/models.py

diff --git a/passerelle/apps/choosit/models.py b/passerelle/apps/choosit/models.py
index afb72b53..55b9bc4e 100644
--- a/passerelle/apps/choosit/models.py
+++ b/passerelle/apps/choosit/models.py
@@ -1,18 +1,18 @@
 # -*- coding: utf-8 -*-
 import json
 import requests
 
+from django.db import models
 from django.utils.six import string_types
 from django.utils.translation import ugettext_lazy as _
-from django.db import models
 
+from passerelle.sms.models import SMSResource
 from passerelle.utils.jsonresponse import APIError
-from passerelle.base.models import SMSResource
 
 
 class ChoositSMSGateway(SMSResource):
     key = models.CharField(verbose_name=_('Key'), max_length=64)
 
     TEST_DEFAULTS = {
         'create_kwargs': {
             'key': '1234',
diff --git a/passerelle/apps/mobyt/models.py b/passerelle/apps/mobyt/models.py
index 1dcb1899..3e1045c1 100644
--- a/passerelle/apps/mobyt/models.py
+++ b/passerelle/apps/mobyt/models.py
@@ -1,15 +1,15 @@
-from django.utils.translation import ugettext_lazy as _
 from django.db import models
+from django.utils.translation import ugettext_lazy as _
 
 import requests
 
+from passerelle.sms.models import SMSResource
 from passerelle.utils.jsonresponse import APIError
-from passerelle.base.models import SMSResource
 
 
 class MobytSMSGateway(SMSResource):
     URL = 'http://multilevel.mobyt.fr/sms/batch.php'
     MESSAGES_QUALITIES = (
         ('l', _('sms direct')),
         ('ll', _('sms low-cost')),
         ('n', _('sms top')),
diff --git a/passerelle/apps/orange/models.py b/passerelle/apps/orange/models.py
index f0fe6a4b..fb6da58c 100644
--- a/passerelle/apps/orange/models.py
+++ b/passerelle/apps/orange/models.py
@@ -17,19 +17,20 @@
 # 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/>.
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 
-from passerelle.base.models import SMSResource
+from passerelle.sms.models import SMSResource
 from passerelle.utils.jsonresponse import APIError
 
+
 BASE_API = 'https://contact-everyone.orange-business.com/api/v1.2/'
 URL_TOKEN = BASE_API + 'oauth/token'
 URL_GROUPS = BASE_API + 'groups'
 URL_DIFFUSION = BASE_API + 'groups/%s/diffusion-requests'
 
 
 class OrangeError(APIError):
     pass
diff --git a/passerelle/apps/ovh/models.py b/passerelle/apps/ovh/models.py
index 529d56c5..a690c330 100644
--- a/passerelle/apps/ovh/models.py
+++ b/passerelle/apps/ovh/models.py
@@ -1,16 +1,16 @@
 import requests
 
-from django.utils.translation import ugettext_lazy as _
 from django.db import models
 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
-from passerelle.base.models import SMSResource
 
 
 class OVHSMSGateway(SMSResource):
     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 '
diff --git a/passerelle/apps/oxyd/models.py b/passerelle/apps/oxyd/models.py
index f1482cae..69babc9f 100644
--- a/passerelle/apps/oxyd/models.py
+++ b/passerelle/apps/oxyd/models.py
@@ -1,16 +1,16 @@
 import requests
 
 from django.db import models
 from django.utils.encoding import force_text
+from django.utils.translation import ugettext_lazy as _
 
 from passerelle.utils.jsonresponse import APIError
-from passerelle.base.models import SMSResource
-from django.utils.translation import ugettext_lazy as _
+from passerelle.sms.models import SMSResource
 
 
 class OxydSMSGateway(SMSResource):
     username = models.CharField(verbose_name=_('Username'), max_length=64)
     password = models.CharField(verbose_name=_('Password'), max_length=64)
 
     manager_view_template_name = 'passerelle/manage/messages_service_view.html'
 
diff --git a/passerelle/base/models.py b/passerelle/base/models.py
index 22b76242..acfeb2d6 100644
--- a/passerelle/base/models.py
+++ b/passerelle/base/models.py
@@ -9,38 +9,35 @@ import sys
 import traceback
 import base64
 import itertools
 import uuid
 
 from django.apps import apps
 from django.conf import settings
 from django.contrib.postgres.fields import JSONField
-from django.core.exceptions import ValidationError, ObjectDoesNotExist, PermissionDenied
+from django.core.exceptions import ValidationError, PermissionDenied
 from django.core.urlresolvers import reverse
 from django.db import connection, models, transaction
 from django.db.models import Q
 from django.test import override_settings
 from django.utils.text import slugify
 from django.utils import timezone, six
-from django.utils import six
 from django.utils.encoding import force_text
 from django.utils.six.moves.urllib.parse import urlparse
 from django.utils.translation import ugettext_lazy as _
 from django.utils.timezone import now
 from django.core.files.base import ContentFile
 
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import fields
 
 from model_utils.managers import InheritanceManager as ModelUtilsInheritanceManager
 
 import passerelle
-import requests
-from passerelle.compat import json_loads
 from passerelle.utils.api import endpoint
 from passerelle.utils.jsonresponse import APIError
 
 KEYTYPE_CHOICES = (
     ('API', _('API Key')),
     ('SIGN', _('HMAC Signature')),
 )
 
diff --git a/passerelle/settings.py b/passerelle/settings.py
index 46aed2e5..ef625eb6 100644
--- a/passerelle/settings.py
+++ b/passerelle/settings.py
@@ -113,16 +113,17 @@ INSTALLED_APPS = (
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'django.contrib.admin',
     'django.contrib.postgres',
     # base app
     'passerelle.base',
+    'passerelle.sms',
     # connectors
     'passerelle.apps.actesweb',
     'passerelle.apps.airquality',
     'passerelle.apps.api_entreprise',
     'passerelle.apps.api_particulier',
     'passerelle.apps.arcgis',
     'passerelle.apps.arpege_ecp',
     'passerelle.apps.astregs',
diff --git a/passerelle/sms/__init__.py b/passerelle/sms/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/sms/migrations/0001_initial.py b/passerelle/sms/migrations/0001_initial.py
new file mode 100644
index 00000000..74361ed3
--- /dev/null
+++ b/passerelle/sms/migrations/0001_initial.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2020-05-02 17:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SMSLog',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('timestamp', models.DateTimeField(auto_now_add=True)),
+                ('appname', models.CharField(max_length=128, null=True, verbose_name='appname')),
+                ('slug', models.CharField(max_length=128, null=True, verbose_name='slug')),
+            ],
+        ),
+    ]
diff --git a/passerelle/sms/migrations/__init__.py b/passerelle/sms/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/sms/models.py b/passerelle/sms/models.py
new file mode 100644
index 00000000..9bf29def
--- /dev/null
+++ b/passerelle/sms/models.py
@@ -0,0 +1,97 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2020  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 logging
+import re
+
+from django.db import models
+from django.utils import six
+from django.utils.translation import ugettext_lazy as _
+
+from passerelle.base.models import BaseResource
+from passerelle.compat import json_loads
+from passerelle.utils.api import endpoint
+from passerelle.utils.jsonresponse import APIError
+
+
+class SMSResource(BaseResource):
+    category = _('SMS Providers')
+    documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
+
+    _can_send_messages_description = _('Sending messages is limited to the following API users:')
+
+    default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
+                                            default=u'33')
+    default_trunk_prefix = models.CharField(verbose_name=_('Default trunk prefix'), max_length=2,
+                                            default=u'0')  # Yeah France first !
+    # FIXME: add regexp field, to check destination and from format
+    max_message_length = models.IntegerField(_('Maximum message length'), default=160)
+
+    def clean_numbers(self, destinations):
+        numbers = []
+        for dest in destinations:
+            # most gateways needs the number prefixed by the country code, this is
+            # really unfortunate.
+            dest = dest.strip()
+            number = ''.join(re.findall('[0-9]', dest))
+            if dest.startswith('+'):
+                number = '00' + number
+            elif number.startswith('00'):
+                # assumes 00 is international access code, remove it
+                pass
+            elif number.startswith(self.default_trunk_prefix):
+                number = '00' + self.default_country_code + number[len(self.default_trunk_prefix):]
+            else:
+                raise APIError('phone number %r is unsupported (no international prefix, '
+                               'no local trunk prefix)' % number)
+            numbers.append(number)
+        return numbers
+
+    @endpoint(perm='can_send_messages', methods=['post'])
+    def send(self, request, *args, **kwargs):
+        try:
+            data = json_loads(request.body)
+            assert isinstance(data, dict), 'JSON payload is not a dict'
+            assert 'message' in data, 'missing "message" in JSON payload'
+            assert 'from' in data, 'missing "from" in JSON payload'
+            assert 'to' in data, 'missing "to" in JSON payload'
+            assert isinstance(data['message'], six.text_type), 'message is not a string'
+            assert isinstance(data['from'], six.text_type), 'from is not a string'
+            assert all(map(lambda x: isinstance(x, six.text_type), data['to'])), \
+                'to is not a list of strings'
+        except (ValueError, AssertionError) as e:
+            raise APIError('Payload error: %s' % e)
+        data['message'] = data['message'][:self.max_message_length]
+        data['to'] = self.clean_numbers(data['to'])
+        stop = not bool('nostop' in request.GET)
+        logging.info('sending message %r to %r with sending number %r',
+                     data['message'], data['to'], data['from'])
+        # unfortunately it lacks a batch API...
+        result = {'data': self.send_msg(data['message'], data['from'], data['to'], stop=stop)}
+        SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug)
+        return result
+
+    class Meta:
+        abstract = True
+
+
+@six.python_2_unicode_compatible
+class SMSLog(models.Model):
+    timestamp = models.DateTimeField(auto_now_add=True)
+    appname = models.CharField(max_length=128, verbose_name='appname', null=True)
+    slug = models.CharField(max_length=128, verbose_name='slug', null=True)
+
+    def __str__(self):
+        return '%s %s %s' % (self.timestamp, self.appname, self.slug)
diff --git a/tests/test_sms.py b/tests/test_sms.py
index d8f74197..5e70605c 100644
--- a/tests/test_sms.py
+++ b/tests/test_sms.py
@@ -1,15 +1,16 @@
 import mock
 import pytest
 
 from django.contrib.contenttypes.models import ContentType
 
 from passerelle.apps.ovh.models import OVHSMSGateway
-from passerelle.base.models import ApiUser, AccessRight, SMSResource, SMSLog
+from passerelle.base.models import ApiUser, AccessRight
+from passerelle.sms.models import SMSResource, SMSLog
 from passerelle.utils.jsonresponse import APIError
 
 from test_manager import login, admin_user
 
 import utils
 
 pytestmark = pytest.mark.django_db
 
-- 
2.26.2

