Projet

Général

Profil

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

Nicolas Roche, 25 mai 2020 17:46

Télécharger (8,97 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
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 APIError('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.opendatasoft',
151 151
    'passerelle.apps.opengis',
152 152
    'passerelle.apps.orange',
153 153
    'passerelle.apps.ovh',
154 154
    'passerelle.apps.oxyd',
155 155
    'passerelle.apps.pastell',
156 156
    'passerelle.apps.phonecalls',
157 157
    'passerelle.apps.solis',
158
    'passerelle.apps.twilio',
158 159
    'passerelle.apps.vivaticket',
159 160
    # backoffice templates and static
160 161
    'gadjo',
161 162
)
162 163

  
163 164
# disable some applications for now
164 165
PASSERELLE_APP_BDP_ENABLED = False
165 166
PASSERELLE_APP_GDC_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
-