Slack Integration Support thread_timestamp (#1033)

This commit is contained in:
Alex Scotland 2023-12-29 18:28:20 -05:00 committed by GitHub
parent c240f8ba48
commit bb4acd0bdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 158 additions and 28 deletions

View File

@ -96,6 +96,10 @@ SLACK_HTTP_ERROR_MAP = {
# Used to break path apart into list of channels
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
# Channel Regular Expression Parsing
CHANNEL_RE = re.compile(
r'^(?P<channel>[+#@]?[A-Z0-9_-]{1,32})(:(?P<thread_ts>[0-9.]+))?$', re.I)
class SlackMode:
"""
@ -547,39 +551,52 @@ class NotifySlack(NotifyBase):
attach_channel_list = []
while len(channels):
channel = channels.pop(0)
if channel is not None:
channel = validate_regex(channel, r'[+#@]?[A-Z0-9_]{1,32}')
if not channel:
# Channel over-ride was specified
self.logger.warning(
"The specified target {} is invalid;"
"skipping.".format(channel))
# We'll perform a user lookup if we detect an email
email = is_email(channel)
if email:
payload['channel'] = \
self.lookup_userid(email['full_email'])
# Mark our failure
has_error = True
continue
if not payload['channel']:
# Move along; any notifications/logging would have
# come from lookup_userid()
has_error = True
continue
if channel[0] == '+':
# Treat as encoded id if prefixed with a +
payload['channel'] = channel[1:]
else: # Channel
result = CHANNEL_RE.match(channel)
elif channel[0] == '@':
# Treat @ value 'as is'
payload['channel'] = channel
if not result:
# Channel over-ride was specified
self.logger.warning(
"The specified Slack target {} is invalid;"
"skipping.".format(channel))
else:
# We'll perform a user lookup if we detect an email
email = is_email(channel)
if email:
payload['channel'] = \
self.lookup_userid(email['full_email'])
# Mark our failure
has_error = True
continue
# Store oure content
channel, thread_ts = \
result.group('channel'), result.group('thread_ts')
if thread_ts:
payload['thread_ts'] = thread_ts
elif 'thread_ts' in payload:
# Handle situations where one channel has a thread_id
# specified, and the next does not. We do not want to
# cary forward the last value specified
del payload['thread_ts']
if channel[0] == '+':
# Treat as encoded id if prefixed with a +
payload['channel'] = channel[1:]
elif channel[0] == '@':
# Treat @ value 'as is'
payload['channel'] = channel
if not payload['channel']:
# Move along; any notifications/logging would have
# come from lookup_userid()
has_error = True
continue
else:
# Prefix with channel hash tag (if not already)
payload['channel'] = \
@ -795,7 +812,6 @@ class NotifySlack(NotifyBase):
"""
Wrapper to the requests (post) object
"""
self.logger.debug('Slack POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))

View File

@ -253,6 +253,29 @@ apprise_url_tests = (
'test_requests_exceptions': True,
'requests_response_text': 'ok',
}),
('slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#b:100', {
'instance': NotifySlack,
'requests_response_text': 'ok',
}),
('slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/+124:100', {
'instance': NotifySlack,
'requests_response_text': 'ok',
}),
# test a case where we have a channel defined alone (without a thread_ts)
# that exists after a definition where a thread_ts does exist. this
# tests the branch of code that ensures we do not pass the same thread_ts
# twice
('slack://notify@T1JJ3T3L2/A1BRTD4JD/'
'TIiajkdnlazkcOXrIdevi7FQ/+124:100/@chan', {
'instance': NotifySlack,
'requests_response_text': 'ok',
}),
('slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#b:bad', {
'instance': NotifySlack,
'requests_response_text': 'ok',
# we'll fail because our thread_ts is bad
'response': False,
}),
)
@ -702,3 +725,94 @@ def test_plugin_slack_markdown(mock_get, mock_post):
"We also want to be able to support <https://slack.com> "\
"links without the\ndescription."\
"\n\nChannel Testing\n<!channelA>\n<!channelA|Description>"
@mock.patch('requests.post')
def test_plugin_slack_single_thread_reply(mock_post):
"""
NotifySlack() Send Notification as a Reply
"""
# Generate a (valid) bot token
token = 'xoxb-1234-1234-abc124'
thread_id = 100
request = mock.Mock()
request.content = dumps({
'ok': True,
'message': '',
'user': {
'id': 'ABCD1234'
}
})
request.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = request
# Variation Initializations
obj = NotifySlack(access_token=token, targets=[f'#general:{thread_id}'])
assert isinstance(obj, NotifySlack) is True
assert isinstance(obj.url(), str) is True
# No calls made yet
assert mock_post.call_count == 0
# Send our notification
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
# Post was made
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'https://slack.com/api/chat.postMessage'
assert loads(mock_post.call_args_list[0][1]['data']).get("thread_ts") \
== str(thread_id)
@mock.patch('requests.post')
def test_plugin_slack_multiple_thread_reply(mock_post):
"""
NotifySlack() Send Notification to multiple channels as Reply
"""
# Generate a (valid) bot token
token = 'xoxb-1234-1234-abc124'
thread_id_1, thread_id_2 = 100, 200
request = mock.Mock()
request.content = dumps({
'ok': True,
'message': '',
'user': {
'id': 'ABCD1234'
}
})
request.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = request
# Variation Initializations
obj = NotifySlack(access_token=token,
targets=[
f'#general:{thread_id_1}',
f'#other:{thread_id_2}'])
assert isinstance(obj, NotifySlack) is True
assert isinstance(obj.url(), str) is True
# No calls made yet
assert mock_post.call_count == 0
# Send our notification
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is True
# Post was made
assert mock_post.call_count == 2
assert mock_post.call_args_list[0][0][0] == \
'https://slack.com/api/chat.postMessage'
assert loads(mock_post.call_args_list[0][1]['data']).get("thread_ts") \
== str(thread_id_1)
assert loads(mock_post.call_args_list[1][1]['data']).get("thread_ts") \
== str(thread_id_2)