From 2a3368108e9c763d094f7c1cdb72707e33563784 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Fri, 1 Jul 2022 14:37:47 +0200 Subject: [PATCH] Add support for setting the apns-push-type header via device data (#309) Signed-off-by: N-Pex Co-authored-by: Brendan Abolivier --- README.md | 3 ++- changelog.d/309.feature | 1 + sygnal/apnspushkin.py | 22 ++++++++++++++- tests/test_apns.py | 59 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 changelog.d/309.feature diff --git a/README.md b/README.md index bd6b4fcf..7bcd196e 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,8 @@ For either type, it can accept: - the `platform` parameter which determines whether the production or sandbox APNS environment is used. Valid values are 'production' or 'sandbox'. If not provided, 'production' is used. - +- the `push_type` parameter which determines what value for the `apns-push-type` header is sent to + APNs. If not provided, the header is not sent. ### gcm diff --git a/changelog.d/309.feature b/changelog.d/309.feature new file mode 100644 index 00000000..97cec9cc --- /dev/null +++ b/changelog.d/309.feature @@ -0,0 +1 @@ +Add a new `push_type` configuration option for APNs apps, to control the value of the `apns-push-type` header when sending requests. \ No newline at end of file diff --git a/sygnal/apnspushkin.py b/sygnal/apnspushkin.py index 8b36a8fd..e47374d7 100644 --- a/sygnal/apnspushkin.py +++ b/sygnal/apnspushkin.py @@ -25,7 +25,7 @@ import aioapns from aioapns import APNs, NotificationRequest -from aioapns.common import NotificationResult +from aioapns.common import NotificationResult, PushType from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_pem_x509_certificate from opentracing import Span, logs, tags @@ -109,8 +109,18 @@ class ApnsPushkin(ConcurrencyLimitedPushkin): "key_id", "keyfile", "topic", + "push_type", } | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS + APNS_PUSH_TYPES = { + "alert": PushType.ALERT, + "background": PushType.BACKGROUND, + "voip": PushType.VOIP, + "complication": PushType.COMPLICATION, + "fileprovider": PushType.FILEPROVIDER, + "mdm": PushType.MDM, + } + def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None: super().__init__(name, sygnal, config) @@ -188,6 +198,15 @@ def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]) -> None: loop=loop, ) + push_type = self.get_config("push_type", str) + if not push_type: + self.push_type = None + else: + if push_type not in self.APNS_PUSH_TYPES.keys(): + raise PushkinSetupException(f"Invalid value for push_type: {push_type}") + + self.push_type = self.APNS_PUSH_TYPES[push_type] + # without this, aioapns will retry every second forever. self.apns_client.pool.max_connection_attempts = 3 @@ -230,6 +249,7 @@ async def _dispatch_request( message=shaved_payload, priority=prio, notification_id=notif_id, + push_type=self.push_type, ) try: diff --git a/tests/test_apns.py b/tests/test_apns.py index 32ccd28e..b7142f36 100644 --- a/tests/test_apns.py +++ b/tests/test_apns.py @@ -14,7 +14,7 @@ # limitations under the License. from unittest.mock import MagicMock, patch -from aioapns.common import NotificationResult +from aioapns.common import NotificationResult, PushType from sygnal import apnstruncate from sygnal.apnspushkin import ApnsPushkin @@ -22,6 +22,7 @@ from tests import testutils PUSHKIN_ID = "com.example.apns" +PUSHKIN_ID_WITH_PUSH_TYPE = "com.example.apns.push_type" TEST_CERTFILE_PATH = "/path/to/my/certfile.pem" @@ -47,6 +48,12 @@ "data": {"default_payload": None}, } +DEVICE_EXAMPLE_FOR_PUSH_TYPE_PUSHKIN = { + "app_id": "com.example.apns.push_type", + "pushkey": "spqr", + "pushkey_ts": 42, +} + class ApnsTestCase(testutils.TestCase): def setUp(self): @@ -64,9 +71,11 @@ def setUp(self): self.apns_pushkin_snotif = MagicMock() test_pushkin = self.get_test_pushkin(PUSHKIN_ID) + test_pushkin_push_type = self.get_test_pushkin(PUSHKIN_ID_WITH_PUSH_TYPE) # type safety: using ignore here due to mypy not handling monkeypatching, # see https://github.com/python/mypy/issues/2427 test_pushkin._send_notification = self.apns_pushkin_snotif # type: ignore[assignment] # noqa: E501 + test_pushkin_push_type._send_notification = self.apns_pushkin_snotif # type: ignore[assignment] # noqa: E501 def get_test_pushkin(self, name: str) -> ApnsPushkin: test_pushkin = self.sygnal.pushkins[name] @@ -76,6 +85,11 @@ def get_test_pushkin(self, name: str) -> ApnsPushkin: def config_setup(self, config): super().config_setup(config) config["apps"][PUSHKIN_ID] = {"type": "apns", "certfile": TEST_CERTFILE_PATH} + config["apps"][PUSHKIN_ID_WITH_PUSH_TYPE] = { + "type": "apns", + "certfile": TEST_CERTFILE_PATH, + "push_type": "alert", + } def test_payload_truncation(self): """ @@ -333,3 +347,46 @@ def test_retry_on_5xx(self): # Assert self.assertGreater(method.call_count, 1) self.assertEqual(502, resp) + + def test_expected_with_push_type(self): + """ + Tests the expected case: a good response from APNS means we pass on + a good response to the homeserver. + """ + # Arrange + method = self.apns_pushkin_snotif + method.side_effect = testutils.make_async_magic_mock( + NotificationResult("notID", "200") + ) + + # Act + resp = self._request( + self._make_dummy_notification([DEVICE_EXAMPLE_FOR_PUSH_TYPE_PUSHKIN]) + ) + + # Assert + self.assertEqual(1, method.call_count) + ((notification_req,), _kwargs) = method.call_args + + self.assertEqual( + { + "room_id": "!slw48wfj34rtnrf:example.com", + "event_id": "$qTOWWTEL48yPm3uT-gdNhFcoHxfKbZuqRVnnWWSkGBs", + "aps": { + "alert": { + "loc-key": "MSG_FROM_USER_IN_ROOM_WITH_CONTENT", + "loc-args": [ + "Major Tom", + "Mission Control", + "I'm floating in a most peculiar way.", + ], + }, + "badge": 3, + }, + }, + notification_req.message, + ) + + self.assertEqual(PushType.ALERT, notification_req.push_type) + + self.assertEqual({"rejected": []}, resp)