Repository: ansible-collections/community.general

Issue:
support slack block kits
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->
support slack block kits, similar to the `attachments` parameter of the slack module.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
slack notifications module

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->
slack considers attachments a legacy functionality: https://api.slack.com/messaging/composing/layouts#when-to-use-attachments. they recommend using block kits.

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: Send notification message via Slack all options
  slack:
    token: thetoken/generatedby/slack
    msg: '{{ inventory_hostname }} completed'
    ...snip...
    blocks: [
     {
       type: "section",
       text: {
         ...snip...
       }
     }
   ]
```

<!--- HINT: You can also paste gist.github.com links for larger files -->
more details here: https://api.slack.com/block-kit


Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
This would be great!

Resolving patch:
diff --git a/plugins/modules/notification/slack.py b/plugins/modules/notification/slack.py
--- 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 <lgoolsbee@atlassian.com>
 # (c) 2020, Michal Middleton <mm.404@icloud.com>
 # (c) 2017, Steve Pletcher <steve@steve-pletcher.com>
 # (c) 2016, René Moser <mail@renemoser.net>
@@ -45,8 +46,7 @@
         URL given to you in that section."
       - "WebAPI token:
         Slack WebAPI requires a personal, bot or work application token. These tokens start with C(xoxp-), C(xoxb-)
-        or C(xoxa-), eg. C(xoxb-1234-56789abcdefghijklmnop). WebAPI token is required if you inted to receive and use
-        thread_id.
+        or C(xoxa-), eg. C(xoxb-1234-56789abcdefghijklmnop). WebAPI token is required if you intend to receive thread_id.
         See Slack's documentation (U(https://api.slack.com/docs/token-types)) for more information."
     required: true
   msg:
@@ -100,7 +100,14 @@
   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).
+    type: list
+    elements: dict
+    version_added: 1.0.0
 """
 
 EXAMPLES = """
@@ -153,6 +160,27 @@
             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 +234,24 @@ def escape_quotes(text):
     return "".join(escape_table.get(c, c) for c in text)
 
 
+def recursive_escape_quotes(obj, keys):
+    '''Recursively escape quotes inside supplied keys inside block kit objects'''
+    if isinstance(obj, dict):
+        escaped = {}
+        for k, v in obj.items():
+            if isinstance(v, str) and k in keys:
+                escaped[k] = escape_quotes(v)
+            else:
+                escaped[k] = recursive_escape_quotes(v, keys)
+    elif isinstance(obj, list):
+        escaped = [recursive_escape_quotes(v, keys) for v in obj]
+    else:
+        escaped = 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 +281,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 +289,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 +298,13 @@ def build_payload_for_slack(module, text, channel, thread_id, username, icon_url
 
             payload['attachments'].append(attachment)
 
+    if blocks is not None:
+        block_keys_to_escape = [
+            'text',
+            'alt_text'
+        ]
+        payload['blocks'] = recursive_escape_quotes(blocks, block_keys_to_escape)
+
     payload = module.jsonify(payload)
     return payload
 
@@ -310,7 +361,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', elements='dict'),
         )
     )
 
@@ -326,6 +378,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 +386,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:


Test patch:
diff --git a/tests/unit/plugins/modules/notification/test_slack.py b/tests/unit/plugins/modules/notification/test_slack.py
--- a/tests/unit/plugins/modules/notification/test_slack.py
+++ b/tests/unit/plugins/modules/notification/test_slack.py
@@ -88,6 +88,43 @@ def test_message_with_thread(self):
             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({