diff --git a/announcement/announcement.py b/announcement/announcement.py index e108405..445b393 100644 --- a/announcement/announcement.py +++ b/announcement/announcement.py @@ -2,7 +2,7 @@ import json from pathlib import Path -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING import discord @@ -11,7 +11,7 @@ from core import checks from core.models import getLogger, PermissionLevel -from .core.models import AnnouncementModel +from .core.models import AnnouncementModel, AnnouncementType from .core.views import AnnouncementView @@ -19,6 +19,8 @@ from bot import ModmailBot +logger = getLogger(__name__) + info_json = Path(__file__).parent.resolve() / "info.json" with open(info_json, encoding="utf-8") as f: __plugin_info__ = json.loads(f.read()) @@ -27,7 +29,35 @@ __description__ = "\n".join(__plugin_info__["description"]).format(__version__) -logger = getLogger(__name__) +news_channel_hyperlink = ( + "[announcement](https://support.discord.com/hc/en-us/articles/360032008192-Announcement-Channels)" +) +type_desc = ( + "Choose a type of announcement.\n\n" + "__**Available types:**__\n" + "- **Plain** : Plain text announcement.\n" + "- **Embed** : Embedded announcement. Image and thumbnail image are also supported.\n" +) +embed_desc = ( + "Click the `Edit` button below to set/edit the embed values.\n\n" + "__**Available fields:**__\n" + "- **Description** : The content of the announcement. Must not exceed 4000 characters.\n" + "- **Thumbnail URL** : URL of the image shown at the top right of the embed.\n" + "- **Image URL** : URL of the large image shown at the bottom of the embed.\n" + "- **Color** : The color code of the embed. If not specified, fallbacks to bot main color. " + "The following formats are accepted:\n - `0x`\n - `#`\n - `0x#`\n - `rgb(, , )`\n" + "Like CSS, `` can be either 0-255 or 0-100% and `` can be either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff).\n" +) +plain_desc = "Click the `Edit` button below to set/edit the content.\n" +mention_desc = ( + "If nothing is selected, the announcement will be posted without any mention.\n" + "To mention Users or Roles, select `Others` in the first dropdown, then in second dropdown select Users or Roles you want to mention.\n" +) +channel_desc = ( + "The destination channel. If nothing is selected, the announcement will be posted " + "in the current channel.\n" + f"The announcement can be published if the type of destination channel is {news_channel_hyperlink} channel.\n" +) class Announcement(commands.Cog): @@ -40,80 +70,64 @@ def __init__(self, bot: ModmailBot): @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def announce(self, ctx: commands.Context): """ - Base command to create announcements. - """ - await ctx.send_help(ctx.command) + Post an announcement. - @announce.command(name="create", aliases=["start"]) - @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - async def announce_create(self, ctx: commands.Context, *, channel: Optional[discord.TextChannel] = None): + Run this command without argument to initiate a creation panel where you can choose and customise the output of the announcement. """ - Post an announcement in a channel specified. + announcement = AnnouncementModel(ctx) + sessions = [ + ("type", type_desc), + ("embed", embed_desc), + ("plain", plain_desc), + ("mention", mention_desc), + ("channel", channel_desc), + ("publish", None), + ] + view = AnnouncementView(ctx, announcement, input_sessions=sessions) + await view.create_base() - This will initiate a creation panel where you can choose and customise the output of the announcement. + await view.wait() + if not announcement.is_ready(): + # cancelled or timed out + return - `channel` if specified may be a channel ID, mention, or name. Otherwise, fallbacks to current channel. + await announcement.send() - __**Note:**__ - - If `channel` is not specified, to ensure cleaner output the creation message will automatically be deleted after the announcement is posted. - """ - delete = False - if channel is None: - channel = ctx.channel - delete = True + if announcement.channel == ctx.channel: + view.stop() try: await ctx.message.delete() + await view.message.delete() except discord.Forbidden: - logger.warning(f"Missing `Manage Messages` permission in {channel} channel.") - - announcement = AnnouncementModel(ctx, channel) - view = AnnouncementView(ctx, announcement) - embed = discord.Embed(title="Announcement Creation Panel", color=self.bot.main_color) - embed.description = ( - "Choose a type of announcement using the dropdown menu below.\n\n" - "__**Available types:**__\n" - "- **Normal** : Plain text announcement.\n" - "- **Embed** : Embedded announcement. Image and thumbnail image are also supported." - ) - view.message = message = await ctx.send(embed=embed, view=view) - await view.wait(input_event=True) - - if not announcement.posted: - return - - if delete: - view.stop() - await message.delete() + logger.warning(f"Missing `Manage Messages` permission in {ctx.channel} channel.") return - embed = message.embeds[0] - description = f"Announcement has been posted in {channel.mention}.\n\n" + embed = view.message.embeds[0] + description = f"Announcement has been posted in {announcement.channel.mention}.\n\n" if announcement.channel.type == discord.ChannelType.news: - description += "Would you like to publish this announcement?\n\n" - view.generate_buttons(confirmation=True) + description += "Would you like to publish the announcement?\n\n" + view.fill_items(confirmation=True) else: view.stop() embed.description = description - await message.edit(embed=embed, view=view) + await view.message.edit(embed=embed, view=view) if view.is_finished(): return await view.wait() - hyper_link = f"[announcement]({announcement.message.jump_url})" - if view.confirm: - await announcement.publish() - embed.description = f"Successfully published this {hyper_link} to all subscribed channels.\n\n" - if view.confirm is not None: - if not view.confirm: + if view.confirmed is not None: + hyper_link = f"[announcement]({announcement.message.jump_url})" + if view.confirmed: + await announcement.publish() + embed.description = f"Successfully published this {hyper_link} to all following servers.\n\n" + else: embed.description = ( f"To manually publish this {hyper_link}, use command:\n" f"```\n{ctx.prefix}publish {announcement.channel.id}-{announcement.message.id}\n```" ) - view = None - - await message.edit(embed=embed, view=view) + await view.message.edit(embed=embed, view=None) @announce.command(name="quick") @checks.has_permissions(PermissionLevel.ADMINISTRATOR) @@ -123,19 +137,25 @@ async def announce_quick(self, ctx: commands.Context, channel: discord.TextChann `channel` may be a channel ID, mention, or name. """ - await channel.send(content) - - @commands.command() + announcement = AnnouncementModel(ctx, type=AnnouncementType.PLAIN, channel=channel, content=content) + await announcement.send() + + @commands.command( + help=( + "Publish a message from announcement channel to all channels in other servers that are " + "following the channel.\n\n" + "`message` may be a message ID, format of `channel_id`-`message_id` " + "(e.g. `1079077919915266210-1079173422967439360`), or message link.\n\n" + "__**Notes:**__\n" + "- If message ID is provided (without channel ID and not the message link), the bot will only " + "look for the message in the current channel.\n" + f"- Only messages in {news_channel_hyperlink} channels can be published." + ), + ) @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def publish(self, ctx: commands.Context, *, message: discord.Message): """ - Publish a message from announcement channel to all subscribed channels. - - `message` may be a message ID, format of `channel ID-message ID`, or message link. - - __**Notes:**__ - - If message ID is provided (without channel ID and not the message link), the bot will only look for the message in the current channel. - - Only messages in [announcement](https://support.discord.com/hc/en-us/articles/360032008192-Announcement-Channels) channels can be published. + Publish a message from announcement channel. """ channel = message.channel if not channel.type == discord.ChannelType.news: @@ -145,7 +165,7 @@ async def publish(self, ctx: commands.Context, *, message: discord.Message): await message.publish() embed = discord.Embed( - description=f"Successfully published this [message]({message.jump_url}) to all subscribed channels.", + description=f"Successfully published this [message]({message.jump_url}) to all following servers.", color=self.bot.main_color, ) await ctx.reply(embed=embed) diff --git a/announcement/core/models.py b/announcement/core/models.py index a4fdadd..ee06fe6 100644 --- a/announcement/core/models.py +++ b/announcement/core/models.py @@ -1,13 +1,16 @@ import asyncio from enum import Enum -from typing import Any, Dict, Optional +from typing import Any, Dict import discord from discord.utils import MISSING from discord.ext import commands +__all__ = ("AnnouncementType", "AnnouncementModel") + + def _color_converter(value: str) -> int: try: return int(value) @@ -21,7 +24,7 @@ def _color_converter(value: str) -> int: class AnnouncementType(Enum): # only two are valid for now. may add more later. - NORMAL = "normal" + PLAIN = "plain" EMBED = "embed" INVALID = "invalid" @@ -45,59 +48,54 @@ def value(self) -> str: class AnnouncementModel: - def __init__(self, ctx: commands.Context, channel: discord.TextChannel): + """ + Represents an instance to manage announcement creation. + """ + + def __init__( + self, + ctx: commands.Context, + *, + type: AnnouncementType = MISSING, + channel: discord.TextChannel = MISSING, + content: str = MISSING, + embed: discord.Embed = MISSING, + ): self.ctx: commands.Context = ctx + self.type: AnnouncementType = type self.channel: discord.TextChannel = channel - self.event: asyncio.Event = asyncio.Event() - self.ready: bool = False + self.content: str = content + self.embed: discord.Embed = embed - self.type: AnnouncementType = MISSING self.message: discord.Message = MISSING - self.content: str = MISSING - self.embed: discord.Embed = MISSING + self.event: asyncio.Event = asyncio.Event() + self.ready: bool = False self.task: asyncio.Task = MISSING - @property - def posted(self) -> bool: - return self.event.is_set() + def is_ready(self) -> bool: + """ + Returns whether the announcement is ready to be posted. + """ + return self.ready and self.event.is_set() - @posted.setter - def posted(self, flag: bool) -> None: - if flag: - self.event.set() - else: - if self.task is not MISSING: - self.task.cancel() - self.event.clear() + def cancel(self) -> None: + """Cancel the announcement.""" + self.ready = False + if self.task is not MISSING: + self.task.cancel() + self.event.clear() async def wait(self) -> None: - self.task = self.ctx.bot.loop.create_task(self.event.wait()) + """ + Wait until the announcement is ready to be posted or cancelled. + """ + if self.task is MISSING: + self.task = self.ctx.bot.loop.create_task(self.event.wait()) try: await self.task except asyncio.CancelledError: pass - async def resolve_mentions(self) -> None: - if not self.content: - return - ret = [] - argument = self.content.split() - for arg in argument: - if arg in ("@here", "@everyone"): - ret.append(arg) - continue - user_or_role = None - try: - user_or_role = await commands.RoleConverter().convert(self.ctx, arg) - except commands.BadArgument: - try: - user_or_role = await commands.MemberConverter().convert(self.ctx, arg) - except commands.BadArgument: - raise commands.BadArgument(f"Unable to convert {arg} to user or role mention.") - if user_or_role is not None: - ret.append(user_or_role.mention) - self.content = ", ".join(ret) if ret else None - def create_embed( self, *, @@ -106,6 +104,9 @@ def create_embed( thumbnail_url: str = MISSING, image_url: str = MISSING, ) -> discord.Embed: + """ + Create the announcement embed. + """ if not color: color = self.ctx.bot.main_color else: @@ -117,7 +118,7 @@ def create_embed( embed.set_thumbnail(url=thumbnail_url) if image_url: embed.set_image(url=image_url) - embed.set_footer(text="Announcement", icon_url=self.channel.guild.icon) + embed.set_footer(text="Announcement", icon_url=self.ctx.guild.icon) self.embed = embed return embed @@ -127,13 +128,17 @@ def send_params(self) -> Dict[str, Any]: params["content"] = self.content return params - async def post(self) -> None: + async def send(self) -> None: + """ + Send the announcement message. + """ + if not self.channel: + self.channel = self.ctx.channel self.message = await self.channel.send(**self.send_params()) - self.posted = True async def publish(self) -> None: """ - Publishes the announcement. This will only work if the channel type is a news channel - and if the announcement has not been posted yet. + Publish the announcement. This will only work if the channel type is a news channel + and if the announcement has never been published yet. """ await self.message.publish() diff --git a/announcement/core/views.py b/announcement/core/views.py index 41dcc90..0506f4b 100644 --- a/announcement/core/views.py +++ b/announcement/core/views.py @@ -1,11 +1,11 @@ from __future__ import annotations -from typing import Any, Awaitable, Callable, Dict, List, Optional, Union, TYPE_CHECKING +from copy import deepcopy +from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING import discord -from discord import ButtonStyle, Interaction, TextStyle +from discord import ButtonStyle, Interaction, TextStyle, ui from discord.ext import commands -from discord.ui import Button, Modal, Select, TextInput, View from discord.utils import MISSING from .models import AnnouncementType @@ -22,270 +22,337 @@ _short_length = 256 _long_length = 4000 -_embed_input_description = ( - "Click the `Edit` button below to set/edit the values.\n\n" - "__**Available fields:**__\n" - "- **Mention** : Mention @User, @Role, @here, or @everyone. " - "Multiple mentions is also supported, just separate the values with space. " - "For User or Role, you may pass an ID, mention (in the format of `<@id>` for User or `<@&id>` for Role), or name.\n" - "- **Description** : The content of the announcement. Must not exceed 4000 characters.\n" - "- **Thumbnail URL** : URL of the image shown at the top right of the embed.\n" - "- **Image URL** : URL of the large image shown at the bottom of the embed.\n" - "- **Color** : The color code of the embed. If not specified, fallbacks to bot main color. " - "The following formats are accepted:\n - `0x`\n - `#`\n - `0x#`\n - `rgb(, , )`\n" - "Like CSS, `` can be either 0-255 or 0-100% and `` can be either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff).\n\n" -) - -_plain_input_description = "Click the `Edit` button below to set/edit the content." - - -class AnnouncementTextInput(TextInput): +type_select_maps = [ + { + "label": "Plain", + "description": "Plain text announcement.", + }, + { + "label": "Embed", + "description": "Embedded announcement. Image and thumbnail image are alose supported.", + }, +] +mention_select_maps = [ + {"label": "@here", "description": "Mention @here."}, + {"label": "@everyone", "description": "Mention @everyone."}, + {"label": "Others", "description": "Mention users or roles."}, +] +embed_modal_payload = { + "description": { + "label": "Announcement", + "style": TextStyle.long, + "max_length": _long_length, + }, + "thumbnail_url": { + "label": "Thumbnail URL", + "required": False, + "max_length": _short_length, + }, + "image_url": { + "label": "Image URL", + "required": False, + "max_length": _short_length, + }, + "color": { + "label": "Embed color", + "required": False, + "max_length": 20, + }, +} + + +class TextInput(ui.TextInput): def __init__(self, name: str, **kwargs): self.name: str = name super().__init__(**kwargs) -class AnnouncementModal(Modal): - children: List[AnnouncementTextInput] +class Modal(ui.Modal): + children: List[TextInput] def __init__(self, view: AnnouncementView, options: Dict[str, Any]): super().__init__(title="Announcement") self.view = view self.view.modals.append(self) for key, value in options.items(): - self.add_item(AnnouncementTextInput(key, **value)) + self.add_item(TextInput(key, **value)) async def on_submit(self, interaction: Interaction) -> None: for child in self.children: self.view.inputs[child.name]["default"] = child.value - await interaction.response.defer() self.stop() await self.view.on_modal_submit(interaction) -class DropdownMenu(Select): - def __init__(self, *, options: List[discord.SelectOption], **kwargs): - super().__init__( - placeholder="Choose a type", - options=options, - **kwargs, - ) - - async def callback(self, interaction: Interaction): - await interaction.response.defer() - assert self.view is not None - value = self.values[0] - self.placeholder = value.title() - self.disabled = True - await self.view.set_announcement_type(value) - +class AnnouncementView(ui.View): + """ + Represents the AnnouncementView class. The announcement creation panel and sessions + will be handled from here. + """ -class AnnouncementViewButton(Button["AnnouncementView"]): def __init__( self, + ctx: commands.Context, + announcement: AnnouncementModel, *, - label: str, - style: ButtonStyle = ButtonStyle.blurple, - callback: ButtonCallbackT = MISSING, + input_sessions: List[Tuple[str]], + timeout: float = 600.0, ): - super().__init__(label=label, style=style) - self.callback_override: ButtonCallbackT = callback - - async def callback(self, interaction: Interaction): - assert self.view is not None - await self.callback_override(interaction) - - -class AnnouncementView(View): - children: List[AnnouncementViewButton] - - def __init__(self, ctx: commands.Context, announcement: AnnouncementModel, *, timeout: float = 600.0): super().__init__(timeout=timeout) self.ctx: commands.Context = ctx self.cog: AnnouncementCog = ctx.cog self.user: discord.Member = ctx.author - self.message: discord.Message = MISSING self.announcement: AnnouncementModel = announcement - self.confirm: Optional[bool] = None - self._underlying_modals: List[AnnouncementModal] = [] - - self.content_data: Dict[str, Any] = { - "label": "Content", - "default": None, - "style": TextStyle.long, - "max_length": _long_length, - } - self.embed_data: Dict[str, Any] = { - "description": { - "label": "Announcement", - "style": TextStyle.long, - "max_length": _long_length, - }, - "thumbnail_url": { - "label": "Thumbnail URL", - "required": False, - "max_length": _short_length, - }, - "image_url": { - "label": "Image URL", - "required": False, - "max_length": _short_length, - }, - "color": { - "label": "Embed color", - "required": False, - "max_length": 20, - }, - } - self.inputs: Dict[str, Any] = {"content": self.content_data} - - self._add_menu() - self.generate_buttons() - self.refresh() + self.input_sessions: List[Tuple[str]] = input_sessions + self.index: int = 0 + self.message: discord.Message = MISSING + self.confirmed: Optional[bool] = None + self._underlying_modals: List[Modal] = [] + self.inputs: Dict[str, Any] = {} @property - def modals(self) -> List[AnnouncementModal]: + def modals(self) -> List[Modal]: return self._underlying_modals - def _add_menu(self) -> None: - attrs = [ - { - "label": "Normal", - "emoji": None, - "description": "Plain text announcement.", - }, - { - "label": "Embed", - "emoji": None, - "description": "Embedded announcement. Image and thumbnail image are alose supported.", - }, - ] - options = [] - for attr in attrs: - option = discord.SelectOption( - label=attr["label"], - emoji=attr["emoji"], - description=attr["description"], - value=attr["label"].lower(), - ) - options.append(option) - self.add_item(DropdownMenu(options=options, row=0)) + @property + def session_description(self) -> None: + return self.input_sessions[self.index][1] + + @property + def current(self) -> str: + return self.input_sessions[self.index][0] + + def fill_items(self, *, post: bool = False, confirmation: bool = False) -> None: + self.select_menu.options.clear() + if self.current == "type": + for ts in type_select_maps: + option = discord.SelectOption(**ts) + option.value = ts["label"].lower() + self.select_menu.append_option(option) + self.select_menu.placeholder = "Choose a type" + self.add_item(self.select_menu) + elif self.current == "mention": + for ms in mention_select_maps: + self.select_menu.append_option(discord.SelectOption(**ms)) + self.select_menu.placeholder = "Select mention" + self.add_item(self.select_menu) + self.add_item(self.mentionable_select) + elif self.current == "channel": + self.add_item(self.channel_select) - def generate_buttons(self, *, confirmation: bool = False) -> None: if confirmation: - buttons = { - "yes": (ButtonStyle.green, self._action_yes), - "no": (ButtonStyle.red, self._action_no), - } + buttons = [self._button_yes, self._button_no] else: - buttons: Dict[str, Any] = { - "post": (ButtonStyle.green, self._action_post), - "edit": (ButtonStyle.grey, self._action_edit), - "preview": (ButtonStyle.grey, self._action_preview), - "cancel": (ButtonStyle.red, self._action_cancel), - } - for label, item in buttons.items(): - self.add_item(AnnouncementViewButton(label=label.title(), style=item[0], callback=item[1])) + if post: + self._button_next_or_post.label = "Post" + self._button_next_or_post.style = ButtonStyle.green + else: + self._button_next_or_post.label = "Next" + self._button_next_or_post.style = ButtonStyle.blurple + buttons = [ + self._button_next_or_post, + self._button_edit, + self._button_preview, + self._button_cancel, + ] + for button in buttons: + self.add_item(button) def refresh(self) -> None: for child in self.children: - if not isinstance(child, AnnouncementViewButton): + if not isinstance(child, ui.Button): continue if child.label.lower() == "cancel": continue if not self.announcement.type: child.disabled = True continue - if child.label.lower() in ("post", "preview"): + if child.label.lower() in ("post", "preview", "next"): child.disabled = not self.announcement.ready + elif child.label.lower() == "edit": + child.disabled = self.current not in ("embed", "plain") else: child.disabled = False - async def update_view(self) -> None: + async def create_base(self) -> None: + """ + Create a base message and attach this view's components to it. + """ + if self.message is not MISSING: + raise RuntimeError("The base message already exists.") + self.clear_items() + self.fill_items() self.refresh() - await self.message.edit(embed=self.message.embeds[0], view=self) + embed = discord.Embed( + title="Announcement Creation Panel", + description=self.session_description, + color=self.ctx.bot.main_color, + ) + self.message = await self.ctx.send(embed=embed, view=self) - async def _action_post(self, interaction: Interaction) -> None: - await interaction.response.defer() - await self.announcement.post() + def _populate_base_inputs(self, type_: AnnouncementType) -> None: + if type_ == AnnouncementType.EMBED: + self.inputs.update(**deepcopy(embed_modal_payload)) + else: + content = { + "label": "Content", + "default": None, + "style": TextStyle.long, + "max_length": _long_length, + } + self.inputs["content"] = content + + def _resolve_unused_sessions(self) -> None: + for session in self.input_sessions: + stype = session[0] + if self.announcement.type == AnnouncementType.EMBED: + if stype == "plain": + self.input_sessions.remove(session) + elif self.announcement.type == AnnouncementType.PLAIN: + if stype in ("embed", "mention"): + self.input_sessions.remove(session) + else: + raise TypeError(f"Invalid type of announcement, `{self.announcement.type}`.") + + async def _action_next(self, *args: Tuple[Interaction, Optional[ui.Button]]) -> None: + """Go to next page.""" + interaction, _ = args + self.index += 1 + self.inputs.clear() self.clear_items() + post = False + if self.current in ("embed", "plain"): + self._populate_base_inputs(self.announcement.type) + description = f"__**{self.current.title()}:**__\n" + elif self.current == "mention": + description = "__**Select mentions:**__\n" + elif self.current == "channel": + post = True + description = "__**Select a channel:**__\n" + else: + raise ValueError(f"Invalid session in `_action_next`: `{self.current}`.") + description += f"{self.session_description}\n" + embed = self.message.embeds[0] + embed.description = description + self.fill_items(post=post) + await self.update_view(interaction) + + @ui.select(placeholder="...", row=0) + async def select_menu(self, interaction: Interaction, select: ui.Select) -> None: + value = select.values[0] + for opt in select.options: + opt.default = opt.value == value + if self.current == "type": + self.announcement.type = AnnouncementType.from_value(value) + self._resolve_unused_sessions() + await self._action_next(interaction, None) + elif self.current == "mention": + if value in ("@here", "@everyone"): + self.mentionable_select.disabled = True + self.announcement.content = value + else: + self.mentionable_select.disabled = False + self.announcement.content = MISSING + await self.update_view(interaction) + else: + raise ValueError(f"Invalid session in `{self.__class__.__name__}.select_menu`: `{self.current}`.") + + @ui.select( + cls=ui.MentionableSelect, + placeholder="Other mentions", + row=1, + min_values=0, + max_values=25, + disabled=True, + ) + async def mentionable_select(self, interaction: Interaction, select: ui.MentionableSelect) -> None: + if select.values: + self.announcement.content = ", ".join(v.mention for v in select.values) + else: + self.announcement.content = MISSING + await interaction.response.defer() + + @ui.select( + cls=ui.ChannelSelect, + placeholder="Select a channel", + channel_types=[discord.ChannelType.news, discord.ChannelType.text], + ) + async def channel_select(self, interaction: Interaction, select: ui.ChannelSelect) -> None: + value = select.values[0] + channel = value.resolve() or await value.fetch() + self.announcement.channel = channel + await interaction.response.defer() - async def _action_edit(self, interaction: Interaction) -> None: - modal = AnnouncementModal(self, self.inputs) + @ui.button(label="...") + async def _button_next_or_post(self, interaction: Interaction, button: ui.Button) -> None: + """ + First button in the row. The label could be `Next` or `Post`. The attributes for this item + are modified in `.fill_items()`. + """ + if button.label == "Post": + self.index += 1 + await interaction.response.defer() + self.clear_items() + self.announcement.event.set() + else: + await self._action_next(interaction, button) + + @ui.button(label="Edit", style=ButtonStyle.grey) + async def _button_edit(self, *args: Tuple[Interaction, ui.Button]) -> None: + interaction, _ = args + modal = Modal(self, self.inputs) await interaction.response.send_modal(modal) - await modal.wait() - async def _action_preview(self, interaction: Interaction) -> None: + @ui.button(label="Preview", style=ButtonStyle.grey) + async def _button_preview(self, *args: Tuple[Interaction, ui.Button]) -> None: + interaction, _ = args try: await interaction.response.send_message(ephemeral=True, **self.announcement.send_params()) except discord.HTTPException as exc: error = f"**Error:**\n```py\n{type(exc).__name__}: {str(exc)}\n```" await interaction.response.send_message(error, ephemeral=True) - async def _action_cancel(self, interaction: Interaction) -> None: - self.announcement.posted = False + @ui.button(label="Cancel", style=ButtonStyle.red) + async def _button_cancel(self, *args: Tuple[Interaction, ui.Button]) -> None: + interaction, _ = args + self.announcement.cancel() self.disable_and_stop() await interaction.response.edit_message(view=self) - async def _action_yes(self, interaction: Interaction) -> None: + @ui.button(label="Yes", style=ButtonStyle.green) + async def _button_yes(self, interaction: Interaction, button: ui.Button) -> None: await interaction.response.defer() - self.confirm = True + self.confirmed = True self.disable_and_stop() - async def _action_no(self, interaction: Interaction) -> None: + @ui.button(label="No", style=ButtonStyle.red) + async def _button_no(self, interaction: Interaction, button: ui.Button) -> None: await interaction.response.defer() - self.confirm = False + self.confirmed = False self.disable_and_stop() async def interaction_check(self, interaction: Interaction) -> bool: if self.user.id == interaction.user.id: return True - await interaction.response.send_message( - "This panel cannot be controlled by you!", - ephemeral=True, - ) return False - async def set_announcement_type(self, value: str) -> None: - self.announcement.type = AnnouncementType.from_value(value) - description = f"__**{value.title()}:**__\n" - if self.announcement.type == AnnouncementType.EMBED: - self.content_data = { - "label": "Mention", - "default": "@here", - "required": False, - "max_length": _short_length, - } - self.inputs.update(content=self.content_data, **self.embed_data) - description += _embed_input_description - else: - description += _plain_input_description - embed = self.message.embeds[0] - embed.description = description - await self.update_view() - async def on_modal_submit(self, interaction: Interaction) -> None: - self.announcement.content = self.inputs["content"].get("default") errors = [] if self.announcement.type == AnnouncementType.EMBED: - try: - await self.announcement.resolve_mentions() - except commands.BadArgument as exc: - errors.append(str(exc)) - kwargs = {} elems = [ "description", "thumbnail_url", "image_url", "color", ] - for elem in elems: - kwargs = {elem: self.inputs[elem].get("default") for elem in elems} + kwargs = {elem: self.inputs[elem].get("default") for elem in elems} try: self.announcement.create_embed(**kwargs) except Exception as exc: errors.append(f"{type(exc).__name__}: {str(exc)}") + else: + self.announcement.content = self.inputs["content"].get("default") if errors: self.announcement.ready = False @@ -295,17 +362,28 @@ async def on_modal_submit(self, interaction: Interaction) -> None: color=self.ctx.bot.error_color, description=content, ) - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.respose.send_message(embed=embed, ephemeral=True) else: self.announcement.ready = True - await self.update_view() + await self.update_view(interaction) - async def wait(self, *, input_event: bool = False) -> None: - if input_event: + async def wait(self) -> None: + if not self.announcement.is_ready(): await self.announcement.wait() else: await super().wait() + async def update_view(self, interaction: Optional[Interaction] = None) -> None: + """ + Refresh the components and update the view. + """ + if interaction and not interaction.response.is_done(): + func = interaction.response.edit_message + else: + func = self.message.edit + self.refresh() + await func(embed=self.message.embeds[0], view=self) + def disable_and_stop(self) -> None: for child in self.children: child.disabled = True @@ -316,6 +394,6 @@ def disable_and_stop(self) -> None: self.stop() async def on_timeout(self) -> None: - self.announcement.posted = False + self.announcement.cancel() self.disable_and_stop() await self.message.edit(view=self) diff --git a/announcement/info.json b/announcement/info.json index 0a40803..5589120 100644 --- a/announcement/info.json +++ b/announcement/info.json @@ -5,7 +5,7 @@ "\n**Version:**\n`{0}`" ], "authors": ["Jerrie-Aries"], - "version": "1.1.4", + "version": "1.2.0", "bot_version": "4.0.0", - "dpy_version": "2.0.0" -} \ No newline at end of file + "dpy_version": "2.1.0" +}