Repository: caronc/apprise (language: python)

Issue:
Onesignal notifications do not work with a template, but without a body (+custom_data support)
:mega: **Notification Service(s) Impacted**
Onesignal

:lady_beetle: **Describe the bug**
Apprise does not send Onesignal notifications without a body and does not support custom arguments.
Some services, like Onesignal, support sending notifications with templates. When a template ID is provided - it is not required (and even not recommended) to pass message body. The message is taken from the template body message, and is then templated using custom data provided in the request (see [Onesignal documentation](https://documentation.onesignal.com/reference/create-notification), search for "custom_data"). Currently, Apprise refuses for work in this framework (it both does not accept None as body and does not accept custom_data in Onesignal requests). It also should not override title with "Apprise Notifications".

:bulb: **Screenshots and Logs**
```python
import apprise

apobj = apprise.Apprise()
apobj.add("onesignal://template_id:app_id@api_key/player_id")
res = apobj.notify(body=None)
print(res)
```
The code above returns False indicating an internal error, and notifications are sent. It should return True, and result in a successful notification with template's message body and title. Also, it should accept a custom_data object.

:computer: **Your System Details:**
 - OS: MacOs 14.5 (23F79)
 - Python Version: Python 3.9.12
 - Apprise version: 1.8.0



Discussion / hints:
this is a bit of a one off; i can update the title to be blank if nothing is provided if the upstream server will accept this.  If i recall testing this in the past, the fields had to be populated (but i could be wrong).

Apprise has only 1 global requirement: either a body is provided at minimum, or an attachment is provided (to which case a body is not needed).  But i do not see value in a use-case to not support a body, title, and attachment all missing.  Even in the case for OneSignal.

I don't mind adding support for custom data, but all fields would be 'text' `{'key': 'value'}` and they would need to be provided into the Apprise URL  (e.g. `onesignal://credentials/:key=value&:key2=custom_value2`)

would that work?
> this is a bit of a one off; i can update the title to be blank if nothing is provided if the upstream server will accept this. If i recall testing this in the past, the fields had to be populated (but i could be wrong).
> 
> Apprise has only 1 global requirement: either a body is provided at minimum, or an attachment is provided (to which case a body is not needed). But i do not see value in a use-case to not support a body, title, and attachment all missing. Even in the case for OneSignal.
> 
> I don't mind adding support for custom data, but all fields would be 'text' `{'key': 'value'}` and they would need to be provided into the Apprise URL (e.g. `onesignal://credentials/:key=value&:key2=custom_value2`)
> 
> would that work?

Hey @caronc 
So body, title or attachment are actually not required either when using a template, because these are defined inside the template, and the only thing left is to pass custom arguments inside the notification request. Then the body defined in the template is Jinja-templated by Onesignal on their server-side. This is an immensely powerful feature that removes the need to store the body of the notification on the sending side, which we are currently plan on utilizing (and are utilizing for Sendgrid).
That said, I have already started my work on adding support for all of this here, and I hope I will submit a PR for this change in the coming days. Does that work for you?
I'll entertain your PR, my concern is just altering the default logic of Apprise to accept a use without any input. This may get your template feature to work, but it could also break the other 106 plugins (that can't handle nothing passed).

My recommendation would be to support the template through url arguments and if a template is provided, just ignore the body provided (but still expect one). `ifttt://` has some logic like this i believe too.

I don't see an issue where the body is just even set to `ignored` in your case. 

Either way, if you have something in mind, i can still at least entertain it 👍

Fix patch:
diff --git a/apprise/plugins/one_signal.py b/apprise/plugins/one_signal.py
index cedf3ba49..aed8485f6 100644
--- a/apprise/plugins/one_signal.py
+++ b/apprise/plugins/one_signal.py
@@ -82,7 +82,7 @@ class NotifyOneSignal(NotifyBase):
     setup_url = 'https://github.com/caronc/apprise/wiki/Notify_onesignal'
 
     # Notification
-    notify_url = "https://onesignal.com/api/v1/notifications"
+    notify_url = "https://api.onesignal.com/notifications"
 
     # Allows the user to specify the NotifyImageSize object
     image_size = NotifyImageSize.XY_72
@@ -187,11 +187,15 @@ class NotifyOneSignal(NotifyBase):
             'name': _('Custom Data'),
             'prefix': ':',
         },
+        'postback': {
+            'name': _('Postback Data'),
+            'prefix': '+',
+        },
     }
 
     def __init__(self, app, apikey, targets=None, include_image=True,
                  template=None, subtitle=None, language=None, batch=None,
-                 use_contents=None, custom=None, **kwargs):
+                 use_contents=None, custom=None, postback=None, **kwargs):
         """
         Initialize OneSignal
 
@@ -298,12 +302,21 @@ def __init__(self, app, apikey, targets=None, include_image=True,
         self.custom_data = {}
         if isinstance(custom, dict):
             self.custom_data.update(custom)
-
         elif custom:
             msg = 'The specified OneSignal Custom Data ' \
                   '({}) are not identified as a dictionary.'.format(custom)
             self.logger.warning(msg)
             raise TypeError(msg)
+
+        # Postback Data
+        self.postback_data = {}
+        if isinstance(postback, dict):
+            self.postback_data.update(postback)
+        elif postback:
+            msg = 'The specified OneSignal Postback Data ' \
+                  '({}) are not identified as a dictionary.'.format(postback)
+            self.logger.warning(msg)
+            raise TypeError(msg)
         return
 
     def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@@ -344,7 +357,13 @@ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
         # Set our data if defined
         if self.custom_data:
             payload.update({
-                'data': self.custom_data,
+                'custom_data': self.custom_data,
+            })
+
+        # Set our postback data if defined
+        if self.postback_data:
+            payload.update({
+                'data': self.postback_data,
             })
 
         if title:
@@ -454,6 +473,10 @@ def url(self, privacy=False, *args, **kwargs):
         params.update(
             {':{}'.format(k): v for k, v in self.custom_data.items()})
 
+        # Save our postback data
+        params.update(
+            {'+{}'.format(k): v for k, v in self.postback_data.items()})
+
         if self.use_contents != self.template_args['contents']['default']:
             params['contents'] = 'yes' if self.use_contents else 'no'
 
@@ -577,4 +600,7 @@ def parse_url(url):
         # Store our custom data
         results['custom'] = results['qsd:']
 
+        # Store our postback data
+        results['postback'] = results['qsd+']
+
         return results


Test patch:
diff --git a/test/test_plugin_onesignal.py b/test/test_plugin_onesignal.py
index cd07f11c5..79d57402d 100644
--- a/test/test_plugin_onesignal.py
+++ b/test/test_plugin_onesignal.py
@@ -110,7 +110,12 @@
         'instance': NotifyOneSignal,
     }),
     ('onesignal://?apikey=abc&template=tp&app=123&to=playerid&body=no'
-     '&:key1=val1&key2=val2', {
+     '&:key1=val1&:key2=val2', {
+         # Test Kwargs
+         'instance': NotifyOneSignal,
+     }),
+    ('onesignal://?apikey=abc&template=tp&app=123&to=playerid&body=no'
+     '&+key1=val1&+key2=val2', {
          # Test Kwargs
          'instance': NotifyOneSignal,
      }),
@@ -259,6 +264,11 @@ def test_plugin_onesignal_edge_cases():
         NotifyOneSignal(
             app='appid', apikey='key', targets=['@user'], custom='not-a-dict')
 
+    # postback must be a dictionary
+    with pytest.raises(TypeError):
+        NotifyOneSignal(
+            app='appid', apikey='key', targets=['@user'], postback='not-a-dict')
+
 
 @mock.patch('requests.post')
 def test_plugin_onesignal_notifications(mock_post):
@@ -272,7 +282,7 @@ def test_plugin_onesignal_notifications(mock_post):
 
     # Load URL with Template
     instance = Apprise.instantiate(
-        'onesignal://templateid:appid@apikey/@user/?:key1=value1')
+        'onesignal://templateid:appid@apikey/@user/?:key1=value1&+key3=value3')
 
     # Validate that it loaded okay
     assert isinstance(instance, NotifyOneSignal)
@@ -281,7 +291,7 @@ def test_plugin_onesignal_notifications(mock_post):
     assert response is True
     assert mock_post.call_count == 1
     assert mock_post.call_args_list[0][0][0] == \
-        'https://onesignal.com/api/v1/notifications'
+        'https://api.onesignal.com/notifications'
 
     details = mock_post.call_args_list[0]
     payload = loads(details[1]['data'])
@@ -291,7 +301,8 @@ def test_plugin_onesignal_notifications(mock_post):
         'contents': {'en': 'hello world'},
         'content_available': True,
         'template_id': 'templateid',
-        'data': {'key1': 'value1'},
+        'custom_data': {'key1': 'value1'},
+        'data': {'key3': 'value3'},
         'large_icon': 'https://github.com/caronc/apprise'
         '/raw/master/apprise/assets/themes/default/apprise-info-72x72.png',
         'small_icon': 'https://github.com/caronc/apprise'
@@ -311,7 +322,7 @@ def test_plugin_onesignal_notifications(mock_post):
     assert response is True
     assert mock_post.call_count == 1
     assert mock_post.call_args_list[0][0][0] == \
-        'https://onesignal.com/api/v1/notifications'
+        'https://api.onesignal.com/notifications'
 
     details = mock_post.call_args_list[0]
     payload = loads(details[1]['data'])
@@ -334,7 +345,7 @@ def test_plugin_onesignal_notifications(mock_post):
     assert response is True
     assert mock_post.call_count == 1
     assert mock_post.call_args_list[0][0][0] == \
-        'https://onesignal.com/api/v1/notifications'
+        'https://api.onesignal.com/notifications'
 
     details = mock_post.call_args_list[0]
     payload = loads(details[1]['data'])