1import random 2import re 3import string 4from abc import ABCMeta, abstractmethod 5from typing import List, Optional, Set, Union 6 7from . import EnumValidator, JsonObject, JsonValidator, extract_json 8from .objects import ButtonStyles, ConfirmObject, Option, OptionGroup, PlainTextObject 9 10 11class BlockElement(JsonObject, metaclass=ABCMeta): 12 def __init__(self, *, subtype: str): 13 self.subtype = subtype 14 15 def to_dict(self) -> dict: 16 json = super().to_dict() 17 json["type"] = self.subtype 18 return json 19 20 21class InteractiveElement(BlockElement): 22 @property 23 def attributes(self) -> Set[str]: 24 return super().attributes.union({"action_id"}) 25 26 action_id_max_length = 255 27 28 def __init__(self, *, action_id: str, subtype: str): 29 super().__init__(subtype=subtype) 30 self.action_id = action_id 31 32 @JsonValidator( 33 f"action_id attribute cannot exceed {action_id_max_length} characters" 34 ) 35 def action_id_length(self): 36 return len(self.action_id) <= self.action_id_max_length 37 38 39class ImageElement(BlockElement): 40 @property 41 def attributes(self) -> Set[str]: 42 return super().attributes.union({"alt_text", "image_url"}) 43 44 image_url_max_length = 3000 45 alt_text_max_length = 2000 46 47 def __init__(self, *, image_url: str, alt_text: str): 48 """ 49 An element to insert an image - this element can be used in section and 50 context blocks only. If you want a block with only an image in it, 51 you're looking for the image block. 52 53 https://api.slack.com/reference/messaging/block-elements#image 54 55 Args: 56 image_url: Publicly hosted URL to be displayed. Cannot exceed 3000 57 characters. 58 alt_text: Plain text summary of image. Cannot exceed 2000 characters. 59 """ 60 super().__init__(subtype="image") 61 self.image_url = image_url 62 self.alt_text = alt_text 63 64 @JsonValidator( 65 f"image_url attribute cannot exceed {image_url_max_length} characters" 66 ) 67 def image_url_length(self): 68 return len(self.image_url) <= self.image_url_max_length 69 70 @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters") 71 def alt_text_length(self): 72 return len(self.alt_text) <= self.alt_text_max_length 73 74 75class ButtonElement(InteractiveElement): 76 @property 77 def attributes(self) -> Set[str]: 78 return super().attributes.union({"style", "value"}) 79 80 text_max_length = 75 81 value_max_length = 75 82 83 def __init__( 84 self, 85 *, 86 text: str, 87 action_id: str, 88 value: str, 89 style: Optional[str] = None, 90 confirm: Optional[ConfirmObject] = None, 91 ): 92 """ 93 An interactive element that inserts a button. The button can be a trigger for 94 anything from opening a simple link to starting a complex workflow. 95 96 https://api.slack.com/reference/messaging/block-elements#button 97 98 Args: 99 text: String that defines the button's text. Cannot exceed 75 characters. 100 action_id: ID to be used for this action - should be unique. Cannot 101 exceed 255 characters. 102 value: The value to send along with the interaction payload. Cannot 103 exceed 2000 characters. 104 style: "primary" or "danger" to add specific styling to this button. 105 confirm: A ConfirmObject that defines an optional confirmation dialog 106 after this element is interacted with. 107 """ 108 super().__init__(action_id=action_id, subtype="button") 109 self.text = text 110 self.value = value 111 self.style = style 112 self.confirm = confirm 113 114 @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") 115 def text_length(self): 116 return len(self.text) <= self.text_max_length 117 118 @JsonValidator(f"value attribute cannot exceed {value_max_length} characters") 119 def value_length(self): 120 return len(self.value) <= self.value_max_length 121 122 @EnumValidator("style", ButtonStyles) 123 def style_valid(self): 124 return self.style is None or self.style in ButtonStyles 125 126 def to_dict(self) -> dict: 127 json = super().to_dict() 128 json["text"] = PlainTextObject.direct_from_string(self.text) 129 if self.confirm is not None: 130 json["confirm"] = extract_json(self.confirm) 131 return json 132 133 134class LinkButtonElement(ButtonElement): 135 @property 136 def attributes(self) -> Set[str]: 137 return super().attributes.union({"url"}) 138 139 url_max_length = 3000 140 141 def __init__(self, *, text: str, url: str, style: Optional[str] = None): 142 """ 143 A simple button that simply opens a given URL. You will still receive an 144 interaction payload and will need to send an acknowledgement response. 145 146 https://api.slack.com/reference/messaging/block-elements#button 147 148 Args: 149 text: String that defines the button's text. Cannot exceed 75 characters. 150 url: A URL to load in the user's browser when the button is 151 clicked. Maximum length for this field is 3000 characters. 152 style: "primary" or "danger" to add specific styling to this button. 153 """ 154 random_id = "".join(random.choice(string.ascii_uppercase) for _ in range(16)) 155 super().__init__(text=text, action_id=random_id, value="", style=style) 156 self.url = url 157 158 @JsonValidator(f"url attribute cannot exceed {url_max_length} characters") 159 def url_length(self): 160 return len(self.url) <= self.url_max_length 161 162 163class AbstractSelector(InteractiveElement, metaclass=ABCMeta): 164 placeholder_max_length = 150 165 166 def __init__( 167 self, 168 *, 169 placeholder: str, 170 action_id: str, 171 subtype: str, 172 confirm: Optional[ConfirmObject] = None, 173 ): 174 super().__init__(action_id=action_id, subtype=subtype) 175 self.placeholder = placeholder 176 self.confirm = confirm 177 178 @JsonValidator( 179 f"placeholder attribute cannot exceed {placeholder_max_length} characters" 180 ) 181 def placeholder_length(self): 182 return len(self.placeholder) <= self.placeholder_max_length 183 184 def to_dict(self,) -> dict: 185 json = super().to_dict() 186 json["placeholder"] = PlainTextObject.direct_from_string(self.placeholder) 187 if self.confirm is not None: 188 json["confirm"] = extract_json(self.confirm) 189 return json 190 191 192class SelectElement(AbstractSelector): 193 options_max_length = 100 194 195 def __init__( 196 self, 197 *, 198 placeholder: str, 199 action_id: str, 200 options: List[Union[Option, OptionGroup]], 201 initial_option: Optional[Option] = None, 202 confirm: Optional[ConfirmObject] = None, 203 ): 204 """ 205 This is the simplest form of select menu, with a static list of options 206 passed in when defining the element. 207 208 https://api.slack.com/reference/messaging/block-elements#static-select 209 210 Args: 211 placeholder: placeholder text shown on this element. Cannot exceed 150 212 characters. 213 action_id: ID to be used for this action - should be unique. Cannot 214 exceed 255 characters. 215 options: An array of Option or OptionGroup objects. Maximum number of 216 options is 100. 217 initial_option: A single option that exactly matches one of the 218 options within options or option_groups. This option will be selected 219 when the menu initially loads. 220 confirm: A ConfirmObject that defines an optional confirmation dialog 221 after this element is interacted with. 222 """ 223 super().__init__( 224 placeholder=placeholder, 225 action_id=action_id, 226 subtype="static_select", 227 confirm=confirm, 228 ) 229 self.options = options 230 self.initial_option = initial_option 231 232 @JsonValidator(f"options attribute cannot exceed {options_max_length} elements") 233 def options_length(self): 234 return len(self.options) <= self.options_max_length 235 236 def to_dict(self) -> dict: 237 json = super().to_dict() 238 if isinstance(self.options[0], OptionGroup): 239 json["option_groups"] = extract_json(self.options, "block") 240 else: 241 json["options"] = extract_json(self.options, "block") 242 if self.initial_option is not None: 243 json["initial_option"] = extract_json(self.initial_option, "block") 244 return json 245 246 247class ExternalDataSelectElement(AbstractSelector): 248 @property 249 def attributes(self) -> Set[str]: 250 return super().attributes.union({"min_query_length"}) 251 252 def __init__( 253 self, 254 *, 255 placeholder: str, 256 action_id: str, 257 initial_option: Union[Optional[Option], Optional[OptionGroup]] = None, 258 min_query_length: Optional[int] = None, 259 confirm: Optional[ConfirmObject] = None, 260 ): 261 """ 262 This select menu will load its options from an external data source, allowing 263 for a dynamic list of options. 264 265 https://api.slack.com/reference/messaging/block-elements#external-select 266 267 Args: 268 placeholder: placeholder text shown on this element. Cannot exceed 150 269 characters. 270 action_id: ID to be used for this action - should be unique. Cannot 271 exceed 255 characters. 272 initial_option: A single option that exactly matches one of the 273 options within options or option_groups. This option will be selected 274 when the menu initially loads. 275 min_query_length: When the typeahead field is used, a request will be 276 sent on every character change. If you prefer fewer requests or more 277 fully ideated queries, use the min_query_length attribute to tell Slack 278 the fewest number of typed characters required before dispatch. 279 confirm: A ConfirmObject that defines an optional confirmation dialog 280 after this element is interacted with. 281 """ 282 super().__init__( 283 action_id=action_id, 284 subtype="external_select", 285 placeholder=placeholder, 286 confirm=confirm, 287 ) 288 self.initial_option = initial_option 289 self.min_query_length = min_query_length 290 291 def to_dict(self) -> dict: 292 json = super().to_dict() 293 if self.initial_option is not None: 294 json["initial_option"] = extract_json(self.initial_option, "block") 295 return json 296 297 298class AbstractDynamicSelector(AbstractSelector, metaclass=ABCMeta): 299 @property 300 @abstractmethod 301 def initial_object_type(self): 302 pass 303 304 def __init__( 305 self, 306 *, 307 placeholder: str, 308 action_id: str, 309 initial_value: Optional[str] = None, 310 confirm: Optional[ConfirmObject] = None, 311 ): 312 super().__init__( 313 action_id=action_id, 314 subtype=f"{self.initial_object_type}s_select", 315 confirm=confirm, 316 placeholder=placeholder, 317 ) 318 self.initial_value = initial_value 319 320 def to_dict(self) -> dict: 321 json = super().to_dict() 322 if self.initial_value is not None: 323 json[f"initial_{self.initial_object_type}"] = self.initial_value 324 return json 325 326 327class UserSelectElement(AbstractDynamicSelector): 328 initial_object_type = "user" 329 330 def __init__( 331 self, 332 *, 333 placeholder: str, 334 action_id: str, 335 initial_user: Optional[str] = None, 336 confirm: Optional[ConfirmObject] = None, 337 ): 338 """ 339 This select menu will populate its options with a list of Slack users visible to 340 the current user in the active workspace. 341 342 https://api.slack.com/reference/messaging/block-elements#users-select 343 344 Args: 345 placeholder: placeholder text shown on this element. Cannot exceed 150 346 characters. 347 action_id: ID to be used for this action - should be unique. Cannot 348 exceed 255 characters. 349 initial_user: An ID to initially select on this selector. 350 confirm: A ConfirmObject that defines an optional confirmation dialog 351 after this element is interacted with. 352 """ 353 super().__init__( 354 placeholder=placeholder, 355 action_id=action_id, 356 initial_value=initial_user, 357 confirm=confirm, 358 ) 359 360 361class ConversationSelectElement(AbstractDynamicSelector): 362 initial_object_type = "conversation" 363 364 def __init__( 365 self, 366 *, 367 placeholder: str, 368 action_id: str, 369 initial_conversation: Optional[str] = None, 370 confirm: Optional[ConfirmObject] = None, 371 ): 372 """ 373 This select menu will populate its options with a list of public and private 374 channels, DMs, and MPIMs visible to the current user in the active workspace. 375 376 https://api.slack.com/reference/messaging/block-elements#conversation-select 377 378 Args: 379 placeholder: placeholder text shown on this element. Cannot exceed 150 380 characters. 381 action_id: ID to be used for this action - should be unique. Cannot 382 exceed 255 characters. 383 initial_conversation: An ID to initially select on this selector. 384 confirm: A ConfirmObject that defines an optional confirmation dialog 385 after this element is interacted with. 386 """ 387 super().__init__( 388 placeholder=placeholder, 389 action_id=action_id, 390 initial_value=initial_conversation, 391 confirm=confirm, 392 ) 393 394 395class ChannelSelectElement(AbstractDynamicSelector): 396 initial_object_type = "channel" 397 398 def __init__( 399 self, 400 *, 401 placeholder: str, 402 action_id: str, 403 initial_channel: Optional[str] = None, 404 confirm: Optional[ConfirmObject] = None, 405 ): 406 """ 407 This select menu will populate its options with a list of public channels 408 visible to the current user in the active workspace. 409 410 https://api.slack.com/reference/messaging/block-elements#channel-select 411 412 Args: 413 placeholder: placeholder text shown on this element. Cannot exceed 150 414 characters. 415 action_id: ID to be used for this action - should be unique. Cannot 416 exceed 255 characters. 417 initial_channel: An ID to initially select on this selector. 418 confirm: A ConfirmObject that defines an optional confirmation dialog 419 after this element is interacted with. 420 """ 421 super().__init__( 422 placeholder=placeholder, 423 action_id=action_id, 424 initial_value=initial_channel, 425 confirm=confirm, 426 ) 427 428 429class OverflowMenuOption(Option): 430 def __init__(self, label: str, value: str, url: Optional[str] = None): 431 """ 432 An extension of a standard option, but with an optional 'url' attribute, 433 which will simply directly navigate to a given URL. Only valid in 434 OverflowMenuElements, as the name implies. 435 436 https://api.slack.com/reference/messaging/composition-objects#option 437 438 Args: 439 label: A short, user-facing string to label this option to users. 440 Cannot exceed 75 characters. 441 value: A short string that identifies this particular option to your 442 application. It will be part of the payload when this option is 443 selected. Cannot exceed 75 characters. 444 url: A URL to load in the user's browser when the option is clicked. 445 Maximum length for this field is 3000 characters. If you're using url, 446 you'll still receive an interaction payload and will need to send an 447 acknowledgement response. 448 """ 449 super().__init__(label=label, value=value) 450 self.url = url 451 452 def to_dict(self, option_type: str = "block") -> dict: 453 json = super().to_dict(option_type) 454 if self.url is not None: 455 json["url"] = self.url 456 return json 457 458 459class OverflowMenuElement(InteractiveElement): 460 options_min_length = 2 461 options_max_length = 5 462 463 def __init__( 464 self, 465 *, 466 options: List[Union[Option, OverflowMenuOption]], 467 action_id: str, 468 confirm: Optional[ConfirmObject] = None, 469 ): 470 """ 471 This is like a cross between a button and a select menu - when a user clicks 472 on this overflow button, they will be presented with a list of options to 473 choose from. Unlike the select menu, there is no typeahead field, and the 474 button always appears with an ellipsis ("…") rather than customisable text. 475 476 As such, it is usually used if you want a more compact layout than a select 477 menu, or to supply a list of less visually important actions after a row of 478 buttons. You can also specify simple URL links as overflow menu options, 479 instead of actions. 480 481 https://api.slack.com/reference/messaging/block-elements#overflow 482 483 Args: 484 options: An array of Option or OverflowMenuOption objects to display 485 in the menu. Maximum number of options is 5, minimum is 2. 486 action_id: ID to be used for this action - should be unique. Cannot 487 exceed 255 characters. 488 confirm: A ConfirmObject that defines an optional confirmation dialog 489 after this element is interacted with. 490 """ 491 super().__init__(action_id=action_id, subtype="overflow") 492 self.options = options 493 self.confirm = confirm 494 495 @JsonValidator( 496 f"options attribute must have between {options_min_length} " 497 f"and {options_max_length} items" 498 ) 499 def options_length(self): 500 return self.options_min_length < len(self.options) <= self.options_max_length 501 502 def to_dict(self) -> dict: 503 json = super().to_dict() 504 json["options"] = extract_json(self.options, "block") 505 if self.confirm is not None: 506 json["confirm"] = extract_json(self.confirm) 507 return json 508 509 510class DatePickerElement(AbstractSelector): 511 @property 512 def attributes(self) -> Set[str]: 513 return super().attributes.union({"initial_date"}) 514 515 def __init__( 516 self, 517 *, 518 action_id: str, 519 placeholder: Optional[str] = None, 520 initial_date: Optional[str] = None, 521 confirm: Optional[ConfirmObject] = None, 522 ): 523 """ 524 An element which lets users easily select a date from a calendar style UI. 525 Date picker elements can be used inside of SectionBlocks and ActionsBlocks. 526 527 https://api.slack.com/reference/messaging/block-elements#datepicker 528 529 Args: 530 action_id: ID to be used for this action - should be unique. Cannot 531 exceed 255 characters. 532 placeholder: placeholder text shown on this element. Cannot exceed 150 533 characters. 534 initial_date: The initial date that is selected when the element is 535 loaded. This must be in the format "YYYY-MM-DD". 536 confirm: A ConfirmObject that defines an optional confirmation dialog 537 after this element is interacted with. 538 """ 539 super().__init__( 540 action_id=action_id, 541 subtype="datepicker", 542 placeholder=placeholder, 543 confirm=confirm, 544 ) 545 self.initial_date = initial_date 546 547 @JsonValidator("initial_date attribute must be in format 'YYYY-MM-DD'") 548 def initial_date_valid(self): 549 return self.initial_date is None or re.match( 550 r"\d{4}-[01][12]-[0123]\d", self.initial_date 551 ) 552