0001-ovh-support-jobs-API-endpoint-44313.patch
passerelle/apps/ovh/migrations/0009_auto_20200730_1047.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-07-30 08:47 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('ovh', '0008_ovhsmsgateway_max_message_length'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AddField( |
|
16 |
model_name='ovhsmsgateway', |
|
17 |
name='application_key', |
|
18 |
field=models.CharField(blank=True, help_text='Random token obtained from OVH.', max_length=16, verbose_name='Application key'), |
|
19 |
), |
|
20 |
migrations.AddField( |
|
21 |
model_name='ovhsmsgateway', |
|
22 |
name='application_secret', |
|
23 |
field=models.CharField(blank=True, help_text='Obtained at the same time as "Application key".', max_length=32, verbose_name='Application secret'), |
|
24 |
), |
|
25 |
migrations.AddField( |
|
26 |
model_name='ovhsmsgateway', |
|
27 |
name='consumer_key', |
|
28 |
field=models.CharField(blank=True, help_text='Obtained at the same time as "Application key".', max_length=32, verbose_name='Consumer key'), |
|
29 |
), |
|
30 |
migrations.AlterField( |
|
31 |
model_name='ovhsmsgateway', |
|
32 |
name='account', |
|
33 |
field=models.CharField(help_text='Account identifier, such as sms-XXXXXX-1.', max_length=64, verbose_name='Account'), |
|
34 |
), |
|
35 |
migrations.AlterField( |
|
36 |
model_name='ovhsmsgateway', |
|
37 |
name='password', |
|
38 |
field=models.CharField(blank=True, help_text='Password for legacy API. This field is obsolete once keys and secret fields below are filled.', max_length=64, verbose_name='Password (deprecated)'), |
|
39 |
), |
|
40 |
migrations.AlterField( |
|
41 |
model_name='ovhsmsgateway', |
|
42 |
name='username', |
|
43 |
field=models.CharField(help_text='API user created on the SMS account.', max_length=64, verbose_name='Username'), |
|
44 |
), |
|
45 |
] |
passerelle/apps/ovh/models.py | ||
---|---|---|
1 |
import hashlib |
|
2 |
import json |
|
1 | 3 |
import requests |
4 |
import time |
|
2 | 5 | |
3 | 6 |
from django.db import models |
4 | 7 |
from django.utils.encoding import force_text |
... | ... | |
9 | 12 | |
10 | 13 | |
11 | 14 |
class OVHSMSGateway(SMSResource): |
15 |
documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/' |
|
16 |
API_URL = 'https://eu.api.ovh.com/1.0/sms/%(serviceName)s/users/%(login)s/jobs/' |
|
12 | 17 |
URL = 'https://www.ovh.com/cgi-bin/sms/http2sms.cgi' |
13 | 18 |
MESSAGES_CLASSES = ( |
14 | 19 |
(0, _('Message are directly shown to users on phone screen ' |
... | ... | |
21 | 26 |
(3, _('Messages are stored in external storage like a PDA or ' |
22 | 27 |
'a PC.')), |
23 | 28 |
) |
24 |
account = models.CharField(verbose_name=_('Account'), max_length=64) |
|
25 |
username = models.CharField(verbose_name=_('Username'), max_length=64) |
|
26 |
password = models.CharField(verbose_name=_('Password'), max_length=64) |
|
29 |
NEW_MESSAGES_CLASSES = ['flash', 'phoneDisplay', 'sim', 'toolkit'] |
|
30 | ||
31 |
account = models.CharField( |
|
32 |
verbose_name=_('Account'), max_length=64, help_text=_('Account identifier, such as sms-XXXXXX-1.') |
|
33 |
) |
|
34 | ||
35 |
application_key = models.CharField( |
|
36 |
verbose_name=_('Application key'), |
|
37 |
max_length=16, |
|
38 |
blank=True, |
|
39 |
help_text=_('Random token obtained from OVH.'), |
|
40 |
) |
|
41 |
application_secret = models.CharField( |
|
42 |
verbose_name=_('Application secret'), |
|
43 |
max_length=32, |
|
44 |
blank=True, |
|
45 |
help_text=_('Obtained at the same time as "Application key".'), |
|
46 |
) |
|
47 |
consumer_key = models.CharField( |
|
48 |
verbose_name=_('Consumer key'), |
|
49 |
max_length=32, |
|
50 |
blank=True, |
|
51 |
help_text=_('Obtained at the same time as "Application key".'), |
|
52 |
) |
|
53 | ||
54 |
username = models.CharField( |
|
55 |
verbose_name=_('Username'), |
|
56 |
max_length=64, |
|
57 |
help_text=_('API user created on the SMS account.'), |
|
58 |
) |
|
59 |
password = models.CharField( |
|
60 |
verbose_name=_('Password (deprecated)'), |
|
61 |
max_length=64, |
|
62 |
blank=True, |
|
63 |
help_text=_( |
|
64 |
'Password for legacy API. This field is obsolete once keys and secret fields below are filled.' |
|
65 |
), |
|
66 |
) |
|
27 | 67 |
msg_class = models.IntegerField(choices=MESSAGES_CLASSES, default=1, |
28 | 68 |
verbose_name=_('Message class')) |
29 | 69 |
credit_threshold_alert = models.PositiveIntegerField(verbose_name=_('Credit alert threshold'), |
... | ... | |
76 | 116 |
db_table = 'sms_ovh' |
77 | 117 | |
78 | 118 |
def send_msg(self, text, sender, destinations, **kwargs): |
79 |
"""Send a SMS using the OVH provider""" |
|
119 |
if not (self.application_key and self.consumer_key and self.application_secret): |
|
120 |
return self.send_msg_legacy(text, sender, destinations, **kwargs) |
|
121 | ||
122 |
url = self.API_URL % {'serviceName': self.account, 'login': self.username} |
|
123 |
body = { |
|
124 |
'sender': sender, |
|
125 |
'receivers': destinations, |
|
126 |
'message': text, |
|
127 |
'class': self.NEW_MESSAGES_CLASSES[self.msg_class], |
|
128 |
} |
|
129 |
if not kwargs['stop']: |
|
130 |
body.update({'noStopClause': 1}) |
|
131 | ||
132 |
# sign request |
|
133 |
now = str(int(time.time())) |
|
134 |
signature = hashlib.sha1() |
|
135 |
to_sign = "+".join((self.application_secret, self.consumer_key, 'POST', url, json.dumps(body), now)) |
|
136 |
signature.update(to_sign.encode()) |
|
137 | ||
138 |
headers = { |
|
139 |
'X-Ovh-Application': self.application_key, |
|
140 |
'X-Ovh-Consumer': self.consumer_key, |
|
141 |
'X-Ovh-Timestamp': now, |
|
142 |
'X-Ovh-Signature': "$1$" + signature.hexdigest(), |
|
143 |
} |
|
144 | ||
145 |
try: |
|
146 |
response = self.requests.post(url, headers=headers, json=body) |
|
147 |
except requests.RequestException as e: |
|
148 |
raise APIError('OVH error: POST failed, %s' % e) |
|
149 |
else: |
|
150 |
try: |
|
151 |
result = response.json() |
|
152 |
except ValueError as e: |
|
153 |
raise APIError('OVH error: bad JSON response') |
|
154 |
try: |
|
155 |
response.raise_for_status() |
|
156 |
except requests.RequestException as e: |
|
157 |
raise APIError('OVH error: %s "%s"' % (e, result)) |
|
158 | ||
159 |
ret = {} |
|
160 |
credits_removed = result['totalCreditsRemoved'] |
|
161 |
# update credit left |
|
162 |
self.credit_left -= credits_removed |
|
163 |
if self.credit_left < 0: |
|
164 |
self.credit_left = 0 |
|
165 |
self.save() |
|
166 |
if self.credit_left < self.credit_threshold_alert: |
|
167 |
ret['warning'] = 'credit level too low for %s: %s (threshold %s)' % ( |
|
168 |
self.slug, |
|
169 |
self.credit_left, |
|
170 |
self.credit_threshold_alert, |
|
171 |
) |
|
172 |
ret['credit_left'] = self.credit_left |
|
173 |
ret['ovh_result'] = result |
|
174 |
ret['sms_ids'] = result.get('ids', []) |
|
175 | ||
176 |
return ret |
|
177 | ||
178 |
def send_msg_legacy(self, text, sender, destinations, **kwargs): |
|
179 |
"""Send a SMS using the HTTP2 endpoint""" |
|
180 |
if not self.password: |
|
181 |
raise APIError('Improperly configured, empty keys or password fields.') |
|
80 | 182 | |
81 | 183 |
text = force_text(text).encode('utf-8') |
82 | 184 |
to = ','.join(destinations) |
tests/test_sms.py | ||
---|---|---|
138 | 138 |
result = app.post_json(path, params=payload) |
139 | 139 |
connector.jobs() |
140 | 140 |
assert SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists() |
141 | ||
142 | ||
143 |
def test_ovh_new_api(app, freezer): |
|
144 |
connector = OVHSMSGateway.objects.create( |
|
145 |
slug='ovh', account='sms-test42', username='john', |
|
146 |
application_key='RHrTdU2oTsrVC0pu', |
|
147 |
application_secret='CLjtS69tTcPgCKxedeoZlgMSoQGSiXMa', |
|
148 |
consumer_key='iF0zi0MJrbjNcI3hvuvwkhNk8skrigxz' |
|
149 |
) |
|
150 |
api = ApiUser.objects.create(username='apiuser') |
|
151 |
obj_type = ContentType.objects.get_for_model(connector) |
|
152 |
# no access check |
|
153 |
AccessRight.objects.create(codename='can_send_messages', apiuser=api, resource_type=obj_type, |
|
154 |
resource_pk=connector.pk) |
|
155 | ||
156 |
payload = { |
|
157 |
'message': 'hello', |
|
158 |
'from': '+33699999999', |
|
159 |
'to': ['+33688888888', '+33677777777'], |
|
160 |
} |
|
161 | ||
162 |
# register job |
|
163 |
freezer.move_to('2019-01-01 00:00:00') |
|
164 |
path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) |
|
165 |
result = app.post_json(path, params=payload) |
|
166 |
assert result.json['err'] == 0 |
|
167 |
job_id = Job.objects.get(status='registered').id |
|
168 | ||
169 |
# perform job |
|
170 |
freezer.move_to('2019-01-01 01:00:03') |
|
171 |
resp = { |
|
172 |
'validReceivers': ['+33688888888', '+33677777777'], |
|
173 |
'totalCreditsRemoved': 1, |
|
174 |
'ids': [241615100], |
|
175 |
'invalidReceivers': [] |
|
176 |
} |
|
177 |
url = connector.API_URL % {'serviceName': 'sms-test42', 'login': 'john'} |
|
178 |
with utils.mock_url(url, resp, 200) as mocked: |
|
179 |
connector.jobs() |
|
180 |
job = Job.objects.get(id=job_id) |
|
181 |
assert job.status == 'completed' |
|
182 | ||
183 |
request = mocked.handlers[0].call['requests'][0] |
|
184 |
assert 'X-Ovh-Signature' in request.headers |
tests/utils.py | ||
---|---|---|
44 | 44 |
if not isinstance(response, str): |
45 | 45 |
response = json.dumps(response) |
46 | 46 | |
47 |
@httmock.remember_called |
|
47 | 48 |
@httmock.urlmatch(**urlmatch_kwargs) |
48 | 49 |
def mocked(url, request): |
49 | 50 |
if exception: |
50 |
- |