Skip to content

Commit 2a84dc5

Browse files
Updates to slack-adapter (#1559)
* Updates to slack-adapter * black * SlackAdapter: Updates per feedback * slack_client: pylint fix * slack_client: black Co-authored-by: Axel Suárez <[email protected]>
1 parent 9506230 commit 2a84dc5

File tree

9 files changed

+139
-131
lines changed

9 files changed

+139
-131
lines changed

libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# --------------------------------------------------------------------------
77

88
from .about import __version__
9-
from .slack_options import SlackAdapterOptions
9+
from .slack_client_options import SlackClientOptions
1010
from .slack_client import SlackClient
1111
from .slack_adapter import SlackAdapter
1212
from .slack_payload import SlackPayload
@@ -15,10 +15,11 @@
1515
from .activity_resourceresponse import ActivityResourceResponse
1616
from .slack_request_body import SlackRequestBody
1717
from .slack_helper import SlackHelper
18+
from .slack_adatper_options import SlackAdapterOptions
1819

1920
__all__ = [
2021
"__version__",
21-
"SlackAdapterOptions",
22+
"SlackClientOptions",
2223
"SlackClient",
2324
"SlackAdapter",
2425
"SlackPayload",
@@ -27,4 +28,5 @@
2728
"ActivityResourceResponse",
2829
"SlackRequestBody",
2930
"SlackHelper",
31+
"SlackAdapterOptions",
3032
]

libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adapter.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .activity_resourceresponse import ActivityResourceResponse
2121
from .slack_client import SlackClient
2222
from .slack_helper import SlackHelper
23+
from .slack_adatper_options import SlackAdapterOptions
2324

2425

2526
class SlackAdapter(BotAdapter, ABC):
@@ -32,10 +33,12 @@ def __init__(
3233
self,
3334
client: SlackClient,
3435
on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None,
36+
options: SlackAdapterOptions = None,
3537
):
3638
super().__init__(on_turn_error)
3739
self.slack_client = client
3840
self.slack_logged_in = False
41+
self.options = options if options else SlackAdapterOptions()
3942

4043
async def send_activities(
4144
self, context: TurnContext, activities: List[Activity]
@@ -62,7 +65,7 @@ async def send_activities(
6265
if activity.type == ActivityTypes.message:
6366
message = SlackHelper.activity_to_slack(activity)
6467

65-
slack_response = await self.slack_client.post_message_to_slack(message)
68+
slack_response = await self.slack_client.post_message(message)
6669

6770
if slack_response and slack_response.status_code / 100 == 2:
6871
resource_response = ActivityResourceResponse(
@@ -99,8 +102,8 @@ async def update_activity(self, context: TurnContext, activity: Activity):
99102
raise Exception("Activity.conversation is required")
100103

101104
message = SlackHelper.activity_to_slack(activity)
102-
results = await self.slack_client.update(
103-
timestamp=message.ts, channel_id=message.channel, text=message.text,
105+
results = await self.slack_client.chat_update(
106+
ts=message.ts, channel=message.channel, text=message.text,
104107
)
105108

106109
if results.status_code / 100 != 2:
@@ -130,17 +133,17 @@ async def delete_activity(
130133
if not context.activity.timestamp:
131134
raise Exception("Activity.timestamp is required")
132135

133-
await self.slack_client.delete_message(
134-
channel_id=reference.channel_id, timestamp=context.activity.timestamp
136+
await self.slack_client.chat_delete(
137+
channel=reference.conversation.id, ts=reference.activity_id
135138
)
136139

137140
async def continue_conversation(
138141
self,
139142
reference: ConversationReference,
140143
callback: Callable,
141-
bot_id: str = None,
144+
bot_id: str = None, # pylint: disable=unused-argument
142145
claims_identity: ClaimsIdentity = None,
143-
audience: str = None,
146+
audience: str = None, # pylint: disable=unused-argument
144147
):
145148
"""
146149
Send a proactive message to a conversation.
@@ -203,15 +206,20 @@ async def process(self, req: Request, logic: Callable) -> Response:
203206
self.slack_logged_in = True
204207

205208
body = await req.text()
209+
210+
if (
211+
self.options.verify_incoming_requests
212+
and not self.slack_client.verify_signature(req, body)
213+
):
214+
return SlackHelper.response(
215+
req, 401, "Rejected due to mismatched header signature"
216+
)
217+
206218
slack_body = SlackHelper.deserialize_body(req.content_type, body)
207219

208220
if slack_body.type == "url_verification":
209221
return SlackHelper.response(req, 200, slack_body.challenge)
210222

211-
if not self.slack_client.verify_signature(req, body):
212-
text = "Rejected due to mismatched header signature"
213-
return SlackHelper.response(req, 401, text)
214-
215223
if (
216224
not self.slack_client.options.slack_verification_token
217225
and slack_body.token != self.slack_client.options.slack_verification_token
@@ -231,7 +239,9 @@ async def process(self, req: Request, logic: Callable) -> Response:
231239
slack_body, self.slack_client
232240
)
233241
else:
234-
raise Exception(f"Unknown Slack event type {slack_body.type}")
242+
return SlackHelper.response(
243+
req, 200, f"Unknown Slack event type {slack_body.type}"
244+
)
235245

236246
context = TurnContext(self, activity)
237247
await self.run_pipeline(context, logic)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
5+
class SlackAdapterOptions:
6+
"""
7+
Class for defining implementation of the SlackAdapter Options.
8+
"""
9+
10+
def __init__(self):
11+
self.verify_incoming_requests = True

libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from slack.web.slack_response import SlackResponse
1515

1616
from botbuilder.schema import Activity
17-
from botbuilder.adapters.slack import SlackAdapterOptions
17+
from botbuilder.adapters.slack.slack_client_options import SlackClientOptions
1818
from botbuilder.adapters.slack.slack_message import SlackMessage
1919

2020
POST_MESSAGE_URL = "https://slack.com/api/chat.postMessage"
@@ -26,7 +26,7 @@ class SlackClient(WebClient):
2626
Slack client that extends https://github.com/slackapi/python-slackclient.
2727
"""
2828

29-
def __init__(self, options: SlackAdapterOptions):
29+
def __init__(self, options: SlackClientOptions):
3030
if not options or not options.slack_bot_token:
3131
raise Exception("SlackAdapterOptions and bot_token are required")
3232

@@ -374,19 +374,10 @@ async def files_upload_ex(
374374

375375
return await self.files_upload(file=file, content=content, **args)
376376

377-
async def get_bot_user_by_team(self, activity: Activity) -> str:
378-
if self.identity:
379-
return self.identity
380-
381-
if not activity.conversation.properties["team"]:
382-
return None
383-
384-
user = await self.options.get_bot_user_by_team(
385-
activity.conversation.properties["team"]
386-
)
387-
if user:
388-
return user
389-
raise Exception("Missing credentials for team.")
377+
async def get_bot_user_identity(
378+
self, activity: Activity # pylint: disable=unused-argument
379+
) -> str:
380+
return self.identity
390381

391382
def verify_signature(self, req: Request, body: str) -> bool:
392383
timestamp = req.headers["X-Slack-Request-Timestamp"]
@@ -402,7 +393,7 @@ def verify_signature(self, req: Request, body: str) -> bool:
402393

403394
return computed_signature == received_signature
404395

405-
async def post_message_to_slack(self, message: SlackMessage) -> SlackResponse:
396+
async def post_message(self, message: SlackMessage) -> SlackResponse:
406397
if not message:
407398
return None
408399

Original file line numberDiff line numberDiff line change
@@ -1,53 +1,32 @@
1-
# Copyright (c) Microsoft Corporation. All rights reserved.
2-
# Licensed under the MIT License.
3-
4-
5-
class SlackAdapterOptions:
6-
"""
7-
Defines the implementation of the SlackAdapter options.
8-
"""
9-
10-
def __init__(
11-
self,
12-
slack_verification_token: str,
13-
slack_bot_token: str,
14-
slack_client_signing_secret: str,
15-
):
16-
"""
17-
Initializes a new instance of SlackAdapterOptions.
18-
19-
:param slack_verification_token: A token for validating the origin of incoming webhooks.
20-
:type slack_verification_token: str
21-
:param slack_bot_token: A token for a bot to work on a single workspace.
22-
:type slack_bot_token: str
23-
:param slack_client_signing_secret: The token used to validate that incoming webhooks originated from Slack.
24-
:type slack_client_signing_secret: str
25-
"""
26-
self.slack_verification_token = slack_verification_token
27-
self.slack_bot_token = slack_bot_token
28-
self.slack_client_signing_secret = slack_client_signing_secret
29-
self.slack_client_id = None
30-
self.slack_client_secret = None
31-
self.slack_redirect_uri = None
32-
self.slack_scopes = [str]
33-
34-
async def get_token_for_team(self, team_id: str) -> str:
35-
"""
36-
Receives a Slack team ID and returns the bot token associated with that team. Required for multi-team apps.
37-
38-
:param team_id: The team ID.
39-
:type team_id: str
40-
:raises: :func:`NotImplementedError`
41-
"""
42-
raise NotImplementedError()
43-
44-
async def get_bot_user_by_team(self, team_id: str) -> str:
45-
"""
46-
A method that receives a Slack team ID and returns the bot user ID associated with that team. Required for
47-
multi-team apps.
48-
49-
:param team_id: The team ID.
50-
:type team_id: str
51-
:raises: :func:`NotImplementedError`
52-
"""
53-
raise NotImplementedError()
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
5+
class SlackClientOptions:
6+
"""
7+
Defines the implementation of the SlackClient options.
8+
"""
9+
10+
def __init__(
11+
self,
12+
slack_verification_token: str,
13+
slack_bot_token: str,
14+
slack_client_signing_secret: str,
15+
):
16+
"""
17+
Initializes a new instance of SlackClientOptions.
18+
19+
:param slack_verification_token: A token for validating the origin of incoming webhooks.
20+
:type slack_verification_token: str
21+
:param slack_bot_token: A token for a bot to work on a single workspace.
22+
:type slack_bot_token: str
23+
:param slack_client_signing_secret: The token used to validate that incoming webhooks originated from Slack.
24+
:type slack_client_signing_secret: str
25+
"""
26+
self.slack_verification_token = slack_verification_token
27+
self.slack_bot_token = slack_bot_token
28+
self.slack_client_signing_secret = slack_client_signing_secret
29+
self.slack_client_id = None
30+
self.slack_client_secret = None
31+
self.slack_redirect_uri = None
32+
self.slack_scopes = [str]

libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -124,24 +124,35 @@ def payload_to_activity(payload: SlackPayload) -> Activity:
124124

125125
activity = Activity(
126126
channel_id="slack",
127-
conversation=ConversationAccount(id=payload.channel.id, properties={}),
127+
conversation=ConversationAccount(id=payload.channel["id"], properties={}),
128128
from_property=ChannelAccount(
129-
id=payload.message.bot_id if payload.message.bot_id else payload.user.id
129+
id=payload.message.bot_id
130+
if payload.message.bot_id
131+
else payload.user["id"]
130132
),
131133
recipient=ChannelAccount(),
132134
channel_data=payload,
133135
text=None,
134136
type=ActivityTypes.event,
137+
value=payload,
135138
)
136139

137140
if payload.thread_ts:
138141
activity.conversation.properties["thread_ts"] = payload.thread_ts
139142

140-
if payload.actions and (
141-
payload.type == "block_actions" or payload.type == "interactive_message"
142-
):
143-
activity.type = ActivityTypes.message
144-
activity.text = payload.actions.value
143+
if payload.actions:
144+
action = payload.actions[0]
145+
146+
if action["type"] == "button":
147+
activity.text = action["value"]
148+
elif action["type"] == "select":
149+
selected_option = action["selected_options"]
150+
activity.text = selected_option["value"] if selected_option else None
151+
elif action["type"] == "static_select":
152+
activity.text = action["selected_options"]["value"]
153+
154+
if activity.text:
155+
activity.type = ActivityTypes.message
145156

146157
return activity
147158

@@ -176,26 +187,27 @@ async def event_to_activity(event: SlackEvent, client: SlackClient) -> Activity:
176187
type=ActivityTypes.event,
177188
)
178189

179-
if event.thread_ts:
180-
activity.conversation.properties["thread_ts"] = event.thread_ts
181-
182190
if not activity.conversation.id:
183191
if event.item and event.item_channel:
184192
activity.conversation.id = event.item_channel
185193
else:
186194
activity.conversation.id = event.team
187195

188-
activity.recipient.id = await client.get_bot_user_by_team(activity=activity)
196+
activity.recipient.id = await client.get_bot_user_identity(activity=activity)
197+
198+
if event.thread_ts:
199+
activity.conversation.properties["thread_ts"] = event.thread_ts
189200

190-
# If this is a message originating from a user, we'll mark it as such
191-
# If this is a message from a bot (bot_id != None), we want to ignore it by
192-
# leaving the activity type as Event. This will stop it from being included in dialogs,
193-
# but still allow the Bot to act on it if it chooses (via ActivityHandler.on_event_activity).
194-
# NOTE: This catches a message from ANY bot, including this bot.
195-
# Note also, bot_id here is not the same as bot_user_id so we can't (yet) identify messages
196-
# originating from this bot without doing an additional API call.
197201
if event.type == "message" and not event.subtype and not event.bot_id:
198-
activity.type = ActivityTypes.message
202+
if not event.subtype:
203+
activity.type = ActivityTypes.message
204+
activity.text = event.text
205+
206+
activity.conversation.properties["channel_type"] = event.channel_type
207+
activity.value = event
208+
else:
209+
activity.name = event.type
210+
activity.value = event
199211

200212
return activity
201213

@@ -226,9 +238,11 @@ async def command_to_activity(
226238
channel_data=body,
227239
text=body.text,
228240
type=ActivityTypes.event,
241+
name="Command",
242+
value=body.command,
229243
)
230244

231-
activity.recipient.id = await client.get_bot_user_by_team(activity)
245+
activity.recipient.id = await client.get_bot_user_identity(activity)
232246
activity.conversation.properties["team"] = body.team_id
233247

234248
return activity

libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_message.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ def __init__(self, **kwargs):
2222
self.icons = kwargs.get("icons")
2323
self.blocks: [Block] = kwargs.get("blocks")
2424

25-
self.attachments = None
26-
if "attachments" in kwargs:
27-
# Create proper Attachment objects
28-
# It would appear that we can get dict fields from the wire that aren't defined
29-
# in the Attachment class. So only pass in known fields.
25+
# Create proper Attachment objects
26+
# It would appear that we can get dict fields from the wire that aren't defined
27+
# in the Attachment class. So only pass in known fields.
28+
attachments = kwargs.get("attachments")
29+
if attachments is not None:
3030
self.attachments = [
3131
Attachment(**{x: att[x] for x in att if x in Attachment.attributes})
3232
for att in kwargs.get("attachments")

0 commit comments

Comments
 (0)