Refactor ExtendedAudio cog to improve track information presentation by removing the create_progress_bar method and updating duration display logic. The progress bar is no longer displayed, but duration formatting is preserved for clarity.
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run

This commit is contained in:
Valerie 2025-05-28 02:31:58 -04:00
parent 1d5b7783dd
commit d8c4007781
5 changed files with 1146 additions and 0 deletions

12
automod/__init__.py Normal file
View file

@ -0,0 +1,12 @@
import json
from pathlib import Path
from .automod import AutoMod
with open(Path(__file__).parent / "info.json") as fp:
__red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"]
async def setup(bot):
cog = AutoMod(bot)
await bot.add_cog(cog)

244
automod/automod.py Normal file
View file

@ -0,0 +1,244 @@
from red_commons.logging import getLogger
from redbot.core import Config, commands
from .converters import AutoModActionFlags, AutoModRuleFlags, AutoModTriggerFlags
from .menus import (
AutoModActionsPages,
AutoModRulePages,
AutoModTriggersPages,
BaseMenu,
ConfirmView,
)
log = getLogger("red.trusty-cogs.automod")
class AutoMod(commands.Cog):
"""
Interact with and view discord's automod
"""
__author__ = ["TrustyJAID"]
__version__ = "1.0.4"
def __init__(self, bot):
self.bot = bot
self.config = Config.get_conf(self, 218773382617890828)
self.config.register_guild(actions={}, triggers={}, rules={})
self._commit = ""
self._repo = ""
def format_help_for_context(self, ctx: commands.Context) -> str:
"""
Thanks Sinbad!
"""
pre_processed = super().format_help_for_context(ctx)
ret = f"{pre_processed}\n\n- Cog Version: {self.__version__}\n"
# we'll only have a repo if the cog was installed through Downloader at some point
if self._repo:
ret += f"- Repo: {self._repo}\n"
# we should have a commit if we have the repo but just incase
if self._commit:
ret += f"- Commit: [{self._commit[:9]}]({self._repo}/tree/{self._commit})"
return ret
async def cog_before_invoke(self, ctx: commands.Context):
await self._get_commit()
async def _get_commit(self):
if self._repo:
return
downloader = self.bot.get_cog("Downloader")
if not downloader:
return
cogs = await downloader.installed_cogs()
for cog in cogs:
if cog.name == "automod":
if cog.repo is not None:
self._repo = cog.repo.clean_url
self._commit = cog.commit
@commands.hybrid_group(name="automod")
@commands.guild_only()
async def automod(self, ctx: commands.Context):
"""Commnads for interacting with automod"""
@automod.command(name="rules", aliases=["list", "rule", "view"])
@commands.bot_has_permissions(manage_guild=True)
@commands.mod_or_permissions(manage_guild=True)
async def view_automod(self, ctx: commands.Context):
"""View the servers current automod rules"""
rules = await ctx.guild.fetch_automod_rules()
if len(rules) <= 0:
await ctx.send("There are no rules setup yet.")
return
pages = AutoModRulePages(rules, guild=ctx.guild)
await BaseMenu(pages, self).start(ctx)
@automod.command(name="actions", aliases=["action"])
@commands.mod_or_permissions(manage_guild=True)
async def view_automod_actions(self, ctx: commands.Context):
"""View the servers saved automod actions"""
actions_dict = await self.config.guild(ctx.guild).actions()
actions = []
for k, v in actions_dict.items():
v.update({"name": k, "guild": ctx.guild})
actions.append(v)
if len(actions) <= 0:
await ctx.send("There are no actions saved.")
return
pages = AutoModActionsPages(actions, guild=ctx.guild)
await BaseMenu(pages, self).start(ctx)
@automod.command(name="triggers", aliases=["trigger"])
@commands.mod_or_permissions(manage_guild=True)
async def view_automod_triggers(self, ctx: commands.Context):
"""View the servers saved automod triggers"""
triggers_dict = await self.config.guild(ctx.guild).triggers()
triggers = []
for k, v in triggers_dict.items():
v.update({"name": k, "guild": ctx.guild})
triggers.append(v)
if len(triggers) <= 0:
await ctx.send("There are no triggers saved.")
return
pages = AutoModTriggersPages(triggers, guild=ctx.guild)
await BaseMenu(pages, self).start(ctx)
@automod.group(name="create", aliases=["c"])
@commands.admin_or_permissions(manage_guild=True)
async def create(self, ctx: commands.Context):
"""Create automod rules, triggers, and actions"""
@create.command(name="rule")
@commands.bot_has_permissions(manage_guild=True)
@commands.admin_or_permissions(manage_guild=True)
async def create_automod_rule(
self, ctx: commands.Context, name: str, *, rule: AutoModRuleFlags
):
"""
Create an automod rule in the server
- `<name>` The name of the rule for future reference.
- `<rule>` What the rule will do using the following information.
Usage:
- `trigger:` The name of a saved trigger.
- `actions:` The name(s) of saved actions.
- `enabled:` yes/true/t to enable this rule right away.
- `roles:` The roles that are exempt from this rule.
- `channels:` The channels that are exempt from this rule.
- `reason:` An optional reason for creating this rule.
Example:
`[p]automod create rule rule_name trigger: mytrigger actions: timeoutuser notifymods enabled: true roles: @mods`
Will create an automod rule with the saved trigger `mytrigger` and
the saved actions `timeoutuser` and `notifymods`.
"""
if not name:
await ctx.send_help()
return
log.debug(f"{rule.to_args()}")
if not rule.trigger:
await ctx.send("No trigger was provided for the rule.")
return
rule_args = rule.to_args()
name = name.lower()
if rule_args.get("reason") is not None:
rule_args["reason"] = f"Created by {ctx.author}\n" + rule_args["reason"]
try:
rule = await ctx.guild.create_automod_rule(name=name, **rule_args)
except Exception as e:
rule_args_str = "\n".join(f"- {k}: {v}" for k, v in rule_args.items())
await ctx.send(
(
"There was an error creating a rule with the following rules:\n"
f"Error: {e}\n"
f"Name: {name}\n"
f"Rules:\n{rule_args_str}"
)
)
return
pages = AutoModRulePages([rule], guild=ctx.guild)
await BaseMenu(pages, self).start(ctx)
@create.command(name="action", aliases=["a"])
@commands.admin_or_permissions(manage_guild=True)
async def create_automod_action(
self, ctx: commands.Context, name: str, *, action: AutoModActionFlags
):
"""
Create a saved action for use in automod Rules.
- `<name>` The name of this action for reference later.
Usage: `<action>`
- `message:` The message to send to a user when triggered.
- `channel:` The channel to send a notification to.
- `duration:` How long to timeout the user for. Max 28 days.
Only one of these options can be applied at a time.
Examples:
`[p]automod create action grumpyuser message: You're being too grumpy`
`[p]automod create action notifymods channel: #modlog`
`[p]automod create action 2hrtimeout duration: 2 hours`
"""
try:
action.get_action()
except ValueError as e:
# d.py errors here are concise enough to explain the issue.
await ctx.send(e)
return
name = name.lower()
async with self.config.guild(ctx.guild).actions() as actions:
if name in actions:
pred = ConfirmView(ctx.author)
pred.message = await ctx.send(
f"An action with the name `{name}` already exists. Would you like to overwrite it?",
view=pred,
)
await pred.wait()
if not pred.result:
await ctx.send("Please choose a different name then.")
return
actions[name] = action.to_json()
await ctx.send(f"Saving action `{name}` with the following settings:\n{action.to_str()}")
@create.command(name="trigger")
@commands.admin_or_permissions(manage_guild=True)
async def create_automod_trigger(
self, ctx: commands.Context, name: str, *, trigger: AutoModTriggerFlags
):
"""
Create a saved trigger for use in automod Rules.
- `<name>` The name of this trigger for reference later.
Usage: `<trigger>`
- `allows:` A space separated list of words to allow.
- `keywords:` A space separated list of words to filter.
- `mentions:` The number of user/role mentions that would trigger this rule (0-50).
- `presets:` Any combination of discord presets. e.g. `profanity`, `sexual_content`, or `slurs`.
- `regex:` A space separated list of regex patterns to include.
Note: If you want to use `mentions` you cannot also use `presets`, `keywords` or
`regex` in the same trigger. Likewise if you use any `presets` you cannot
use `keywords`, `regex`, or `mentions`.
Examples:
`[p]automod create trigger mytrigger regex: ^b(a|@)dw(o|0)rd(s|5)$`
"""
try:
trigger.get_trigger()
except ValueError as e:
# d.py errors here are concise enough to explain the issue.
await ctx.send(e)
return
name = name.lower()
async with self.config.guild(ctx.guild).triggers() as triggers:
if name in triggers:
pred = ConfirmView(ctx.author)
pred.message = await ctx.send(
f"A trigger with the name `{name}` already exists. Would you like to overwrite it?",
view=pred,
)
await pred.wait()
if not pred.result:
await ctx.send("Please choose a different name then.")
return
triggers[name] = trigger.to_json()
await ctx.send(f"Saving trigger `{name}` with the following settings:\n{trigger.to_str()}")

383
automod/converters.py Normal file
View file

@ -0,0 +1,383 @@
from __future__ import annotations
import enum
from datetime import timedelta
from typing import List, Optional, Tuple
import discord
from discord.ext.commands import Converter, FlagConverter, flag
from redbot.core import commands
from redbot.core.commands.converter import get_timedelta_converter
TimedeltaConverter = get_timedelta_converter(
maximum=timedelta(days=28), allowed_units=["minutes", "seconds", "weeks", "days", "hours"]
)
class EnumConverter(Converter):
_enum: enum.Enum
async def convert(self, ctx: commands.Context, argument: str):
for e in self._enum:
if e.name.lower() == argument.lower():
return e
valid_choices = "\n".join(f"- {e.name}" for e in self._enum)
raise commands.BadArgument(f"`{argument}` is not valid. Choose from:\n{valid_choices}")
class AutoModRuleConverter(EnumConverter):
_enum = discord.AutoModRuleEventType
class StrListTransformer(discord.app_commands.Transformer):
async def convert(self, ctx: commands.Context, argument: str) -> List[str]:
return argument.split(" ")
async def transform(self, interaction: discord.Interaction, argument: str) -> List[str]:
return argument.split(" ")
class RoleListTransformer(discord.app_commands.Transformer):
async def convert(self, ctx: commands.Context, argument: str) -> List[discord.Role]:
possible_roles = argument.split(" ")
roles = []
for role in possible_roles:
if not role:
continue
try:
r = await commands.RoleConverter().convert(ctx, role.strip())
roles.append(r)
except commands.BadArgument:
raise
return roles
async def transform(
self, interaction: discord.Interaction, argument: str
) -> List[discord.Role]:
ctx = await interaction.client.get_context(interaction)
return await self.convert(ctx, argument)
class ChannelListTransformer(discord.app_commands.Transformer):
async def convert(
self, ctx: commands.Context, argument: str
) -> List[discord.abc.GuildChannel]:
possible_channels = argument.split(" ")
channels = []
for channel in possible_channels:
if not channel:
continue
try:
c = await commands.GuildChannelConverter().convert(ctx, channel.strip())
channels.append(c)
except commands.BadArgument:
raise
return channels
async def transform(
self, interaction: discord.Interaction, argument: str
) -> List[discord.abc.GuildChannel]:
ctx = await interaction.client.get_context(interaction)
return await self.convert(ctx, argument)
class AutoModTriggerConverter(discord.app_commands.Transformer):
async def convert(self, ctx: commands.Context, argument: str) -> discord.AutoModTrigger:
cog = ctx.bot.get_cog("AutoMod")
async with cog.config.guild(ctx.guild).triggers() as triggers:
if argument.lower() in triggers:
kwargs = triggers[argument.lower()].copy()
passed_args = {}
if kwargs.get("presets") is not None:
# as far as I can tell this is the sanest way to manage
# this until d.py adds a better method of dealing with
# these awful flags
presets = discord.AutoModPresets.none()
saved_presets = kwargs.pop("presets", []) or []
for p in saved_presets:
presets.value |= p
passed_args["presets"] = presets
for key, value in kwargs.items():
if value is not None:
passed_args[key] = value
return discord.AutoModTrigger(**kwargs)
else:
raise commands.BadArgument(
("Trigger with name `{name}` does not exist.").format(name=argument.lower())
)
async def transform(
self, interaction: discord.Interaction, argument: str
) -> discord.AutoModTrigger:
ctx = await interaction.client.get_context(interaction)
return await self.convert(ctx, argument)
async def autocomplete(
self, interaction: discord.Interaction, current: str
) -> List[discord.app_commands.Choice]:
cog = interaction.client.get_cog("AutoMod")
choices = []
async with cog.config.guild(interaction.guild).triggers() as triggers:
for t in triggers.keys():
choices.append(discord.app_commands.Choice(name=t, value=t))
return [t for t in choices if current.lower() in t.name.lower()][:25]
class AutoModActionConverter(discord.app_commands.Transformer):
async def convert(
self, ctx: commands.Context, argument: str
) -> List[discord.AutoModRuleAction]:
cog = ctx.bot.get_cog("AutoMod")
ret = []
actions = await cog.config.guild(ctx.guild).actions()
for a in argument.split(" "):
if a.lower() in actions:
action_args = actions[a.lower()]
duration = action_args.pop("duration", None)
if duration:
duration = timedelta(seconds=duration)
ret.append(discord.AutoModRuleAction(**action_args, duration=duration))
if not ret:
raise commands.BadArgument(
("Action with name `{name}` does not exist.").format(name=argument.lower())
)
ret.append(discord.AutoModRuleAction())
return ret
async def transform(
self, interaction: discord.Interaction, argument: str
) -> List[discord.AutoModRuleAction]:
ctx = await interaction.client.get_context(interaction)
return await self.convert(ctx, argument)
async def autocomplete(
self, interaction: discord.Interaction, current: str
) -> List[discord.app_commands.Choice]:
cog = interaction.client.get_cog("AutoMod")
ret = []
supplied_actions = ""
new_action = ""
actions = await cog.config.guild(interaction.guild).actions()
for sup in current.lower().split(" "):
if sup in actions:
supplied_actions += f"{sup} "
else:
new_action = sup
ret = [
discord.app_commands.Choice(
name=f"{supplied_actions} {g}", value=f"{supplied_actions} {g}"
)
for g in actions.keys()
if new_action in g
]
if supplied_actions:
ret.insert(
0, discord.app_commands.Choice(name=supplied_actions, value=supplied_actions)
)
return ret[:25]
class AutoModRuleFlags(FlagConverter, case_insensitive=True):
"""AutoMod Rule converter"""
"""
# remove the event_type for now since there's only one possible option
event_type: Optional[AutoModRuleConverter] = flag(
name="event",
aliases=[],
default=discord.AutoModRuleEventType.message_send,
description="",
)
"""
trigger: Optional[AutoModTriggerConverter] = flag(
name="trigger",
aliases=[],
default=None,
description="The name of the trigger you have setup.",
)
actions: AutoModActionConverter = flag(
name="actions",
aliases=[],
default=[],
description="The name(s) of the action(s) you have setup.",
)
enabled: bool = flag(
name="enabled",
aliases=[],
default=False,
description="Wheter to immediately enable this rule.",
)
exempt_roles: RoleListTransformer = flag(
name="roles",
aliases=["exempt_roles"],
default=[],
description="The roles to be exempt from this rule.",
)
exempt_channels: ChannelListTransformer = flag(
name="channels",
aliases=[],
default=[],
description="The channels to be exempt from this rule.",
)
reason: Optional[str] = flag(
name="reason",
aliases=[],
default=None,
description="The reason for creating this rule.",
)
def to_str(self):
ret = ""
for k, v in self.to_json().items():
if v is None:
continue
if k == "presets" and self.presets:
v = "\n".join(f" - {k}" for k, v in dict(self.presets).items() if v)
ret += f"- {k}:\n{v}\n"
continue
ret += f"- {k}: {v}\n"
return ret
def to_args(self):
actions = self.actions
if not actions:
actions = [discord.AutoModRuleAction()]
return {
"event_type": discord.AutoModRuleEventType.message_send,
"trigger": self.trigger,
"actions": actions,
"enabled": self.enabled,
"exempt_roles": self.exempt_roles,
"exempt_channels": self.exempt_channels,
"reason": self.reason,
}
class AutoModPresetsConverter(discord.app_commands.Transformer):
async def convert(self, ctx: commands.Context, argument: str) -> discord.AutoModPresets:
ret = discord.AutoModPresets.none()
for possible in argument.lower().split(" "):
if possible in dict(discord.AutoModPresets.all()):
ret |= discord.AutoModPresets(**{possible.lower(): True})
if ret is discord.AutoModPresets.none():
valid_choices = "\n".join(f"- {e}" for e in dict(discord.AutoModPresets.all()).keys())
raise commands.BadArgument(f"`{argument}` is not valid. Choose from:\n{valid_choices}")
return ret
async def transform(
self, interaction: discord.Interaction, argument: str
) -> discord.AutoModPresets:
ctx = await interaction.client.get_context(interaction)
return await self.convert(ctx, argument)
class AutoModTriggerFlags(FlagConverter, case_insensitive=True):
allow_list: List[str] = flag(
name="allows",
aliases=[],
default=None,
converter=StrListTransformer,
description="A space separated list of words to allow.",
)
keyword_filter: List[str] = flag(
name="keywords",
aliases=[],
default=None,
converter=StrListTransformer,
description="A space separated list of words to filter.",
)
mention_limit: Optional[commands.Range[int, 0, 50]] = flag(
name="mentions",
aliases=[],
default=None,
description="The number of mentions to allow (0-50).",
)
presets: Optional[discord.AutoModPresets] = flag(
name="presets",
aliases=[],
default=None,
converter=AutoModPresetsConverter,
description="Use any combination of discords default presets.",
)
regex_patterns: List[str] = flag(
name="regex",
aliases=[],
default=None,
converter=StrListTransformer,
description="A space separated list of regex patterns to include.",
)
def to_str(self):
ret = ""
for k, v in self.to_json().items():
if v is None:
continue
if k == "presets" and self.presets:
v = "\n".join(f" - {k}" for k, v in dict(self.presets).items() if v)
ret += f"- {k}:\n{v}\n"
continue
ret += f"- {k}: {v}\n"
return ret
def to_json(self):
return {
"allow_list": self.allow_list,
"keyword_filter": self.keyword_filter,
"mention_limit": self.mention_limit,
"regex_patterns": self.regex_patterns,
"presets": self.presets.to_array() if self.presets else None,
}
def get_trigger(self):
return discord.AutoModTrigger(
keyword_filter=self.keyword_filter,
presets=self.presets,
allow_list=self.allow_list,
mention_limit=self.mention_limit,
regex_patterns=self.regex_patterns,
)
class AutoModActionFlags(FlagConverter, case_insensitive=True):
custom_message: Optional[commands.Range[str, 1, 150]] = flag(
name="message",
aliases=[],
default=None,
description="A custom message to send to the user.",
)
channel_id: Optional[discord.TextChannel] = flag(
name="channel",
aliases=[],
default=None,
description="The channel to send a notification to.",
)
duration: Optional[timedelta] = flag(
name="duration",
aliases=[],
default=None,
description="How long to timeout the user for.",
converter=TimedeltaConverter,
)
def to_str(self):
ret = ""
for k, v in self.to_json().items():
if v is None:
continue
ret += f"- {k}: {v}\n"
return ret
def to_json(self):
return {
"custom_message": self.custom_message,
"channel_id": self.channel_id.id if self.channel_id else None,
"duration": int(self.duration.total_seconds()) if self.duration else None,
}
def get_action(self):
return discord.AutoModRuleAction(
custom_message=self.custom_message,
channel_id=self.channel_id.id if self.channel_id else None,
duration=self.duration,
)

28
automod/info.json Normal file
View file

@ -0,0 +1,28 @@
{
"author": [
"TrustyJAID"
],
"description": "A cog to interact with Discord Automod.",
"disabled": false,
"end_user_data_statement": "This cog does not persistently store end user data.",
"hidden": false,
"install_msg": "See `[p]automod` for available commands.",
"max_bot_version": "0.0.0",
"min_bot_version": "3.5.0",
"min_python_version": [
3,
9,
0
],
"name": "Automod",
"permissions": [],
"required_cogs": {},
"requirements": [],
"short": "Discord Automod",
"tags": [
"automod",
"moderation",
"mod"
],
"type": "COG"
}

479
automod/menus.py Normal file
View file

@ -0,0 +1,479 @@
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