From 0c0490298f54359c28a59df87f070886ea8b6129 Mon Sep 17 00:00:00 2001 From: Michal Middleton Date: Fri, 17 Apr 2020 06:44:55 -0500 Subject: [PATCH] Slack: Add bot/user token support, correct thread_ts support (#123) * Slack: Add bot/user token support, correct thread_ts support Add support for user/bot/application tokens (and Slack WebAPI). Fix input type for thread_id, which needs to be string. Return thread_ts/thread_id when user/bot tokens are used, so they can be reused later * Slack: Add changelog fragment, fix YAML syntax Co-authored-by: Middleton, Michal --- ...-slack-add_bot_token_support_thread_id.yml | 6 ++ plugins/modules/notification/slack.py | 78 +++++++++++++++---- .../modules/notification/test_slack.py | 4 +- 3 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 changelogs/fragments/123-slack-add_bot_token_support_thread_id.yml diff --git a/changelogs/fragments/123-slack-add_bot_token_support_thread_id.yml b/changelogs/fragments/123-slack-add_bot_token_support_thread_id.yml new file mode 100644 index 0000000000..0eb694cd5e --- /dev/null +++ b/changelogs/fragments/123-slack-add_bot_token_support_thread_id.yml @@ -0,0 +1,6 @@ +bugfixes: +- "slack - Fix ``thread_id`` data type" + +minor_changes: +- "slack - Add support for user/bot/application tokens (using Slack WebAPI)" +- "slack - Return ``thread_id`` with thread timestamp when user/bot/application tokens are used" diff --git a/plugins/modules/notification/slack.py b/plugins/modules/notification/slack.py index 46b94557c7..9854bbe2ef 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, Michal Middleton # (c) 2017, Steve Pletcher # (c) 2016, René Moser # (c) 2015, Stefan Berggren @@ -17,7 +18,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', 'supported_by': 'community'} -DOCUMENTATION = ''' +DOCUMENTATION = """ module: slack short_description: Send Slack notifications description: @@ -31,7 +32,9 @@ options: be ignored. See token documentation for information. token: description: - - Slack integration token. This authenticates you to the slack service. + - Slack integration token. This authenticates you to the slack service. + Make sure to use the correct type of token, depending on what method you use. + - "Webhook token: Prior to 1.8, a token looked like C(3Ffe373sfhRE6y42Fg3rvf4GlK). In 1.8 and above, ansible adapts to the new slack API where tokens look like C(G922VJP24/D921DW937/3Ffe373sfhRE6y42Fg3rvf4GlK). If tokens @@ -44,7 +47,12 @@ options: The incoming webhooks can be added in that area. In some cases this may be locked by your Slack admin and you must request access. It is there that the incoming webhooks can be added. The key is on the end of the - URL given to you in that section. + 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. + See Slack's documentation (U(https://api.slack.com/docs/token-types)) for more information." required: true msg: description: @@ -56,7 +64,8 @@ options: - Channel to send the message to. If absent, the message goes to the channel selected for the I(token). thread_id: description: - - Optional. Timestamp of message to thread this message to as a float. https://api.slack.com/docs/message-threading + - Optional. Timestamp of parent message to thread this message. https://api.slack.com/docs/message-threading + type: str username: description: - This is the sender of the message. @@ -97,7 +106,7 @@ options: 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)). -''' +""" EXAMPLES = """ - name: Send notification message via Slack @@ -111,7 +120,7 @@ EXAMPLES = """ token: thetoken/generatedby/slack msg: '{{ inventory_hostname }} completed' channel: '#ansible' - thread_id: 1539917263.000100 + thread_id: '1539917263.000100' username: 'Ansible on {{ inventory_hostname }}' icon_url: http://www.example.com/some-image-file.png link_names: 0 @@ -158,6 +167,20 @@ EXAMPLES = """ slack: token: thetoken/generatedby/slack msg: This message has <brackets> & ampersands in plain text. + +- name: Initial Threaded Slack message + slack: + channel: '#ansible' + token: xoxb-1234-56789abcdefghijklmnop + msg: 'Starting a thread with my initial post.' + register: slack_response +- name: Add more info to thread + slack: + channel: '#ansible' + token: xoxb-1234-56789abcdefghijklmnop + thread_id: "{{ slack_response['ts'] }}" + color: good + msg: 'And this is my threaded response!' """ import re @@ -167,6 +190,7 @@ from ansible.module_utils.urls import fetch_url OLD_SLACK_INCOMING_WEBHOOK = 'https://%s/services/hooks/incoming-webhook?token=%s' SLACK_INCOMING_WEBHOOK = 'https://hooks.slack.com/services/%s' +SLACK_POSTMESSAGE_WEBAPI = 'https://slack.com/api/chat.postMessage' # Escaping quotes and apostrophes to avoid ending string prematurely in ansible call. # We do not escape other characters used as Slack metacharacters (e.g. &, <, >). @@ -240,25 +264,41 @@ def build_payload_for_slack(module, text, channel, thread_id, username, icon_url def do_notify_slack(module, domain, token, payload): + use_webapi = False if token.count('/') >= 2: - # New style token - slack_incoming_webhook = SLACK_INCOMING_WEBHOOK % (token) + # New style webhook token + slack_uri = SLACK_INCOMING_WEBHOOK % (token) + elif re.match(r'^xox[abp]-\w+-\w+$', token): + slack_uri = SLACK_POSTMESSAGE_WEBAPI + use_webapi = True else: if not domain: module.fail_json(msg="Slack has updated its webhook API. You need to specify a token of the form " "XXXX/YYYY/ZZZZ in your playbook") - slack_incoming_webhook = OLD_SLACK_INCOMING_WEBHOOK % (domain, token) + slack_uri = OLD_SLACK_INCOMING_WEBHOOK % (domain, token) headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', } - response, info = fetch_url(module=module, url=slack_incoming_webhook, headers=headers, method='POST', data=payload) + if use_webapi: + headers['Authorization'] = 'Bearer ' + token + + response, info = fetch_url(module=module, url=slack_uri, headers=headers, method='POST', data=payload) if info['status'] != 200: - obscured_incoming_webhook = SLACK_INCOMING_WEBHOOK % ('[obscured]') + if use_webapi: + obscured_incoming_webhook = slack_uri + else: + obscured_incoming_webhook = SLACK_INCOMING_WEBHOOK % ('[obscured]') module.fail_json(msg=" failed to send %s to %s: %s" % (payload, obscured_incoming_webhook, info['msg'])) + # each API requires different handling + if use_webapi: + return module.from_json(response.read()) + else: + return {'webhook': 'ok'} + def main(): module = AnsibleModule( @@ -267,7 +307,7 @@ def main(): token=dict(type='str', required=True, no_log=True), msg=dict(type='str', required=False, default=None), channel=dict(type='str', default=None), - thread_id=dict(type='float', default=None), + thread_id=dict(type='str', default=None), username=dict(type='str', default='Ansible'), icon_url=dict(type='str', default='https://www.ansible.com/favicon.ico'), icon_emoji=dict(type='str', default=None), @@ -299,9 +339,19 @@ def main(): payload = build_payload_for_slack(module, text, channel, thread_id, username, icon_url, icon_emoji, link_names, parse, color, attachments) - do_notify_slack(module, domain, token, payload) + slack_response = do_notify_slack(module, domain, token, payload) - module.exit_json(msg="OK") + if 'ok' in slack_response: + # Evaluate WebAPI response + if slack_response['ok']: + module.exit_json(changed=True, ts=slack_response['ts'], channel=slack_response['channel'], + api=slack_response, payload=payload) + else: + module.fail_json(msg="Slack API error", error=slack_response['error']) + else: + # Exit with plain OK from WebHook, since we don't have more information + # If we get 200 from webhook, the only answer is OK + module.exit_json(msg="OK") if __name__ == '__main__': diff --git a/tests/unit/plugins/modules/notification/test_slack.py b/tests/unit/plugins/modules/notification/test_slack.py index 64f21d8af1..2bd2b35a67 100644 --- a/tests/unit/plugins/modules/notification/test_slack.py +++ b/tests/unit/plugins/modules/notification/test_slack.py @@ -68,7 +68,7 @@ class TestSlackModule(ModuleTestCase): set_module_args({ 'token': 'XXXX/YYYY/ZZZZ', 'msg': 'test', - 'thread_id': 100.00 + 'thread_id': '100.00' }) with patch.object(slack, "fetch_url") as fetch_url_mock: @@ -80,7 +80,7 @@ class TestSlackModule(ModuleTestCase): call_data = json.loads(fetch_url_mock.call_args[1]['data']) assert call_data['username'] == "Ansible" assert call_data['text'] == "test" - assert call_data['thread_ts'] == 100.00 + 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_invalid_color(self):