Ruby-Cogs/welcome/menus.py

600 lines
23 KiB
Python

from __future__ import annotations
import re
from enum import Enum
from typing import Any, List, Optional, Union
import discord
from red_commons.logging import getLogger
from redbot.core import commands
from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import box, escape
from redbot.vendored.discord.ext import menus
IMAGE_LINKS = re.compile(r"(http[s]?:\/\/[^\"\']*\.(?:png|jpg|jpeg|gif|png|webp))")
class EventType(Enum):
greeting = 0
goodbye = 1
def get_name(self):
return {EventType.greeting: _("greeting"), EventType.goodbye: _("goodbye")}.get(self)
def __str__(self):
return self.get_name()
def key(self):
return self.name.upper()
log = getLogger("red.Trusty-cogs.welcome")
_ = Translator("Welcome", __file__)
class WelcomePages(menus.ListPageSource):
def __init__(self, pages: List[str]):
super().__init__(pages, per_page=1)
self.pages = pages
self.select_options = []
for count, page in enumerate(pages):
self.select_options.append(discord.SelectOption(label=f"Page {count+1}", value=count))
self.current_page = None
self.current_selection = None
def is_paginating(self):
return True
async def format_page(self, view: BaseMenu, page: str):
self.current_page = view.current_page
config = view.cog.config
msgs = await config.guild(view.ctx.guild).get_raw(view.event_type.key())
try:
raw_text = msgs[self.current_page]
view.enable_extra_buttons()
except IndexError:
raw_text = _("Deleted Greeting")
view.disable_extra_buttons()
self.current_selection = raw_text
is_welcome = view.event_type is EventType.greeting
colour = await config.guild(view.ctx.guild).EMBED_DATA.colour()
colour_goodbye = await config.guild(view.ctx.guild).EMBED_DATA.colour_goodbye()
em_colour = discord.Colour(colour)
if not is_welcome:
em_colour = colour_goodbye or colour
if view.show_preview:
grouped = await config.guild(view.ctx.guild).GROUPED()
members = view.ctx.author if not grouped else [view.ctx.author, view.ctx.me]
if await config.guild(view.ctx.guild).EMBED():
return await view.cog.make_embed(members, view.ctx.guild, raw_text, is_welcome)
else:
return await view.cog.convert_parms(members, view.ctx.guild, raw_text, is_welcome)
display_text = raw_text
if view.show_raw:
display_text = escape(display_text, formatting=True)
display_text = re.sub(r"(<@?[!&#]?[0-9]{17,20}>)", r"\\\1", display_text)
if view.ctx.channel.permissions_for(view.ctx.guild.me).embed_links:
em = discord.Embed(
title=_("{event_type} message {count}").format(
event_type=view.event_type.get_name().title(), count=view.current_page + 1
),
description=display_text,
colour=em_colour,
)
em.set_footer(text=f"Page {view.current_page + 1}/{self.get_max_pages()}")
return em
return display_text
class StopButton(discord.ui.Button):
def __init__(
self,
style: discord.ButtonStyle,
row: Optional[int],
):
super().__init__(style=style, row=row)
self.style = style
self.emoji = "\N{HEAVY MULTIPLICATION X}\N{VARIATION SELECTOR-16}"
async def callback(self, interaction: discord.Interaction):
self.view.stop()
await self.view.message.delete()
class _NavigateButton(discord.ui.Button):
# Borrowed from myself mainly
# https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/redbot/core/utils/views.py#L44
def __init__(
self, style: discord.ButtonStyle, emoji: Union[str, discord.PartialEmoji], direction: int
):
super().__init__(style=style, emoji=emoji)
self.direction = direction
async def callback(self, interaction: discord.Interaction):
if self.direction == 0:
self.view.current_page = 0
elif self.direction == self.view.source.get_max_pages():
self.view.current_page = self.view.source.get_max_pages() - 1
else:
self.view.current_page += self.direction
await self.view.show_checked_page(self.view.current_page, interaction)
class ToggleButton(discord.ui.Button):
def __init__(self, attr_toggle: str, style: discord.ButtonStyle, label: str):
super().__init__(style=style, label=label)
self.attr_toggle = attr_toggle
async def callback(self, interaction: discord.Interaction):
current = getattr(self.view, self.attr_toggle)
self.style = discord.ButtonStyle.blurple if not current else discord.ButtonStyle.grey
setattr(self.view, self.attr_toggle, not current)
await self.view.show_checked_page(self.view.current_page, interaction)
class WelcomeEditModal(discord.ui.Modal):
def __init__(
self, welcome_message: str, index: int, button: discord.ui.Button, event_type: EventType
):
super().__init__(
title=_("Edit {event_type} {count}").format(
event_type=event_type.get_name(), count=index
)
)
self.text = discord.ui.TextInput(
style=discord.TextStyle.paragraph,
label=_("{style} text").format(style=event_type.get_name().title()),
default=welcome_message,
max_length=2000,
)
self.add_item(self.text)
self.og_button = button
self.index = index
self.original_welcome = welcome_message
self.event_type = event_type
async def on_submit(self, interaction: discord.Interaction):
edited_text = False
guild = interaction.guild
config = self.og_button.view.cog.config
if self.original_welcome != self.text.value:
self.original_welcome = self.text.value
edited_text = True
try:
settings = await config.guild(guild).get_raw(self.event_type.key())
settings[self.index] = self.text.value
await config.guild(guild).set_raw(self.event_type.key(), value=settings)
except IndexError:
await interaction.response.send_message(
_("There was an error editing this {event_type} message.").format(
event_type=self.event_type.get_name()
)
)
if edited_text:
await interaction.response.send_message(
_("I have edited {style} {number}.").format(
style=self.event_type.get_name(), number=self.index + 1
)
)
else:
await interaction.response.send_message(_("None of the values have changed."))
await self.og_button.view.show_checked_page(self.og_button.view.current_page, interaction)
async def interaction_check(self, interaction: discord.Interaction):
"""Just extends the default reaction_check to use owner_ids"""
owner_id = interaction.guild.owner.id
if interaction.user.id not in (
owner_id,
*interaction.client.owner_ids,
):
await interaction.response.send_message(
content=_("You are not authorized to interact with this."), ephemeral=True
)
return False
return True
class EmbedEditModal(discord.ui.Modal):
def __init__(self, embed_settings: dict, button: discord.ui.Button, event_type: EventType):
super().__init__(title=_("Edit Embed Settings"))
self.old_settings = embed_settings
self.event_type = event_type
self.embed_title = discord.ui.TextInput(
style=discord.TextStyle.short,
label=_("Embed Title"),
default=embed_settings["title"],
max_length=256,
placeholder="The title of the embed",
required=False,
)
self.embed_footer = discord.ui.TextInput(
style=discord.TextStyle.short,
label=_("Embed Footer"),
default=embed_settings["footer"],
max_length=256,
placeholder="The footer of the embed",
required=False,
)
self.embed_thumbnail = discord.ui.TextInput(
style=discord.TextStyle.short,
label=_("Embed Thumbnail"),
default=embed_settings["thumbnail"],
max_length=256,
placeholder="guild, splash, avatar, or a custom url",
required=False,
)
self.embed_image = discord.ui.TextInput(
style=discord.TextStyle.short,
label=_("Embed Image"),
default=embed_settings["image"]
if self.event_type is EventType.greeting
else embed_settings["image_goodbye"],
max_length=256,
placeholder="guild, splash, avatar, or a custom url",
required=False,
)
self.embed_icon_url = discord.ui.TextInput(
style=discord.TextStyle.short,
label=_("Embed Icon URL"),
default=embed_settings["icon_url"],
max_length=256,
placeholder="guild, splash, avatar, or a custom url",
required=False,
)
self.add_item(self.embed_title)
self.add_item(self.embed_footer)
self.add_item(self.embed_thumbnail)
self.add_item(self.embed_image)
self.add_item(self.embed_icon_url)
self.og_button = button
async def on_submit(self, interaction: discord.Interaction):
items = {
"title": self.embed_title,
"footer": self.embed_footer,
"thumbnail": self.embed_thumbnail,
# "image": self.embed_image,
"icon_url": self.embed_icon_url,
}
if self.event_type is EventType.greeting:
items["image"] = self.embed_image
else:
items["image_goodbye"] = self.embed_image
changes = set()
config = self.og_button.view.cog.config
invalid = set()
for key, item in items.items():
if self.old_settings[key] != item.value:
if key in ("title", "footer"):
changes.add(key)
await config.guild(interaction.guild).EMBED_DATA.set_raw(key, value=item.value)
else:
if item.value in ("guild", "splash", "avatar", ""):
set_to = item.value
if not item.value:
set_to = None
if set_to != self.old_settings[key]:
changes.add(key)
await config.guild(interaction.guild).EMBED_DATA.set_raw(
key, value=set_to
)
else:
search = IMAGE_LINKS.search(item.value)
if not search:
invalid.add(
_(
"`{key}` must contain guild, splash, avatar, or a valid image URL."
).format(key=key)
)
else:
if search.group(1) != self.old_settings[key]:
changes.add(key)
await config.guild(interaction.guild).EMBED_DATA.set_raw(
key, value=search.group(1)
)
if not changes:
await interaction.response.send_message(
_("None of the values have changed.\n") + "\n".join(f"- {i}" for i in invalid)
)
else:
changes_str = "\n".join(f"- {i}" for i in changes)
await interaction.response.send_message(
_("I have edited the following embed settings:\n{changes}").format(
changes=changes_str
)
)
await self.og_button.view.show_checked_page(self.og_button.view.current_page, interaction)
async def interaction_check(self, interaction: discord.Interaction):
"""Just extends the default reaction_check to use owner_ids"""
owner_id = interaction.guild.owner.id
if interaction.user.id not in (
owner_id,
*interaction.client.owner_ids,
):
await interaction.response.send_message(
content=_("You are not authorized to interact with this."), ephemeral=True
)
return False
return True
class WelcomeEditButton(discord.ui.Button):
def __init__(
self,
event_type: EventType,
style: discord.ButtonStyle,
row: Optional[int],
):
super().__init__(style=style, row=row)
self.style = style
self.emoji = "\N{GEAR}\N{VARIATION SELECTOR-16}"
self.label = _("Edit {event_type}").format(event_type=event_type.get_name().title())
self.event_type = event_type
async def callback(self, interaction: discord.Interaction):
modal = WelcomeEditModal(
self.view.source.current_selection,
self.view.source.current_page,
self,
self.event_type,
)
await interaction.response.send_modal(modal)
class EmbedEditButton(discord.ui.Button):
def __init__(
self,
event_type: EventType,
style: discord.ButtonStyle,
row: Optional[int],
):
super().__init__(style=style, row=row)
self.style = style
self.emoji = "\N{GEAR}\N{VARIATION SELECTOR-16}"
self.label = _("Edit Embed Settings")
self.event_type = event_type
async def callback(self, interaction: discord.Interaction):
settings = await self.view.cog.config.guild(interaction.guild).EMBED_DATA()
modal = EmbedEditModal(settings, self, self.event_type)
await interaction.response.send_modal(modal)
class DeleteWelcomeButton(discord.ui.Button):
def __init__(
self,
event_type: EventType,
style: discord.ButtonStyle,
row: Optional[int],
):
self.event_type = event_type
super().__init__(
style=style,
row=row,
label=_("Delete {event_type}").format(event_type=event_type.get_name().title()),
)
self.style = style
self.emoji = "\N{PUT LITTER IN ITS PLACE SYMBOL}"
async def keep_trigger(self, interaction: discord.Interaction):
await interaction.response.edit_message(
content=_("Okay this {event_type} will not be deleted.").format(
event_type=self.event_type.get_name()
),
view=None,
)
async def delete_trigger(self, interaction: discord.Interaction):
config = self.view.cog.config
guild = interaction.guild
settings = await config.guild(guild).get_raw(self.event_type.key())
settings.pop(self.view.source.current_page)
await config.guild(guild).set_raw(self.event_type.key(), value=settings)
await interaction.response.edit_message(
content=_("This {event_type} has been deleted.").format(
event_type=self.event_type.get_name()
),
view=None,
)
self.view.disable_extra_buttons()
await self.view.show_checked_page(self.view.current_page, interaction=None)
async def callback(self, interaction: discord.Interaction):
"""Enables and disables triggers"""
new_view = discord.ui.View()
approve_button = discord.ui.Button(style=discord.ButtonStyle.green, label=_("Yes"))
approve_button.callback = self.delete_trigger
deny_button = discord.ui.Button(style=discord.ButtonStyle.red, label=_("No"))
deny_button.callback = self.keep_trigger
new_view.add_item(approve_button)
new_view.add_item(deny_button)
await interaction.response.send_message(
_("Are you sure you want to delete {event_type} {name}?").format(
event_type=self.event_type.get_name(), name=self.view.source.current_page + 1
),
ephemeral=True,
view=new_view,
)
if not interaction.response.is_done():
await interaction.response.defer()
class BaseMenu(discord.ui.View):
def __init__(
self,
source: menus.PageSource,
cog: commands.Cog,
*,
message: Optional[discord.Message] = None,
clear_reactions_after: bool = True,
delete_message_after: bool = False,
timeout: int = 180,
raw: bool = False,
event_type: EventType = EventType.greeting,
**kwargs: Any,
) -> None:
super().__init__(
timeout=timeout,
)
self.cog = cog
self.bot = None
self.message = message
self._source = source
self.ctx = None
self.event_type = event_type
self.current_page = kwargs.get("page_start", 0)
self.forward_button = _NavigateButton(
discord.ButtonStyle.grey,
"\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}",
direction=1,
)
self.backward_button = _NavigateButton(
discord.ButtonStyle.grey,
"\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}",
direction=-1,
)
self.first_button = _NavigateButton(
discord.ButtonStyle.grey,
"\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}",
direction=0,
)
self.last_button = _NavigateButton(
discord.ButtonStyle.grey,
"\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}",
direction=self.source.get_max_pages(),
)
self.stop_button = StopButton(discord.ButtonStyle.red, 0)
self.add_item(self.stop_button)
self.add_item(self.first_button)
self.add_item(self.backward_button)
self.add_item(self.forward_button)
self.add_item(self.last_button)
self.delete_after = delete_message_after
self.clear_after = clear_reactions_after
self.show_raw = raw
self.raw_button = ToggleButton(
"show_raw",
discord.ButtonStyle.blurple if raw else discord.ButtonStyle.grey,
_("Show Raw"),
)
self.add_item(self.raw_button)
self.preview_button = ToggleButton(
"show_preview",
discord.ButtonStyle.grey,
_("Show Preview"),
)
self.add_item(self.preview_button)
self.edit_button = WelcomeEditButton(self.event_type, discord.ButtonStyle.grey, 2)
self.edit_embed_button = EmbedEditButton(self.event_type, discord.ButtonStyle.grey, 2)
self.add_item(self.edit_button)
self.add_item(self.edit_embed_button)
self.delete_button = DeleteWelcomeButton(self.event_type, discord.ButtonStyle.red, 2)
self.add_item(self.delete_button)
self.show_preview = False
@property
def source(self):
return self._source
async def on_timeout(self):
if self.message is None:
return
if self.clear_after and not self.delete_after:
await self.message.edit(view=None)
elif self.delete_after:
await self.message.delete()
def disable_extra_buttons(self):
self.raw_button.disabled = True
self.preview_button.disabled = True
self.edit_button.disabled = True
self.edit_embed_button.disabled = True
self.delete_button.disabled = True
def enable_extra_buttons(self):
self.raw_button.disabled = False
self.preview_button.disabled = False
self.edit_button.disabled = False
self.edit_embed_button.disabled = False
self.delete_button.disabled = False
async def start(self, ctx: commands.Context):
self.ctx = ctx
self.bot = self.cog.bot
# await self.source._prepare_once()
self.message = await self.send_initial_message(ctx)
async def _get_kwargs_from_page(self, page: int):
value = await discord.utils.maybe_coroutine(self._source.format_page, self, page)
if self.event_type is EventType.greeting:
mentions = await self.cog.config.guild(self.ctx.guild).MENTIONS()
else:
mentions = await self.cog.config.guild(self.ctx.guild).GOODBYE_MENTIONS()
allowed_mentions = discord.AllowedMentions(**mentions)
if isinstance(value, dict):
value.update({"allowed_mentions": discord.AllowedMentions(**mentions)})
return value
elif isinstance(value, str):
return {"content": value, "embeds": [], "allowed_mentions": allowed_mentions}
elif isinstance(value, discord.Embed):
return {"embeds": [value], "content": None, "allowed_mentions": allowed_mentions}
async def send_initial_message(self, ctx: commands.Context):
"""|coro|
The default implementation of :meth:`Menu.send_initial_message`
for the interactive pagination session.
This implementation shows the first page of the source.
"""
self.author = ctx.author
if self.ctx is None:
self.ctx = ctx
page = await self._source.get_page(self.current_page)
kwargs = await self._get_kwargs_from_page(page)
self.message = await ctx.send(**kwargs, view=self)
return self.message
async def show_page(self, page_number: int, interaction: Optional[discord.Interaction]):
page = await self._source.get_page(page_number)
self.current_page = self.source.pages.index(page)
kwargs = await self._get_kwargs_from_page(page)
if interaction is None or interaction.response.is_done():
await self.message.edit(**kwargs, view=self)
else:
await interaction.response.edit_message(**kwargs, view=self)
# await self.message.edit(**kwargs)
async def show_checked_page(
self, page_number: int, interaction: Optional[discord.Interaction]
) -> None:
max_pages = self._source.get_max_pages()
try:
if max_pages is None:
# If it doesn't give maximum pages, it cannot be checked
await self.show_page(page_number, interaction)
elif page_number >= max_pages:
await self.show_page(0, interaction)
elif page_number < 0:
await self.show_page(max_pages - 1, interaction)
elif max_pages > page_number >= 0:
await self.show_page(page_number, interaction)
except IndexError:
# An error happened that can be handled, so ignore it.
pass
async def interaction_check(self, interaction: discord.Interaction):
"""Just extends the default reaction_check to use owner_ids"""
if interaction.user.id not in (
*interaction.client.owner_ids,
self.author.id,
):
await interaction.response.send_message(
content=_("You are not authorized to interact with this."), ephemeral=True
)
return False
return True