diff --git a/.gitignore b/.gitignore index 2fed3c4f0..742832339 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,7 @@ coverage.xml .python-version pip .mypy_cache/ + + +Pipfile.lock +.idea diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..b087b65d3 --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +aiohttp = "*" +[packages] + +[requires] +python_version = "3.7" diff --git a/slack/web/classes/__init__.py b/slack/web/classes/__init__.py index f15b3e1f8..f0ca44023 100644 --- a/slack/web/classes/__init__.py +++ b/slack/web/classes/__init__.py @@ -1,6 +1,7 @@ -from abc import ABCMeta, abstractmethod +import json +from abc import ABCMeta from functools import wraps -from typing import Callable, Iterable, List, Set, Union +from typing import Callable, Iterable from ...errors import SlackObjectFormationError @@ -11,13 +12,6 @@ def __str__(self): class JsonObject(BaseObject, metaclass=ABCMeta): - @property - @abstractmethod - def attributes(self) -> Set[str]: - """ - Provide a set of attributes of this object that will make up its JSON structure - """ - return set() def validate_json(self) -> None: """ @@ -29,34 +23,25 @@ def validate_json(self) -> None: if callable(method) and hasattr(method, "validator"): method() - def get_non_null_attributes(self) -> dict: + @staticmethod + def __get_non_null_attributes(dict_: dict) -> dict: """ Construct a dictionary out of non-null keys (from attributes property) present on this object """ - return { - key: getattr(self, key, None) - for key in sorted(self.attributes) - if getattr(self, key, None) is not None - } + return {key: value for key, value in dict_.items() if value is not None} def to_dict(self, *args) -> dict: - """ - Extract this object as a JSON-compatible, Slack-API-valid dictionary - - Args: - *args: Any specific formatting args (rare; generally not required) - - Raises: - SlackObjectFormationError if the object was not valid - """ self.validate_json() - return self.get_non_null_attributes() + _json = json.dumps(self, default=lambda o: o.__dict__) + _dict = json.loads(_json, object_hook=self.__get_non_null_attributes) + + return _dict def __repr__(self): - json = self.to_dict() - if json: - return f"" + _json = self.to_dict() + if _json: + return f"" else: return self.__str__() @@ -89,28 +74,26 @@ def __init__(self, attribute: str, enum: Iterable[str]): f"{', '.join(enum)}" ) - -def extract_json( - item_or_items: Union[JsonObject, List[JsonObject], str], *format_args -) -> Union[dict, List[dict], str]: - """ - Given a sequence (or single item), attempt to call the to_dict() method on each - item and return a plain list. If item is not the expected type, return it - unmodified, in case it's already a plain dict or some other user created class. - - Args: - item_or_items: item(s) to go through - format_args: Any formatting specifiers to pass into the object's to_dict - method - """ - try: - return [ - elem.to_dict(*format_args) if isinstance(elem, JsonObject) else elem - for elem in item_or_items - ] - except TypeError: # not iterable, so try returning it as a single item - return ( - item_or_items.to_dict(*format_args) - if isinstance(item_or_items, JsonObject) - else item_or_items - ) +# def extract_json(item_or_items: Union[JsonObject, List[JsonObject], str], *format_args +# ) -> Union[dict, List[dict], str]: +# """ +# Given a sequence (or single item), attempt to call the to_dict() method on each +# item and return a plain list. If item is not the expected type, return it +# unmodified, in case it's already a plain dict or some other user created class. +# +# Args: +# item_or_items: item(s) to go through +# format_args: Any formatting specifiers to pass into the object's to_dict +# method +# """ +# try: +# return [ +# elem.to_dict(*format_args) if isinstance(elem, JsonObject) else elem +# for elem in item_or_items +# ] +# except TypeError: # not iterable, so try returning it as a single item +# return ( +# item_or_items.to_dict(*format_args) +# if isinstance(item_or_items, JsonObject) +# else item_or_items +# ) diff --git a/slack/web/classes/actions.py b/slack/web/classes/actions.py index cb678eba3..cb48c4c85 100644 --- a/slack/web/classes/actions.py +++ b/slack/web/classes/actions.py @@ -1,7 +1,7 @@ from abc import ABCMeta, abstractmethod from typing import List, Optional, Set, Union -from . import EnumValidator, JsonObject, JsonValidator, extract_json +from . import EnumValidator, JsonObject, JsonValidator from .objects import ( ButtonStyles, ConfirmObject, @@ -21,26 +21,26 @@ class Action(JsonObject): attributes = {"name", "text", "url"} def __init__( - self, - *, - text: str, - subtype: str, - name: Optional[str] = None, - url: Optional[str] = None, + self, + *, + text: str, + subtype: str, + name: Optional[str] = None, + url: Optional[str] = None, ): self.name = name self.url = url self.text = text - self.subtype = subtype + self.type = subtype @JsonValidator("name or url attribute is required") def name_or_url_present(self): return self.name is not None or self.url is not None - def to_dict(self) -> dict: - json = super().to_dict() - json["type"] = self.subtype - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["type"] = self.subtype + # return json class ActionButton(Action): @@ -51,13 +51,13 @@ def attributes(self): value_max_length = 2000 def __init__( - self, - *, - name: str, - text: str, - value: str, - confirm: Optional[ConfirmObject] = None, - style: Optional[str] = None, + self, + *, + name: str, + text: str, + value: str, + confirm: Optional[ConfirmObject] = None, + style: Optional[str] = None, ): """ Simple button for use inside attachments @@ -93,11 +93,11 @@ def value_length(self): def style_valid(self): return self.style is None or self.style in ButtonStyles - def to_dict(self) -> dict: - json = super().to_dict() - if self.confirm is not None: - json["confirm"] = extract_json(self.confirm, "action") - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if self.confirm is not None: + # json["confirm"] = extract_json(self.confirm, "action") + # return json class ActionLinkButton(Action): @@ -125,7 +125,7 @@ def data_source(self) -> str: pass def __init__( - self, *, name: str, text: str, selected_option: Optional[Option] = None + self, *, name: str, text: str, selected_option: Optional[Option] = None ): super().__init__(text=text, name=name, subtype="select") self.selected_option = selected_option @@ -139,7 +139,7 @@ def to_dict(self) -> dict: if self.selected_option is not None: # this is a special case for ExternalActionSelectElement - in that case, # you pass the initial value of the selector as a selected_options array - json["selected_options"] = extract_json([self.selected_option], "action") + json["selected_options"] = self.selected_option.to_dict() # extract_json([self.selected_option], "action") return json @@ -157,12 +157,13 @@ class ActionStaticSelector(AbstractActionSelector): options_max_length = 100 def __init__( - self, - *, - name: str, - text: str, - options: List[Union[Option, OptionGroup]], - selected_option: Optional[Option] = None, + self, + *, + name: str, + text: str, + options: List[Union[Option]], + option_groups: List[OptionGroup] = None, + selected_option: Optional[Option] = None, ): """ Help users make clear, concise decisions by providing a menu of options @@ -182,18 +183,19 @@ def __init__( """ super().__init__(name=name, text=text, selected_option=selected_option) self.options = options + self.option_groups = option_groups @JsonValidator(f"options attribute cannot exceed {options_max_length} items") def options_length(self): return len(self.options) < self.options_max_length - def to_dict(self) -> dict: - json = super().to_dict() - if isinstance(self.options[0], OptionGroup): - json["option_groups"] = extract_json(self.options, "action") - else: - json["options"] = extract_json(self.options, "action") - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if isinstance(self.options[0], OptionGroup): + # json["option_groups"] = + # else: + # json["options"] = extract_json(self.options, "action") + # return json class ActionUserSelector(AbstractActionSelector): @@ -243,7 +245,7 @@ class ActionConversationSelector(AbstractActionSelector): data_source = "conversations" def __init__( - self, name: str, text: str, selected_conversation: Optional[Option] = None + self, name: str, text: str, selected_conversation: Optional[Option] = None ): """ Automatically populate the selector with a list of conversations they have in @@ -271,12 +273,12 @@ def attributes(self) -> Set[str]: return super().attributes.union({"min_query_length"}) def __init__( - self, - *, - name: str, - text: str, - selected_option: Optional[Option] = None, - min_query_length: Optional[int] = None, + self, + *, + name: str, + text: str, + selected_option: Optional[Option] = None, + min_query_length: Optional[int] = None, ): """ Populate a message select menu from your own application dynamically. diff --git a/slack/web/classes/attachments.py b/slack/web/classes/attachments.py index 0352a8c92..a3b1c63ac 100644 --- a/slack/web/classes/attachments.py +++ b/slack/web/classes/attachments.py @@ -1,7 +1,7 @@ import re from typing import List, Optional, Set -from . import EnumValidator, JsonObject, JsonValidator, extract_json +from . import EnumValidator, JsonObject, JsonValidator from .actions import Action from .blocks import Block @@ -143,7 +143,7 @@ def __init__( self.footer_icon = footer_icon self.ts = ts self.fields = fields or [] - self.markdown_in = markdown_in or [] + self.mrkdwn_in = markdown_in or [] @JsonValidator(f"footer attribute cannot exceed {footer_max_length} characters") def footer_length(self): @@ -155,8 +155,8 @@ def ts_without_footer(self): @EnumValidator("markdown_in", MarkdownFields) def markdown_in_valid(self): - return not self.markdown_in or all( - e in self.MarkdownFields for e in self.markdown_in + return not self.mrkdwn_in or all( + e in self.MarkdownFields for e in self.mrkdwn_in ) @JsonValidator( @@ -181,13 +181,13 @@ def author_link_without_author_name(self): def author_link_without_author_icon(self): return self.author_link is None or self.author_icon is not None - def to_dict(self) -> dict: - json = super().to_dict() - if self.fields is not None: - json["fields"] = extract_json(self.fields) - if self.markdown_in: - json["mrkdwn_in"] = self.markdown_in - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if self.fields is not None: + # json["fields"] = extract_json(self.fields) + # if self.mrkdwn_in: + # json["mrkdwn_in"] = self.mrkdwn_in + # return json class BlockAttachment(Attachment): @@ -215,11 +215,11 @@ def __init__(self, *, blocks: List[Block], color: Optional[str] = None): def fields_attribute_absent(self): return not self.fields - def to_dict(self) -> dict: - json = super().to_dict() - json.update({"blocks": extract_json(self.blocks)}) - del json["fields"] # cannot supply fields and blocks at the same time - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # json.update({"blocks": extract_json(self.blocks)}) + # del json["fields"] # cannot supply fields and blocks at the same time + # return json class InteractiveAttachment(Attachment): @@ -341,7 +341,7 @@ def __init__( def actions_length(self): return len(self.actions) <= self.actions_max_length - def to_dict(self) -> dict: - json = super().to_dict() - json["actions"] = extract_json(self.actions) - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["actions"] = extract_json(self.actions) + # return json diff --git a/slack/web/classes/blocks.py b/slack/web/classes/blocks.py index 1c5375dd0..4728efdf0 100644 --- a/slack/web/classes/blocks.py +++ b/slack/web/classes/blocks.py @@ -1,17 +1,32 @@ from typing import List, Optional, Set, Union -from . import JsonObject, JsonValidator, extract_json -from .elements import BlockElement, InteractiveElement +from . import JsonObject, JsonValidator +from .elements import BlockElement, InteractiveElement, PlainTextElement from .objects import MarkdownTextObject, PlainTextObject, TextObject class Block(JsonObject): + """Blocks are a series of components that can be combined + to create visually rich and compellingly interactive messages. + + You can include up to 50 blocks in each message. + + The lists of fields and values below describe the JSON that apps can use to generate each block: + Section + Divider + Image + Actions + Context + Input + File + """ + attributes = {"block_id"} block_id_max_length = 255 - def __init__(self, *, subtype: str, block_id: Optional[str] = None): - self.subtype = subtype + def __init__(self, *, _type: str, block_id: Optional[str] = None): + self.type = _type self.block_id = block_id self.color = None @@ -19,51 +34,51 @@ def __init__(self, *, subtype: str, block_id: Optional[str] = None): def block_id_length(self): return self.block_id is None or len(self.block_id) <= self.block_id_max_length - def to_dict(self) -> dict: - json = super().to_dict() - json["type"] = self.subtype - return json - - -class DividerBlock(Block): - def __init__(self): - """ - A simple divider - equivalent to
- - https://api.slack.com/reference/messaging/blocks#divider - """ - super().__init__(subtype="divider") + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["type"] = self.subtype + # return json class SectionBlock(Block): fields_max_length = 10 def __init__( - self, - *, - text: Union[str, TextObject] = None, - fields: List[str] = None, - block_id: Optional[str] = None, - accessory: Optional[BlockElement] = None, + self, + *, + text: TextObject = None, + block_id: Optional[str] = None, + fields: List[MarkdownTextObject] = None, + accessory: Optional[BlockElement] = None, ): """ - A general purpose block capable of holding text, fields (displayed in a - semi-tabular format) and one 'accessory' element + A section is one of the most flexible blocks available. + It can be used as a simple text block, in combination with + text fields, or side-by-side with any of the available block elements. - https://api.slack.com/reference/messaging/blocks#section + https://api.slack.com/reference/block-kit/blocks#section Args: - text: either a plain string, or a richer TextObject + text: The text for the block, in the form of string or a text object. + Maximum length for the text in this field is 3000 characters. + block_id: A string acting as a unique identifier for a block. + You can use this block_id when you receive an interaction + payload to identify the source of the action. If not + specified, one will be generated. Maximum length for this + field is 255 characters. block_id should be unique for each + message and each iteration of a message. + If a message is updated, use a new block_id. fields: optional: a sequence of strings that will be rendered using - MarkdownTextObjects - block_id: ID to be used for this block - autogenerated if left blank. - Cannot exceed 255 characters. + MarkdownTextObjects. Any strings included with fields will be rendered + in a compact format that allows for 2 columns of side-by-side text. + Maximum number of items is 10. + Maximum length for the text in each item is 2000 characters. accessory: an optional BlockElement to attach to this SectionBlock as secondary content """ - super().__init__(subtype="section", block_id=block_id) + super().__init__(_type="section", block_id=block_id) self.text = text - self.fields = fields or [] + self.fields = fields self.accessory = accessory @JsonValidator("text or fields attribute must be specified") @@ -74,20 +89,38 @@ def text_or_fields_populated(self): def fields_length(self): return self.fields is None or len(self.fields) <= self.fields_max_length - def to_dict(self) -> dict: - json = super().to_dict() - if self.text is not None: - if isinstance(self.text, TextObject): - json["text"] = self.text.to_dict() - else: - json["text"] = MarkdownTextObject.direct_from_string(self.text) - if self.fields: - json["fields"] = [ - MarkdownTextObject.direct_from_string(field) for field in self.fields - ] - if self.accessory is not None: - json["accessory"] = extract_json(self.accessory) - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if self.text is not None: + # if isinstance(self.text, TextObject): + # json["text"] = self.text.to_dict() + # else: + # json["text"] = MarkdownTextObject.direct_from_string(self.text) + # if self.fields: + # json["fields"] = [ + # MarkdownTextObject.direct_from_string(field) for field in self.fields + # ] + # if self.accessory is not None: + # json["accessory"] = extract_json(self.accessory) + # return json + + +class DividerBlock(Block): + def __init__(self, *, block_id: Optional[str] = None): + """A content divider, like an
, to split up different blocks inside of a message. + + Args: + block_id: A string acting as a unique identifier for a block. + You can use this block_id when you receive an interaction + payload to identify the source of the action. If not + specified, one will be generated. Maximum length for this + field is 255 characters. block_id should be unique for each + message and each iteration of a message. + If a message is updated, use a new block_id. + + https://api.slack.com/reference/block-kit/blocks#divider + """ + super().__init__(_type="divider", block_id=block_id) class ImageBlock(Block): @@ -100,17 +133,17 @@ def attributes(self) -> Set[str]: title_max_length = 2000 def __init__( - self, - *, - image_url: str, - alt_text: str, - title: Optional[str] = None, - block_id: Optional[str] = None, + self, + *, + image_url: str, + alt_text: str, + title: Optional[str] = None, + block_id: Optional[str] = None, ): """ A simple image block, designed to make those cat photos really pop. - https://api.slack.com/reference/messaging/blocks#image + https://api.slack.com/reference/block-kit/blocks#image Args: image_url: Publicly hosted URL to be displayed. Cannot exceed 3000 @@ -121,7 +154,7 @@ def __init__( block_id: ID to be used for this block - autogenerated if left blank. Cannot exceed 255 characters. """ - super().__init__(subtype="image", block_id=block_id) + super().__init__(_type="image", block_id=block_id) self.image_url = image_url self.alt_text = alt_text self.title = title @@ -140,69 +173,138 @@ def alt_text_length(self): def title_length(self): return self.title is None or len(self.title) <= self.title_max_length - def to_dict(self) -> dict: - json = super().to_dict() - if self.title is not None: - json["title"] = PlainTextObject.direct_from_string(self.title) - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if self.title is not None: + # json["title"] = PlainTextObject.direct_from_string(self.title) + # return json class ActionsBlock(Block): elements_max_length = 5 def __init__( - self, *, elements: List[InteractiveElement], block_id: Optional[str] = None + self, *, elements: List[InteractiveElement], block_id: Optional[str] = None ): """ A block that is used to hold interactive elements. - https://api.slack.com/reference/messaging/blocks#actions + https://api.slack.com/reference/block-kit/blocks#actions Args: elements: Up to 5 InteractiveElement objects - buttons, date pickers, etc block_id: ID to be used for this block - autogenerated if left blank. Cannot exceed 255 characters. """ - super().__init__(subtype="actions", block_id=block_id) + super().__init__(_type="actions", block_id=block_id) self.elements = elements @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements") def elements_length(self): return len(self.elements) <= self.elements_max_length - def to_dict(self) -> dict: - json = super().to_dict() - json["elements"] = extract_json(self.elements) - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["elements"] = extract_json(self.elements) + # return json class ContextBlock(Block): elements_max_length = 10 def __init__( - self, - *, - elements: List[Union[ImageBlock, TextObject]], - block_id: Optional[str] = None, + self, + *, + elements: List[Union[ImageBlock, TextObject]], + block_id: Optional[str] = None, ): """ Displays message context, which can include both images and text. - https://api.slack.com/reference/messaging/blocks#context + https://api.slack.com/reference/block-kit/blocks#context Args: elements: Up to 10 ImageElements and TextObjects block_id: ID to be used for this block - autogenerated if left blank. Cannot exceed 255 characters. """ - super().__init__(subtype="context", block_id=block_id) + super().__init__(_type="context", block_id=block_id) self.elements = elements @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements") def elements_length(self): return len(self.elements) <= self.elements_max_length - def to_dict(self) -> dict: - json = super().to_dict() - json["elements"] = extract_json(self.elements) - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["elements"] = extract_json(self.elements) + # return json + + +class InputBlock(Block): + attributes = {"label", "hint", "optional"} + label_max_length = 2000 + hint_max_length = 2000 + + def __init__( + self, + *, + label: PlainTextObject, + element: Union[InteractiveElement, PlainTextElement], + hint: Optional[str] = None, + optional: Optional[bool] = False, + block_id: Optional[str] = None + ): + """ + A block that collects information from users - it can hold a plain-text input element, + a select menu element, a multi-select menu element, or a datepicker. + + Important Note: Input blocks are only available in modals. + + https://api.slack.com/reference/block-kit/blocks#input + + Args: + label: A label that appears above an input element in the + form of a text object that must have type of plain_text. + Maximum length for the text in this field is 2000 characters. + element: An plain-text input element, a select menu element, + a multi-select menu element, or a datepicker. + hint: An optional hint that appears below an input element in a lighter grey. + Maximum length for the text in this field is 2000 characters. + optional: A boolean that indicates whether the input element + may be empty when a user submits the modal. Defaults to false. + """ + super().__init__(_type="input", block_id=block_id) + self.label = label + self.element = element + self.hint = hint + self.optional = optional + + @JsonValidator(f"label attribute cannot exceed {label_max_length} characters") + def label_length(self): + return len(self.label.text) <= self.label_max_length + + @JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters") + def hint_length(self): + return self.hint is None or len(self.hint) <= self.hint_max_length + + +class FileBlock(Block): + @property + def attributes(self) -> Set[str]: + return super().attributes.union({"external_id", "source"}) + + def __init__( + self, + *, + external_id: str, + source: str = "remote", + block_id: Optional[str] = None, + ): + """Displays a remote file. + + https://api.slack.com/reference/block-kit/blocks#file + """ + super().__init__(_type="file", block_id=block_id) + self.external_id = external_id + self.source = source diff --git a/slack/web/classes/dialog_elements.py b/slack/web/classes/dialog_elements.py index 892dbfe05..2bbaaed8c 100644 --- a/slack/web/classes/dialog_elements.py +++ b/slack/web/classes/dialog_elements.py @@ -1,7 +1,7 @@ from abc import ABCMeta, abstractmethod from typing import List, Optional, Set, Union -from . import EnumValidator, JsonObject, JsonValidator, extract_json +from . import EnumValidator, JsonObject, JsonValidator from .objects import DynamicSelectElementTypes, Option, OptionGroup TextElementSubtypes = {"email", "number", "tel", "url"} @@ -185,19 +185,19 @@ def placeholder_length(self): def data_source_valid(self): return self.data_source in self.DataSourceTypes - def to_dict(self) -> dict: - json = super().to_dict() - if self.data_source == "external": - if isinstance(self.value, Option): - json["selected_options"] = extract_json([self.value], "dialog") - elif self.value is not None: - json["selected_options"] = Option.from_single_value(self.value) - else: - if isinstance(self.value, Option): - json["value"] = self.value.value - elif self.value is not None: - json["value"] = self.value - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if self.data_source == "external": + # if isinstance(self.value, Option): + # json["selected_options"] = extract_json([self.value], "dialog") + # elif self.value is not None: + # json["selected_options"] = Option.from_single_value(self.value) + # else: + # if isinstance(self.value, Option): + # json["value"] = self.value.value + # elif self.value is not None: + # json["value"] = self.value + # return json class DialogStaticSelector(AbstractDialogSelector): @@ -257,13 +257,13 @@ def __init__( def options_length(self): return len(self.options) < self.options_max_length - def to_dict(self) -> dict: - json = super().to_dict() - if isinstance(self.options[0], OptionGroup): - json["option_groups"] = extract_json(self.options, "dialog") - else: - json["options"] = extract_json(self.options, "dialog") - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if isinstance(self.options[0], OptionGroup): + # json["option_groups"] = extract_json(self.options, "dialog") + # else: + # json["options"] = extract_json(self.options, "dialog") + # return json class DialogUserSelector(AbstractDialogSelector): diff --git a/slack/web/classes/dialogs.py b/slack/web/classes/dialogs.py index b9ae86eda..8bc2496b0 100644 --- a/slack/web/classes/dialogs.py +++ b/slack/web/classes/dialogs.py @@ -1,7 +1,7 @@ from json import dumps from typing import List, Optional, Union -from . import JsonObject, JsonValidator, extract_json +from . import JsonObject, JsonValidator from .dialog_elements import ( AbstractDialogSelector, DialogChannelSelector, @@ -19,11 +19,11 @@ class DialogBuilder(JsonObject): attributes = {} # no attributes because to_dict has unique implementation - _callback_id: Optional[str] - _elements: List[Union[DialogTextComponent, AbstractDialogSelector]] - _submit_label: Optional[str] - _notify_on_cancel: bool - _state: Optional[str] + callback_id: Optional[str] + elements: List[Union[DialogTextComponent, AbstractDialogSelector]] + submit_label: Optional[str] + notify_on_cancel: bool + state: Optional[str] title_max_length = 24 submit_label_max_length = 24 @@ -35,12 +35,12 @@ def __init__(self): Create a DialogBuilder to more easily construct the JSON required to submit a dialog to Slack """ - self._title = None - self._callback_id = None - self._elements = [] - self._submit_label = None - self._notify_on_cancel = False - self._state = None + self.title = None + self.callback_id = None + self.elements = [] + self.submit_label = None + self.notify_on_cancel = False + self.state = None def title(self, title: str) -> "DialogBuilder": """ @@ -49,7 +49,7 @@ def title(self, title: str) -> "DialogBuilder": Args: title: must not exceed 24 characters """ - self._title = title + self.title = title return self def state(self, state: Union[dict, str]) -> "DialogBuilder": @@ -62,9 +62,9 @@ def state(self, state: Union[dict, str]) -> "DialogBuilder": back to your application on submission """ if isinstance(state, dict): - self._state = dumps(state) + self.state = dumps(state) else: - self._state = state + self.state = state return self def callback_id(self, callback_id: str) -> "DialogBuilder": @@ -75,7 +75,7 @@ def callback_id(self, callback_id: str) -> "DialogBuilder": Args: callback_id: a string identifying this particular dialog """ - self._callback_id = callback_id + self.callback_id = callback_id return self def submit_label(self, label: str) -> "DialogBuilder": @@ -87,7 +87,7 @@ def submit_label(self, label: str) -> "DialogBuilder": label: must not exceed 24 characters, and must be a single word (no spaces) """ - self._submit_label = label + self.submit_label = label return self def notify_on_cancel(self, notify: bool) -> "DialogBuilder": @@ -99,7 +99,7 @@ def notify_on_cancel(self, notify: bool) -> "DialogBuilder": notify: Set to True to indicate that your application should receive a request even if the user cancels interaction with the dialog. """ - self._notify_on_cancel = notify + self.notify_on_cancel = notify return self def text_field( @@ -138,7 +138,7 @@ def text_field( or url. In some form factors, optimized input is provided for this subtype. """ - self._elements.append( + self.elements.append( DialogTextField( name=name, label=label, @@ -193,7 +193,7 @@ def text_area( or url. In some form factors, optimized input is provided for this subtype. """ - self._elements.append( + self.elements.append( DialogTextArea( name=name, label=label, @@ -239,7 +239,7 @@ def static_selector( placeholder: A string displayed as needed to help guide users in completing the element. 150 character maximum. """ - self._elements.append( + self.elements.append( DialogStaticSelector( name=name, label=label, @@ -285,7 +285,7 @@ def external_selector( placeholder: A string displayed as needed to help guide users in completing the element. 150 character maximum. """ - self._elements.append( + self.elements.append( DialogExternalSelector( name=name, label=label, @@ -323,7 +323,7 @@ def user_selector( placeholder: A string displayed as needed to help guide users in completing the element. 150 character maximum. """ - self._elements.append( + self.elements.append( DialogUserSelector( name=name, label=label, @@ -358,7 +358,7 @@ def channel_selector( placeholder: A string displayed as needed to help guide users in completing the element. 150 character maximum. """ - self._elements.append( + self.elements.append( DialogChannelSelector( name=name, label=label, @@ -394,7 +394,7 @@ def conversation_selector( placeholder: A string displayed as needed to help guide users in completing the element. 150 character maximum. """ - self._elements.append( + self.elements.append( DialogConversationSelector( name=name, label=label, @@ -407,45 +407,45 @@ def conversation_selector( @JsonValidator("title attribute is required") def title_present(self): - return self._title is not None + return self.title is not None @JsonValidator(f"title attribute cannot exceed {title_max_length} characters") def title_length(self): - return self._title is not None and len(self._title) <= self.title_max_length + return self.title is not None and len(self.title) <= self.title_max_length @JsonValidator("callback_id attribute is required") def callback_id_present(self): - return self._callback_id is not None + return self.callback_id is not None @JsonValidator(f"dialogs must contain between 1 and {elements_max_length} elements") def elements_length(self): - return 0 < len(self._elements) <= self.elements_max_length + return 0 < len(self.elements) <= self.elements_max_length @JsonValidator(f"submit_label cannot exceed {submit_label_max_length} characters") def submit_label_length(self): return ( - self._submit_label is None - or len(self._submit_label) <= self.submit_label_max_length + self.submit_label is None + or len(self.submit_label) <= self.submit_label_max_length ) @JsonValidator("submit_label can only be one word") def submit_label_valid(self): - return self._submit_label is None or " " not in self._submit_label + return self.submit_label is None or " " not in self.submit_label @JsonValidator(f"state cannot exceed {state_max_length} characters") def state_length(self): - return not self._state or len(self._state) <= self.state_max_length - - def to_dict(self) -> dict: - self.validate_json() - json = { - "title": self._title, - "callback_id": self._callback_id, - "elements": extract_json(self._elements), - "notify_on_cancel": self._notify_on_cancel, - } - if self._submit_label is not None: - json["submit_label"] = self._submit_label - if self._state is not None: - json["state"] = self._state - return json + return not self.state or len(self.state) <= self.state_max_length + + # def to_dict(self) -> dict: + # self.validate_json() + # json = { + # "title": self.title, + # "callback_id": self.callback_id, + # "elements": extract_json(self.elements), + # "notify_on_cancel": self.notify_on_cancel, + # } + # if self.submit_label is not None: + # json["submit_label"] = self.submit_label + # if self.state is not None: + # json["state"] = self.state + # return json diff --git a/slack/web/classes/elements.py b/slack/web/classes/elements.py index d448d31e9..c79055d14 100644 --- a/slack/web/classes/elements.py +++ b/slack/web/classes/elements.py @@ -1,32 +1,47 @@ import random import re import string -from abc import ABCMeta, abstractmethod -from typing import List, Optional, Set, Union +import warnings +from abc import ABCMeta +from typing import List, Optional, Union -from . import EnumValidator, JsonObject, JsonValidator, extract_json +from . import EnumValidator, JsonObject, JsonValidator from .objects import ButtonStyles, ConfirmObject, Option, OptionGroup, PlainTextObject class BlockElement(JsonObject, metaclass=ABCMeta): - def __init__(self, *, subtype: str): - self.subtype = subtype - - def to_dict(self) -> dict: - json = super().to_dict() - json["type"] = self.subtype - return json + """Block Elements are things that exists inside of your Blocks. + + Some elements include: + Image + Button + Select Menus + Multi-select Menus + Overflow Menu + Date Picker + Input - These can only be used inside of input blocks. + + https://api.slack.com/reference/block-kit/block-elements + """ + + def __init__(self, *, type: str): + # Note: "subtype" is actually the "type" parameter, + # but was renamed due to name already being used in Python Builtins. + self.type = type + # + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["type"] = self.subtype + # return json class InteractiveElement(BlockElement): - @property - def attributes(self) -> Set[str]: - return super().attributes.union({"action_id"}) - action_id_max_length = 255 - def __init__(self, *, action_id: str, subtype: str): - super().__init__(subtype=subtype) + def __init__(self, *, + action_id: str, + type: str): + super().__init__(type=type) self.action_id = action_id @JsonValidator( @@ -37,27 +52,25 @@ def action_id_length(self): class ImageElement(BlockElement): - @property - def attributes(self) -> Set[str]: - return super().attributes.union({"alt_text", "image_url"}) - image_url_max_length = 3000 alt_text_max_length = 2000 - def __init__(self, *, image_url: str, alt_text: str): + def __init__(self, *, + image_url: str, + alt_text: str): """ An element to insert an image - this element can be used in section and context blocks only. If you want a block with only an image in it, you're looking for the image block. - https://api.slack.com/reference/messaging/block-elements#image + https://api.slack.com/reference/block-kit/block-elements#image Args: image_url: Publicly hosted URL to be displayed. Cannot exceed 3000 characters. alt_text: Plain text summary of image. Cannot exceed 2000 characters. """ - super().__init__(subtype="image") + super().__init__(type="image") self.image_url = image_url self.alt_text = alt_text @@ -73,47 +86,54 @@ def alt_text_length(self): class ButtonElement(InteractiveElement): - @property - def attributes(self) -> Set[str]: - return super().attributes.union({"style", "value"}) - text_max_length = 75 + url_max_length = 3000 value_max_length = 2000 def __init__( - self, - *, - text: str, - action_id: str, - value: str, - style: Optional[str] = None, - confirm: Optional[ConfirmObject] = None, + self, + *, + text: PlainTextObject, + action_id: str, + url: str = None, + value: str = None, + style: Optional[str] = None, + confirm: Optional[ConfirmObject] = None, ): """ An interactive element that inserts a button. The button can be a trigger for anything from opening a simple link to starting a complex workflow. - https://api.slack.com/reference/messaging/block-elements#button + https://api.slack.com/reference/block-kit/block-elements#button Args: text: String that defines the button's text. Cannot exceed 75 characters. action_id: ID to be used for this action - should be unique. Cannot exceed 255 characters. + url: A URL to load in the user's browser when the button is clicked. + 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. value: The value to send along with the interaction payload. Cannot exceed 2000 characters. style: "primary" or "danger" to add specific styling to this button. confirm: A ConfirmObject that defines an optional confirmation dialog after this element is interacted with. """ - super().__init__(action_id=action_id, subtype="button") + super().__init__(action_id=action_id, type="button") self.text = text + self.url = url self.value = value self.style = style self.confirm = confirm @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") def text_length(self): - return len(self.text) <= self.text_max_length + return len(self.text.text) <= self.text_max_length + + @JsonValidator(f"url attribute cannot exceed {url_max_length} characters") + def url_length(self): + return self.url is None or len(self.url) <= self.url_max_length @JsonValidator(f"value attribute cannot exceed {value_max_length} characters") def value_length(self): @@ -123,27 +143,21 @@ def value_length(self): def style_valid(self): return self.style is None or self.style in ButtonStyles - def to_dict(self) -> dict: - json = super().to_dict() - json["text"] = PlainTextObject.direct_from_string(self.text) - if self.confirm is not None: - json["confirm"] = extract_json(self.confirm) - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["text"] = PlainTextObject.direct_from_string(self.text) + # if self.confirm is not None: + # json["confirm"] = extract_json(self.confirm) + # return json class LinkButtonElement(ButtonElement): - @property - def attributes(self) -> Set[str]: - return super().attributes.union({"url"}) - - url_max_length = 3000 - - def __init__(self, *, text: str, url: str, style: Optional[str] = None): + def __init__(self, *, text: PlainTextObject, url: str, style: Optional[str] = None): """ A simple button that simply opens a given URL. You will still receive an interaction payload and will need to send an acknowledgement response. - https://api.slack.com/reference/messaging/block-elements#button + This is a helper class that makes creating links simpler. Args: text: String that defines the button's text. Cannot exceed 75 characters. @@ -152,26 +166,21 @@ def __init__(self, *, text: str, url: str, style: Optional[str] = None): style: "primary" or "danger" to add specific styling to this button. """ random_id = "".join(random.choice(string.ascii_uppercase) for _ in range(16)) - super().__init__(text=text, action_id=random_id, value="", style=style) - self.url = url - - @JsonValidator(f"url attribute cannot exceed {url_max_length} characters") - def url_length(self): - return len(self.url) <= self.url_max_length + super().__init__(text=text, url=url, action_id=random_id, value="", style=style) class AbstractSelector(InteractiveElement, metaclass=ABCMeta): placeholder_max_length = 150 def __init__( - self, - *, - placeholder: str, - action_id: str, - subtype: str, - confirm: Optional[ConfirmObject] = None, + self, + *, + placeholder: PlainTextObject, + action_id: str, + type: str, + confirm: Optional[ConfirmObject] = None, ): - super().__init__(action_id=action_id, subtype=subtype) + super().__init__(action_id=action_id, type=type) self.placeholder = placeholder self.confirm = confirm @@ -179,13 +188,162 @@ def __init__( f"placeholder attribute cannot exceed {placeholder_max_length} characters" ) def placeholder_length(self): - return len(self.placeholder) <= self.placeholder_max_length + return len(self.placeholder.text) <= self.placeholder_max_length + + # def to_dict(self, ) -> dict: + # json = super().to_dict() + # json["placeholder"] = PlainTextObject.direct_from_string(self.placeholder) + # if self.confirm is not None: + # json["confirm"] = extract_json(self.confirm) + # return json + + +class StaticSelectElement(AbstractSelector): + options_max_length = 100 + option_groups_max_length = 100 + + def __init__( + self, + *, + placeholder: PlainTextObject, + action_id: str, + options: Optional[List[Option]] = None, + option_groups: Optional[List[OptionGroup]] = None, + initial_option: Optional[Option] = None, + confirm: Optional[ConfirmObject] = None, + ): + """ + This is the simplest form of select menu, with a static list of options passed in when defining the element. + + https://api.slack.com/reference/block-kit/block-elements#static_select + + Args: + placeholder: placeholder text shown on this element. Cannot exceed 150 + characters. + action_id: ID to be used for this action - should be unique. Cannot + exceed 255 characters. + options: An array of option objects. Maximum number of options is 100. + If option_groups is specified, this field should not be. + option_groups: An array of option group objects. Maximum number of option groups is 100. + If options is specified, this field should not be. + initial_option: A single option that exactly matches one of the + options within options or option_groups. This option will be selected + when the menu initially loads. + confirm: A ConfirmObject that defines an optional confirmation dialog + after this element is interacted with. + """ + super().__init__( + placeholder=placeholder, + action_id=action_id, + type="static_select", + confirm=confirm, + ) + self.options = options + self.option_groups = option_groups + self.initial_option = initial_option + + @JsonValidator(f"options attribute cannot exceed {options_max_length} elements") + def options_length(self): + return self.options is None or len(self.options) <= self.options_max_length + + @JsonValidator( + f"option_groups attribute cannot exceed {option_groups_max_length} elements" + ) + def option_groups_length(self): + return ( + self.option_groups is None + or len(self.option_groups) <= self.option_groups_max_length + ) + + @JsonValidator(f"options and option_groups cannot both be specified") + def options_and_option_groups_both_specified(self): + return not (self.options is not None and self.option_groups is not None) + + @JsonValidator(f"options or option_groups must be specified") + def neither_options_or_option_groups_is_specified(self): + return self.options is not None or self.option_groups is not None - def to_dict(self,) -> dict: + # def to_dict(self) -> dict: + # json = super().to_dict() + # if self.option_groups: + # json["option_groups"] = [option.to_dict() for option in self.option_groups] + # elif self.options: + # json["options"] = [option.to_dict() for option in self.options] + # if self.initial_option is not None: + # json["initial_option"] = self.initial_option.to_dict() + # return json + + +class StaticMultiSelectElement(AbstractSelector): + options_max_length = 100 + option_groups_max_length = 100 + + def __init__( + self, + *, + placeholder: PlainTextObject, + action_id: str, + options: Optional[List[Option]] = None, + option_groups: Optional[List[OptionGroup]] = None, + initial_options: Optional[List[Option]] = None, + confirm: Optional[ConfirmObject] = None, + ): + """ + This is the simplest form of select menu, with a static list of options passed in when defining the element. + + https://api.slack.com/reference/block-kit/block-elements#static_multi_select + + Args: + placeholder: placeholder text shown on this element. Cannot exceed 150 + characters. + action_id: ID to be used for this action - should be unique. Cannot + exceed 255 characters. + options: An array of option objects. Maximum number of options is 100. + If option_groups is specified, this field should not be. + option_groups: An array of option group objects. Maximum number of option groups is 100. + If options is specified, this field should not be. + initial_options: An array of option objects that exactly match one + or more of the options within options or option_groups. + These options will be selected when the menu initially loads. + confirm: A ConfirmObject that defines an optional confirmation dialog + after this element is interacted with. + """ + super().__init__( + placeholder=placeholder, + action_id=action_id, + type="multi_static_select", + confirm=confirm, + ) + self.options = options + self.option_groups = option_groups + self.initial_options = initial_options + + @JsonValidator(f"options attribute cannot exceed {options_max_length} elements") + def options_length(self): + return len(self.options) <= self.options_max_length + + @JsonValidator( + f"option_groups attribute cannot exceed {option_groups_max_length} elements" + ) + def option_groups_length(self): + return len(self.option_groups) <= self.option_groups_max_length + + @JsonValidator(f"options and option_groups cannot both be specified") + def options_and_option_groups_both_specified(self): + return self.options is not None and self.option_groups is not None + + @JsonValidator(f"options or option_groups must be specified") + def neither_options_or_option_groups_is_specified(self): + return self.options is None and self.option_groups is None + + def to_dict(self) -> dict: json = super().to_dict() - json["placeholder"] = PlainTextObject.direct_from_string(self.placeholder) - if self.confirm is not None: - json["confirm"] = extract_json(self.confirm) + if self.option_groups: + json["option_groups"] = [option.to_dict() for option in self.option_groups] + else: + json["options"] = [option.to_dict() for option in self.options] + if self.initial_options is not None: + json["initial_options"] = [option.to_dict() for option in self.initial_options] return json @@ -193,13 +351,13 @@ class SelectElement(AbstractSelector): options_max_length = 100 def __init__( - self, - *, - placeholder: str, - action_id: str, - options: List[Union[Option, OptionGroup]], - initial_option: Optional[Option] = None, - confirm: Optional[ConfirmObject] = None, + self, + *, + placeholder: PlainTextObject, + action_id: str, + options: List[Union[Option, OptionGroup]], + initial_option: Optional[Option] = None, + confirm: Optional[ConfirmObject] = None, ): """ This is the simplest form of select menu, with a static list of options @@ -223,11 +381,15 @@ def __init__( super().__init__( placeholder=placeholder, action_id=action_id, - subtype="static_select", + type="static_select", confirm=confirm, ) self.options = options self.initial_option = initial_option + warnings.warn( + "SelectElement will be deprecated in version 3, use StaticSelectElement instead", + PendingDeprecationWarning, + ) @JsonValidator(f"options attribute cannot exceed {options_max_length} elements") def options_length(self): @@ -236,27 +398,24 @@ def options_length(self): def to_dict(self) -> dict: json = super().to_dict() if isinstance(self.options[0], OptionGroup): - json["option_groups"] = extract_json(self.options, "block") + json["option_groups"] = [option.to_dict() for option in self.options] else: - json["options"] = extract_json(self.options, "block") + json["options"] = [option.to_dict() for option in self.options] if self.initial_option is not None: - json["initial_option"] = extract_json(self.initial_option, "block") + json["initial_option"] = self.initial_option.to_dict() return json class ExternalDataSelectElement(AbstractSelector): - @property - def attributes(self) -> Set[str]: - return super().attributes.union({"min_query_length"}) def __init__( - self, - *, - placeholder: str, - action_id: str, - initial_option: Union[Optional[Option], Optional[OptionGroup]] = None, - min_query_length: Optional[int] = None, - confirm: Optional[ConfirmObject] = None, + self, + *, + placeholder: PlainTextObject, + action_id: str, + initial_option: Union[Optional[Option], Optional[OptionGroup]] = None, + min_query_length: Optional[int] = None, + confirm: Optional[ConfirmObject] = None, ): """ This select menu will load its options from an external data source, allowing @@ -281,65 +440,81 @@ def __init__( """ super().__init__( action_id=action_id, - subtype="external_select", + type="external_select", placeholder=placeholder, confirm=confirm, ) self.initial_option = initial_option self.min_query_length = min_query_length - def to_dict(self) -> dict: - json = super().to_dict() - if self.initial_option is not None: - json["initial_option"] = extract_json(self.initial_option, "block") - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if self.initial_option is not None: + # json["initial_option"] = [option.to_dict() for option in self.initial_option] + # return json -class AbstractDynamicSelector(AbstractSelector, metaclass=ABCMeta): - @property - @abstractmethod - def initial_object_type(self): - pass - +class ExternalDataMultiSelectElement(AbstractSelector): def __init__( - self, - *, - placeholder: str, - action_id: str, - initial_value: Optional[str] = None, - confirm: Optional[ConfirmObject] = None, + self, + *, + placeholder: PlainTextObject, + action_id: str, + initial_options: Union[Optional[Option], Optional[OptionGroup]] = None, + min_query_length: Optional[int] = None, + confirm: Optional[ConfirmObject] = None, ): + """ + This select menu will load its options from an external data source, allowing + for a dynamic list of options. + + https://api.slack.com/reference/messaging/block-elements#external-select + + Args: + placeholder: placeholder text shown on this element. Cannot exceed 150 + characters. + action_id: ID to be used for this action - should be unique. Cannot + exceed 255 characters. + initial_options: An array of option objects that exactly match one or + more of the options within options or option_groups. + These options will be selected when the menu initially loads. + min_query_length: When the typeahead field is used, a request will be + sent on every character change. If you prefer fewer requests or more + fully ideated queries, use the min_query_length attribute to tell Slack + the fewest number of typed characters required before dispatch. + confirm: A ConfirmObject that defines an optional confirmation dialog + after this element is interacted with. + """ super().__init__( action_id=action_id, - subtype=f"{self.initial_object_type}s_select", - confirm=confirm, + type="multi_external_select", placeholder=placeholder, + confirm=confirm, ) - self.initial_value = initial_value - - def to_dict(self) -> dict: - json = super().to_dict() - if self.initial_value is not None: - json[f"initial_{self.initial_object_type}"] = self.initial_value - return json + self.initial_options = initial_options + self.min_query_length = min_query_length + # def to_dict(self) -> dict: + # json = super().to_dict() + # if self.initial_options is not None: + # json["initial_options"] = extract_json(self.initial_options, "block") + # return json -class UserSelectElement(AbstractDynamicSelector): - initial_object_type = "user" +class UserSelectElement(AbstractSelector): def __init__( - self, - *, - placeholder: str, - action_id: str, - initial_user: Optional[str] = None, - confirm: Optional[ConfirmObject] = None, + self, + *, + placeholder: PlainTextObject, + action_id: str, + initial_user: Optional[str] = None, + confirm: Optional[ConfirmObject] = None, ): """ This select menu will populate its options with a list of Slack users visible to the current user in the active workspace. - https://api.slack.com/reference/messaging/block-elements#users-select + https://api.slack.com/reference/block-kit/block-elements#users_select Args: placeholder: placeholder text shown on this element. Cannot exceed 150 @@ -353,27 +528,60 @@ def __init__( super().__init__( placeholder=placeholder, action_id=action_id, - initial_value=initial_user, + type="users_select", confirm=confirm, ) + self.initial_user = initial_user, -class ConversationSelectElement(AbstractDynamicSelector): - initial_object_type = "conversation" +class UserMultiSelectElement(AbstractSelector): + def __init__( + self, + *, + placeholder: PlainTextObject, + action_id: str, + initial_users: Optional[List[str]] = None, + confirm: Optional[ConfirmObject] = None, + ): + """ + This select menu will populate its options with a list of Slack users visible to + the current user in the active workspace. + + https://api.slack.com/reference/block-kit/block-elements#users_multi_select + Args: + placeholder: placeholder text shown on this element. Cannot exceed 150 + characters. + action_id: ID to be used for this action - should be unique. Cannot + exceed 255 characters. + initial_users: An array of user IDs of any valid users to be + pre-selected when the menu loads. + confirm: A ConfirmObject that defines an optional confirmation dialog + after this element is interacted with. + """ + super().__init__( + placeholder=placeholder, + action_id=action_id, + type="multi_users_select", + confirm=confirm, + ) + self.initial_users = initial_users + + +class ConversationSelectElement(AbstractSelector): def __init__( - self, - *, - placeholder: str, - action_id: str, - initial_conversation: Optional[str] = None, - confirm: Optional[ConfirmObject] = None, + self, + *, + placeholder: PlainTextObject, + action_id: str, + initial_conversation: Optional[str] = None, + confirm: Optional[ConfirmObject] = None, ): """ This select menu will populate its options with a list of public and private channels, DMs, and MPIMs visible to the current user in the active workspace. - https://api.slack.com/reference/messaging/block-elements#conversation-select + https://api.slack.com/reference/block-kit/block-elements#conversation_select Args: placeholder: placeholder text shown on this element. Cannot exceed 150 @@ -387,27 +595,60 @@ def __init__( super().__init__( placeholder=placeholder, action_id=action_id, - initial_value=initial_conversation, + type="conversations_select", confirm=confirm, ) + self.initial_conversation = initial_conversation, -class ChannelSelectElement(AbstractDynamicSelector): - initial_object_type = "channel" +class ConversationMultiSelectElement(AbstractSelector): + def __init__( + self, + *, + placeholder: PlainTextObject, + action_id: str, + initial_conversations: Optional[List[str]] = None, + confirm: Optional[ConfirmObject] = None, + ): + """ + This select menu will populate its options with a list of public and private + channels, DMs, and MPIMs visible to the current user in the active workspace. + + https://api.slack.com/reference/block-kit/block-elements#conversation_multi_select + + Args: + placeholder: placeholder text shown on this element. Cannot exceed 150 + characters. + action_id: ID to be used for this action - should be unique. Cannot + exceed 255 characters. + initial_conversations: An array of one or more IDs of any valid + conversations to be pre-selected when the menu loads. + confirm: A ConfirmObject that defines an optional confirmation dialog + after this element is interacted with. + """ + super().__init__( + placeholder=placeholder, + action_id=action_id, + type="multi_conversations_select", + confirm=confirm, + ) + self.initial_conversations = initial_conversations + +class ChannelSelectElement(AbstractSelector): def __init__( - self, - *, - placeholder: str, - action_id: str, - initial_channel: Optional[str] = None, - confirm: Optional[ConfirmObject] = None, + self, + *, + placeholder: PlainTextObject, + action_id: str, + initial_channel: Optional[str] = None, + confirm: Optional[ConfirmObject] = None, ): """ This select menu will populate its options with a list of public channels visible to the current user in the active workspace. - https://api.slack.com/reference/messaging/block-elements#channel-select + https://api.slack.com/reference/block-kit/block-elements#channel_select Args: placeholder: placeholder text shown on this element. Cannot exceed 150 @@ -421,13 +662,48 @@ def __init__( super().__init__( placeholder=placeholder, action_id=action_id, - initial_value=initial_channel, + type="channels_select", confirm=confirm, ) + self.initial_channel = initial_channel + + +class ChannelMultiSelectElement(AbstractSelector): + def __init__( + self, + *, + placeholder: PlainTextObject, + action_id: str, + initial_channels: Optional[List[str]] = None, + confirm: Optional[ConfirmObject] = None, + ): + """ + This select menu will populate its options with a list of public channels + visible to the current user in the active workspace. + + https://api.slack.com/reference/block-kit/block-elements#channel_multi_select + + Args: + placeholder: placeholder text shown on this element. Cannot exceed 150 + characters. + action_id: ID to be used for this action - should be unique. Cannot + exceed 255 characters. + initial_channels: An array of one or more IDs of any valid public + channel to be pre-selected when the menu loads. + confirm: A ConfirmObject that defines an optional confirmation dialog + after this element is interacted with. + """ + super().__init__( + placeholder=placeholder, + action_id=action_id, + type="multi_channels_select", + confirm=confirm + ) + self.initial_channels = initial_channels class OverflowMenuOption(Option): - def __init__(self, label: str, value: str, url: Optional[str] = None): + def __init__(self, text: PlainTextObject, value: str, url: Optional[str] = None): """ An extension of a standard option, but with an optional 'url' attribute, which will simply directly navigate to a given URL. Only valid in @@ -436,7 +712,7 @@ def __init__(self, label: str, value: str, url: Optional[str] = None): https://api.slack.com/reference/messaging/composition-objects#option Args: - label: A short, user-facing string to label this option to users. + text: 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 @@ -446,14 +722,14 @@ def __init__(self, label: str, value: str, url: Optional[str] = None): you'll still receive an interaction payload and will need to send an acknowledgement response. """ - super().__init__(label=label, value=value) + super().__init__(text=text, value=value) self.url = url - def to_dict(self, option_type: str = "block") -> dict: - json = super().to_dict(option_type) - if self.url is not None: - json["url"] = self.url - return json + # def to_dict(self, option_type: str = "block") -> dict: + # json = super().to_dict(option_type) + # if self.url is not None: + # json["url"] = self.url + # return json class OverflowMenuElement(InteractiveElement): @@ -461,11 +737,11 @@ class OverflowMenuElement(InteractiveElement): options_max_length = 5 def __init__( - self, - *, - options: List[Union[Option, OverflowMenuOption]], - action_id: str, - confirm: Optional[ConfirmObject] = None, + self, + *, + options: List[Union[Option, OverflowMenuOption]], + action_id: str, + confirm: Optional[ConfirmObject] = None, ): """ This is like a cross between a button and a select menu - when a user clicks @@ -488,7 +764,7 @@ def __init__( confirm: A ConfirmObject that defines an optional confirmation dialog after this element is interacted with. """ - super().__init__(action_id=action_id, subtype="overflow") + super().__init__(action_id=action_id, type="overflow") self.options = options self.confirm = confirm @@ -499,26 +775,22 @@ def __init__( def options_length(self): return self.options_min_length < len(self.options) <= self.options_max_length - def to_dict(self) -> dict: - json = super().to_dict() - json["options"] = extract_json(self.options, "block") - if self.confirm is not None: - json["confirm"] = extract_json(self.confirm) - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["options"] = extract_json(self.options, "block") + # if self.confirm is not None: + # json["confirm"] = extract_json(self.confirm) + # return json class DatePickerElement(AbstractSelector): - @property - def attributes(self) -> Set[str]: - return super().attributes.union({"initial_date"}) - def __init__( - self, - *, - action_id: str, - placeholder: Optional[str] = None, - initial_date: Optional[str] = None, - confirm: Optional[ConfirmObject] = None, + self, + *, + action_id: str, + placeholder: Optional[PlainTextObject] = None, + initial_date: Optional[str] = None, + confirm: Optional[ConfirmObject] = None, ): """ An element which lets users easily select a date from a calendar style UI. @@ -538,7 +810,7 @@ def __init__( """ super().__init__( action_id=action_id, - subtype="datepicker", + type="datepicker", placeholder=placeholder, confirm=confirm, ) @@ -549,3 +821,46 @@ def initial_date_valid(self): return self.initial_date is None or re.match( r"\d{4}-[01][12]-[0123]\d", self.initial_date ) + + +class PlainTextElement(BlockElement): + min_length_max_value = 3000 + + def __init__(self, *, + action_id: str, + placeholder: PlainTextObject = None, + initial_value: str = None, + multiline: bool = None, + min_length: int = None, + max_length: int = None): + """ + A plain-text input, similar to the HTML tag, creates a field where a user can enter freeform data. + It can appear as a single-line field or a larger textarea using the multiline flag.. + + https://api.slack.com/reference/block-kit/block-elements#input + + Args: + placeholder:placeholder text shown on this element. Cannot exceed 150 + characters. + action_id: ID to be used for this action - should be unique. Cannot + exceed 255 characters. + initial_value: The initial value in the plain-text input when it is loaded + multiline: Indicates whether the input will be a single line + min_length: The minimum length of input that the user must provide. + If the user provides less, they will receive an error. + max_length: The maximum length of input that the user can provide. + If the user provides more, they will receive an error. + """ + super().__init__(type="plain_text_input") + self.placeholder = placeholder + self.action_id = action_id + self.initial_value = initial_value + self.multiline = multiline + self.min_length = min_length + self.max_length = max_length + + @JsonValidator( + f"max value for min length is {min_length_max_value}" + ) + def max_value_length(self): + return self.min_length is None or self.min_length <= self.min_length_max_value diff --git a/slack/web/classes/messages.py b/slack/web/classes/messages.py index 45e331c0d..3f0bf6f7e 100644 --- a/slack/web/classes/messages.py +++ b/slack/web/classes/messages.py @@ -1,7 +1,7 @@ import logging from typing import List, Optional -from . import JsonObject, JsonValidator, extract_json +from . import JsonObject, JsonValidator from .attachments import Attachment from .blocks import Block @@ -9,8 +9,6 @@ class Message(JsonObject): - attributes = {"text"} - attachments_max_length = 100 def __init__( @@ -38,9 +36,9 @@ def __init__( bold/italics, or leave text completely unmodified. """ self.text = text - self.attachments = attachments or [] - self.blocks = blocks or [] - self.markdown = markdown + self.attachments = attachments + self.blocks = blocks + self.mrkdwn = markdown @JsonValidator( f"attachments attribute cannot exceed {attachments_max_length} items" @@ -51,20 +49,20 @@ def attachments_length(self): or len(self.attachments) <= self.attachments_max_length ) - def to_dict(self) -> dict: - json = super().to_dict() - if len(self.text) > 40000: - LOGGER.error( - "Messages over 40,000 characters are automatically truncated by Slack" - ) - if self.text and self.blocks: - # Slack doesn't render the text property if there are blocks, so: - LOGGER.info( - "text attribute is treated as fallback text if blocks are attached to " - "a message - insert text as a new SectionBlock if you want it to be " - "displayed " - ) - json["attachments"] = extract_json(self.attachments) - json["blocks"] = extract_json(self.blocks) - json["mrkdwn"] = self.markdown - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # if len(self.text) > 40000: + # LOGGER.error( + # "Messages over 40,000 characters are automatically truncated by Slack" + # ) + # if self.text and self.blocks: + # # Slack doesn't render the text property if there are blocks, so: + # LOGGER.info( + # "text attribute is treated as fallback text if blocks are attached to " + # "a message - insert text as a new SectionBlock if you want it to be " + # "displayed " + # ) + # json["attachments"] = extract_json(self.attachments) + # json["blocks"] = extract_json(self.blocks) + # json["mrkdwn"] = self.mrkdwn + # return json diff --git a/slack/web/classes/modals.py b/slack/web/classes/modals.py new file mode 100644 index 000000000..5e73d39ec --- /dev/null +++ b/slack/web/classes/modals.py @@ -0,0 +1,406 @@ +from typing import List, Optional, Union + +from . import JsonObject, JsonValidator +from .blocks import ( + Block, + BlockElement, + SectionBlock, + DividerBlock, + ImageBlock, + ActionsBlock, + InteractiveElement, + ContextBlock, + InputBlock, + FileBlock, +) +from .elements import PlainTextObject, PlainTextElement +from .objects import TextObject + + +class ModalBuilder(JsonObject): + """The ModalBuilder enables you to more easily construct the JSON required + to create a modal in Slack. + + Modals are a focused surface to collect data from users + or display dynamic and interactive information. + + To learn how modals are invoked, how to compose their contents, + and how to enable and handle complex interactivity read this guide: + + https://api.slack.com/block-kit/surfaces/modals + """ + + type: str + title: PlainTextObject + blocks: List[Block] + close: Optional[PlainTextObject] + submit: Optional[PlainTextObject] + private_metadata: Optional[str] + callback_id: Optional[str] + clear_on_close: Optional[bool] + notify_on_close: Optional[bool] + external_id: Optional[str] + + title_max_length = 24 + blocks_max_length = 100 + close_max_length = 24 + submit_max_length = 24 + private_metadata_max_length = 3000 + callback_id_max_length = 255 + + attributes = { + "title", + "blocks", + "close", + "submit", + "private_metadata", + "callback_id", + "clear_on_close", + "notify_on_close", + "external_id", + } + + class Modal(JsonObject): + def __init__(self): + self.type = "modal" + self.title = None + self.blocks = [] + self.close = None + self.submit = None + self.private_metadata = None + self.callback_id = None + self.clear_on_close = None + self.notify_on_close = None + self.external_id = None + + def __init__(self): + self.modal = self.Modal() + # self.type = "modal" + # self.title = PlainTextObject(text="None") + # self.blocks = [] + # self.close = None + # self.submit = None + # self.private_metadata = None + # self.callback_id = None + # self.clear_on_close = False + # self.notify_on_close = False + # self.external_id = None + + def title(self, title: str) -> "ModalBuilder": + """ + Specify a title for this modal + + Args: + title: must not exceed 24 characters + """ + self.modal.title = PlainTextObject(text=title) + return self + + def close(self, close: str) -> "ModalBuilder": + """ + Specify the text displayed in the close button at the bottom-right of the view. + + Max length of 24 characters. + + Args: + close: must not exceed 24 characters + """ + self.modal.close = PlainTextObject(text=close) + return self + + def submit(self, submit: str) -> "ModalBuilder": + """ + Specify the text displayed in the submit button at the bottom-right of the view. + + Important Note: submit is required when an input block is within the blocks array. + + Max length of 24 characters. + + Args: + submit: must not exceed 24 characters + """ + self.modal.submit = PlainTextObject(text=submit) + return self + + def private_metadata(self, private_metadata: str) -> "ModalBuilder": + """An optional string that will be sent to your app in view_submission + and block_actions events. + + Args: + private_metadata: must not exceed 3000 characters + """ + self.modal.private_metadata = private_metadata + return self + + def callback_id(self, callback_id: str) -> "ModalBuilder": + """An identifier to recognize interactions and submissions of this particular modal. + Don't use this to store sensitive information (use private_metadata instead). + + Args: + callback_id: must not exceed 255 characters + """ + self.modal.callback_id = callback_id + return self + + def clear_on_close(self, clear_on_close: bool) -> "ModalBuilder": + """When set to true, clicking on the close button will clear + all views in a modal and close it. + + Args: + clear_on_close: Default is false. + """ + self.modal.clear_on_close = clear_on_close + return self + + def notify_on_close(self, notify_on_close: bool) -> "ModalBuilder": + """ Indicates whether Slack will send your request URL a view_closed + event when a user clicks the close button. + + Args: + notify_on_close: Default is false. + """ + self.notify_on_close = notify_on_close + return self + + def external_id(self, external_id: str) -> "ModalBuilder": + """A custom identifier that must be unique for all views on a per-team basis. + + Args: + external_id: A unique identifier. + """ + self.modal.external_id = external_id + return self + + def section( + self, + *, + text: TextObject = None, + block_id: Optional[str] = None, + fields: List[str] = None, + accessory: Optional[BlockElement] = None, + ) -> "ModalBuilder": + """A section is one of the most flexible blocks available. + It can be used as a simple text block, in combination with + text fields, or side-by-side with any of the available block elements. + + https://api.slack.com/reference/block-kit/blocks#section + + Args: + text: The text for the block, in the form of string or a text object. + Maximum length for the text in this field is 3000 characters. + block_id: A string acting as a unique identifier for a block. + You can use this block_id when you receive an interaction + payload to identify the source of the action. If not + specified, one will be generated. Maximum length for this + field is 255 characters. block_id should be unique for each + message and each iteration of a message. + If a message is updated, use a new block_id. + fields: optional: a sequence of strings that will be rendered using + MarkdownTextObjects. Any strings included with fields will be rendered + in a compact format that allows for 2 columns of side-by-side text. + Maximum number of items is 10. + Maximum length for the text in each item is 2000 characters. + accessory: an optional BlockElement to attach to this SectionBlock as + secondary content + """ + self.modal.blocks.append( + SectionBlock( + text=text, block_id=block_id, fields=fields, accessory=accessory + ) + ) + return self + + def divider(self, *, block_id: Optional[str] = None): + """A content divider, like an
, to split up different blocks inside of a message. + + Args: + block_id: A string acting as a unique identifier for a block. + You can use this block_id when you receive an interaction + payload to identify the source of the action. If not + specified, one will be generated. Maximum length for this + field is 255 characters. block_id should be unique for each + message and each iteration of a message. + If a message is updated, use a new block_id. + + https://api.slack.com/reference/block-kit/blocks#divider + """ + self.modal.blocks.append(DividerBlock(block_id=block_id)) + return self + + def image( + self, + *, + image_url: str, + alt_text: str, + title: Optional[str] = None, + block_id: Optional[str] = None, + ): + """A simple image block, designed to make those cat photos really pop. + + https://api.slack.com/reference/block-kit/blocks#image + + Args: + image_url: Publicly hosted URL to be displayed. Cannot exceed 3000 + characters. + alt_text: Plain text summary of image. Cannot exceed 2000 characters. + title: A title to be displayed above the image. Cannot exceed 2000 + characters. + block_id: ID to be used for this block - autogenerated if left blank. + Cannot exceed 255 characters. + """ + self.modal.blocks.append( + ImageBlock( + image_url=image_url, alt_text=alt_text, title=title, block_id=block_id + ) + ) + return self + + def actions( + self, *, elements: List[InteractiveElement], block_id: Optional[str] = None + ): + """A block that is used to hold interactive elements. + + https://api.slack.com/reference/block-kit/blocks#actions + + Args: + elements: Up to 5 InteractiveElement objects - buttons, date pickers, etc + block_id: ID to be used for this block - autogenerated if left blank. + Cannot exceed 255 characters. + """ + self.modal.blocks.append(ActionsBlock(elements=elements, block_id=block_id)) + return self + + def context( + self, *, elements: List[Union[ImageBlock, TextObject]], block_id: Optional[str] = None + ): + """Displays message context, which can include both images and text. + + https://api.slack.com/reference/block-kit/blocks#context + + Args: + elements: Up to 10 ImageElements and TextObjects + block_id: ID to be used for this block - autogenerated if left blank. + Cannot exceed 255 characters. + """ + self.modal.blocks.append(ContextBlock(elements=elements, block_id=block_id)) + return self + + def input( + self, + *, + label: PlainTextObject, + element: Union[InteractiveElement, PlainTextElement], + hint: Optional[str] = None, + optional: Optional[bool] = None, + block_id: Optional[str] = None + ): + """A block that collects information from users - it can hold a + plain-text input element, a select menu element, a multi-select menu element, + or a datepicker. + + Important Note: Input blocks are only available in modals. + + https://api.slack.com/reference/block-kit/blocks#input + + Args: + label: A label that appears above an input element in the + form of a text object that must have type of plain_text. + Maximum length for the text in this field is 2000 characters. + element: An plain-text input element, a select menu element, + a multi-select menu element, or a datepicker. + hint: An optional hint that appears below an input element in a lighter grey. + Maximum length for the text in this field is 2000 characters. + optional: A boolean that indicates whether the input element + may be empty when a user submits the modal. Defaults to false. + block_id: ID to be used for this block - autogenerated if left blank. + Cannot exceed 255 characters. + """ + self.modal.blocks.append( + InputBlock(label=label, element=element, hint=hint, optional=optional, block_id=block_id) + ) + return self + + def file( + self, + *, + external_id: str, + source: str = "remote", + block_id: Optional[str] = None, + ): + """Displays a remote file. + + https://api.slack.com/reference/block-kit/blocks#file + """ + self.modal.blocks.append( + FileBlock(external_id=external_id, source=source, block_id=block_id) + ) + return self + + @JsonValidator(f"title must be between 1 and {title_max_length} characters") + def title_length(self): + if self.modal.title is not None: + return len(self.modal.title.text) <= self.title_max_length + + return False + + @JsonValidator(f"modals must contain between 1 and {blocks_max_length} blocks") + def blocks_length(self): + return 0 < len(self.modal.blocks) <= self.blocks_max_length + + @JsonValidator(f"close cannot exceed {close_max_length} characters") + def close_length(self): + if self.modal.close is not None: + return len(self.modal.close.text) <= self.close_max_length + + return True + + @JsonValidator(f"submit cannot exceed {submit_max_length} characters") + def submit_length(self): + if self.submit is not None: + return len(self.modal.submit.text) <= self.submit_max_length + + return True + + @JsonValidator( + f"submit is required when an 'input' block is within the blocks array" + ) + def submit_required_when_input_block_used(self): + if self.modal.submit is None: + return InputBlock not in [b.__class__ for b in self.modal.blocks] + + return True + + @JsonValidator( + f"private_metadata cannot exceed {private_metadata_max_length} characters" + ) + def private_metadata__length(self): + if self.modal.private_metadata is None: + return True + + return len(self.modal.private_metadata) <= self.private_metadata_max_length + + @JsonValidator(f"callback_id cannot exceed {callback_id_max_length} characters") + def callback_id__length(self): + if self.modal.callback_id is None: + return True + + return len(self.modal.callback_id) <= self.callback_id_max_length + + def to_dict(self) -> dict: + return self.modal.to_dict() + # self.validate_json() + # + # fields = { + # "type": self.type, + # "title": extract_json(self.title), + # "blocks": extract_json(self.blocks), + # "close": extract_json(self.close), + # "submit": extract_json(self.submit), + # "private_metadata": self.private_metadata, + # "callback_id": self.callback_id, + # "clear_on_close": self.clear_on_close, + # "notify_on_close": self.notify_on_close, + # "external_id": self.external_id, + # } + # + # return {k: v for k, v in fields.items() if v is not None} diff --git a/slack/web/classes/objects.py b/slack/web/classes/objects.py index 0dd871d58..41ecfe9d6 100644 --- a/slack/web/classes/objects.py +++ b/slack/web/classes/objects.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import List, Optional, Set, Union -from . import BaseObject, JsonObject, JsonValidator, extract_json +from . import BaseObject, JsonObject, JsonValidator ButtonStyles = {"danger", "primary"} DynamicSelectElementTypes = {"channels", "conversations", "users"} @@ -32,12 +32,12 @@ def __str__(self): class DateLink(Link): def __init__( - self, - *, - date: Union[datetime, int], - date_format: str, - fallback: str, - link: Optional[str] = None, + 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 @@ -127,17 +127,17 @@ def __init__(self): class TextObject(JsonObject): attributes = {"text", "type"} - def __init__(self, *, text: str, subtype: str): + def __init__(self, *, text: str, type: str): """ Super class for new text "objects" used in Block kit """ self.text = text - self.subtype = subtype + self.type = type - def to_dict(self) -> dict: - json = super().to_dict() - json["type"] = self.subtype - return json + # def to_dict(self) -> dict: + # json = super().to_dict() + # json["type"] = self.type + # return json class PlainTextObject(TextObject): @@ -155,7 +155,7 @@ def __init__(self, *, text: str, emoji: bool = True): Args: emoji: Whether to escape emoji in text into Slack's :emoji: format """ - super().__init__(text=text, subtype="plain_text") + super().__init__(text=text, type="plain_text") self.emoji = emoji @staticmethod @@ -183,7 +183,7 @@ def __init__(self, *, text: str, verbatim: bool = False): auto-converted into links, conversation names will be link-ified, and certain mentions will be automatically parsed. """ - super().__init__(text=text, subtype="mrkdwn") + super().__init__(text=text, type="mrkdwn") self.verbatim = verbatim @staticmethod @@ -214,12 +214,12 @@ class ConfirmObject(JsonObject): deny_max_length = 30 def __init__( - self, - *, - title: str, - text: Union[TextObject, str], - confirm: str = "Yes", - deny: str = "No", + self, + *, + title: PlainTextObject, + text: TextObject, + confirm: PlainTextObject = PlainTextObject(text="Yes"), + deny: PlainTextObject = PlainTextObject(text="No"), ): """ An object that defines a dialog that provides a confirmation step to any @@ -245,45 +245,45 @@ def __init__( @JsonValidator(f"title attribute cannot exceed {title_max_length} characters") def title_length(self): - return len(self.title) <= self.title_max_length + return len(self.title.text) <= 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 + return len(self.text.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 + return len(self.confirm.text) <= 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 + return len(self.deny.text) <= 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.text), + # "deny": PlainTextObject.direct_from_string(self.deny.text), + # } + # if isinstance(self.text, TextObject): + # json["text"] = self.text.to_dict() + # else: + # json["text"] = MarkdownTextObject.direct_from_string(self.text.text) + # return json class Option(JsonObject): @@ -299,7 +299,7 @@ class Option(JsonObject): label_max_length = 75 value_max_length = 75 - def __init__(self, *, label: str, value: str, description: Optional[str] = None): + def __init__(self, *, text: PlainTextObject, value: str, description: Optional[str] = None): """ An object that represents a single selectable item in a block element ( SelectElement, OverflowMenuElement) or dialog element @@ -315,7 +315,7 @@ def __init__(self, *, label: str, value: str, description: Optional[str] = None) https://api.slack.com/docs/interactive-message-field-guide#option_fields Args: - label: A short, user-facing string to label this option to users. + text: 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 @@ -324,13 +324,13 @@ def __init__(self, *, label: str, value: str, description: Optional[str] = None) this option. Only supported in legacy message actions, not in blocks or dialogs. """ - self.label = label + self.text = text 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 + return len(self.text.text) <= self.label_max_length @JsonValidator(f"value attribute cannot exceed {value_max_length} characters") def value_length(self): @@ -344,15 +344,15 @@ def to_dict(self, option_type: str = "block") -> dict: """ self.validate_json() if option_type == "dialog": - return {"label": self.label, "value": self.value} + return {"label": self.text, "value": self.value} elif option_type == "action": - json = {"text": self.label, "value": self.value} + json = {"text": self.text, "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), + "text": PlainTextObject.direct_from_string(self.text.text), "value": self.value, } @@ -361,7 +361,7 @@ 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) + return Option(value=value_and_label, text=PlainTextObject(text=value_and_label)) class OptionGroup(JsonObject): @@ -409,15 +409,15 @@ def to_dict(self, option_type: str = "block") -> dict: if option_type == "dialog": return { "label": self.label, - "options": extract_json(self.options, option_type), + "options": [option.to_dict() for option in self.options] } elif option_type == "action": return { "text": self.label, - "options": extract_json(self.options, option_type), + "options": [option.to_dict() for option in self.options] } 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), + "options": [option.to_dict() for option in self.options] } diff --git a/slack/web/client.py b/slack/web/client.py index 94a6ff17f..15e4c4b6a 100644 --- a/slack/web/client.py +++ b/slack/web/client.py @@ -1550,6 +1550,23 @@ def views_push( kwargs.update({"trigger_id": trigger_id, "view": view}) return self.api_call("views.push", json=kwargs) + def views_publish( + self, *, user_id: str, view: dict, **kwargs + ) -> Union[Future, SlackResponse]: + """Publish a static view for a User. + + Create or update the view that comprises an + app's Home tab (https://api.slack.com/surfaces/tabs) + for a specific user. + + Args: + user_id (str): id of the user you want publish a view to. + e.g. 'U0BPQUNTA' + view (dict): The view payload. + """ + kwargs.update({"user_id": user_id, "view": view}) + return self.api_call("views.push", json=kwargs) + def views_update( self, *, view: dict, external_id: str = None, view_id: str = None, **kwargs ) -> Union[Future, SlackResponse]: diff --git a/tests/web/classes/test_actions.py b/tests/web/classes/test_actions.py index 3042d8dbb..8cfcb9ec9 100644 --- a/tests/web/classes/test_actions.py +++ b/tests/web/classes/test_actions.py @@ -10,40 +10,42 @@ ActionStaticSelector, ActionUserSelector, ) -from slack.web.classes.objects import ConfirmObject, Option, OptionGroup +from slack.web.classes.objects import ConfirmObject, Option, OptionGroup, PlainTextObject, MarkdownTextObject from tests.web.classes import STRING_3001_CHARS class ButtonTests(unittest.TestCase): - def test_json(self): - self.assertDictEqual( - ActionButton(name="button_1", text="Click me!", value="btn_1").to_dict(), - { - "name": "button_1", - "text": "Click me!", - "value": "btn_1", - "type": "button", - }, - ) - confirm = ConfirmObject(title="confirm_title", text="confirm_text") - self.assertDictEqual( - ActionButton( - name="button_1", - text="Click me!", - value="btn_1", - confirm=confirm, - style="danger", - ).to_dict(), - { - "name": "button_1", - "text": "Click me!", - "value": "btn_1", - "type": "button", - "confirm": confirm.to_dict("action"), - "style": "danger", - }, - ) + def test_json_simple(self): + button = ActionButton(name="button_1", text="Click me!", value="btn_1").to_dict() + coded = { + "name": "button_1", + "text": "Click me!", + "value": "btn_1", + "type": "button", + } + self.assertDictEqual(button, coded) + + def test_json_with_confirm(self): + confirm = ConfirmObject(title=PlainTextObject(text="confirm_title"), + text=MarkdownTextObject(text="confirm_text")) + button = ActionButton( + name="button_1", + text="Click me!", + value="btn_1", + confirm=confirm, + style="danger", + ).to_dict() + + coded = { + "name": "button_1", + "text": "Click me!", + "value": "btn_1", + "type": "button", + "confirm": confirm.to_dict("action"), + "style": "danger", + } + self.assertDictEqual(button, coded) def test_value_length(self): with self.assertRaises(SlackObjectFormationError): @@ -79,20 +81,18 @@ def setUp(self) -> None: self.option_group = [OptionGroup(label="group_1", options=self.options)] - def test_json(self): - self.assertDictEqual( - ActionStaticSelector( - name="select_1", text="selector_1", options=self.options - ).to_dict(), - { + def test_json_with_option(self): + selector = ActionStaticSelector(name="select_1", text="selector_1", options=self.options).to_dict() + coded = { "name": "select_1", "text": "selector_1", "options": [o.to_dict("action") for o in self.options], "type": "select", "data_source": "static", - }, - ) + } + self.assertDictEqual(selector, coded) + def test_json_with_option_group(self): self.assertDictEqual( ActionStaticSelector( name="select_1", text="selector_1", options=self.option_group diff --git a/tests/web/classes/test_blocks.py b/tests/web/classes/test_blocks.py index 1509f6424..12094ff47 100644 --- a/tests/web/classes/test_blocks.py +++ b/tests/web/classes/test_blocks.py @@ -9,7 +9,7 @@ SectionBlock, ) from slack.web.classes.elements import ButtonElement, ImageElement, LinkButtonElement -from slack.web.classes.objects import PlainTextObject +from slack.web.classes.objects import PlainTextObject, MarkdownTextObject from . import STRING_3001_CHARS @@ -19,19 +19,21 @@ def test_json(self): class SectionBlockTests(unittest.TestCase): - def test_json(self): - self.assertDictEqual( - SectionBlock(text="some text", block_id="a_block").to_dict(), - { - "text": {"text": "some text", "type": "mrkdwn", "verbatim": False}, - "block_id": "a_block", - "type": "section", - }, - ) + def test_json_simple(self): + section = SectionBlock(text=MarkdownTextObject(text="some text"), block_id="a_block").to_dict() + json = { + "text": {"text": "some text", "type": "mrkdwn", "verbatim": False}, + "block_id": "a_block", + "type": "section", + } + self.assertDictEqual(section, json) + + def test_json_with_fields(self): self.assertDictEqual( SectionBlock( - text="some text", fields=[f"field{i}" for i in range(5)] + text=MarkdownTextObject(text="some text"), + fields=[MarkdownTextObject(text=f"field{i}") for i in range(5)] ).to_dict(), { "text": {"text": "some text", "type": "mrkdwn", "verbatim": False}, @@ -46,15 +48,15 @@ def test_json(self): }, ) - button = LinkButtonElement(text="Click me!", url="http://google.com") - self.assertDictEqual( - SectionBlock(text="some text", accessory=button).to_dict(), - { - "text": {"text": "some text", "type": "mrkdwn", "verbatim": False}, - "accessory": button.to_dict(), - "type": "section", - }, - ) + def test_json_with_accessory(self): + button = LinkButtonElement(text=PlainTextObject(text="Click me!"), url="http://google.com") + section = SectionBlock(text=MarkdownTextObject(text="some text"), accessory=button).to_dict() + coded = { + "text": {"text": "some text", "type": "mrkdwn", "verbatim": False}, + "accessory": button.to_dict(), + "type": "section", + } + self.assertDictEqual(section, coded) def test_text_or_fields_populated(self): with self.assertRaises(SlackObjectFormationError): @@ -62,7 +64,7 @@ def test_text_or_fields_populated(self): def test_fields_length(self): with self.assertRaises(SlackObjectFormationError): - SectionBlock(fields=[f"field{i}" for i in range(11)]).to_dict() + SectionBlock(fields=[MarkdownTextObject(text=f"field{i}") for i in range(11)]).to_dict() class ImageBlockTests(unittest.TestCase): @@ -98,15 +100,14 @@ def test_title_length(self): class ActionsBlockTests(unittest.TestCase): def setUp(self) -> None: self.elements = [ - ButtonElement(text="Click me", action_id="reg_button", value="1"), - LinkButtonElement(text="URL Button", url="http://google.com"), + ButtonElement(text=PlainTextObject(text="Click me"), action_id="reg_button", value="1"), + LinkButtonElement(text=PlainTextObject(text="URL Button"), url="http://google.com"), ] def test_json(self): - self.assertDictEqual( - ActionsBlock(elements=self.elements).to_dict(), - {"elements": [e.to_dict() for e in self.elements], "type": "actions"}, - ) + block = ActionsBlock(elements=self.elements).to_dict() + hard = {"type": "actions", "elements": [e.to_dict() for e in self.elements]} + self.assertDictEqual(block, hard) def test_elements_length(self): with self.assertRaises(SlackObjectFormationError): diff --git a/tests/web/classes/test_elements.py b/tests/web/classes/test_elements.py index f9ca47f11..07a38f03c 100644 --- a/tests/web/classes/test_elements.py +++ b/tests/web/classes/test_elements.py @@ -8,10 +8,9 @@ ExternalDataSelectElement, ImageElement, LinkButtonElement, - SelectElement, UserSelectElement, -) -from slack.web.classes.objects import ConfirmObject, Option + StaticSelectElement) +from slack.web.classes.objects import ConfirmObject, Option, PlainTextObject from . import STRING_3001_CHARS, STRING_301_CHARS @@ -19,65 +18,64 @@ class InteractiveElementTests(unittest.TestCase): def test_action_id(self): with self.assertRaises(SlackObjectFormationError): ButtonElement( - text="click me!", action_id=STRING_301_CHARS, value="clickable button" + text=PlainTextObject(text="click me!"), action_id=STRING_301_CHARS, value="clickable button" ).to_dict() class ButtonElementTests(unittest.TestCase): - def test_json(self): - self.assertDictEqual( - ButtonElement( - text="button text", action_id="some_button", value="button_123" - ).to_dict(), - { - "text": {"emoji": True, "text": "button text", "type": "plain_text"}, - "action_id": "some_button", - "value": "button_123", - "type": "button", - }, - ) - confirm = ConfirmObject(title="really?", text="are you sure?") - - self.assertDictEqual( - ButtonElement( - text="button text", - action_id="some_button", - value="button_123", - style="primary", - confirm=confirm, - ).to_dict(), - { - "text": {"emoji": True, "text": "button text", "type": "plain_text"}, - "action_id": "some_button", - "value": "button_123", - "type": "button", - "style": "primary", - "confirm": confirm.to_dict(), - }, - ) + def test_json_simple(self): + button = ButtonElement(text=PlainTextObject(text="button text"), action_id="some_button", + value="button_123").to_dict() + coded = { + "text": {"emoji": True, "text": "button text", "type": "plain_text"}, + "action_id": "some_button", + "value": "button_123", + "type": "button", + } + self.assertDictEqual(button, coded) + + def test_json_with_confirm(self): + confirm = ConfirmObject(title=PlainTextObject(text="really?"), + text=PlainTextObject(text="are you sure?")) + button = ButtonElement( + text=PlainTextObject(text="button text"), + action_id="some_button", + value="button_123", + style="primary", + confirm=confirm, + ).to_dict() + coded = { + "text": {"emoji": True, "text": "button text", "type": "plain_text"}, + "action_id": "some_button", + "value": "button_123", + "type": "button", + "style": "primary", + "confirm": confirm.to_dict(), + } + self.assertDictEqual(button, coded) def test_text_length(self): with self.assertRaises(SlackObjectFormationError): ButtonElement( - text=STRING_301_CHARS, action_id="button", value="click_me" + text=PlainTextObject(text=STRING_301_CHARS), action_id="button", value="click_me" ).to_dict() def test_value_length(self): with self.assertRaises(SlackObjectFormationError): ButtonElement( - text="Button", action_id="button", value=STRING_3001_CHARS + text=PlainTextObject(text="button text"), action_id="button", value=STRING_3001_CHARS ).to_dict() def test_invalid_style(self): with self.assertRaises(SlackObjectFormationError): ButtonElement( - text="Button", action_id="button", value="button", style="invalid" + text=PlainTextObject(text="button text"), action_id="button", value="button", style="invalid" ).to_dict() class LinkButtonElementTests(unittest.TestCase): def test_json(self): - button = LinkButtonElement(text="button text", url="http://google.com") + button = LinkButtonElement(text=PlainTextObject(text="button text"), url="http://google.com") self.assertDictEqual( button.to_dict(), { @@ -91,7 +89,7 @@ def test_json(self): def test_url_length(self): with self.assertRaises(SlackObjectFormationError): - LinkButtonElement(text="Button", url=STRING_3001_CHARS).to_dict() + LinkButtonElement(text=PlainTextObject(text="button text"), url=STRING_3001_CHARS).to_dict() class ImageElementTests(unittest.TestCase): @@ -125,50 +123,50 @@ class SelectElementTests(unittest.TestCase): def test_json(self): self.maxDiff = None - self.assertDictEqual( - SelectElement( - placeholder="selectedValue", - action_id="dropdown", - options=self.options, - initial_option=self.option_two, - ).to_dict(), - { - "placeholder": { - "emoji": True, - "text": "selectedValue", - "type": "plain_text", - }, - "action_id": "dropdown", - "options": [o.to_dict("block") for o in self.options], - "initial_option": self.option_two.to_dict(), - "type": "static_select", + select = StaticSelectElement( + placeholder=PlainTextObject(text="selectedValue"), + action_id="dropdown", + options=self.options, + initial_option=self.option_two, + ).to_dict() + coded = { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", }, - ) - - self.assertDictEqual( - SelectElement( - placeholder="selectedValue", - action_id="dropdown", - options=self.options, - confirm=ConfirmObject(title="title", text="text"), - ).to_dict(), - { - "placeholder": { - "emoji": True, - "text": "selectedValue", - "type": "plain_text", - }, - "action_id": "dropdown", - "options": [o.to_dict("block") for o in self.options], - "confirm": ConfirmObject(title="title", text="text").to_dict("block"), - "type": "static_select", + "action_id": "dropdown", + "options": [o.to_dict("block") for o in self.options], + "initial_option": self.option_two.to_dict(), + "type": "static_select", + } + self.assertDictEqual(select, coded) + + def test_json_with_confirm(self): + confirm = ConfirmObject(title=PlainTextObject(text="title"), text=PlainTextObject(text="text")) + select = StaticSelectElement( + placeholder=PlainTextObject(text="selectedValue"), + action_id="dropdown", + options=self.options, + confirm=confirm, + ).to_dict() + coded = { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", }, - ) + "action_id": "dropdown", + "options": [o.to_dict() for o in self.options], + "confirm": confirm.to_dict(), + "type": "static_select", + } + self.assertDictEqual(select, coded) def test_options_length(self): with self.assertRaises(SlackObjectFormationError): - SelectElement( - placeholder="select", + StaticSelectElement( + placeholder=PlainTextObject(text="select"), action_id="selector", options=[self.option_one] * 101, ).to_dict() @@ -178,7 +176,7 @@ class ExternalDropdownElementTests(unittest.TestCase): def test_json(self): self.assertDictEqual( ExternalDataSelectElement( - placeholder="selectedValue", action_id="dropdown", min_query_length=5 + placeholder=PlainTextObject(text="selectedValue"), action_id="dropdown", min_query_length=5 ).to_dict(), { "placeholder": { @@ -194,9 +192,9 @@ def test_json(self): self.assertDictEqual( ExternalDataSelectElement( - placeholder="selectedValue", + placeholder=PlainTextObject(text="selectedValue"), action_id="dropdown", - confirm=ConfirmObject(title="title", text="text"), + confirm=ConfirmObject(title=PlainTextObject(text="title"), text=PlainTextObject(text="text")), ).to_dict(), { "placeholder": { @@ -205,33 +203,33 @@ def test_json(self): "type": "plain_text", }, "action_id": "dropdown", - "confirm": ConfirmObject(title="title", text="text").to_dict("block"), + "confirm": ConfirmObject(title=PlainTextObject(text="title"), + text=PlainTextObject(text="text")).to_dict("block"), "type": "external_select", }, ) -class DynamicDropdownTests(unittest.TestCase): - dynamic_types = {UserSelectElement, ConversationSelectElement, ChannelSelectElement} - - def test_json(self): - for dropdown_type in self.dynamic_types: - with self.subTest(dropdown_type=dropdown_type): - self.assertDictEqual( - dropdown_type( - placeholder="abc", - action_id="dropdown", - # somewhat silly abuse of kwargs ahead: - **{f"initial_{dropdown_type.initial_object_type}": "def"}, - ).to_dict(), - { - "placeholder": { - "emoji": True, - "text": "abc", - "type": "plain_text", - }, - "action_id": "dropdown", - f"initial_{dropdown_type.initial_object_type}": "def", - "type": f"{dropdown_type.initial_object_type}s_select", - }, - ) +# class DynamicDropdownTests(unittest.TestCase): +# dynamic_types = {UserSelectElement, ConversationSelectElement, ChannelSelectElement} +# +# def test_json(self): +# for dropdown_type in self.dynamic_types: +# with self.subTest(dropdown_type=dropdown_type): +# type = dropdown_type( +# placeholder="abc", +# action_id="dropdown", +# # somewhat silly abuse of kwargs ahead: +# **{f"initial_{dropdown_type}": "def"}, +# ).to_dict() +# coded = { +# "placeholder": { +# "emoji": True, +# "text": "abc", +# "type": "plain_text", +# }, +# "action_id": "dropdown", +# f"initial_{dropdown_type.initial_object_type}": "def", +# "type": f"{dropdown_type.initial_object_type}s_select", +# } +# self.assertDictEqual(type, coded) diff --git a/tests/web/classes/test_modals.py b/tests/web/classes/test_modals.py new file mode 100644 index 000000000..d72d20fc4 --- /dev/null +++ b/tests/web/classes/test_modals.py @@ -0,0 +1,83 @@ +import json +import unittest + +from slack.web.classes.elements import UserMultiSelectElement, PlainTextElement +from slack.web.classes.modals import ModalBuilder +from slack.web.classes.objects import PlainTextObject + + +class TestModals(unittest.TestCase): + def test_json_form(self): + modal = ModalBuilder() \ + .title("Test Check title") \ + .submit("Go") \ + .section(text=PlainTextObject(text="Hi, is a test text block in a section")) \ + .divider() \ + .input(label=PlainTextObject(text="single input"), + element=PlainTextElement(placeholder=PlainTextObject(text="Hello, Slack"), + action_id="plain_text")) \ + .input(label=PlainTextObject(text="Label"), + element=UserMultiSelectElement(placeholder=PlainTextObject(text="Select users"), + action_id="users")) \ + .to_dict() + coded = { + "type": "modal", + "title": { + "type": "plain_text", + "text": "Test Check title", + "emoji": True + }, + "submit": { + "text": "Go", + "type": "plain_text", + "emoji": True + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Hi, is a test text block in a section", + "emoji": True + } + }, + { + "type": "divider" + }, + { + "type": "input", + "label": { + "text": "single input", + "type": "plain_text", + "emoji": True + }, + "element": { + "type": "plain_text_input", + "placeholder": { + "text": "Hello, Slack", + "type": "plain_text", + "emoji": True + }, + "action_id": "plain_text" + }, + }, + { + "type": "input", + "element": { + "type": "multi_users_select", + "action_id": "users", + "placeholder": { + "type": "plain_text", + "text": "Select users", + "emoji": True + } + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": True + } + } + ] + } + self.assertDictEqual(modal, coded) diff --git a/tests/web/classes/test_objects.py b/tests/web/classes/test_objects.py index f928ac6d7..91605af81 100644 --- a/tests/web/classes/test_objects.py +++ b/tests/web/classes/test_objects.py @@ -194,25 +194,16 @@ def test_basic_json(self): "text": {"text": "are you sure?", "type": "mrkdwn", "verbatim": False}, "title": {"emoji": True, "text": "some title", "type": "plain_text"}, } - simple_object = ConfirmObject(title="some title", text="are you sure?") - self.assertDictEqual(simple_object.to_dict(), expected) - self.assertDictEqual(simple_object.to_dict("block"), expected) - self.assertDictEqual( - simple_object.to_dict("action"), - { - "text": "are you sure?", - "title": "some title", - "ok_text": "Okay", - "dismiss_text": "Cancel", - }, - ) + simple_object = ConfirmObject(title=PlainTextObject(text="some title"), + text=MarkdownTextObject(text="are you sure?")).to_dict() + self.assertDictEqual(simple_object, expected) def test_confirm_overrides(self): confirm = ConfirmObject( - title="some title", - text="are you sure?", - confirm="I'm really sure", - deny="Nevermind", + title=PlainTextObject(text="some title"), + text=MarkdownTextObject(text="are you sure?"), + confirm=PlainTextObject(text="I'm really sure"), + deny=PlainTextObject(text="Nevermind"), ) expected = { "confirm": {"emoji": True, "text": "I'm really sure", "type": "plain_text"}, @@ -221,29 +212,20 @@ def test_confirm_overrides(self): "title": {"emoji": True, "text": "some title", "type": "plain_text"}, } self.assertDictEqual(confirm.to_dict(), expected) - self.assertDictEqual(confirm.to_dict("block"), expected) - self.assertDictEqual( - confirm.to_dict("action"), - { - "text": "are you sure?", - "title": "some title", - "ok_text": "I'm really sure", - "dismiss_text": "Nevermind", - }, - ) def test_passing_text_objects(self): - direct_construction = ConfirmObject(title="title", text="Are you sure?") + direct_construction = ConfirmObject(title=PlainTextObject(text="title"), + text=MarkdownTextObject(text="Are you sure?")) mrkdwn = MarkdownTextObject(text="Are you sure?") - preconstructed = ConfirmObject(title="title", text=mrkdwn) + preconstructed = ConfirmObject(title=PlainTextObject(text="title"), text=mrkdwn) self.assertDictEqual(direct_construction.to_dict(), preconstructed.to_dict()) plaintext = PlainTextObject(text="Are you sure?", emoji=False) - passed_plaintext = ConfirmObject(title="title", text=plaintext) + passed_plaintext = ConfirmObject(title=PlainTextObject(text="title"), text=plaintext) self.assertDictEqual( passed_plaintext.to_dict(), @@ -257,56 +239,51 @@ def test_passing_text_objects(self): def test_title_length(self): with self.assertRaises(SlackObjectFormationError): - ConfirmObject(title=STRING_301_CHARS, text="Are you sure?").to_dict() + ConfirmObject(title=PlainTextObject(text=STRING_301_CHARS), + text=MarkdownTextObject(text="Are you sure?")).to_dict() def test_text_length(self): with self.assertRaises(SlackObjectFormationError): - ConfirmObject(title="title", text=STRING_301_CHARS).to_dict() + ConfirmObject(title=PlainTextObject(text="title"), text=PlainTextObject(text=STRING_301_CHARS)).to_dict() def test_text_length_with_object(self): with self.assertRaises(SlackObjectFormationError): plaintext = PlainTextObject(text=STRING_301_CHARS) - ConfirmObject(title="title", text=plaintext).to_dict() + ConfirmObject(title=PlainTextObject(text="title"), text=plaintext).to_dict() with self.assertRaises(SlackObjectFormationError): markdown = MarkdownTextObject(text=STRING_301_CHARS) - ConfirmObject(title="title", text=markdown).to_dict() + ConfirmObject(title=PlainTextObject(text="title"), text=markdown).to_dict() def test_confirm_length(self): with self.assertRaises(SlackObjectFormationError): ConfirmObject( - title="title", text="Are you sure?", confirm=STRING_51_CHARS + title=PlainTextObject(text="title"), text=MarkdownTextObject(text="Are you sure?"), + confirm=PlainTextObject(text=STRING_51_CHARS) ).to_dict() def test_deny_length(self): with self.assertRaises(SlackObjectFormationError): ConfirmObject( - title="title", text="Are you sure?", deny=STRING_51_CHARS + title=PlainTextObject(text="title"), text=MarkdownTextObject(text="Are you sure?"), + deny=PlainTextObject(text=STRING_51_CHARS) ).to_dict() class OptionTests(unittest.TestCase): def setUp(self) -> None: - self.common = Option(label="an option", value="option_1") + self.common = Option(text=PlainTextObject(text="an option"), value="option_1") def test_block_style_json(self): expected = { "text": {"type": "plain_text", "text": "an option", "emoji": True}, "value": "option_1", } - self.assertDictEqual(self.common.to_dict("block"), expected) self.assertDictEqual(self.common.to_dict(), expected) - - def test_dialog_style_json(self): - expected = {"label": "an option", "value": "option_1"} - self.assertDictEqual(self.common.to_dict("dialog"), expected) - - def test_action_style_json(self): - expected = {"text": "an option", "value": "option_1"} - self.assertDictEqual(self.common.to_dict("action"), expected) + self.assertDictEqual(self.common.to_dict(), expected) def test_from_single_value(self): - option = Option(label="option_1", value="option_1") + option = Option(text=PlainTextObject(text="option_1"), value="option_1") self.assertDictEqual( option.to_dict("text"), option.from_single_value("option_1").to_dict("text"), @@ -314,11 +291,11 @@ def test_from_single_value(self): def test_label_length(self): with self.assertRaises(SlackObjectFormationError): - Option(label=STRING_301_CHARS, value="option_1").to_dict("text") + Option(text=PlainTextObject(text=STRING_301_CHARS), value="option_1").to_dict("text") def test_value_length(self): with self.assertRaises(SlackObjectFormationError): - Option(label="option_1", value=STRING_301_CHARS).to_dict("text") + Option(text=PlainTextObject(text="option_1"), value=STRING_301_CHARS).to_dict("text") class OptionGroupTests(unittest.TestCase): @@ -352,32 +329,6 @@ def test_block_style_json(self): self.assertDictEqual(self.common.to_dict("block"), expected) self.assertDictEqual(self.common.to_dict(), expected) - def test_dialog_style_json(self): - self.assertDictEqual( - self.common.to_dict("dialog"), - { - "label": "an option", - "options": [ - {"label": "one", "value": "one"}, - {"label": "two", "value": "two"}, - {"label": "three", "value": "three"}, - ], - }, - ) - - def test_action_style_json(self): - self.assertDictEqual( - self.common.to_dict("action"), - { - "text": "an option", - "options": [ - {"text": "one", "value": "one"}, - {"text": "two", "value": "two"}, - {"text": "three", "value": "three"}, - ], - }, - ) - def test_label_length(self): with self.assertRaises(SlackObjectFormationError): OptionGroup(label=STRING_301_CHARS, options=self.common_options).to_dict(