forked from slackapi/python-slack-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathobjects.py
More file actions
423 lines (344 loc) · 14.7 KB
/
objects.py
File metadata and controls
423 lines (344 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
from datetime import datetime
from typing import List, Optional, Set, Union
from . import BaseObject, JsonObject, JsonValidator, extract_json
ButtonStyles = {"danger", "primary"}
DynamicSelectElementTypes = {"channels", "conversations", "users"}
class Link(BaseObject):
def __init__(self, *, url: str, text: str):
"""
Base class used to generate links in Slack's not-quite Markdown, not quite HTML
syntax
https://api.slack.com/docs/message-formatting#linking_to_urls
Args:
url: The URL (or other special text, see subclasses) to link to
text: Text to display on the link. Often only a fallback.
"""
self.url = url
self.text = text
def __str__(self):
if self.text:
separator = "|"
else:
separator = ""
return f"<{self.url}{separator}{self.text}>"
class DateLink(Link):
def __init__(
self,
*,
date: Union[datetime, int],
date_format: str,
fallback: str,
link: Optional[str] = None,
):
"""
Messages containing a date or time should be displayed in the local timezone
of the person seeing the message. The <!date> command will format a Unix
timestamp using tokens within a string that you set. You may also optionally
link a date using a standard URL. A <!date> must include some fallback text
for older Slack clients (in case the conversion fails).
https://api.slack.com/docs/message-formatting#formatting_dates
Args:
date: A Unix timestamp (as int) or datetime.datetime object
date_format: Describe your date and time as a string, using any
combination of the following tokens and plain text: {date_num}, {date},
{date_short}, {date_long}, {date_pretty}, {date_short_pretty},
{date_long_pretty}, {time}, {time_secs}
fallback: text to display on clients that don't support date rendering
link: an optional URL to hyperlink to with this date
"""
if isinstance(date, datetime):
epoch = int(date.timestamp())
else:
epoch = date
if link is not None:
link = f"^{link}"
else:
link = ""
super().__init__(url=f"{epoch}^{date_format}{link}", text=fallback)
class ObjectLink(Link):
prefix_mapping = {
"C": "#", # channel
"G": "#", # group message
"U": "@", # user
"W": "@", # workspace user (enterprise)
"B": "@", # bot user
"S": "!subteam^", # user groups, originally known as subteams
}
def __init__(self, *, object_id: str, text: str = ""):
"""
Convenience class to create links to specific object types
https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
Args:
object_id: An ID to create a link to, eg 'U12345' for a user,
or 'C6789' for a channel
text: Optional text to attach to the link - may or may not be
displayed by Slack client
"""
prefix = self.prefix_mapping.get(object_id[0].upper(), "@")
super().__init__(url=f"{prefix}{object_id}", text=text)
class ChannelLink(Link):
def __init__(self):
"""
Represents an @channel link, which notifies everyone present in this channel.
https://api.slack.com/docs/message-formatting#variables
"""
super().__init__(url="!channel", text="channel")
class HereLink(Link):
def __init__(self):
"""
Represents an @here link, which notifies all online users of this channel.
https://api.slack.com/docs/message-formatting#variables
"""
super().__init__(url="!here", text="here")
class EveryoneLink(Link):
def __init__(self):
"""
Represents an @everyone link, which notifies all users of this workspace.
https://api.slack.com/docs/message-formatting#variables
"""
super().__init__(url="!everyone", text="everyone")
class TextObject(JsonObject):
attributes = {"text", "type"}
def __init__(self, *, text: str, subtype: str):
"""
Super class for new text "objects" used in Block kit
"""
self.text = text
self.subtype = subtype
def to_dict(self) -> dict:
json = super().to_dict()
json["type"] = self.subtype
return json
class PlainTextObject(TextObject):
@property
def attributes(self) -> Set[str]:
return super().attributes.union({"emoji"})
def __init__(self, *, text: str, emoji: bool = True):
"""
A plain text object, meaning markdown characters will not be parsed as
formatting information.
https://api.slack.com/reference/messaging/composition-objects#text
Args:
emoji: Whether to escape emoji in text into Slack's :emoji: format
"""
super().__init__(text=text, subtype="plain_text")
self.emoji = emoji
@staticmethod
def direct_from_string(text: str) -> dict:
"""
Transforms a string into the required object shape to act as a PlainTextObject
"""
return PlainTextObject(text=text).to_dict()
class MarkdownTextObject(TextObject):
@property
def attributes(self) -> Set[str]:
return super().attributes.union({"verbatim"})
def __init__(self, *, text: str, verbatim: bool = False):
"""
A Markdown text object, meaning markdown characters will be parsed as
formatting information.
https://api.slack.com/reference/messaging/composition-objects#text
Args:
verbatim: When set to false (as is default) URLs will be
auto-converted into links, conversation names will be link-ified, and
certain mentions will be automatically parsed.
"""
super().__init__(text=text, subtype="mrkdwn")
self.verbatim = verbatim
@staticmethod
def direct_from_string(text: str) -> dict:
"""
Transforms a string into the required object shape to act as a
MarkdownTextObject
"""
return MarkdownTextObject(text=text).to_dict()
@staticmethod
def direct_from_link(link: Link, title: str = "") -> dict:
"""
Transform a Link object directly into the required object shape to act as a
MarkdownTextObject
"""
if title:
title = f": {title}"
return MarkdownTextObject(text=f"{link}{title}").to_dict()
class ConfirmObject(JsonObject):
attributes = {} # no attributes because to_dict has unique implementations
title_max_length = 100
text_max_length = 300
confirm_max_length = 30
deny_max_length = 30
def __init__(
self,
*,
title: str,
text: Union[TextObject, str],
confirm: str = "Yes",
deny: str = "No",
):
"""
An object that defines a dialog that provides a confirmation step to any
interactive element. This dialog will ask the user to confirm their action by
offering a confirm and deny button.
https://api.slack.com/reference/messaging/composition-objects#confirm
Args:
title: A string that defines the dialog's title. Cannot exceed 100
characters.
text: A string or TextObject that defines the explanatory text
that appears in the confirm dialog. Cannot exceed 300 characters.
confirm: A string to define the text on the button that confirms the
action. Cannot exceed 30 characters.
deny: A string to define the text on the button that cancels the
action. Cannot exceed 30 characters.
"""
self.title = title
self.text = text
self.confirm = confirm
self.deny = deny
@JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
def title_length(self):
return len(self.title) <= self.title_max_length
@JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
def text_length(self):
if isinstance(self.text, TextObject):
return len(self.text.text) <= self.text_max_length
else:
return len(self.text) <= self.text_max_length
@JsonValidator(f"confirm attribute cannot exceed {confirm_max_length} characters")
def confirm_length(self):
return len(self.confirm) <= self.confirm_max_length
@JsonValidator(f"deny attribute cannot exceed {deny_max_length} characters")
def deny_length(self):
return len(self.deny) <= self.deny_max_length
def to_dict(self, option_type: str = "block") -> dict:
if option_type == "action":
# deliberately skipping JSON validators here - can't find documentation
# on actual limits here
return {
"text": self.text,
"title": self.title,
"ok_text": self.confirm if self.confirm != "Yes" else "Okay",
"dismiss_text": self.deny if self.deny != "No" else "Cancel",
}
else:
self.validate_json()
json = {
"title": PlainTextObject.direct_from_string(self.title),
"confirm": PlainTextObject.direct_from_string(self.confirm),
"deny": PlainTextObject.direct_from_string(self.deny),
}
if isinstance(self.text, TextObject):
json["text"] = self.text.to_dict()
else:
json["text"] = MarkdownTextObject.direct_from_string(self.text)
return json
class Option(JsonObject):
"""
Option object used in dialogs, legacy message actions, and blocks
JSON must be retrieved with an explicit option_type - the Slack API has
different required formats in different situations
"""
attributes = {} # no attributes because to_dict has unique implementations
label_max_length = 75
value_max_length = 75
def __init__(self, *, label: str, value: str, description: Optional[str] = None):
"""
An object that represents a single selectable item in a block element (
SelectElement, OverflowMenuElement) or dialog element
(StaticDialogSelectElement)
Blocks:
https://api.slack.com/reference/messaging/composition-objects#option
Dialogs:
https://api.slack.com/dialogs#select_elements
Legacy interactive attachments:
https://api.slack.com/docs/interactive-message-field-guide#option_fields
Args:
label: A short, user-facing string to label this option to users.
Cannot exceed 75 characters.
value: A short string that identifies this particular option to your
application. It will be part of the payload when this option is selected
. Cannot exceed 75 characters.
description: A user-facing string that provides more details about
this option. Only supported in legacy message actions, not in blocks or
dialogs.
"""
self.label = label
self.value = value
self.description = description
@JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
def label_length(self):
return len(self.label) <= self.label_max_length
@JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
def value_length(self):
return len(self.value) <= self.value_max_length
def to_dict(self, option_type: str = "block") -> dict:
"""
Different parent classes must call this with a valid value from OptionTypes -
either "dialog", "action", or "block", so that JSON is returned in the
correct shape.
"""
self.validate_json()
if option_type == "dialog":
return {"label": self.label, "value": self.value}
elif option_type == "action":
json = {"text": self.label, "value": self.value}
if self.description is not None:
json["description"] = self.description
return json
else: # if option_type == "block"; this should be the most common case
return {
"text": PlainTextObject.direct_from_string(self.label),
"value": self.value,
}
@staticmethod
def from_single_value(value_and_label: str):
"""
Creates a simple Option instance with the same value and label
"""
return Option(value=value_and_label, label=value_and_label)
class OptionGroup(JsonObject):
"""
JSON must be retrieved with an explicit option_type - the Slack API has
different required formats in different situations
"""
attributes = {} # no attributes because to_dict has unique implementations
label_max_length = 75
options_max_length = 100
def __init__(self, *, label: str, options: List[Option]):
"""
Create a group of Option objects - pass in a label (that will be part of the
UI) and a list of Option objects.
Blocks:
https://api.slack.com/reference/messaging/composition-objects#option-group
Dialogs:
https://api.slack.com/dialogs#select_elements
Legacy interactive attachments:
https://api.slack.com/docs/interactive-message-field-guide#option_groups_to_place_within_message_menu_actions
Args:
label: Text to display at the top of this group of options.
options: A list of no more than 100 Option objects.
""" # noqa prevent flake8 blowing up on the long URL
self.label = label
self.options = options
@JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
def label_length(self):
return len(self.label) <= self.label_max_length
@JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
def options_length(self):
return len(self.options) <= self.options_max_length
def to_dict(self, option_type: str = "block") -> dict:
self.validate_json()
if option_type == "dialog":
return {
"label": self.label,
"options": extract_json(self.options, option_type),
}
elif option_type == "action":
return {
"text": self.label,
"options": extract_json(self.options, option_type),
}
else: # if option_type == "block"; this should be the most common case
return {
"label": PlainTextObject.direct_from_string(self.label),
"options": extract_json(self.options, option_type),
}