Repository URL to install this package:
|
Version:
3.7.0 ▾
|
import copy
import logging
import warnings
from typing import List, Optional, Set, Union, Sequence
from slack_sdk.models import show_unknown_key_warning
from slack_sdk.models.basic_objects import (
JsonObject,
JsonValidator,
)
from slack_sdk.models.messages import Link
ButtonStyles = {"danger", "primary"}
DynamicSelectElementTypes = {"channels", "conversations", "users"}
class TextObject(JsonObject):
"""The interface for text objects (types: plain_text, mrkdwn)"""
attributes = {"text", "type", "emoji"}
logger = logging.getLogger(__name__)
def _subtype_warning(self): # skipcq: PYL-R0201
warnings.warn(
"subtype is deprecated since slackclient 2.6.0, use type instead",
DeprecationWarning,
)
@property
def subtype(self) -> Optional[str]:
return self.type
@classmethod
def parse(
cls, text: Union[str, dict, "TextObject"], default_type: str = "mrkdwn"
) -> Optional["TextObject"]:
if not text: # skipcq: PYL-R1705
return None
elif isinstance(text, str):
if default_type == PlainTextObject.type: # skipcq: PYL-R1705
return PlainTextObject.from_str(text)
else:
return MarkdownTextObject.from_str(text)
elif isinstance(text, dict):
d = copy.copy(text)
t = d.pop("type")
if t == PlainTextObject.type: # skipcq: PYL-R1705
return PlainTextObject(**d)
else:
return MarkdownTextObject(**d)
elif isinstance(text, TextObject):
return text
else:
cls.logger.warning(
f"Unknown type ({type(text)}) detected when parsing a TextObject"
)
return None
def __init__(
self,
text: str,
type: Optional[str] = None, # skipcq: PYL-W0622
subtype: Optional[str] = None,
emoji: Optional[bool] = None,
**kwargs,
):
"""Super class for new text "objects" used in Block kit"""
if subtype:
self._subtype_warning()
self.text = text
self.type = type if type else subtype
self.emoji = emoji
class PlainTextObject(TextObject):
"""plain_text typed text object"""
type = "plain_text"
@property
def attributes(self) -> Set[str]:
return super().attributes.union({"emoji"})
def __init__(self, *, text: str, emoji: Optional[bool] = None):
"""A plain text object, meaning markdown characters will not be parsed as
formatting information.
https://api.slack.com/reference/block-kit/composition-objects#text
"""
super().__init__(text=text, type=self.type)
self.emoji = emoji
@staticmethod
def from_str(text: str) -> "PlainTextObject":
return PlainTextObject(text=text, emoji=True)
@staticmethod
def direct_from_string(text: str) -> dict:
"""Transforms a string into the required object shape to act as a PlainTextObject"""
return PlainTextObject.from_str(text).to_dict()
class MarkdownTextObject(TextObject):
"""mrkdwn typed text object"""
type = "mrkdwn"
@property
def attributes(self) -> Set[str]:
return super().attributes.union({"verbatim"})
def __init__(self, *, text: str, verbatim: Optional[bool] = None):
"""A Markdown text object, meaning markdown characters will be parsed as
formatting information.
https://api.slack.com/reference/block-kit/composition-objects#text
"""
super().__init__(text=text, type=self.type)
self.verbatim = verbatim
@staticmethod
def from_str(text: str) -> "MarkdownTextObject":
"""Transforms a string into the required object shape to act as a MarkdownTextObject"""
return MarkdownTextObject(text=text)
@staticmethod
def direct_from_string(text: str) -> dict:
"""Transforms a string into the required object shape to act as a MarkdownTextObject"""
return MarkdownTextObject.from_str(text).to_dict()
@staticmethod
def from_link(link: Link, title: str = "") -> "MarkdownTextObject":
"""
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}")
@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
"""
return MarkdownTextObject.from_link(link, title).to_dict()
class Option(JsonObject):
"""Option object used in dialogs, legacy message actions (interactivity in attachments),
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
logger = logging.getLogger(__name__)
label_max_length = 75
value_max_length = 75
def __init__(
self,
*,
value: str,
label: Optional[str] = None,
text: Optional[Union[str, dict, TextObject]] = None, # Block Kit
description: Optional[Union[str, dict, TextObject]] = None,
url: Optional[str] = None,
**others: dict,
):
"""
An object that represents a single selectable item in a block element (
SelectElement, OverflowMenuElement) or dialog element
(StaticDialogSelectElement)
Blocks:
https://api.slack.com/reference/block-kit/composition-objects#option
Dialogs:
https://api.slack.com/dialogs#select_elements
Legacy interactive attachments:
https://api.slack.com/legacy/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.
"""
if text:
self._text: Optional[TextObject] = TextObject.parse(text)
self._label: Optional[str] = None
else:
self._text: Optional[TextObject] = None
self._label: Optional[str] = label
# for backward-compatibility with version 2.0-2.5, the following fields return str values
self.text: Optional[str] = self._text.text if self._text else None
self.label: Optional[str] = self._label
self.value: str = value
# for backward-compatibility with version 2.0-2.5, the following fields return str values
if isinstance(description, str):
self.description = description
self._block_description = PlainTextObject.from_str(description)
elif isinstance(description, dict):
self.description = description["text"]
self._block_description = TextObject.parse(description)
elif isinstance(description, TextObject):
self.description = description.text
self._block_description = description
else:
self.description = None
self._block_description = None
# A URL to load in the user's browser when the option is clicked.
# The url attribute is only available in overflow menus.
# Maximum length for this field is 3000 characters.
# If you're using url, you'll still receive an interaction payload
# and will need to send an acknowledgement response.
self.url: Optional[str] = url
show_unknown_key_warning(self, others)
@JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
def _validate_label_length(self):
return self._label is None or len(self._label) <= self.label_max_length
@JsonValidator(f"text attribute cannot exceed {label_max_length} characters")
def _validate_text_length(self):
return (
self._text is None
or self._text.text is None
or len(self._text.text) <= self.label_max_length
)
@JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
def _validate_value_length(self):
return len(self.value) <= self.value_max_length
@classmethod
def parse_all(
cls, options: Optional[Sequence[Union[dict, "Option"]]]
) -> Optional[List["Option"]]:
if options is None:
return None
option_objects: List[Option] = []
for o in options:
if isinstance(o, dict):
d = copy.copy(o)
option_objects.append(Option(**d))
elif isinstance(o, Option):
option_objects.append(o)
else:
cls.logger.warning(f"Unknown option object detected and skipped ({o})")
return option_objects
def to_dict(self, option_type: str = "block") -> dict: # skipcq: PYL-W0221
"""
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": # skipcq: PYL-R1705
return {"label": self.label, "value": self.value}
elif option_type == "action" or option_type == "attachment":
# "action" can be confusing but it means a legacy message action in attachments
# we don't remove the type name for backward compatibility though
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
text: TextObject = self._text or PlainTextObject.from_str(self.label)
json: dict = {
"text": text.to_dict(),
"value": self.value,
}
if self._block_description:
json["description"] = self._block_description.to_dict()
if self.url:
json["url"] = self.url
return json
@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
logger = logging.getLogger(__name__)
def __init__(
self,
*,
label: Optional[Union[str, dict, TextObject]] = None,
options: Sequence[Union[dict, Option]],
**others: dict,
):
"""
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/block-kit/composition-objects#option-group
Dialogs:
https://api.slack.com/dialogs#select_elements
Legacy interactive attachments:
https://api.slack.com/legacy/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
# default_type=PlainTextObject.type is for backward-compatibility
self._label: Optional[TextObject] = TextObject.parse(
label, default_type=PlainTextObject.type
)
self.label: Optional[str] = self._label.text if self._label else None
self.options = Option.parse_all(options) # compatible with version 2.5
show_unknown_key_warning(self, others)
@JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
def _validate_label_length(self):
return self.label is None or len(self.label) <= self.label_max_length
@JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
def _validate_options_length(self):
return self.options is None or len(self.options) <= self.options_max_length
@classmethod
def parse_all(
cls, option_groups: Optional[Sequence[Union[dict, "OptionGroup"]]]
) -> Optional[List["OptionGroup"]]:
if option_groups is None:
return None
option_group_objects = []
for o in option_groups:
if isinstance(o, dict):
d = copy.copy(o)
option_group_objects.append(OptionGroup(**d))
elif isinstance(o, OptionGroup):
option_group_objects.append(o)
else:
cls.logger.warning(
f"Unknown option group object detected and skipped ({o})"
)
return option_group_objects
def to_dict(self, option_type: str = "block") -> dict: # skipcq: PYL-W0221
self.validate_json()
dict_options = [o.to_dict(option_type) for o in self.options]
if option_type == "dialog": # skipcq: PYL-R1705
return {
"label": self.label,
"options": dict_options,
}
elif option_type == "action":
return {
"text": self.label,
"options": dict_options,
}
else: # if option_type == "block"; this should be the most common case
dict_label: dict = self._label.to_dict()
return {
"label": dict_label,
"options": dict_options,
}
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
@classmethod
def parse(cls, confirm: Union["ConfirmObject", dict]):
if confirm:
if isinstance(confirm, ConfirmObject): # skipcq: PYL-R1705
return confirm
elif isinstance(confirm, dict):
return ConfirmObject(**confirm)
else:
# Not yet implemented: show some warning here
return None
return None
def __init__(
self,
*,
title: Union[str, dict, PlainTextObject],
text: Union[str, dict, TextObject],
confirm: Union[str, dict, PlainTextObject] = "Yes",
deny: Union[str, dict, PlainTextObject] = "No",
style: str = None,
):
"""
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/block-kit/composition-objects#confirm
"""
self._title = TextObject.parse(title, default_type=PlainTextObject.type)
self._text = TextObject.parse(text, default_type=MarkdownTextObject.type)
self._confirm = TextObject.parse(confirm, default_type=PlainTextObject.type)
self._deny = TextObject.parse(deny, default_type=PlainTextObject.type)
self._style = style
# for backward-compatibility with version 2.0-2.5, the following fields return str values
self.title = self._title.text if self._title else None
self.text = self._text.text if self._text else None
self.confirm = self._confirm.text if self._confirm else None
self.deny = self._deny.text if self._deny else None
self.style = self._style
@JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
def title_length(self):
return self._title is None or len(self._title.text) <= self.title_max_length
@JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
def text_length(self):
return self._text is None or len(self._text.text) <= self.text_max_length
@JsonValidator(f"confirm attribute cannot exceed {confirm_max_length} characters")
def confirm_length(self):
return (
self._confirm is None or len(self._confirm.text) <= self.confirm_max_length
)
@JsonValidator(f"deny attribute cannot exceed {deny_max_length} characters")
def deny_length(self):
return self._deny is None or len(self._deny.text) <= self.deny_max_length
@JsonValidator('style for confirm must be either "primary" or "danger"')
def _validate_confirm_style(self):
return self._style is None or self._style in ["primary", "danger"]
def to_dict(self, option_type: str = "block") -> dict: # skipcq: PYL-W0221
if option_type == "action": # skipcq: PYL-R1705
# deliberately skipping JSON validators here - can't find documentation
# on actual limits here
json = {
"ok_text": self._confirm.text
if self._confirm and self._confirm.text != "Yes"
else "Okay",
"dismiss_text": self._deny.text
if self._deny and self._deny.text != "No"
else "Cancel",
}
if self._title:
json["title"] = self._title.text
if self._text:
json["text"] = self._text.text
return json
else:
self.validate_json()
json = {}
if self._title:
json["title"] = self._title.to_dict()
if self._text:
json["text"] = self._text.to_dict()
if self._confirm:
json["confirm"] = self._confirm.to_dict()
if self._deny:
json["deny"] = self._deny.to_dict()
if self._style:
json["style"] = self._style
return json
class DispatchActionConfig(JsonObject):
attributes = {"trigger_actions_on"}
@classmethod
def parse(cls, config: Union["DispatchActionConfig", dict]):
if config:
if isinstance(config, DispatchActionConfig): # skipcq: PYL-R1705
return config
elif isinstance(config, dict):
return DispatchActionConfig(**config)
else:
# Not yet implemented: show some warning here
return None
return None
def __init__(
self,
*,
trigger_actions_on: Optional[list] = None,
):
"""
Determines when a plain-text input element will return a block_actions interaction payload.
https://api.slack.com/reference/block-kit/composition-objects#dispatch_action_config
"""
self._trigger_actions_on = trigger_actions_on or []
def to_dict(self) -> dict: # skipcq: PYL-W0221
self.validate_json()
json = {}
if self._trigger_actions_on:
json["trigger_actions_on"] = self._trigger_actions_on
return json