From 9620901afce56f720e856aca600951c9b61a9460 Mon Sep 17 00:00:00 2001 From: phantom943 <105282577+phantom943@users.noreply.github.com> Date: Tue, 23 Jul 2024 00:26:28 +0200 Subject: [PATCH] #1170 allow json data in OneSignal template arguments (#1171) Co-authored-by: phantom --- apprise/plugins/one_signal.py | 36 ++++++++++++++++++++++-- apprise/utils.py | 36 +++++++++++++++++++++++- test/test_plugin_onesignal.py | 53 +++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 3 deletions(-) diff --git a/apprise/plugins/one_signal.py b/apprise/plugins/one_signal.py index 9a5207e2..c8340cd2 100644 --- a/apprise/plugins/one_signal.py +++ b/apprise/plugins/one_signal.py @@ -40,6 +40,8 @@ from itertools import chain from .base import NotifyBase from ..common import NotifyType from ..common import NotifyImageSize +from ..utils import decode_b64_dict +from ..utils import encode_b64_dict from ..utils import validate_regex from ..utils import parse_list from ..utils import parse_bool @@ -167,6 +169,12 @@ class NotifyOneSignal(NotifyBase): 'default': True, 'map_to': 'use_contents', }, + 'decode': { + 'name': _('Decode Template Args'), + 'type': 'bool', + 'default': False, + 'map_to': 'decode_tpl_args', + }, 'template': { 'alias_of': 'template', }, @@ -195,7 +203,8 @@ class NotifyOneSignal(NotifyBase): def __init__(self, app, apikey, targets=None, include_image=True, template=None, subtitle=None, language=None, batch=None, - use_contents=None, custom=None, postback=None, **kwargs): + use_contents=None, decode_tpl_args=None, + custom=None, postback=None, **kwargs): """ Initialize OneSignal @@ -228,6 +237,11 @@ class NotifyOneSignal(NotifyBase): use_contents if use_contents is not None else self.template_args['contents']['default']) else False + # Prepare Decode Template Arguments Flag + self.decode_tpl_args = True if ( + decode_tpl_args if decode_tpl_args is not None else + self.template_args['decode']['default']) else False + # Place a thumbnail image inline with the message body self.include_image = include_image @@ -301,6 +315,9 @@ class NotifyOneSignal(NotifyBase): # Custom Data self.custom_data = {} if custom and isinstance(custom, dict): + if self.decode_tpl_args: + custom = decode_b64_dict(custom) + self.custom_data.update(custom) elif custom: @@ -471,9 +488,12 @@ class NotifyOneSignal(NotifyBase): # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + custom_data, needs_decoding = encode_b64_dict(self.custom_data) + # custom_data, needs_decoding = self.custom_data, False # Save our template data params.update( - {':{}'.format(k): v for k, v in self.custom_data.items()}) + {':{}'.format(k): v for k, v in custom_data.items()} + ) # Save our postback data params.update( @@ -482,6 +502,11 @@ class NotifyOneSignal(NotifyBase): if self.use_contents != self.template_args['contents']['default']: params['contents'] = 'yes' if self.use_contents else 'no' + if (self.decode_tpl_args != self.template_args['decode']['default'] + or needs_decoding): + params['decode'] = 'yes' if (self.decode_tpl_args or + needs_decoding) else 'no' + return '{schema}://{tp_id}{app}@{apikey}/{targets}?{params}'.format( schema=self.secure_protocol, tp_id='{}:'.format( @@ -568,6 +593,13 @@ class NotifyOneSignal(NotifyBase): 'contents', NotifyOneSignal.template_args['contents']['default'])) + # Get Use Contents Boolean (if set) + results['decode_tpl_args'] = \ + parse_bool( + results['qsd'].get( + 'decode', + NotifyOneSignal.template_args['decode']['default'])) + # The API Key is stored in the hostname results['apikey'] = NotifyOneSignal.unquote(results['host']) diff --git a/apprise/utils.py b/apprise/utils.py index 5cd8256f..2aff569e 100644 --- a/apprise/utils.py +++ b/apprise/utils.py @@ -25,13 +25,15 @@ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. - +import copy import re import sys import json import contextlib import os import locale +import typing +import base64 from itertools import chain from os.path import expanduser from functools import reduce @@ -1600,3 +1602,35 @@ def dict_full_update(dict1, dict2): _merge(dict1, dict2) return + + +def decode_b64_dict(di: dict) -> dict: + di = copy.deepcopy(di) + for k, v in di.items(): + if not isinstance(v, str) or not v.startswith("b64:"): + continue + try: + parsed_v = base64.b64decode(v[4:]) + parsed_v = json.loads(parsed_v) + except Exception: + parsed_v = v + di[k] = parsed_v + return di + + +def encode_b64_dict( + di: dict +) -> typing.Tuple[dict, bool]: + di = copy.deepcopy(di) + needs_decoding = False + for k, v in di.items(): + if isinstance(v, str): + continue + try: + encoded = base64.urlsafe_b64encode(json.dumps(v).encode()) + encoded = "b64:{}".format(encoded.decode()) + needs_decoding = True + except Exception: + encoded = str(v) + di[k] = encoded + return di, needs_decoding diff --git a/test/test_plugin_onesignal.py b/test/test_plugin_onesignal.py index e9bfecea..3ba30785 100644 --- a/test/test_plugin_onesignal.py +++ b/test/test_plugin_onesignal.py @@ -361,3 +361,56 @@ def test_plugin_onesignal_notifications(mock_post): 'small_icon': 'https://github.com/caronc/apprise' '/raw/master/apprise/assets/themes/default/apprise-info-32x32.png', 'include_external_user_ids': ['@user']} + + # Test without decoding parameters + instance = Apprise.instantiate( + 'onesignal://templateid:appid@apikey/@user/' + '?:par=b64:eyJhIjoxLCJiIjoyfQ==&decode=no') + assert isinstance(instance, NotifyOneSignal) and \ + instance.custom_data == {"par": "b64:eyJhIjoxLCJiIjoyfQ=="} + + # Now same with loading parameters + instance = Apprise.instantiate( + 'onesignal://templateid:appid@apikey/@user/' + '?:par=b64:eyJhIjoxLCJiIjoyfQ==&decode=yes') + assert isinstance(instance, NotifyOneSignal) and \ + instance.custom_data == {"par": {"a": 1, "b": 2}} + + # Test bad data in general + instance = Apprise.instantiate( + 'onesignal://templateid:appid@apikey/@user/' + '?:par=garbage1&decode=yes') + assert isinstance(instance, NotifyOneSignal) and \ + instance.custom_data == {"par": 'garbage1'} + + instance = Apprise.instantiate( + 'onesignal://templateid:appid@apikey/@user/' + '?:par=b64:garbage2&decode=yes') + assert isinstance(instance, NotifyOneSignal) and \ + instance.custom_data == {"par": 'b64:garbage2'} + + instance = Apprise.instantiate( + 'onesignal://templateid:appid@apikey/@user/' + '?:par=b64:garbage3==&decode=yes') + assert isinstance(instance, NotifyOneSignal) and \ + instance.custom_data == {"par": 'b64:garbage3=='} + + # Now same with not-base64 parameters + instance = Apprise.instantiate( + 'onesignal://templateid:appid@apikey/@user/' + '?:par=eyJhIjoxLCJiIjoyfQ==&:par2=123&decode=yes') + assert isinstance(instance, NotifyOneSignal) and \ + instance.custom_data == { + "par": "eyJhIjoxLCJiIjoyfQ==", "par2": "123" + } + + # Test incorrect base64 parameters. Second one has incorrect padding + url = 'onesignal://templateid:appid@apikey/@user/' \ + '?:par=b64:1234=&:par2=b64:eyJhIjoxLCJiIjoyfQ&' \ + ':par3=b64:eyJhIjoxLCJiIjoyfQ==&decode=yes' + instance = Apprise.instantiate(url) + assert isinstance(instance, NotifyOneSignal) and instance.custom_data == { + "par": "b64:1234=", + "par2": "b64:eyJhIjoxLCJiIjoyfQ", + "par3": {"a": 1, "b": 2} + }