Discord user/role ping support added (#1004)

pull/1012/head
Chris Caron 2023-11-19 11:13:27 -05:00 committed by GitHub
parent c49976237a
commit ccb97bc92e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 167 additions and 2 deletions

View File

@ -60,6 +60,11 @@ from ..AppriseLocale import gettext_lazy as _
from ..attachment.AttachBase import AttachBase from ..attachment.AttachBase import AttachBase
# Used to detect user/role IDs
USER_ROLE_DETECTION_RE = re.compile(
r'\s*(?:<@(?P<role>&?)(?P<id>[0-9]+)>|@(?P<value>[a-z0-9]+))', re.I)
class NotifyDiscord(NotifyBase): class NotifyDiscord(NotifyBase):
""" """
A wrapper to Discord Notifications A wrapper to Discord Notifications
@ -336,6 +341,33 @@ class NotifyDiscord(NotifyBase):
payload['content'] = \ payload['content'] = \
body if not title else "{}\r\n{}".format(title, body) body if not title else "{}\r\n{}".format(title, body)
# parse for user id's <@123> and role IDs <@&456>
results = USER_ROLE_DETECTION_RE.findall(body)
if results:
payload['allow_mentions'] = {
'parse': [],
'users': [],
'roles': [],
}
_content = []
for (is_role, no, value) in results:
if value:
payload['allow_mentions']['parse'].append(value)
_content.append(f'@{value}')
elif is_role:
payload['allow_mentions']['roles'].append(no)
_content.append(f'<@&{no}>')
else: # is_user
payload['allow_mentions']['users'].append(no)
_content.append(f'<@{no}>')
if self.notify_format == NotifyFormat.MARKDOWN:
# Add pingable elements to content field
payload['content'] = '👉 ' + ' '.join(_content)
if not self._send(payload, params=params): if not self._send(payload, params=params):
# We failed to post our message # We failed to post our message
return False return False
@ -360,16 +392,21 @@ class NotifyDiscord(NotifyBase):
'wait': True, 'wait': True,
}) })
#
# Remove our text/title based content for attachment use # Remove our text/title based content for attachment use
#
if 'embeds' in payload: if 'embeds' in payload:
# Markdown
del payload['embeds'] del payload['embeds']
if 'content' in payload: if 'content' in payload:
# Markdown
del payload['content'] del payload['content']
if 'allow_mentions' in payload:
del payload['allow_mentions']
#
# Send our attachments # Send our attachments
#
for attachment in attach: for attachment in attach:
self.logger.info( self.logger.info(
'Posting Discord Attachment {}'.format(attachment.name)) 'Posting Discord Attachment {}'.format(attachment.name))

View File

@ -32,6 +32,7 @@ from datetime import datetime, timedelta
from datetime import timezone from datetime import timezone
import pytest import pytest
import requests import requests
from json import loads
from apprise.plugins.NotifyDiscord import NotifyDiscord from apprise.plugins.NotifyDiscord import NotifyDiscord
from helpers import AppriseURLTester from helpers import AppriseURLTester
@ -184,6 +185,113 @@ def test_plugin_discord_urls():
AppriseURLTester(tests=apprise_url_tests).run_all() AppriseURLTester(tests=apprise_url_tests).run_all()
@mock.patch('requests.post')
def test_plugin_discord_notifications(mock_post):
"""
NotifyDiscord() Notifications/Ping Support
"""
# Initialize some generic (but valid) tokens
webhook_id = 'A' * 24
webhook_token = 'B' * 64
# Prepare Mock
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
# Test our header parsing when not lead with a header
body = """
# Heading
@everyone and @admin, wake and meet our new user <@123>; <@&456>"
"""
results = NotifyDiscord.parse_url(
f'discord://{webhook_id}/{webhook_token}/?format=markdown')
assert isinstance(results, dict)
assert results['user'] is None
assert results['webhook_id'] == webhook_id
assert results['webhook_token'] == webhook_token
assert results['password'] is None
assert results['port'] is None
assert results['host'] == webhook_id
assert results['fullpath'] == f'/{webhook_token}/'
assert results['path'] == f'/{webhook_token}/'
assert results['query'] is None
assert results['schema'] == 'discord'
assert results['url'] == f'discord://{webhook_id}/{webhook_token}/'
instance = NotifyDiscord(**results)
assert isinstance(instance, NotifyDiscord)
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] == \
f'https://discord.com/api/webhooks/{webhook_id}/{webhook_token}'
payload = loads(details[1]['data'])
assert 'allow_mentions' in payload
assert 'users' in payload['allow_mentions']
assert len(payload['allow_mentions']['users']) == 1
assert '123' in payload['allow_mentions']['users']
assert 'roles' in payload['allow_mentions']
assert len(payload['allow_mentions']['roles']) == 1
assert '456' in payload['allow_mentions']['roles']
assert 'parse' in payload['allow_mentions']
assert len(payload['allow_mentions']['parse']) == 2
assert 'everyone' in payload['allow_mentions']['parse']
assert 'admin' in payload['allow_mentions']['parse']
# Reset our object
mock_post.reset_mock()
results = NotifyDiscord.parse_url(
f'discord://{webhook_id}/{webhook_token}/?format=text')
assert isinstance(results, dict)
assert results['user'] is None
assert results['webhook_id'] == webhook_id
assert results['webhook_token'] == webhook_token
assert results['password'] is None
assert results['port'] is None
assert results['host'] == webhook_id
assert results['fullpath'] == f'/{webhook_token}/'
assert results['path'] == f'/{webhook_token}/'
assert results['query'] is None
assert results['schema'] == 'discord'
assert results['url'] == f'discord://{webhook_id}/{webhook_token}/'
instance = NotifyDiscord(**results)
assert isinstance(instance, NotifyDiscord)
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] == \
f'https://discord.com/api/webhooks/{webhook_id}/{webhook_token}'
payload = loads(details[1]['data'])
assert 'allow_mentions' in payload
assert 'users' in payload['allow_mentions']
assert len(payload['allow_mentions']['users']) == 1
assert '123' in payload['allow_mentions']['users']
assert 'roles' in payload['allow_mentions']
assert len(payload['allow_mentions']['roles']) == 1
assert '456' in payload['allow_mentions']['roles']
assert 'parse' in payload['allow_mentions']
assert len(payload['allow_mentions']['parse']) == 2
assert 'everyone' in payload['allow_mentions']['parse']
assert 'admin' in payload['allow_mentions']['parse']
@mock.patch('requests.post') @mock.patch('requests.post')
def test_plugin_discord_general(mock_post): def test_plugin_discord_general(mock_post):
""" """
@ -564,6 +672,26 @@ def test_plugin_discord_attachments(mock_post):
'https://discord.com/api/webhooks/{}/{}'.format( 'https://discord.com/api/webhooks/{}/{}'.format(
webhook_id, webhook_token) webhook_id, webhook_token)
# Reset our object
mock_post.reset_mock()
# Test notifications with mentions and attachments in it
assert obj.notify(
body='Say hello to <@1234>!', notify_type=NotifyType.INFO,
attach=attach) is True
# Test our call count
assert mock_post.call_count == 2
assert mock_post.call_args_list[0][0][0] == \
'https://discord.com/api/webhooks/{}/{}'.format(
webhook_id, webhook_token)
assert mock_post.call_args_list[1][0][0] == \
'https://discord.com/api/webhooks/{}/{}'.format(
webhook_id, webhook_token)
# Reset our object
mock_post.reset_mock()
# An invalid attachment will cause a failure # An invalid attachment will cause a failure
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg') path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
attach = AppriseAttachment(path) attach = AppriseAttachment(path)