diff --git a/Packs/Slack/.secrets-ignore b/Packs/Slack/.secrets-ignore index d447a381a448..09f7d13ac54a 100644 --- a/Packs/Slack/.secrets-ignore +++ b/Packs/Slack/.secrets-ignore @@ -22,4 +22,5 @@ https://avatars.slack-edge.com C0R2D2C3PO amshamah@gmail.com andrew@lexiapplications.com -bird@slack.com \ No newline at end of file +bird@slack.com +https://someimage.png \ No newline at end of file diff --git a/Packs/Slack/Integrations/SlackV3/README.md b/Packs/Slack/Integrations/SlackV3/README.md index 87278fb09443..0292d4e00ee9 100644 --- a/Packs/Slack/Integrations/SlackV3/README.md +++ b/Packs/Slack/Integrations/SlackV3/README.md @@ -660,3 +660,71 @@ Reset user session token in Slack. #### Context Output There is no context output for this command. +### slack-list-channels + +*** +List all of the channels in the organization workspace. This command required scopes depend on the type of channel-like object you're working with. To use the command, you'll need at least one of the channels:, groups:, im: or mpim: scopes corresponding to the conversation type you're working with. + +#### Base Command + +`slack-list-channels` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| name_filter | Supply this argument to only return channels with this name . | Optional | +| channel_types | Default is "public_channel". You can provide a comma separated list of other channels to include in your results. Possible options are: "public_channel", "private_channel", "mpim", and "im". Including these options may require changes to your Bot's OAuth scopes in order to read channels like private, group message, or personal messages. | Optional | +| exclude_archived | Default is true (exclude archived channels). This setting allows the command to read channels that have been archived. | Optional | +| limit | Default is 100. Set this argument to specify how many results to return. If you have more results than the limit you set, you will need to use the cursor argument to paginate your results. | Optional | +| cursor | Default is the first page of results. If you have more results than your limit, you need to paginate your results with this argument. This is found with the next_cursor attribute returned by a previous request's response_metadata . | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Slack.Channels.ID | string | The ID for the channel | +| Slack.Channels.Name | string | Name of the channel | +| Slack.Channels.Created | number | Epoch timestamp when the channel was created | +| Slack.Channels.Creator | string | ID for the creator of the channel | +| Slack.Channels.Purpose | string | The purpose, or description, of the channel | +### slack-get-conversation-history + +*** +Fetches a conversation's history of messages and events + +#### Base Command + +`slack-get-conversation-history` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| channel_id | The channel ID associated with the Slack channel. | Required | +| limit | Default is 100. Set this argument to specify how many results to return. If you have more results than the limit you set, you will need to use the cursor argument to paginate your results. | Optional | +| conversation_id | Conversation id. | Optional | + +#### Context Output + +There is no context output for this command. +### slack-get-conversation-replies + +*** +Retrieves replies to specific messages, regardless of whether it's from a public or private channel, direct message, or otherwise. + +#### Base Command + +`slack-get-conversation-replies` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| channel_id | ID of the channel. | Required | +| thread_id | ID of the thread. | Required | +| limit | limit. | Optional | + +#### Context Output + +There is no context output for this command. \ No newline at end of file diff --git a/Packs/Slack/Integrations/SlackV3/SlackV3.py b/Packs/Slack/Integrations/SlackV3/SlackV3.py index 4dcb20a4b62e..9c5d6e8dd733 100644 --- a/Packs/Slack/Integrations/SlackV3/SlackV3.py +++ b/Packs/Slack/Integrations/SlackV3/SlackV3.py @@ -6,7 +6,6 @@ import ssl import threading from distutils.util import strtobool - import aiohttp import slack_sdk from slack_sdk.errors import SlackApiError @@ -17,7 +16,6 @@ from slack_sdk.web.async_slack_response import AsyncSlackResponse from slack_sdk.web.slack_response import SlackResponse -from CommonServerUserPython import * # noqa ''' CONSTANTS ''' @@ -2570,6 +2568,189 @@ def pin_message(): return_error(f"{slack_error}") +def list_channels(): + """ + List the conversations in the workspace + """ + args = demisto.args() + # Default for the SDK is public channels, but users can specify "public_channel", "private_channel", "mpim", and "im" + # Multiple values can be passed for this argument as a comma separated list + # By default archived channels are NOT included by the SDK. Explicitly set this if not set from the CLI or set to False + body = { + 'types': args.get('channel_types'), + 'exclude_archived': argToBoolean(args.get('exclude_archived', 'true')), + 'limit': args.get('limit') + } + if args.get('cursor'): + body['cursor'] = args.get('cursor') + raw_response = send_slack_request_sync(CLIENT, 'conversations.list', http_verb="GET", body=body) + # Provide an option to only select a channel by a name. Instead of returning a full list of results this allows granularity + # Supports a single channel name + if name_filter := args.get('name_filter'): + for channel in raw_response['channels']: + if channel['name'] == name_filter: + channels = [channel] + break + else: + raise DemistoException(f'No channel found with name: {name_filter}') + else: + channels = raw_response['channels'] + # Force list for consistent parsing + if isinstance(channels, dict): + channels = [channels] + context = [] # type: List + for channel in channels: + entry = { + 'ID': channel.get('id'), + 'Name': channel.get('name'), + 'Created': channel.get('created'), + 'Purpose': channel.get('purpose', {}).get('value') + } + if channel.get('creator'): + creator_details_response = send_slack_request_sync(CLIENT, 'users.info', http_verb="GET", + body={'user': channel.get('creator')}) + entry['Creator'] = creator_details_response['user']['name'] + context.append(entry) + readable_output = tableToMarkdown(f'Channels list for {args.get("channel_types")} with filter {name_filter}', context) + demisto.results({ + 'Type': entryTypes['note'], + 'Contents': channels, + 'EntryContext': {'Slack.Channels': context}, + 'ContentsFormat': formats['json'], + 'HumanReadable': readable_output, + 'ReadableContentsFormat': formats['markdown'] + }) + + +def conversation_history(): + """ + Fetches a conversation's history of messages + and events + """ + args = demisto.args() + channel_id = args.get('channel_id') + limit = arg_to_number(args.get('limit')) + conversation_id = args.get('conversation_id') + body = {'channel': channel_id, 'limit': limit} if not conversation_id else {'channel': channel_id, + 'oldest': conversation_id, + 'inclusive': "true", + 'limit': 1} + readable_output = '' + raw_response = send_slack_request_sync(CLIENT, 'conversations.history', http_verb="GET", body=body) + messages = raw_response.get('messages', '') + if not raw_response.get('ok'): + raise DemistoException(f'An error occurred while listing conversation history: {raw_response.get("error")}', + res=raw_response) + if isinstance(messages, dict): + messages = [messages] + if not isinstance(messages, list): + raise DemistoException(f'An error occurred while listing conversation history: {raw_response.get("error")}', + res=raw_response) + context = [] # type: List + for message in messages: + thread_ts = 'N/A' + has_replies = 'No' + name = 'N/A' + full_name = 'N/A' + if 'subtype' not in message: + user_id = message.get('user') + user_details_response = send_slack_request_sync(CLIENT, 'users.info', http_verb="GET", + body={'user': user_id}) + user_details = user_details_response.get('user') + full_name = user_details.get('real_name') + name = user_details.get('name') + if 'thread_ts' in message: + thread_ts = message.get('thread_ts') + has_replies = 'Yes' + elif 'thread_ts' in message: + thread_ts = message.get('thread_ts') + has_replies = 'Yes' + full_name = message.get('username') + name = message.get('username') + thread_ts = message.get('thread_ts') + has_replies = 'Yes' + entry = { + 'Type': message.get('type'), + 'Text': message.get('text'), + 'UserId': message.get('user'), + 'Name': name, + 'FullName': full_name, + 'TimeStamp': message.get('ts'), + 'HasReplies': has_replies, + 'ThreadTimeStamp': thread_ts + } + context.append(entry) + readable_output = tableToMarkdown(f'Channel details from Channel ID - {channel_id}', context) + demisto.results({ + 'Type': entryTypes['note'], + 'Contents': messages, + 'EntryContext': {'Slack.Messages': context}, + 'ContentsFormat': formats['json'], + 'HumanReadable': readable_output, + 'ReadableContentsFormat': formats['markdown'] + }) + + +def conversation_replies(): + """ + Retrieves replies to specific messages, regardless of whether it's + from a public or private channel, direct message, or otherwise. + """ + args = demisto.args() + channel_id = args.get('channel_id') + context: list = [] + readable_output: str = '' + body = { + 'channel': channel_id, + 'ts': args.get('thread_timestamp'), + 'limit': arg_to_number(args.get('limit')) + } + raw_response = send_slack_request_sync(CLIENT, 'conversations.replies', http_verb="GET", body=body) + messages = raw_response.get('messages', '') + if not raw_response.get('ok'): + error = raw_response.get('error') + return_error(f'An error occurred while listing conversation replies: {error}') + if isinstance(messages, dict): + messages = [messages] + if not isinstance(messages, list): + raise DemistoException(f'An error occurred while listing conversation replies: {raw_response.get("error")}') + for message in messages: + reply_count = 'No' + name = 'N/A' + full_name = 'N/A' + if 'subtype' not in message: + user_id = message.get('user') + body = { + 'user': user_id + } + user_details_response = send_slack_request_sync(CLIENT, 'users.info', http_verb="GET", body=body) + user_details = user_details_response.get('user') + name = user_details.get('name') + full_name = user_details.get('real_name') + if 'reply_count' in message: + reply_count = 'Yes' + entry = { + 'Type': message.get('type'), + 'Text': message.get('text'), + 'UserId': message.get('user'), + 'Name': name, + 'FullName': full_name, + 'TimeStamp': message.get('ts'), + 'ThreadTimeStamp': message.get('thread_ts'), + 'IsParent': reply_count + } + context.append(entry) + readable_output = tableToMarkdown(f'Channel details from Channel ID - {channel_id}', context) + demisto.results({ + 'Type': entryTypes['note'], + 'Contents': messages, + 'EntryContext': {'Slack.Threads': context}, + 'ContentsFormat': formats['json'], + 'HumanReadable': readable_output, + 'ReadableContentsFormat': formats['markdown'] + }) + + def long_running_main(): """ Starts the long running thread. @@ -2779,7 +2960,10 @@ def main() -> None: 'slack-get-integration-context': slack_get_integration_context, 'slack-edit-message': slack_edit_message, 'slack-pin-message': pin_message, - 'slack-user-session-reset': user_session_reset + 'slack-user-session-reset': user_session_reset, + 'slack-get-conversation-history': conversation_history, + 'slack-list-channels': list_channels, + 'slack-get-conversation-replies': conversation_replies, } command_name: str = demisto.command() @@ -2793,7 +2977,7 @@ def main() -> None: support_multithreading() command_func() except Exception as e: - LOG(e) + demisto.debug(e) return_error(str(e)) finally: demisto.info(f'{command_name} completed.') # type: ignore diff --git a/Packs/Slack/Integrations/SlackV3/SlackV3.yml b/Packs/Slack/Integrations/SlackV3/SlackV3.yml index 7d0b633d25ab..8342133c64e8 100644 --- a/Packs/Slack/Integrations/SlackV3/SlackV3.yml +++ b/Packs/Slack/Integrations/SlackV3/SlackV3.yml @@ -431,7 +431,62 @@ script: required: true description: Reset user session token in Slack. name: slack-user-session-reset - dockerimage: demisto/slackv3:1.0.0.69226 + - arguments: + - description: 'Supply this argument to only return channels with this name ' + name: name_filter + - defaultValue: public_channel + description: 'You can provide a comma separated list of other channels to include in your results. Possible options are: "public_channel", "private_channel", "mpim", and "im". Including these options may require changes to your Bot''s OAuth scopes in order to read channels like private, group message, or personal messages.' + name: channel_types + - defaultValue: 'true' + description: Default is true (exclude archived channels). This setting allows the command to read channels that have been archived + name: exclude_archived + - defaultValue: 100 + description: Set this argument to specify how many results to return. If you have more results than the limit you set, you will need to use the cursor argument to paginate your results. + name: limit + - description: 'Default is the first page of results. If you have more results than your limit, you need to paginate your results with this argument. This is found with the next_cursor attribute returned by a previous request''s response_metadata ' + name: cursor + description: 'List all of the channels in the organization workspace. This command required scopes depend on the type of channel-like object you''re working with. To use the command, you''ll need at least one of the channels:, groups:, im: or mpim: scopes corresponding to the conversation type you''re working with.' + name: slack-list-channels + outputs: + - contextPath: Slack.Channels.ID + description: The ID for the channel + type: string + - contextPath: Slack.Channels.Name + description: Name of the channel + type: string + - contextPath: Slack.Channels.Created + description: Epoch timestamp when the channel was created + type: number + - contextPath: Slack.Channels.Creator + description: ID for the creator of the channel + type: string + - contextPath: Slack.Channels.Purpose + description: The purpose, or description, of the channel + type: string + - arguments: + - description: The channel ID associated with the Slack channel + name: channel_id + required: true + - defaultValue: 100 + description: Set this argument to specify how many results to return. If you have more results than the limit you set, you will need to use the cursor argument to paginate your results. + name: limit + - description: The conversation ID. + name: conversation_id + description: Fetches a conversation's history of messages and events + name: slack-get-conversation-history + - arguments: + - name: channel_id + description: ID of the channel + required: true + - name: thread_timestamp + description: The timestamp of the thread, that can be extracted using "slack-get-conversation-history" command. + required: true + - defaultValue: 100 + name: limit + description: Set this argument to specify how many results to return. + description: Retrieves replies to specific messages, regardless of whether it's from a public or private channel, direct message, or otherwise. + name: slack-get-conversation-replies + dockerimage: demisto/slackv3:1.0.0.72328 longRunning: true runonce: false script: '-' diff --git a/Packs/Slack/Integrations/SlackV3/SlackV3_test.py b/Packs/Slack/Integrations/SlackV3/SlackV3_test.py index 31651c6dc100..a7b30a1357f6 100644 --- a/Packs/Slack/Integrations/SlackV3/SlackV3_test.py +++ b/Packs/Slack/Integrations/SlackV3/SlackV3_test.py @@ -1,17 +1,13 @@ import json as js import threading import io - import pytest import slack_sdk from slack_sdk.web.async_slack_response import AsyncSlackResponse from slack_sdk.web.slack_response import SlackResponse from slack_sdk.errors import SlackApiError - from unittest.mock import MagicMock - from CommonServerPython import * - import datetime @@ -20,8 +16,10 @@ def load_test_data(path): return f.read() +CHANNELS = load_test_data('./test_data/channels.txt') USERS = load_test_data('./test_data/users.txt') CONVERSATIONS = load_test_data('./test_data/conversations.txt') +MESSAGES = load_test_data('./test_data/messages.txt') PAYLOAD_JSON = load_test_data('./test_data/payload.txt') INTEGRATION_CONTEXT: dict @@ -132,11 +130,11 @@ def load_test_data(path): "event": { "type": "message", "subtype": "bot_message", - "text": "This is a bot message\nView it on: ", + "text": "This is a bot message\nView it on: ", "ts": "1644999987.969789", "username": "I'm a BOT", "icons": { - "image_48": "https:\/\/someimage.png" + "image_48": "https://someimage.png" }, "bot_id": "B01UZHGMQ9G", "channel": "C033HLL3N81", @@ -206,11 +204,11 @@ def load_test_data(path): "event": { "type": "message", "subtype": "This is missing", - "text": "This is a bot message\nView it on: ", + "text": "This is a bot message\nView it on: ", "ts": "1644999987.969789", "username": "I'm a BOT", "icons": { - "image_48": "https:\/\/someimage.png" + "image_48": "https://someimage.png" }, "bot_id": "W12345678", "channel": "C033HLL3N81", @@ -260,7 +258,7 @@ def load_test_data(path): "ts": "1645712173.407939", "username": "test", "icons": { - "image_48": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/bot_icons\/2021-07-14\/2273797940146_48.png" + "image_48": "https://s3-us-west-2.amazonaws.com/slack-files2/bot_icons/2021-07-14/2273797940146_48.png" }, "bot_id": "B0342JWALTG", "blocks": [{ @@ -300,7 +298,7 @@ def load_test_data(path): "state": { "values": {} }, - "response_url": "https:\/\/hooks.slack.com\/actions\/T019C4MM2VD\/3146697353558\/Y6ic5jAvlJ6p9ZU9HmyU9sPZ", + "response_url": "https://hooks.slack.com/actions/T019C4MM2VD/3146697353558/Y6ic5jAvlJ6p9ZU9HmyU9sPZ", "actions": [{ "action_id": "o2pI", "block_id": "06eO", @@ -4957,3 +4955,100 @@ def test_check_for_unanswered_questions(mocker): total_questions = js.loads(updated_context.get('questions')) assert len(total_questions) == 0 + + +def test_list_channels(mocker): + """ + Given: + A list of channels. + When: + Listing Channels. + Assert: + fields match and are listed. + """ + import SlackV3 + slack_response_mock = { + 'ok': True, + 'channels': json.loads(CHANNELS)} + mocker.patch.object(SlackV3, 'send_slack_request_sync', side_effect=[slack_response_mock, {'user': js.loads(USERS)[0]}]) + mocker.patch.object(demisto, 'args', return_value={'channel_id': 1, 'public_channel': 'public_channel', 'limit': 1}) + mocker.patch.object(demisto, 'results') + mocker.patch.object(demisto, 'setIntegrationContext', side_effect=set_integration_context) + # mocker.patch.object(SlackV3, 'send_slack_request_sync', side_effect=slack_response_mock) + SlackV3.list_channels() + assert demisto.results.called + assert demisto.results.call_args[0][0]['HumanReadable'] == '### Channels list for None with filter None\n' \ + '|Created|Creator|ID|Name|Purpose|\n|---|---|---|---|---|\n' \ + '| 1666361240 | spengler | C0475674L3Z | general | This is the' \ + ' one channel that will always include everyone. It’s a great'\ + ' spot for announcements and team-wide conversations. |\n' + assert demisto.results.call_args[0][0]['ContentsFormat'] == 'json' + + +def test_conversation_history(mocker): + """ + Given: + A set of conversations. + When: + Listing conversation history. + Assert: + Conversations are returned. + """ + import SlackV3 + slack_response_mock = { + 'ok': True, + 'messages': json.loads(MESSAGES)} + mocker.patch.object(SlackV3, 'send_slack_request_sync', + side_effect=[slack_response_mock, {'user': js.loads(USERS)[0]}, + {'user': js.loads(USERS)[0]}]) + mocker.patch.object(demisto, 'args', return_value={'channel_id': 1, 'conversation_id': 1, 'limit': 1}) + mocker.patch.object(demisto, 'setIntegrationContext', side_effect=set_integration_context) + mocker.patch.object(demisto, 'results') + + SlackV3.conversation_history() + + assert demisto.results.call_args[0][0]['HumanReadable'] == '### Channel details from Channel ID ' \ + '- 1\n|FullName|HasReplies|Name|Text|ThreadTimeStamp' \ + '|TimeStamp|Type|UserId|\n|---|---|---|---|---|' \ + '---|---|---|\n| spengler | No | spengler | There' \ + ' are two types of people in this world, those' \ + ' who can extrapolate from incomplete data... | N/A ' \ + '| 1690479909.804939 | message | U047D5QSZD4 |\n|' \ + ' spengler | Yes | spengler | Give me a fresh dad joke' \ + ' | 1690479887.647239 | 1690479887.647239 | message ' \ + '| U047D5QSZD4 |\n' + assert demisto.results.call_args[0][0]['ContentsFormat'] == 'json' + + +def test_conversation_replies(mocker): + """ + Given: + A conversation with replies + When: + Looking for conversations with replies + Assert: + Conversations has replies. + """ + import SlackV3 + mocker.patch.object(slack_sdk.WebClient, 'api_call') + mocker.patch.object(demisto, 'args', return_value={'channel_id': 1, 'thread_timestamp': 1234, 'limit': 1}) + mocker.patch.object(demisto, 'results') + slack_response_mock = { + 'ok': True, + 'messages': json.loads(MESSAGES)} + mocker.patch.object(SlackV3, 'send_slack_request_sync', + side_effect=[slack_response_mock, + {'user': js.loads(USERS)[0]}, + {'user': js.loads(USERS)[0]}]) + SlackV3.conversation_replies() + assert demisto.results.call_args[0][0]['HumanReadable'] == '### Channel details from Channel ID' \ + ' - 1\n|FullName|IsParent|Name|Text|ThreadTimeStamp' \ + '|TimeStamp|Type|UserId|\n|---|---|---|---|---|---' \ + '|---|---|\n| spengler | No | spengler | There are ' \ + 'two types of people in this world, those who can ' \ + 'extrapolate from incomplete data... | ' \ + ' | 1690479909.804939 | message | U047D5QSZD4 ' \ + '|\n| spengler | Yes | spengler | Give me a fresh dad' \ + ' joke | 1690479887.647239 | 1690479887.647239 | message |' \ + ' U047D5QSZD4 |\n' + assert demisto.results.call_args[0][0]['ContentsFormat'] == 'json' diff --git a/Packs/Slack/Integrations/SlackV3/test_data/channels.txt b/Packs/Slack/Integrations/SlackV3/test_data/channels.txt new file mode 100644 index 000000000000..ea150ec309e7 --- /dev/null +++ b/Packs/Slack/Integrations/SlackV3/test_data/channels.txt @@ -0,0 +1,40 @@ +{ + "context_team_id": "T047KKY8H7V", + "created": 1666361240, + "creator": "U047N6DC8VA", + "id": "C0475674L3Z", + "is_archived": false, + "is_channel": true, + "is_ext_shared": false, + "is_general": true, + "is_group": false, + "is_im": false, + "is_member": true, + "is_mpim": false, + "is_org_shared": false, + "is_pending_ext_shared": false, + "is_private": false, + "is_shared": false, + "name": "general", + "name_normalized": "general", + "num_members": 3, + "parent_conversation": null, + "pending_connected_team_ids": [], + "pending_shared": [], + "previous_names": [], + "purpose": { + "creator": "U047N6DC8VA", + "last_set": 1666361240, + "value": "This is the one channel that will always include everyone. It’s a great spot for announcements and team-wide conversations." + }, + "shared_team_ids": [ + "T047KKY8H7V" + ], + "topic": { + "creator": "", + "last_set": 0, + "value": "" + }, + "unlinked": 0, + "updated": 1681198909631 +} \ No newline at end of file diff --git a/Packs/Slack/Integrations/SlackV3/test_data/messages.txt b/Packs/Slack/Integrations/SlackV3/test_data/messages.txt new file mode 100644 index 000000000000..444843a0a439 --- /dev/null +++ b/Packs/Slack/Integrations/SlackV3/test_data/messages.txt @@ -0,0 +1,61 @@ +[ + { + "blocks": [ + { + "block_id": "fV/Yh", + "elements": [ + { + "elements": [ + { + "text": "There are two types of people in this world, those who can extrapolate from incomplete data...", + "type": "text" + } + ], + "type": "rich_text_section" + } + ], + "type": "rich_text" + } + ], + "client_msg_id": "69eee5b9-977a-482b-ae29-0cd9962c6948", + "team": "T047KKY8H7V", + "text": "There are two types of people in this world, those who can extrapolate from incomplete data...", + "ts": "1690479909.804939", + "type": "message", + "user": "U047D5QSZD4" + }, + { + "blocks": [ + { + "block_id": "tdXt", + "elements": [ + { + "elements": [ + { + "text": "Give me a fresh dad joke", + "type": "text" + } + ], + "type": "rich_text_section" + } + ], + "type": "rich_text" + } + ], + "client_msg_id": "3ba949bd-017d-4b93-9b5c-8b0fa406c9c6", + "is_locked": false, + "latest_reply": "1690479892.219209", + "reply_count": 1, + "reply_users": [ + "U047D5QSZD4" + ], + "reply_users_count": 1, + "subscribed": false, + "team": "T047KKY8H7V", + "text": "Give me a fresh dad joke", + "thread_ts": "1690479887.647239", + "ts": "1690479887.647239", + "type": "message", + "user": "U047D5QSZD4" + } +] \ No newline at end of file diff --git a/Packs/Slack/ReleaseNotes/3_2_0.md b/Packs/Slack/ReleaseNotes/3_2_0.md new file mode 100644 index 000000000000..374503f03d57 --- /dev/null +++ b/Packs/Slack/ReleaseNotes/3_2_0.md @@ -0,0 +1,10 @@ + +#### Integrations + +##### Slack v3 +- Added a new commands: + - *slack-list-channels* + - *slack-get-conversation-history* + - *slack-get-conversation-replies* +- Updated the Docker image to: *demisto/slackv3:1.0.0.72328*. + \ No newline at end of file diff --git a/Packs/Slack/pack_metadata.json b/Packs/Slack/pack_metadata.json index 9187f4cd563c..3976d9f928e3 100644 --- a/Packs/Slack/pack_metadata.json +++ b/Packs/Slack/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Slack", "description": "Interact with Slack API - collect logs, send messages and notifications to your Slack team.", "support": "xsoar", - "currentVersion": "3.1.46", + "currentVersion": "3.2.0", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "",