479 lines
18 KiB
Python
479 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, List, Optional
|
|
|
|
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 humanize_timedelta, inline
|
|
from redbot.vendored.discord.ext import menus
|
|
|
|
log = getLogger("red.trusty-cogs.automod")
|
|
_ = Translator("This string isn't used for anything", __file__)
|
|
|
|
|
|
class AutoModRulePages(menus.ListPageSource):
|
|
def __init__(self, pages: List[discord.AutoModRule], *, guild: discord.Guild):
|
|
super().__init__(pages, per_page=1)
|
|
self.pages = pages
|
|
self.current_item: discord.AutoModRule = None
|
|
self.guild = guild
|
|
|
|
async def delete(self, view: BaseMenu, author: discord.Member) -> None:
|
|
try:
|
|
await self.current_item.delete(reason=f"Deleted by {author}")
|
|
except Exception:
|
|
return
|
|
|
|
async def toggle(self, view: BaseMenu, author: discord.Member) -> bool:
|
|
try:
|
|
self.current_itme = await self.current_item.edit(
|
|
enabled=not self.current_item.enabled, reason=f"Toggled by {author}"
|
|
)
|
|
except Exception:
|
|
return False
|
|
return True
|
|
|
|
async def format_page(self, view: discord.ui.View, page: discord.AutoModRule):
|
|
# fetch the most recently edited version since we can toggle it through this
|
|
# menu and our cached ones may be out of date
|
|
self.current_item = page = await self.guild.fetch_automod_rule(page.id)
|
|
title = (
|
|
f"\N{WHITE HEAVY CHECK MARK} {page.name}"
|
|
if page.enabled
|
|
else f"\N{CROSS MARK} {page.name}"
|
|
)
|
|
em = discord.Embed(
|
|
title=title, colour=await view.cog.bot.get_embed_colour(view.ctx.channel)
|
|
)
|
|
em.set_author(name=f"AutoMod Rules for {page.guild.name}", icon_url=page.guild.icon)
|
|
trigger = page.trigger
|
|
trigger_type_str = trigger.type.name.replace("_", " ").title()
|
|
description = f"Type: {trigger_type_str}\n"
|
|
if trigger.mention_limit:
|
|
description += f"Mention Limit: {trigger.mention_limit}\n"
|
|
if trigger.presets:
|
|
description += "Discord Preset Triggers:\n"
|
|
description += (
|
|
"\n".join(f" - {k}" for k, v in dict(trigger.presets).items() if v) + "\n"
|
|
)
|
|
trigger_keys = (
|
|
"allow_list",
|
|
"keyword_filter",
|
|
"regex_patterns",
|
|
)
|
|
for key in trigger_keys:
|
|
if triggers := getattr(trigger, key, None):
|
|
key_name = key.replace("_", " ").title()
|
|
description += f"- {key_name}:\n"
|
|
description += "\n".join(f" - {inline(t)}" for t in triggers) + "\n"
|
|
em.description = description[:4096]
|
|
|
|
# em.add_field(name="Enabled", value=str(page.enabled))
|
|
actions_str = ""
|
|
for action in page.actions:
|
|
if action.type is discord.AutoModRuleActionType.block_message:
|
|
actions_str += "- Block Message\n"
|
|
if action.custom_message:
|
|
actions_str += f"- Send this Message to the user:\n - {action.custom_message}"
|
|
elif action.type is discord.AutoModRuleActionType.timeout:
|
|
actions_str += f"- Timeout for {humanize_timedelta(timedelta=action.duration)}\n"
|
|
else:
|
|
actions_str += f"- Send alert to <#{action.channel_id}>\n"
|
|
em.add_field(name="Actions", value=actions_str, inline=False)
|
|
em.add_field(
|
|
name="Creator",
|
|
value=page.creator.mention if page.creator else inline(str(page.creator_id)),
|
|
)
|
|
if page.exempt_roles:
|
|
em.add_field(
|
|
name="Exempt Roles",
|
|
value="\n".join(f"- {r.mention}" for r in page.exempt_roles),
|
|
)
|
|
if page.exempt_channels:
|
|
em.add_field(
|
|
name="Exempt Channels",
|
|
value="\n".join(f"- {c.mention}" for c in page.exempt_channels),
|
|
)
|
|
em.add_field(name="ID", value=inline(str(page.id)))
|
|
em.set_footer(text=f"Page {view.current_page + 1}/{self.get_max_pages()}")
|
|
return em
|
|
|
|
|
|
class AutoModActionsPages(menus.ListPageSource):
|
|
def __init__(self, pages: List[dict], *, guild: discord.Guild):
|
|
super().__init__(pages, per_page=1)
|
|
self.pages = pages
|
|
self.current_item: dict = None
|
|
self.guild = guild
|
|
|
|
async def delete(self, view: BaseMenu, author: discord.User) -> None:
|
|
name = self.current_item.get("name", None)
|
|
async with view.cog.config.guild(self.guild).actions() as actions:
|
|
if name in actions:
|
|
del actions[name]
|
|
|
|
async def format_page(self, view: discord.ui.View, page: dict):
|
|
self.current_item = page
|
|
guild = page["guild"]
|
|
name = page["name"]
|
|
em = discord.Embed(
|
|
title=name, colour=await view.cog.bot.get_embed_colour(view.ctx.channel)
|
|
)
|
|
ret = ""
|
|
for k, v in page.items():
|
|
if v is None or k in ("guild", "name"):
|
|
continue
|
|
ret += f"- {k}: {v}\n"
|
|
em.description = ret
|
|
em.set_author(name=f"AutoMod Actions for {guild.name}", icon_url=guild.icon)
|
|
em.set_footer(text=f"Page {view.current_page + 1}/{self.get_max_pages()}")
|
|
return em
|
|
|
|
|
|
class AutoModTriggersPages(menus.ListPageSource):
|
|
def __init__(self, pages: List[dict], *, guild: discord.Guild):
|
|
super().__init__(pages, per_page=1)
|
|
self.pages = pages
|
|
self.current_item: dict = None
|
|
self.guild: discord.Guild = guild
|
|
|
|
async def delete(self, view: BaseMenu, author: discord.User) -> None:
|
|
name = self.current_item.get("name", None)
|
|
async with view.cog.config.guild(self.guild).triggers() as triggers:
|
|
if name in triggers:
|
|
del triggers[name]
|
|
|
|
async def format_page(self, view: discord.ui.View, page: dict):
|
|
self.current_item = page
|
|
guild = page["guild"]
|
|
self.guild = guild
|
|
name = page["name"]
|
|
em = discord.Embed(
|
|
title=name, colour=await view.cog.bot.get_embed_colour(view.ctx.channel)
|
|
)
|
|
ret = ""
|
|
for k, v in page.items():
|
|
if v is None or k in ("guild", "name"):
|
|
continue
|
|
if k == "presets":
|
|
presets = dict(discord.AutoModPresets._from_value(value=v)).items()
|
|
v = "\n".join(f" - {x}" for x, y in presets if y)
|
|
name = "Discord Presets"
|
|
ret += f"- {name}:\n{v}\n"
|
|
continue
|
|
name = k.replace("_", " ").title()
|
|
v = "\n".join(f" - {inline(x)}" for x in v)
|
|
ret += f"- {name}:\n{v}\n"
|
|
em.description = ret
|
|
em.set_author(name=f"AutoMod Triggers for {guild.name}", icon_url=guild.icon)
|
|
em.set_footer(text=f"Page {view.current_page + 1}/{self.get_max_pages()}")
|
|
return em
|
|
|
|
|
|
class StopButton(discord.ui.Button):
|
|
def __init__(
|
|
self,
|
|
style: discord.ButtonStyle,
|
|
row: Optional[int],
|
|
):
|
|
self.view: BaseMenu
|
|
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 ForwardButton(discord.ui.Button):
|
|
def __init__(
|
|
self,
|
|
style: discord.ButtonStyle,
|
|
row: Optional[int],
|
|
):
|
|
self.view: BaseMenu
|
|
super().__init__(style=style, row=row)
|
|
self.style = style
|
|
self.emoji = "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}"
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
await self.view.show_checked_page(self.view.current_page + 1, interaction)
|
|
|
|
|
|
class BackButton(discord.ui.Button):
|
|
def __init__(
|
|
self,
|
|
style: discord.ButtonStyle,
|
|
row: Optional[int],
|
|
):
|
|
self.view: BaseMenu
|
|
super().__init__(style=style, row=row)
|
|
self.style = style
|
|
self.emoji = "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}"
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
await self.view.show_checked_page(self.view.current_page - 1, interaction)
|
|
|
|
|
|
class LastItemButton(discord.ui.Button):
|
|
def __init__(
|
|
self,
|
|
style: discord.ButtonStyle,
|
|
row: Optional[int],
|
|
):
|
|
self.view: BaseMenu
|
|
super().__init__(style=style, row=row)
|
|
self.style = style
|
|
self.emoji = (
|
|
"\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}"
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
await self.view.show_page(self.view._source.get_max_pages() - 1, interaction)
|
|
|
|
|
|
class FirstItemButton(discord.ui.Button):
|
|
def __init__(
|
|
self,
|
|
style: discord.ButtonStyle,
|
|
row: Optional[int],
|
|
):
|
|
self.view: BaseMenu
|
|
super().__init__(style=style, row=row)
|
|
self.style = style
|
|
self.emoji = (
|
|
"\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}"
|
|
)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
await self.view.show_page(0, interaction)
|
|
|
|
|
|
class ToggleRuleButton(discord.ui.Button):
|
|
def __init__(
|
|
self,
|
|
row: int,
|
|
):
|
|
self.view: BaseMenu
|
|
super().__init__(style=discord.ButtonStyle.secondary, row=row)
|
|
|
|
def modify(self):
|
|
item: discord.AutoModRule = self.view.source.current_item
|
|
self.emoji = (
|
|
"\N{NEGATIVE SQUARED CROSS MARK}" if item.enabled else "\N{WHITE HEAVY CHECK MARK}"
|
|
)
|
|
self.label = _("Disable Rule") if item.enabled else _("Enable Rule")
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
"""Enables and disables triggers"""
|
|
member = interaction.user
|
|
await self.view.source.toggle(self.view, member)
|
|
self.modify()
|
|
await self.view.show_page(self.view.current_page, interaction)
|
|
|
|
|
|
class DeleteButton(discord.ui.Button):
|
|
def __init__(
|
|
self,
|
|
row: int,
|
|
):
|
|
self.view: BaseMenu
|
|
super().__init__(label=_("Delete"), style=discord.ButtonStyle.red, row=row)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
"""Enables and disables triggers"""
|
|
member = interaction.user
|
|
await self.view.source.delete(self.view, member)
|
|
await interaction.response.edit_message(content="This item has been deleted.", view=None)
|
|
|
|
|
|
class BaseMenu(discord.ui.View):
|
|
def __init__(
|
|
self,
|
|
source: menus.PageSource,
|
|
cog: commands.Cog,
|
|
clear_reactions_after: bool = True,
|
|
delete_message_after: bool = False,
|
|
timeout: int = 180,
|
|
message: discord.Message = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
super().__init__(
|
|
timeout=timeout,
|
|
)
|
|
self.cog = cog
|
|
self.bot = None
|
|
self.message = message
|
|
self._source = source
|
|
self.ctx = None
|
|
self.current_page = kwargs.get("page_start", 0)
|
|
self.forward_button = ForwardButton(discord.ButtonStyle.grey, 0)
|
|
self.back_button = BackButton(discord.ButtonStyle.grey, 0)
|
|
self.first_item = FirstItemButton(discord.ButtonStyle.grey, 0)
|
|
self.last_item = LastItemButton(discord.ButtonStyle.grey, 0)
|
|
self.stop_button = StopButton(discord.ButtonStyle.red, 0)
|
|
self.toggle_button: Optional[ToggleRuleButton] = None
|
|
self.add_item(self.stop_button)
|
|
self.add_item(self.first_item)
|
|
self.add_item(self.back_button)
|
|
self.add_item(self.forward_button)
|
|
self.add_item(self.last_item)
|
|
self.delete_button = DeleteButton(1)
|
|
self.add_item(self.delete_button)
|
|
if isinstance(source, AutoModRulePages):
|
|
self.toggle_button = ToggleRuleButton(1)
|
|
self.add_item(self.toggle_button)
|
|
|
|
@property
|
|
def source(self):
|
|
return self._source
|
|
|
|
async def on_timeout(self):
|
|
await self.message.edit(view=None)
|
|
|
|
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)
|
|
|
|
def check_paginating(self):
|
|
if not self.source.is_paginating():
|
|
self.forward_button.disabled = True
|
|
self.back_button.disabled = True
|
|
self.first_item.disabled = True
|
|
self.last_item.disabled = True
|
|
else:
|
|
self.forward_button.disabled = False
|
|
self.back_button.disabled = False
|
|
self.first_item.disabled = False
|
|
self.last_item.disabled = False
|
|
|
|
async def _get_kwargs_from_page(self, page):
|
|
self.check_paginating()
|
|
value = await discord.utils.maybe_coroutine(self._source.format_page, self, page)
|
|
if self.toggle_button is not None:
|
|
self.toggle_button.modify()
|
|
if isinstance(value, dict):
|
|
return value
|
|
elif isinstance(value, str):
|
|
return {"content": value, "embed": None}
|
|
elif isinstance(value, discord.Embed):
|
|
return {"embed": value, "content": None}
|
|
|
|
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: 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.response.is_done():
|
|
await interaction.followup.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: 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
|
|
|
|
|
|
class ConfirmView(discord.ui.View):
|
|
"""
|
|
This is just a copy of my version from Red to be removed later possibly
|
|
https://github.com/Cog-Creators/Red-DiscordBot/pull/6176
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
author: Optional[discord.abc.User] = None,
|
|
*,
|
|
timeout: float = 180.0,
|
|
disable_buttons: bool = False,
|
|
):
|
|
if timeout is None:
|
|
raise TypeError("This view should not be used as a persistent view.")
|
|
super().__init__(timeout=timeout)
|
|
self.result: Optional[bool] = None
|
|
self.author: Optional[discord.abc.User] = author
|
|
self.message: Optional[discord.Message] = None
|
|
self.disable_buttons = disable_buttons
|
|
|
|
async def on_timeout(self):
|
|
if self.message is None:
|
|
# we can't do anything here if message is none
|
|
return
|
|
|
|
if self.disable_buttons:
|
|
self.confirm_button.disabled = True
|
|
self.dismiss_button.disabled = True
|
|
await self.message.edit(view=self)
|
|
else:
|
|
await self.message.edit(view=None)
|
|
|
|
@discord.ui.button(label=_("Yes"), style=discord.ButtonStyle.green)
|
|
async def confirm_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
self.result = True
|
|
self.stop()
|
|
# respond to the interaction so the user does not see "interaction failed".
|
|
await interaction.response.defer()
|
|
# call `on_timeout` explicitly here since it's not called when `stop()` is called.
|
|
await self.on_timeout()
|
|
|
|
@discord.ui.button(label=_("No"), style=discord.ButtonStyle.secondary)
|
|
async def dismiss_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
self.result = False
|
|
self.stop()
|
|
# respond to the interaction so the user does not see "interaction failed".
|
|
await interaction.response.defer()
|
|
# call `on_timeout` explicitly here since it's not called when `stop()` is called.
|
|
await self.on_timeout()
|
|
|
|
async def interaction_check(self, interaction: discord.Interaction):
|
|
if self.message is None:
|
|
self.message = interaction.message
|
|
if self.author and interaction.user.id != self.author.id:
|
|
await interaction.response.send_message(
|
|
content=_("You are not authorized to interact with this."), ephemeral=True
|
|
)
|
|
return False
|
|
return True
|