diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d06549..fe3bb64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes in **django-sms** are documented below. ## [Unreleased] +### Added +- The **sms.backends.messagebird.SmsBackend** to send text messages using [MessageBird](https://messagebird.com/) ([#6](https://github.com/roaldnefs/django-sms/issues/6)). + ### Changed - Simplified the attributes of the **sms.signals.post_send** signal to include the instance of the originating **Message** instead of all attributes ([#11](https://github.com/roaldnefs/django-sms/pull/11)). diff --git a/README.md b/README.md index b1aa218..ae19b58 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [File backend](#file-backend) - [In-memory backend](#in-memory-backend) - [Dummy backend](#dummy-backend) + - [MessageBird backend](#messagebird-backend) - [Defining a custom SMS backend](#defining-a-custom-sms-backend) - [Signals](#signals) - [sms.signals.post_send](#sms.signals.post_send) @@ -183,6 +184,20 @@ SMS_BACKEND = 'sms.backends.dummy.SmsBackend' This backend is not intended for use in production - it is provided as a convenience that can be used during development. +#### MessageBird backend +The [MessageBird](https://messagebird.com/) backend sends text messages using the [MessageBird SMS API](https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms). To specify this backend, put the following in your settings: + +```python +SMS_BACKEND = 'sms.backends.messagebird.SmsBackend' +MESSAGEBIRD_ACCESS_KEY = 'live_redacted-messagebird-access-key' +``` + +Make sure the MessageBird Python SDK is installed by running the following command: + +```console +pip install "django-sms[messagebird]" +``` + ### Defining a custom SMS backend If you need to change how text messages are sent you can write your own SMS backend. The **SMS_BACKEND** setting in your settings file is then the Python import path for you backend class. diff --git a/setup.py b/setup.py index c874330..4b0c6db 100644 --- a/setup.py +++ b/setup.py @@ -51,5 +51,8 @@ def long_description() -> str: packages=find_packages(exclude=['tests', 'tests.*']), include_package_data=True, test_suite='tests.runtests.main', - install_requires=['Django>=2.2'] + install_requires=['Django>=2.2'], + extras_require={ + 'messagebird': ['messagebird'], + } ) diff --git a/sms/backends/base.py b/sms/backends/base.py index b721328..1b5fd6e 100644 --- a/sms/backends/base.py +++ b/sms/backends/base.py @@ -18,11 +18,7 @@ class BaseSmsBackend: # do something with connection pass """ - def __init__( - self, - fail_silently: bool = False, - **kwargs - ) -> None: + def __init__(self, fail_silently: bool = False, **kwargs) -> None: self.fail_silently = fail_silently def open(self) -> bool: diff --git a/sms/backends/messagebird.py b/sms/backends/messagebird.py new file mode 100644 index 0000000..2e72888 --- /dev/null +++ b/sms/backends/messagebird.py @@ -0,0 +1,59 @@ +""" +SMS backend for sending text messages using MessageBird. +""" +from typing import List, Optional + +from django.conf import settings # type: ignore +from django.core.exceptions import ImproperlyConfigured # type: ignore + +from sms.backends.base import BaseSmsBackend +from sms.message import Message + +try: + import messagebird # type: ignore + HAS_MESSAGEBIRD = True +except ImportError: + HAS_MESSAGEBIRD = False + + +class SmsBackend(BaseSmsBackend): + def __init__(self, fail_silently: bool = False, **kwargs) -> None: + super().__init__(fail_silently=fail_silently, **kwargs) + + if not HAS_MESSAGEBIRD and not self.fail_silently: + raise ImproperlyConfigured( + "You're using the SMS backend " + "'sms.backends.messagebird.SmsBackend' without having " + "'messagebird' installed. Install 'messagebird' or use " + "another SMS backend." + ) + + access_key: Optional[str] = getattr(settings, 'MESSAGEBIRD_ACCESS_KEY') + if not access_key and not self.fail_silently: + raise ImproperlyConfigured( + "You're using the SMS backend " + "'sms.backends.messagebird.SmsBackend' without having the " + "setting 'MESSAGEBIRD_ACCESS_KEY' set." + ) + + self.client = None + if HAS_MESSAGEBIRD: + self.client = messagebird.Client(access_key) + + def send_messages(self, messages: List[Message]) -> int: + if not self.client: + return 0 + + msg_count: int = 0 + for message in messages: + try: + self.client.message_create( + message.originator, + message.recipients, + message.body + ) + except Exception as exc: + if not self.fail_silently: + raise exc + msg_count += 1 + return msg_count diff --git a/tests/tests.py b/tests/tests.py index a1d27ae..c15a967 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -6,6 +6,8 @@ from typing import List, Type, Optional from io import StringIO +from unittest.mock import MagicMock + from django.dispatch import receiver # type: ignore from django.test import SimpleTestCase, override_settings # type: ignore @@ -179,6 +181,38 @@ def test_file_sessions(self) -> None: self.assertEqual(message.recipients, ['+441134960000']) +class MessageBirdBackendTests(BaseSmsBackendTests, SimpleTestCase): + sms_backend = 'sms.backends.messagebird.SmsBackend' + + def setUp(self) -> None: + super().setUp() + self._settings_override = override_settings( + MESSAGEBIRD_ACCESS_KEY='fake_access_key' + ) + self._settings_override.enable() + + def tearDown(self) -> None: + self._settings_override.disable() + super().tearDown() + + def test_send_messages(self) -> None: + """Test send_messages with the MessageBird backend.""" + message = Message( + 'Here is the message', + '+12065550100', + ['+441134960000'] + ) + + connection = sms.get_connection() + connection.client.message_create = MagicMock() # type: ignore + connection.send_messages([message]) # type: ignore + connection.client.message_create.assert_called_with( # type: ignore + '+12065550100', + ['+441134960000'], + 'Here is the message' + ) + + class SignalTests(SimpleTestCase): def flush_mailbox(self) -> None: diff --git a/tox.ini b/tox.ini index 33761a6..2592d17 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ commands = setenv= PYTHONWARNINGS=default deps= + messagebird coverage mypy django22: Django==2.2.*