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