Projet

Général

Profil

0001-sms-add-a-twilio-connector-19663.patch

Nicolas Roche, 02 mai 2020 12:27

Télécharger (9 ko)

Voir les différences:

Subject: [PATCH] sms: add a twilio connector (#19663)

 passerelle/apps/twilio/__init__.py            |   0
 .../apps/twilio/migrations/0001_initial.py    |  36 ++++++
 passerelle/apps/twilio/migrations/__init__.py |   0
 passerelle/apps/twilio/models.py              | 105 ++++++++++++++++++
 passerelle/settings.py                        |   1 +
 tests/test_sms.py                             |   6 +-
 6 files changed, 147 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/apps/twilio/__init__.py
 create mode 100644 passerelle/apps/twilio/migrations/0001_initial.py
 create mode 100644 passerelle/apps/twilio/migrations/__init__.py
 create mode 100644 passerelle/apps/twilio/models.py
passerelle/apps/twilio/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-05-02 09:38
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    initial = True
11

  
12
    dependencies = [
13
        ('base', '0018_smslog'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='TwilioSMSGateway',
19
            fields=[
20
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
                ('title', models.CharField(max_length=50, verbose_name='Title')),
22
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
23
                ('description', models.TextField(verbose_name='Description')),
24
                ('max_message_length', models.IntegerField(default=160, verbose_name='Maximum message length')),
25
                ('account_sid', models.CharField(max_length=64, verbose_name='Account Sid')),
26
                ('auth_token', models.CharField(max_length=64, verbose_name='Auth Token')),
27
                ('default_country_code', models.CharField(default='33', max_length=3, verbose_name='Default country code')),
28
                ('default_trunk_prefix', models.CharField(default='0', max_length=2, verbose_name='Default trunk prefix')),
29
                ('users', models.ManyToManyField(blank=True, related_name='_twiliosmsgateway_users_+', related_query_name='+', to='base.ApiUser')),
30
            ],
31
            options={
32
                'verbose_name': 'Twilio',
33
                'db_table': 'sms_twilio',
34
            },
35
        ),
36
    ]
passerelle/apps/twilio/models.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
import requests
17
from requests.auth import HTTPBasicAuth
18

  
19
from django.db import models
20
from django.utils.translation import ugettext_lazy as _
21

  
22
from passerelle.utils.jsonresponse import APIError, APIRecoverableError
23
from passerelle.base.models import SMSResource
24

  
25

  
26
class TwilioSMSGateway(SMSResource):
27
    account_sid = models.CharField(verbose_name=_('Account Sid'), max_length=64)
28
    auth_token = models.CharField(verbose_name=_('Auth Token'), max_length=64)
29
    default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
30
                                            default=u'33')
31
    default_trunk_prefix = models.CharField(verbose_name=_('Default trunk prefix'), max_length=2,
32
                                            default=u'0')
33
    # FIXME: add regexp field, to check destination and from format
34

  
35
    class Meta:
36
        verbose_name = 'Twilio'
37
        db_table = 'sms_twilio'
38

  
39
    TEST_DEFAULTS = {
40
        'create_kwargs': {
41
            'account_sid': 'ACxxx',
42
            'auth_token': 'yyy',
43
        },
44
        'test_vectors': [
45
            {
46
                'status_code': 400,
47
                'response': 'my error message',
48
                'result': {
49
                    'err': 1,
50
                    'err_desc': 'Twilio error: some destinations failed',
51
                    'data': [
52
                        ['+33688888888', "Twilio error: my error message"],
53
                        ['+33677777777', "Twilio error: my error message"],
54
                    ],
55
                }
56
            },
57
            {
58
                'status_code': 201,
59
                'result': {
60
                    'err': 0,
61
                    'data': None,
62
                }
63
            }
64
        ],
65
    }
66
    URL = 'https://api.twilio.com/2010-04-01/Accounts'
67

  
68
    def send_msg(self, text, sender, destinations, **kwargs):
69
        """Send a SMS using the Twilio provider"""
70
        # from https://www.twilio.com/docs/usage/requests-to-twilio
71
        # unfortunately it lacks a batch API...
72
        destinations = self.clean_numbers(destinations,
73
                                          self.default_country_code,
74
                                          self.default_trunk_prefix)
75

  
76
        # set destinations phone number in E.164 format
77
        # [+][country code][phone number including area code]
78
        numbers = []
79
        for dest in destinations:
80
            numbers.append('+' + dest[2:])
81
        destinations = numbers
82

  
83
        url = '%s/%s/Messages.json' % (TwilioSMSGateway.URL, self.account_sid)
84
        auth = HTTPBasicAuth(self.account_sid, self.auth_token)
85
        results = []
86
        for dest in destinations:
87
            params = {
88
                'Body': text,
89
                'From': sender,
90
                'To': dest
91
            }
92
            try:
93
                resp = self.requests.post(url, params, auth=auth)
94
            except requests.RequestException as exc:
95
                raise APIRecoverableError('Twilio error: POST failed, %s' % exc)
96
            else:
97
                if resp.status_code != 201:
98
                    results.append('Twilio error: %s' % resp.text)
99
                else:
100
                    results.append(0)
101
        if any(results):
102
            raise APIError(
103
                'Twilio error: some destinations failed',
104
                data=list(zip(destinations, results)))
105
        return None
passerelle/settings.py
150 150
    'passerelle.apps.opengis',
151 151
    'passerelle.apps.orange',
152 152
    'passerelle.apps.ovh',
153 153
    'passerelle.apps.oxyd',
154 154
    'passerelle.apps.pastell',
155 155
    'passerelle.apps.phonecalls',
156 156
    'passerelle.apps.solis',
157 157
    'passerelle.apps.vivaticket',
158
    'passerelle.apps.twilio',
158 159
    # backoffice templates and static
159 160
    'gadjo',
160 161
)
161 162

  
162 163
# disable some applications for now
163 164
PASSERELLE_APP_BDP_ENABLED = False
164 165
PASSERELLE_APP_GDC_ENABLED = False
165 166
PASSERELLE_APP_PASTELL_ENABLED = False
tests/test_sms.py
51 51
    assert result.json['err_desc'].startswith('Payload error: ')
52 52

  
53 53
    payload = {
54 54
        'message': 'hello',
55 55
        'from': '+33699999999',
56 56
        'to': ['+33688888888', '+33677777777'],
57 57
    }
58 58
    for test_vector in getattr(connector, 'TEST_DEFAULTS', {}).get('test_vectors', []):
59
        with utils.mock_url(connector.URL, test_vector['response']):
59
        with utils.mock_url(
60
                connector.URL,
61
                test_vector.get('response', ''),
62
                test_vector.get('status_code', 200)):
63

  
60 64
            result = app.post_json(path, params=payload)
61 65
            for key, value in test_vector['result'].items():
62 66
                assert key in result.json
63 67
                assert result.json[key] == value
64 68

  
65 69

  
66 70
def test_manage_views(admin_user, app, connector):
67 71
    url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug)
68
-