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
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
This commit is contained in:
parent
1d5b7783dd
commit
d8c4007781
5 changed files with 1146 additions and 0 deletions
12
automod/__init__.py
Normal file
12
automod/__init__.py
Normal 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
244
automod/automod.py
Normal 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
383
automod/converters.py
Normal 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
28
automod/info.json
Normal 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
479
automod/menus.py
Normal 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
|
Loading…
Add table
Reference in a new issue