diff --git a/plugins/modules/notification/slack.py b/plugins/modules/notification/slack.py index c9217c7867..9d44c34abb 100644 --- a/plugins/modules/notification/slack.py +++ b/plugins/modules/notification/slack.py @@ -1,6 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# (c) 2020, Lee Goolsbee # (c) 2020, Michal Middleton # (c) 2017, Steve Pletcher # (c) 2016, René Moser @@ -100,7 +101,11 @@ options: attachments: description: - Define a list of attachments. This list mirrors the Slack JSON API. - - For more information, see also in the (U(https://api.slack.com/docs/attachments)). + - For more information, see (U(https://api.slack.com/docs/attachments)). + blocks: + description: + - Define a list of blocks. This list mirrors the Slack JSON API. + - For more information, see (U(https://api.slack.com/block-kit)). """ EXAMPLES = """ @@ -153,6 +158,27 @@ EXAMPLES = """ value: 'load average: 5,16, 4,64, 2,43' short: True +- name: Use the blocks API + community.general.slack: + token: thetoken/generatedby/slack + blocks: + - type: section + text: + type: mrkdwn + text: |- + *System load* + Display my system load on host A and B + - type: context + elements: + - type: mrkdwn + text: |- + *System A* + load average: 0,74, 0,66, 0,63 + - type: mrkdwn + text: |- + *System B* + load average: 5,16, 4,64, 2,43 + - name: Send a message with a link using Slack markup community.general.slack: token: thetoken/generatedby/slack @@ -206,8 +232,25 @@ def escape_quotes(text): return "".join(escape_table.get(c, c) for c in text) +def recursive_escape(obj): + '''Recursively escape quotes inside "text" and "alt_text" strings inside block kit objects''' + block_keys_to_escape = ['text', 'alt_text'] + if isinstance(obj, dict): + escaped = {} + for k, v in obj.items(): + if isinstance(v, str) and k in block_keys_to_escape: + escaped[k] = escape_quotes(v) + else: + escaped[k] = recursive_escape(v) + elif isinstance(obj, list): + escaped = [recursive_escape(v) for v in obj] + else: + return obj + return escaped + + def build_payload_for_slack(module, text, channel, thread_id, username, icon_url, icon_emoji, link_names, - parse, color, attachments): + parse, color, attachments, blocks): payload = {} if color == "normal" and text is not None: payload = dict(text=escape_quotes(text)) @@ -237,7 +280,7 @@ def build_payload_for_slack(module, text, channel, thread_id, username, icon_url payload['attachments'] = [] if attachments is not None: - keys_to_escape = [ + attachment_keys_to_escape = [ 'title', 'text', 'author_name', @@ -245,7 +288,7 @@ def build_payload_for_slack(module, text, channel, thread_id, username, icon_url 'fallback', ] for attachment in attachments: - for key in keys_to_escape: + for key in attachment_keys_to_escape: if key in attachment: attachment[key] = escape_quotes(attachment[key]) @@ -254,6 +297,9 @@ def build_payload_for_slack(module, text, channel, thread_id, username, icon_url payload['attachments'].append(attachment) + if blocks is not None: + payload['blocks'] = recursive_escape(blocks) + payload = module.jsonify(payload) return payload @@ -310,7 +356,8 @@ def main(): parse=dict(type='str', default=None, choices=['none', 'full']), validate_certs=dict(default=True, type='bool'), color=dict(type='str', default='normal'), - attachments=dict(type='list', required=False, default=None) + attachments=dict(type='list', required=False, default=None), + blocks=dict(type='list', required=False, default=None) ) ) @@ -326,6 +373,7 @@ def main(): parse = module.params['parse'] color = module.params['color'] attachments = module.params['attachments'] + blocks = module.params['blocks'] color_choices = ['normal', 'good', 'warning', 'danger'] if color not in color_choices and not is_valid_hex_color(color): @@ -333,7 +381,7 @@ def main(): "or any valid hex value with length 3 or 6." % color_choices) payload = build_payload_for_slack(module, text, channel, thread_id, username, icon_url, icon_emoji, link_names, - parse, color, attachments) + parse, color, attachments, blocks) slack_response = do_notify_slack(module, domain, token, payload) if 'ok' in slack_response: diff --git a/tests/unit/plugins/modules/notification/test_slack.py b/tests/unit/plugins/modules/notification/test_slack.py index 2338ef544b..209cfd235f 100644 --- a/tests/unit/plugins/modules/notification/test_slack.py +++ b/tests/unit/plugins/modules/notification/test_slack.py @@ -88,6 +88,43 @@ class TestSlackModule(ModuleTestCase): assert call_data['thread_ts'] == '100.00' assert fetch_url_mock.call_args[1]['url'] == "https://hooks.slack.com/services/XXXX/YYYY/ZZZZ" + def test_message_with_blocks(self): + """tests sending a message with blocks""" + set_module_args({ + 'token': 'XXXX/YYYY/ZZZZ', + 'msg': 'test', + 'blocks': [{ + 'type': 'section', + 'text': { + 'type': 'mrkdwn', + 'text': '*test*' + }, + 'accessory': { + 'type': 'image', + 'image_url': 'https://www.ansible.com/favicon.ico', + 'alt_text': 'test' + } + }, { + 'type': 'section', + 'text': { + 'type': 'plain_text', + 'text': 'test', + 'emoji': True + } + }] + }) + + with patch.object(slack, "fetch_url") as fetch_url_mock: + fetch_url_mock.return_value = (None, {"status": 200}) + with self.assertRaises(AnsibleExitJson): + self.module.main() + + self.assertTrue(fetch_url_mock.call_count, 1) + call_data = json.loads(fetch_url_mock.call_args[1]['data']) + assert call_data['username'] == "Ansible" + assert call_data['blocks'][1]['text']['text'] == "test" + assert fetch_url_mock.call_args[1]['url'] == "https://hooks.slack.com/services/XXXX/YYYY/ZZZZ" + def test_message_with_invalid_color(self): """tests sending invalid color value to module""" set_module_args({