tgram:// support for topic values as target arguments (#1028)

This commit is contained in:
Chris Caron 2023-12-27 20:10:24 -05:00 committed by GitHub
parent 9dcf769397
commit d6e0d2ee07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 52 deletions

View File

@ -74,8 +74,10 @@ TELEGRAM_IMAGE_XY = NotifyImageSize.XY_256
# Chat ID is required # Chat ID is required
# If the Chat ID is positive, then it's addressed to a single person # If the Chat ID is positive, then it's addressed to a single person
# If the Chat ID is negative, then it's targeting a group # If the Chat ID is negative, then it's targeting a group
# We can support :topic (an integer) if specified as well
IS_CHAT_ID_RE = re.compile( IS_CHAT_ID_RE = re.compile(
r'^(@*(?P<idno>-?[0-9]{1,32})|(?P<name>[a-z_-][a-z0-9_-]+))$', r'^((?P<idno>-?[0-9]{1,32})|(@|%40)?(?P<name>[a-z_-][a-z0-9_-]+))'
r'((:|%3A)(?P<topic>[0-9]+))?$',
re.IGNORECASE, re.IGNORECASE,
) )
@ -360,9 +362,6 @@ class NotifyTelegram(NotifyBase):
self.logger.warning(err) self.logger.warning(err)
raise TypeError(err) raise TypeError(err)
# Parse our list
self.targets = parse_list(targets)
# Define whether or not we should make audible alarms # Define whether or not we should make audible alarms
self.silent = self.template_args['silent']['default'] \ self.silent = self.template_args['silent']['default'] \
if silent is None else bool(silent) if silent is None else bool(silent)
@ -403,15 +402,41 @@ class NotifyTelegram(NotifyBase):
# URL later to directly include the user that we should message. # URL later to directly include the user that we should message.
self.detect_owner = detect_owner self.detect_owner = detect_owner
if self.user: # Parse our list
# Treat this as a channel too self.targets = []
self.targets.append(self.user) for target in parse_list(targets):
results = IS_CHAT_ID_RE.match(target)
if not results:
self.logger.warning(
'Dropped invalid Telegram chat/group ({}) specified.'
.format(target),
)
# Ensure we don't fall back to owner detection
self.detect_owner = False
continue
try:
topic = int(
results.group('topic')
if results.group('topic') else self.topic)
except TypeError:
# No worries
topic = None
if results.group('name') is not None:
# Name
self.targets.append(('@%s' % results.group('name'), topic))
else: # ID
self.targets.append((int(results.group('idno')), topic))
# Track whether or not we want to send an image with our notification # Track whether or not we want to send an image with our notification
# or not. # or not.
self.include_image = include_image self.include_image = include_image
def send_media(self, chat_id, notify_type, attach=None): def send_media(self, target, notify_type, attach=None):
""" """
Sends a sticker based on the specified notify type Sends a sticker based on the specified notify type
@ -470,9 +495,12 @@ class NotifyTelegram(NotifyBase):
# content can arrive together. # content can arrive together.
self.throttle() self.throttle()
# Extract our target
chat_id, topic = target
payload = {'chat_id': chat_id} payload = {'chat_id': chat_id}
if self.topic: if topic:
payload['message_thread_id'] = self.topic payload['message_thread_id'] = topic
try: try:
with open(path, 'rb') as f: with open(path, 'rb') as f:
@ -658,7 +686,7 @@ class NotifyTelegram(NotifyBase):
_id = self.detect_bot_owner() _id = self.detect_bot_owner()
if _id: if _id:
# Permanently store our id in our target list for next time # Permanently store our id in our target list for next time
self.targets.append(str(_id)) self.targets.append((str(_id), None))
self.logger.info( self.logger.info(
'Update your Telegram Apprise URL to read: ' 'Update your Telegram Apprise URL to read: '
'{}'.format(self.url(privacy=True))) '{}'.format(self.url(privacy=True)))
@ -681,26 +709,23 @@ class NotifyTelegram(NotifyBase):
'sendMessage' 'sendMessage'
) )
payload = { _payload = {
# Notification Audible Control # Notification Audible Control
'disable_notification': self.silent, 'disable_notification': self.silent,
# Display Web Page Preview (if possible) # Display Web Page Preview (if possible)
'disable_web_page_preview': not self.preview, 'disable_web_page_preview': not self.preview,
} }
if self.topic:
payload['message_thread_id'] = self.topic
# Prepare Message Body # Prepare Message Body
if self.notify_format == NotifyFormat.MARKDOWN: if self.notify_format == NotifyFormat.MARKDOWN:
payload['parse_mode'] = 'MARKDOWN' _payload['parse_mode'] = 'MARKDOWN'
payload['text'] = body _payload['text'] = body
else: # HTML else: # HTML
# Use Telegram's HTML mode # Use Telegram's HTML mode
payload['parse_mode'] = 'HTML' _payload['parse_mode'] = 'HTML'
for r, v, m in self.__telegram_escape_html_entries: for r, v, m in self.__telegram_escape_html_entries:
if 'html' in m: if 'html' in m:
@ -712,7 +737,7 @@ class NotifyTelegram(NotifyBase):
body = r.sub(v, body) body = r.sub(v, body)
# Prepare our payload based on HTML or TEXT # Prepare our payload based on HTML or TEXT
payload['text'] = body _payload['text'] = body
# Handle payloads without a body specified (but an attachment present) # Handle payloads without a body specified (but an attachment present)
attach_content = \ attach_content = \
@ -721,41 +746,31 @@ class NotifyTelegram(NotifyBase):
# Create a copy of the chat_ids list # Create a copy of the chat_ids list
targets = list(self.targets) targets = list(self.targets)
while len(targets): while len(targets):
chat_id = targets.pop(0) target = targets.pop(0)
chat_id = IS_CHAT_ID_RE.match(chat_id) chat_id, topic = target
if not chat_id:
self.logger.warning(
"The specified chat_id '%s' is invalid; skipping." % (
chat_id,
)
)
# Flag our error # Printable chat_id details
has_error = True pchat_id = f'{chat_id}' if not topic else f'{chat_id}:{topic}'
continue
if chat_id.group('name') is not None: payload = _payload.copy()
# Name payload['chat_id'] = chat_id
payload['chat_id'] = '@%s' % chat_id.group('name') if topic:
payload['message_thread_id'] = topic
else:
# ID
payload['chat_id'] = int(chat_id.group('idno'))
if self.include_image is True: if self.include_image is True:
# Define our path # Define our path
if not self.send_media(payload['chat_id'], notify_type): if not self.send_media(target, notify_type):
# We failed to send the image associated with our # We failed to send the image associated with our
notify_type notify_type
self.logger.warning( self.logger.warning(
'Failed to send Telegram type image to {}.', 'Failed to send Telegram type image to {}.',
payload['chat_id']) pchat_id)
if attach and self.attachment_support and \ if attach and self.attachment_support and \
attach_content == TelegramContentPlacement.AFTER: attach_content == TelegramContentPlacement.AFTER:
# Send our attachments now (if specified and if it exists) # Send our attachments now (if specified and if it exists)
if not self._send_attachments( if not self._send_attachments(
chat_id=payload['chat_id'], notify_type=notify_type, target, notify_type=notify_type,
attach=attach): attach=attach):
has_error = True has_error = True
@ -803,7 +818,7 @@ class NotifyTelegram(NotifyBase):
self.logger.warning( self.logger.warning(
'Failed to send Telegram notification to {}: ' 'Failed to send Telegram notification to {}: '
'{}, error={}.'.format( '{}, error={}.'.format(
payload['chat_id'], pchat_id,
error_msg if error_msg else status_str, error_msg if error_msg else status_str,
r.status_code)) r.status_code))
@ -817,7 +832,7 @@ class NotifyTelegram(NotifyBase):
except requests.RequestException as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A connection error occurred sending Telegram:%s ' % ( 'A connection error occurred sending Telegram:%s ' % (
payload['chat_id']) + 'notification.' pchat_id) + 'notification.'
) )
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
@ -833,7 +848,7 @@ class NotifyTelegram(NotifyBase):
# it was identified to send the content before the attachments # it was identified to send the content before the attachments
# which is now done. # which is now done.
if not self._send_attachments( if not self._send_attachments(
chat_id=payload['chat_id'], target=target,
notify_type=notify_type, notify_type=notify_type,
attach=attach): attach=attach):
@ -842,14 +857,14 @@ class NotifyTelegram(NotifyBase):
return not has_error return not has_error
def _send_attachments(self, chat_id, notify_type, attach): def _send_attachments(self, target, notify_type, attach):
""" """
Sends our attachments Sends our attachments
""" """
has_error = False has_error = False
# Send our attachments now (if specified and if it exists) # Send our attachments now (if specified and if it exists)
for attachment in attach: for attachment in attach:
if not self.send_media(chat_id, notify_type, attach=attachment): if not self.send_media(target, notify_type, attach=attachment):
# We failed; don't continue # We failed; don't continue
has_error = True has_error = True
@ -880,13 +895,21 @@ class NotifyTelegram(NotifyBase):
# Extend our parameters # Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
targets = []
for (chat_id, _topic) in self.targets:
topic = _topic if _topic else self.topic
targets.append(''.join(
[NotifyTelegram.quote(f'{chat_id}', safe='@')
if isinstance(chat_id, str) else f'{chat_id}',
'' if not topic else f':{topic}']))
# No need to check the user token because the user automatically gets # No need to check the user token because the user automatically gets
# appended into the list of chat ids # appended into the list of chat ids
return '{schema}://{bot_token}/{targets}/?{params}'.format( return '{schema}://{bot_token}/{targets}/?{params}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
bot_token=self.pprint(self.bot_token, privacy, safe=''), bot_token=self.pprint(self.bot_token, privacy, safe=''),
targets='/'.join( targets='/'.join(targets),
[NotifyTelegram.quote('@{}'.format(x)) for x in self.targets]),
params=NotifyTelegram.urlencode(params)) params=NotifyTelegram.urlencode(params))
def __len__(self): def __len__(self):
@ -987,6 +1010,7 @@ class NotifyTelegram(NotifyBase):
# Include images with our message # Include images with our message
results['detect_owner'] = \ results['detect_owner'] = \
parse_bool(results['qsd'].get('detect', True)) parse_bool(
results['qsd'].get('detect', not results['targets']))
return results return results

View File

@ -243,7 +243,7 @@ def test_plugin_telegram_general(mock_post):
invalid_bot_token = 'abcd:123' invalid_bot_token = 'abcd:123'
# Chat ID # Chat ID
chat_ids = 'l2g, lead2gold' chat_ids = 'l2g:1234, lead2gold'
# Prepare Mock # Prepare Mock
mock_post.return_value = requests.Request() mock_post.return_value = requests.Request()
@ -397,7 +397,7 @@ def test_plugin_telegram_general(mock_post):
obj = NotifyTelegram(bot_token=bot_token, targets='12345') obj = NotifyTelegram(bot_token=bot_token, targets='12345')
assert len(obj.targets) == 1 assert len(obj.targets) == 1
assert obj.targets[0] == '12345' assert obj.targets[0] == (12345, None)
# Test the escaping of characters since Telegram escapes stuff for us to # Test the escaping of characters since Telegram escapes stuff for us to
# which we need to consider # which we need to consider
@ -440,7 +440,7 @@ def test_plugin_telegram_general(mock_post):
assert obj.notify(title='hello', body='world') is True assert obj.notify(title='hello', body='world') is True
assert len(obj.targets) == 1 assert len(obj.targets) == 1
assert obj.targets[0] == '532389719' assert obj.targets[0] == ('532389719', None)
# Do the test again, but without the expected (parsed response) # Do the test again, but without the expected (parsed response)
mock_post.return_value.content = dumps({ mock_post.return_value.content = dumps({
@ -550,7 +550,7 @@ def test_plugin_telegram_general(mock_post):
'tgram://123456789:ABCdefghijkl123456789opqyz/-123456789525') 'tgram://123456789:ABCdefghijkl123456789opqyz/-123456789525')
assert isinstance(obj, NotifyTelegram) assert isinstance(obj, NotifyTelegram)
assert len(obj.targets) == 1 assert len(obj.targets) == 1
assert '-123456789525' in obj.targets assert (-123456789525, None) in obj.targets
@mock.patch('requests.post') @mock.patch('requests.post')