Skip to content

Commit

Permalink
Add support for Zenvia SMS
Browse files Browse the repository at this point in the history
  • Loading branch information
norkans7 committed Apr 8, 2021
1 parent 1c29dd3 commit 225660b
Show file tree
Hide file tree
Showing 15 changed files with 277 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ __pycache__/
.Python
env/
venv/
.venv/
build/
develop-eggs/
dist/
Expand Down
2 changes: 1 addition & 1 deletion code_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_current_msgids():
saved_msgids = get_current_msgids()

# re-extract locale files from source code
ignore_paths = ("env/*", "fabric/*", "media/*", "sitestatic/*", "static/*", "node_modules/*")
ignore_paths = ("env/*", ".venv/*", "fabric/*", "media/*", "sitestatic/*", "static/*", "node_modules/*")
ignore_args = " ".join([f'--ignore="{p}"' for p in ignore_paths])

cmd(f"python manage.py makemessages -a -e haml,html,txt,py --no-location --no-wrap {ignore_args}")
Expand Down
6 changes: 5 additions & 1 deletion locale/en_US/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-06 21:32+0000\n"
"POT-Creation-Date: 2021-04-08 20:37+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -1843,6 +1843,10 @@ msgstr ""
msgid "The account password provided by Zenvia"
msgstr ""

#, python-format
msgid "If you have a %(link)s number, you can connect it to communicate with your contacts."
msgstr ""

#, python-format
msgid "Unable to register webhook subscriptions: %(resp)s"
msgstr ""
Expand Down
7 changes: 6 additions & 1 deletion locale/es/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-06 21:32+0000\n"
"POT-Creation-Date: 2021-04-08 20:24+0000\n"
"PO-Revision-Date: 2019-05-13 20:14+0000\n"
"Last-Translator: Moy Moussan <[email protected]>, 2021\n"
"Language-Team: Spanish (https://www.transifex.com/rapidpro/teams/226/es/)\n"
Expand Down Expand Up @@ -1859,6 +1859,11 @@ msgstr "El nombre de usuario de la cuenta proporcionado por Zenvia"
msgid "The account password provided by Zenvia"
msgstr "La contraseña de la cuenta proporcionada por Zenvia"

#, fuzzy, python-format
#| msgid "If you have a %(link)s number, you can connect it to communicate with your WhatsApp contacts."
msgid "If you have a %(link)s number, you can connect it to communicate with your contacts."
msgstr "Si tiene un número %(link)s, puede conectarlo para comunicarse con sus contactos de WhatsApp."

#, python-format
msgid "Unable to register webhook subscriptions: %(resp)s"
msgstr ""
Expand Down
7 changes: 6 additions & 1 deletion locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-06 21:32+0000\n"
"POT-Creation-Date: 2021-04-08 20:37+0000\n"
"PO-Revision-Date: 2019-05-13 20:14+0000\n"
"Last-Translator: Jesus EKIE <[email protected]>, 2020\n"
"Language-Team: French (https://www.transifex.com/rapidpro/teams/226/fr/)\n"
Expand Down Expand Up @@ -1858,6 +1858,11 @@ msgstr "Le nom d'utilisateur du compte fourni par Zenvia"
msgid "The account password provided by Zenvia"
msgstr "Le mot de passe du compte fourni par Zenvia"

#, fuzzy, python-format
#| msgid "If you have a %(link)s number, you can connect it to communicate with your WhatsApp contacts."
msgid "If you have a %(link)s number, you can connect it to communicate with your contacts."
msgstr "Si vous possédez un numéro %(link)s , vous pouvez le connecter pour communiquer avec vos contacts WhatsApp."

#, python-format
msgid "Unable to register webhook subscriptions: %(resp)s"
msgstr ""
Expand Down
7 changes: 6 additions & 1 deletion locale/pt_BR/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-06 21:32+0000\n"
"POT-Creation-Date: 2021-04-08 20:37+0000\n"
"PO-Revision-Date: 2019-05-13 20:14+0000\n"
"Last-Translator: Ilhasoft <[email protected]>, 2021\n"
"Language-Team: Portuguese (Brazil) (https://www.transifex.com/rapidpro/teams/226/pt_BR/)\n"
Expand Down Expand Up @@ -1865,6 +1865,11 @@ msgstr "O nome de usuário da conta fornecida pela Zenvia"
msgid "The account password provided by Zenvia"
msgstr "A senha da conta fornecida pela Zenvia"

#, fuzzy, python-format
#| msgid "If you have a %(link)s number, you can connect it to communicate with your WhatsApp contacts."
msgid "If you have a %(link)s number, you can connect it to communicate with your contacts."
msgstr "Se você possui um número %(link)s, você pode conectá-lo para se comunicar com os seus contatos do WhatsApp."

#, python-format
msgid "Unable to register webhook subscriptions: %(resp)s"
msgstr ""
Expand Down
7 changes: 6 additions & 1 deletion locale/ru/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-06 21:32+0000\n"
"POT-Creation-Date: 2021-04-08 20:37+0000\n"
"PO-Revision-Date: 2019-05-13 20:14+0000\n"
"Last-Translator: Hudson Brendon <[email protected]>, 2020\n"
"Language-Team: Russian (https://www.transifex.com/rapidpro/teams/226/ru/)\n"
Expand Down Expand Up @@ -1872,6 +1872,11 @@ msgstr "Имя пользователя учетной записи, предо
msgid "The account password provided by Zenvia"
msgstr "Пароль учетной записи, предоставленный Zenvia"

#, fuzzy, python-format
#| msgid "If you have an enterprise WhatsApp account, you can connect it to communicate with your contacts"
msgid "If you have a %(link)s number, you can connect it to communicate with your contacts."
msgstr "Если у Вас есть корпоративная учетная запись WhatsApp, Вы можете подключить ее для общения с Вашими контактами"

#, python-format
msgid "Unable to register webhook subscriptions: %(resp)s"
msgstr ""
Expand Down
16 changes: 9 additions & 7 deletions temba/channels/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,12 +918,13 @@ def test_claim_all(self):
self.assertEqual(response.context["channel_types"]["PHONE"][0].code, "AC")
self.assertEqual(response.context["channel_types"]["PHONE"][1].code, "T")
self.assertEqual(response.context["channel_types"]["PHONE"][2].code, "TMS")
self.assertEqual(response.context["channel_types"]["PHONE"][-2].code, "WV")
self.assertEqual(response.context["channel_types"]["PHONE"][-1].code, "YO")
self.assertEqual(response.context["channel_types"]["PHONE"][-2].code, "YO")
self.assertEqual(response.context["channel_types"]["PHONE"][-1].code, "ZVS")

self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][0].code, "D3")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][1].code, "TWA")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][2].code, "FBA")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][1].code, "ZVW")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][2].code, "TWA")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][3].code, "FBA")

self.admin.groups.add(Group.objects.get(name="Beta"))

Expand All @@ -936,12 +937,13 @@ def test_claim_all(self):
self.assertEqual(response.context["channel_types"]["PHONE"][0].code, "AC")
self.assertEqual(response.context["channel_types"]["PHONE"][1].code, "T")
self.assertEqual(response.context["channel_types"]["PHONE"][2].code, "TMS")
self.assertEqual(response.context["channel_types"]["PHONE"][-2].code, "WV")
self.assertEqual(response.context["channel_types"]["PHONE"][-1].code, "YO")
self.assertEqual(response.context["channel_types"]["PHONE"][-2].code, "YO")
self.assertEqual(response.context["channel_types"]["PHONE"][-1].code, "ZVS")

self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][0].code, "WA")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][1].code, "D3")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][2].code, "TWA")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][2].code, "ZVW")
self.assertEqual(response.context["channel_types"]["SOCIAL_MEDIA"][3].code, "TWA")

def test_register_unsupported_android(self):
# remove our explicit country so it needs to be derived from channels
Expand Down
1 change: 1 addition & 0 deletions temba/channels/types/zenvia_sms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .type import ZenviaSMSType # noqa
120 changes: 120 additions & 0 deletions temba/channels/types/zenvia_sms/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from unittest.mock import patch

from django.forms import ValidationError
from django.urls import reverse

from temba.tests import MockResponse, TembaTest

from ...models import Channel
from .type import ZENVIA_MESSAGE_SUBSCRIPTION_ID, ZENVIA_STATUS_SUBSCRIPTION_ID, ZenviaSMSType


class ZenviaSMSTypeTest(TembaTest):
def test_claim(self):
Channel.objects.all().delete()

self.login(self.admin)

url = reverse("channels.types.zenvia_sms.claim")

# should see the general channel claim page
response = self.client.get(reverse("channels.channel_claim"))
self.assertContains(response, url)

# try to claim a channel
response = self.client.get(url)
post_data = response.context["form"].initial

post_data["token"] = "12345"
post_data["country"] = "US"
post_data["number"] = "(206) 555-1212"

response = self.client.post(url, post_data)

channel = Channel.objects.get()

self.assertEqual("US", channel.country)
self.assertTrue(channel.uuid)
self.assertEqual("+12065551212", channel.address)
self.assertEqual("12345", channel.config["api_key"])
self.assertEqual("ZVS", channel.channel_type)
self.assertEqual("Zenvia SMS: +12065551212", channel.name)

with patch("requests.post") as mock_patch:
mock_patch.side_effect = [MockResponse(400, '{ "error": true }')]

try:
ZenviaSMSType().activate(channel)
self.fail("Should have thrown error activating channel")
except ValidationError:
pass

self.assertFalse(channel.config.get(ZENVIA_MESSAGE_SUBSCRIPTION_ID))
self.assertFalse(channel.config.get(ZENVIA_STATUS_SUBSCRIPTION_ID))

with patch("requests.post") as mock_post:
mock_post.side_effect = [
MockResponse(200, '{"id": "message_123"}'),
MockResponse(400, '{"error": "failed"}'),
]
try:
ZenviaSMSType().activate(channel)
except ValidationError:
pass

self.assertEqual("12345", mock_post.call_args_list[0][1]["headers"]["X-API-TOKEN"])

self.assertEqual("message_123", channel.config.get(ZENVIA_MESSAGE_SUBSCRIPTION_ID))
self.assertIsNone(channel.config.get(ZENVIA_STATUS_SUBSCRIPTION_ID))

with patch("requests.delete") as mock_delete:
mock_delete.return_value = MockResponse(204, "")

# deactivate our channel
with self.settings(IS_PROD=True):
channel.release()

self.assertEqual(1, mock_delete.call_count)
self.assertEqual(
"https://api.zenvia.com/v2/subscriptions/message_123", mock_delete.call_args_list[0][0][0]
)
self.assertEqual("12345", mock_delete.call_args_list[0][1]["headers"]["X-API-TOKEN"])

# try to claim a channel
response = self.client.get(url)
post_data = response.context["form"].initial

post_data["token"] = "12345"
post_data["country"] = "US"
post_data["number"] = "(206) 555-1212"

response = self.client.post(url, post_data)

channel = Channel.objects.filter(is_active=True).first()

with patch("requests.post") as mock_post:
mock_post.side_effect = [
MockResponse(200, '{"id": "message_123"}'),
MockResponse(200, '{"id": "status_123"}'),
]
ZenviaSMSType().activate(channel)

self.assertEqual("12345", mock_post.call_args_list[0][1]["headers"]["X-API-TOKEN"])

self.assertEqual("message_123", channel.config.get(ZENVIA_MESSAGE_SUBSCRIPTION_ID))
self.assertEqual("status_123", channel.config.get(ZENVIA_STATUS_SUBSCRIPTION_ID))

with patch("requests.delete") as mock_delete:
mock_delete.return_value = MockResponse(400, "Error")

# deactivate our channel
with self.settings(IS_PROD=True):
channel.release()

self.assertEqual(2, mock_delete.call_count)
self.assertEqual(
"https://api.zenvia.com/v2/subscriptions/message_123", mock_delete.call_args_list[0][0][0]
)
self.assertEqual("12345", mock_delete.call_args_list[0][1]["headers"]["X-API-TOKEN"])
self.assertEqual("https://api.zenvia.com/v2/subscriptions/status_123", mock_delete.call_args_list[1][0][0])
self.assertEqual("12345", mock_delete.call_args_list[1][1]["headers"]["X-API-TOKEN"])
104 changes: 104 additions & 0 deletions temba/channels/types/zenvia_sms/type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import requests

from django.forms import ValidationError
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _

from temba.channels.types.zenvia_whatsapp.views import ClaimView
from temba.contacts.models import URN

from ...models import Channel, ChannelType

ZENVIA_MESSAGE_SUBSCRIPTION_ID = "zenvia_message_subscription_id"
ZENVIA_STATUS_SUBSCRIPTION_ID = "zenvia_status_subscription_id"


class ZenviaSMSType(ChannelType):
"""
An Zenvia SMS channel
"""

code = "ZVS"
category = ChannelType.Category.PHONE

courier_url = r"^zvs/(?P<uuid>[a-z0-9\-]+)/(?P<action>receive|status)$"

name = "Zenvia SMS"

claim_blurb = _("If you have a %(link)s number, you can connect it to communicate with your contacts.") % {
"link": '<a href="https://www.zenvia.com/">Zenvia SMS</a>'
}

claim_view = ClaimView

schemes = [URN.TEL_SCHEME]
max_length = 1600

def update_webhook(self, channel, url, event_type):
headers = {
"X-API-TOKEN": channel.config[Channel.CONFIG_API_KEY],
"Content-Type": "application/json",
}

conf_url = "https://api.zenvia.com/v2/subscriptions"

# set our webhook
payload = {
"eventType": event_type,
"webhook": {"url": url, "headers": {}},
"status": "ACTIVE",
"version": "v2",
"criteria": {"channel": "sms"},
}
if event_type == "MESSAGE":
payload["criteria"]["direction"] = "IN"

resp = requests.post(conf_url, json=payload, headers=headers)

if resp.status_code != 200:
raise ValidationError(
_("Unable to register webhook subscriptions: %(resp)s"), params={"resp": resp.content}
)

return resp.json()["id"]

def deactivate(self, channel):
headers = {
"X-API-TOKEN": channel.config[Channel.CONFIG_API_KEY],
"Content-Type": "application/json",
}

subscriptionIds = [
channel.config.get(ZENVIA_MESSAGE_SUBSCRIPTION_ID),
channel.config.get(ZENVIA_STATUS_SUBSCRIPTION_ID),
]

errored = False

for subscriptionId in subscriptionIds:
if not subscriptionId:
continue

conf_url = f"https://api.zenvia.com/v2/subscriptions/{subscriptionId}"
resp = requests.delete(conf_url, headers=headers)

if resp.status_code != 204:
errored = True

if errored:
raise ValidationError(_("Unable to remove webhook subscriptions: %(resp)s"), params={"resp": resp.content})

def activate(self, channel):
domain = channel.org.get_brand_domain()

receive_url = "https://" + domain + reverse("courier.zvs", args=[channel.uuid, "receive"])
messageSubscriptionId = self.update_webhook(channel, receive_url, "MESSAGE")

channel.config[ZENVIA_MESSAGE_SUBSCRIPTION_ID] = messageSubscriptionId

status_url = "https://" + domain + reverse("courier.zvs", args=[channel.uuid, "status"])
statusSubscriptionId = self.update_webhook(channel, status_url, "MESSAGE_STATUS")

channel.config[ZENVIA_STATUS_SUBSCRIPTION_ID] = statusSubscriptionId

channel.save()
1 change: 1 addition & 0 deletions temba/channels/types/zenvia_whatsapp/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def test_claim(self):
self.assertEqual("+12065551212", channel.address)
self.assertEqual("12345", channel.config["api_key"])
self.assertEqual("ZVW", channel.channel_type)
self.assertEqual("Zenvia WhatsApp: +12065551212", channel.name)

with patch("requests.post") as mock_patch:
mock_patch.side_effect = [MockResponse(400, '{ "error": true }')]
Expand Down
Loading

0 comments on commit 225660b

Please sign in to comment.