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