diff --git a/apprise/plugins/notifiarr.py b/apprise/plugins/notifiarr.py index cc13e1a56..b455e58a7 100644 --- a/apprise/plugins/notifiarr.py +++ b/apprise/plugins/notifiarr.py @@ -37,6 +37,7 @@ from ..common import NotifyImageSize from ..utils import parse_list, parse_bool from ..utils import validate_regex +from .discord import USER_ROLE_DETECTION_RE # Used to break path apart into list of channels CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+') @@ -118,14 +119,6 @@ class NotifyNotifiarr(NotifyBase): 'apikey': { 'alias_of': 'apikey', }, - 'discord_user': { - 'name': _('Ping Discord User'), - 'type': 'int', - }, - 'discord_role': { - 'name': _('Ping Discord Role'), - 'type': 'int', - }, 'event': { 'name': _('Discord Event ID'), 'type': 'int', @@ -149,7 +142,6 @@ class NotifyNotifiarr(NotifyBase): }) def __init__(self, apikey=None, include_image=None, - discord_user=None, discord_role=None, event=None, targets=None, source=None, **kwargs): """ Initialize Notifiarr Object @@ -172,30 +164,6 @@ def __init__(self, apikey=None, include_image=None, if isinstance(include_image, bool) \ else self.template_args['image']['default'] - # Set up our user if specified - self.discord_user = 0 - if discord_user: - try: - self.discord_user = int(discord_user) - - except (ValueError, TypeError): - msg = 'An invalid Notifiarr User ID ' \ - '({}) was specified.'.format(discord_user) - self.logger.warning(msg) - raise TypeError(msg) - - # Set up our role if specified - self.discord_role = 0 - if discord_role: - try: - self.discord_role = int(discord_role) - - except (ValueError, TypeError): - msg = 'An invalid Notifiarr Role ID ' \ - '({}) was specified.'.format(discord_role) - self.logger.warning(msg) - raise TypeError(msg) - # Prepare our source (if set) self.source = validate_regex(source) @@ -244,12 +212,6 @@ def url(self, privacy=False, *args, **kwargs): if self.source: params['source'] = self.source - if self.discord_user: - params['discord_user'] = self.discord_user - - if self.discord_role: - params['discord_role'] = self.discord_role - if self.event: params['event'] = self.event @@ -287,6 +249,29 @@ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): # Acquire image_url image_url = self.image_url(notify_type) + # Define our mentions + mentions = { + 'pingUser': [], + 'pingRole': [], + 'content': [], + } + + # parse for user id's <@123> and role IDs <@&456> + results = USER_ROLE_DETECTION_RE.findall(body) + if results: + for (is_role, no, value) in results: + if value: + # @everybody, @admin, etc - unsupported + mentions['content'].append(f'@{value}') + + elif is_role: + mentions['pingRole'].append(no) + mentions['content'].append(f'<@&{no}>') + + else: # is_user + mentions['pingUser'].append(no) + mentions['content'].append(f'<@{no}>') + for idx, channel in enumerate(self.targets['channels']): # prepare Notifiarr Object payload = { @@ -301,14 +286,17 @@ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): 'discord': { 'color': self.color(notify_type), 'ping': { - 'pingUser': self.discord_user - if not idx and self.discord_user else 0, - 'pingRole': self.discord_role - if not idx and self.discord_role else 0, + # Only 1 user is supported, so truncate the rest + 'pingUser': 0 if not mentions['pingUser'] + else mentions['pingUser'][0], + # Only 1 role is supported, so truncate the rest + 'pingRole': 0 if not mentions['pingRole'] + else mentions['pingRole'][0], }, 'text': { 'title': title, - 'content': '', + 'content': '' if not mentions['content'] + else '👉 ' + ' '.join(mentions['content']), 'description': body, 'footer': self.app_desc, }, @@ -410,17 +398,6 @@ def parse_url(url): # Get channels results['targets'] = NotifyNotifiarr.split_path(results['fullpath']) - if 'discord_user' in results['qsd'] and \ - len(results['qsd']['discord_user']): - results['discord_user'] = \ - NotifyNotifiarr.unquote( - results['qsd']['discord_user']) - - if 'discord_role' in results['qsd'] and \ - len(results['qsd']['discord_role']): - results['discord_role'] = \ - NotifyNotifiarr.unquote(results['qsd']['discord_role']) - if 'event' in results['qsd'] and \ len(results['qsd']['event']): results['event'] = \ diff --git a/test/test_plugin_notifiarr.py b/test/test_plugin_notifiarr.py index bac556dbb..cec36e105 100644 --- a/test/test_plugin_notifiarr.py +++ b/test/test_plugin_notifiarr.py @@ -27,9 +27,11 @@ # POSSIBILITY OF SUCH DAMAGE. import requests - +from unittest import mock from apprise.plugins.notifiarr import NotifyNotifiarr from helpers import AppriseURLTester +from json import loads +from inspect import cleandoc # Disable logging for a cleaner testing output import logging @@ -52,12 +54,6 @@ # Our expected url(privacy=True) startswith() response: 'privacy_url': 'notifiarr://a...y', }), - ('notifiarr://apikey/1234/?discord_user=invalid', { - 'instance': TypeError, - }), - ('notifiarr://apikey/1234/?discord_role=invalid', { - 'instance': TypeError, - }), ('notifiarr://apikey/1234/?event=invalid', { 'instance': TypeError, }), @@ -131,11 +127,6 @@ # Our expected url(privacy=True) startswith() response: 'privacy_url': 'notifiarr://m...y/#123/#325', }), - ('notifiarr://12/?key=myapikey&discord_user=23' - '&discord_role=12&event=123', { - 'instance': NotifyNotifiarr, - # Our expected url(privacy=True) startswith() response: - 'privacy_url': 'notifiarr://m...y/#12'}), ('notifiarr://apikey/123/', { 'instance': NotifyNotifiarr, }), @@ -160,7 +151,7 @@ ) -def test_plugin_custom_notifiarr_urls(): +def test_plugin_notifiarr_urls(): """ NotifyNotifiarr() Apprise URLs @@ -168,3 +159,64 @@ def test_plugin_custom_notifiarr_urls(): # Run our general tests AppriseURLTester(tests=apprise_url_tests).run_all() + + +@mock.patch('requests.post') +def test_plugin_notifiarr_notifications(mock_post): + """ + NotifyNotifiarr() Notifications/Ping Support + + """ + + # Test our header parsing when not lead with a header + body = cleandoc(""" + # Heading + @everyone and @admin, wake and meet our new user <@123> and <@987>; + Attention Roles: <@&456> and <@&765> + """) + + # Prepare a good response + response = mock.Mock() + response.status_code = requests.codes.ok + + # Prepare Mock return object + mock_post.return_value = response + + results = NotifyNotifiarr.parse_url('notifiarr://apikey/12345') + + instance = NotifyNotifiarr(**results) + assert isinstance(instance, NotifyNotifiarr) + + response = instance.send(body=body) + assert response is True + assert mock_post.call_count == 1 + + details = mock_post.call_args_list[0] + assert details[0][0] == \ + 'https://notifiarr.com/api/v1/notification/apprise' + + payload = loads(details[1]['data']) + + # First role and first user stored + assert payload == { + 'source': 'Apprise', + 'type': 'info', + 'notification': {'update': False, 'name': 'Apprise', 'event': ''}, + 'discord': { + 'color': '#3AA3E3', 'ping': { + # Only supports 1 entry each; so first one is parsed + 'pingUser': '123', + 'pingRole': '456', + }, + 'text': { + 'title': '', + 'content': '👉 @everyone @admin <@123> <@987> <@&456> <@&765>', + 'description': + '# Heading\n@everyone and @admin, wake and meet our new ' + 'user <@123> and <@987>;\nAttention Roles: <@&456> and ' + '<@&765>\n ', + 'footer': 'Apprise Notifications', + }, + 'ids': {'channel': 12345}, + }, + }