Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WarnSystem] Replace role mute system, with Discord built-in timeouts #217

Open
wants to merge 11 commits into
base: v3
Choose a base branch
from
14 changes: 4 additions & 10 deletions warnsystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is the WarnSystem cog for Red, rewrite of the V2 cog, BetterMod. It is an alternative to the core moderation system of Red, similar to Dyno. You can warn members up to 5 levels:
1. simple warning
2. mute (can be a temporary mute)
2. timeout
3. kick
4. softban (ban then unban, which can cleanup the messages of an user up to a week)
5. ban (can be a temporary ban, or even a hack ban, which allows you to ban on an user not on the server)
Expand Down Expand Up @@ -42,20 +42,14 @@ The cog is highly customizable and frequently updated. It's also built with an A
```
*Note: If you already set a modlog channel with the modlogs cog, it will be used if you skip this part.*

5. Setup a mute role. **This might take a long time, depending on the number of text channels on your server.**
```
[p]warnset mute
```
The mutes are done with a role. Feel free to edit its permissions, but make sure it stays under the bot's top role!
5. (Optional) Import your data from BetterMod.

6. (Optional) Import your data from BetterMod.

**You must have the latest version of BetterMod before using this command. Using an outdated body will break the data of the cog!**
**You must have the latest version of BetterMod before using this command. Using an outdated body will break the data of the cog!**

1. Grab your server ID. To get this, you can either:
- Use the `[p]serverinfo` command in the General cog.
- Enable the developer mode (User settings > Appearance), right click on your server, then click "Copy ID"

2. Go to your V2 bot's directory, and navigate to `data/bettermod/history/` and find the file which name is your server ID, and copy its full path.

3. Type this command: `[p]warnset convert <path_to_file>`
Expand Down
257 changes: 38 additions & 219 deletions warnsystem/api.py

Large diffs are not rendered by default.

54 changes: 33 additions & 21 deletions warnsystem/automod.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ def _format_embed_for_autowarn(
"bot will set a level {level} warning on him{duration} for the reason: {reason}"
).format(
number=number_of_warns,
level_lock=_(" level {level}").format(level=lock_level) if lock_level else "",
level_lock=(_(" level {level}").format(level=lock_level) if lock_level else ""),
from_bot=_(" from the automod") if only_automod else "",
within_time=_(" within {time}").format(time=time_str) if time else "",
level=warn_level,
duration=_(" during {time}").format(time=duration_str) if duration else "",
duration=(_(" during {time}").format(time=duration_str) if duration else ""),
reason=warn_reason,
),
inline=False,
Expand Down Expand Up @@ -232,16 +232,20 @@ async def automod_regex_show(self, ctx: commands.Context, name: str):
embed = discord.Embed(title=_("Regex trigger: {name}").format(name=name))
embed.description = _("Regex trigger details.")
embed.add_field(
name=_("Regular expression"), value=box(automod_regex["regex"].pattern), inline=False
name=_("Regular expression"),
value=box(automod_regex["regex"].pattern),
inline=False,
)
embed.add_field(
name=_("Warning"),
value=_("**Level:** {level}\n**Reason:** {reason}\n**Duration:** {time}").format(
level=automod_regex["level"],
reason=automod_regex["reason"],
time=self.api._format_timedelta(automod_regex["time"])
if automod_regex["time"]
else _("Not set."),
time=(
self.api._format_timedelta(automod_regex["time"])
if automod_regex["time"]
else _("Not set.")
),
),
inline=False,
)
Expand Down Expand Up @@ -323,7 +327,11 @@ async def automod_warn_add(self, ctx: commands.Context):
else:
await ctx.send(_("Level must be between 1 and 5."))
warn_reason = await self._ask_for_value(
ctx, msg, embed, _("What's the reason of the automod's warning?"), optional=True
ctx,
msg,
embed,
_("What's the reason of the automod's warning?"),
optional=True,
)
time: timedelta = await self._ask_for_value(
ctx,
Expand All @@ -334,7 +342,7 @@ async def automod_warn_add(self, ctx: commands.Context):
"For example, you can make it trigger if a member got 3 warnings"
" __within a day__\nOmitting this value will make the automod look across the "
"entire member's modlog without time limit.\n\n"
"Format is the same as temp mutes/bans: `30m` = 30 minutes, `2h` = 2 hours, "
"Format is the same as timeouts/temp bans: `30m` = 30 minutes, `2h` = 2 hours, "
"`4d` = 4 days..."
),
need="time",
Expand All @@ -347,13 +355,13 @@ async def automod_warn_add(self, ctx: commands.Context):
msg,
embed,
_(
"Level 2 and 5 warnings can be temporary (unmute or unban "
"Level 2 have to be and Level 5 warnings can be temporary (unban "
"after some time). For how long should the the member stay punished?\n"
"Skip this value to make the mute/ban unlimited.\n"
"Skip this value for bans to make it unlimited.\n"
"Time format is the same as the previous question."
),
need="time",
optional=True,
optional=warn_level == 5,
)
while True:
lock_level = await self._ask_for_value(
Expand Down Expand Up @@ -652,7 +660,7 @@ async def automod_antispam_warn(
Examples: `[p]automod antispam warn 1 Spamming` `[p]automod antispam warn 2 30m Spamming`

You can use the `[p]automod warn` command to configure an automatic warning after multiple\
automod infractions, like a mute after 3 warns.
automod infractions, like a timeout after 3 warns.
"""
guild = ctx.guild
await self.data.guild(guild).automod.antispam.warn.set(
Expand All @@ -670,11 +678,13 @@ async def automod_antispam_warn(
).format(
level=level,
reason=reason,
duration=_("that will last for {time} ").format(
time=self.api._format_timedelta(duration)
)
if duration
else "",
duration=(
_("that will last for {time} ").format(
time=self.api._format_timedelta(duration)
)
if duration
else ""
),
)
)

Expand Down Expand Up @@ -800,11 +810,13 @@ async def automod_antispam_info(self, ctx: commands.Context):
reason = antispam_settings["warn"]["reason"]
if level == 2 or level == 5:
time = _("Time: {time}\n").format(
time=self.api._format_timedelta(
timedelta(seconds=antispam_settings["warn"]["time"])
time=(
self.api._format_timedelta(
timedelta(seconds=antispam_settings["warn"]["time"])
)
if antispam_settings["warn"]["time"]
else _("Unlimited.")
)
if antispam_settings["warn"]["time"]
else _("Unlimited.")
)
else:
time = ""
Expand Down
16 changes: 0 additions & 16 deletions warnsystem/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ def __init__(self, bot: Red, config: Config):
self.bot = bot
self.data = config

self.mute_roles = {}
self.temp_actions = {}
self.automod_enabled = []
self.automod_antispam = {}
Expand All @@ -45,33 +44,18 @@ async def _debug_info(self) -> str:
This calls a huge part of the Config database and will not load it into the cache.
"""
config_data = await self.data.all_guilds()
mute_roles_cached = len(self.mute_roles)
mute_roles = len([x for x in config_data.values() if x["mute_role"] is not None])
guild_temp_actions_cached = len(self.temp_actions)
guild_temp_actions = len([x for x in config_data.values() if x["temporary_warns"]])
temp_actions_cached = sum(len(x) for x in self.temp_actions.values())
temp_actions = sum((len(x["temporary_warns"]) for x in config_data.values()))
text = (
f"Debug info requested\n"
f"{mute_roles_cached}/{mute_roles} mute roles loaded in cache.\n"
f"{guild_temp_actions_cached}/{guild_temp_actions} guilds with temp actions loaded in cache.\n"
f"{temp_actions_cached}/{temp_actions} temporary actions loaded in cache."
)
log.info(text)
return text

async def get_mute_role(self, guild: discord.Guild):
role_id = self.mute_roles.get(guild.id, False)
if role_id is not False:
return role_id
role_id = await self.data.guild(guild).mute_role()
self.mute_roles[guild.id] = role_id
return role_id

async def update_mute_role(self, guild: discord.Guild, role: discord.Role):
await self.data.guild(guild).mute_role.set(role.id)
self.mute_roles[guild.id] = role.id

async def get_temp_action(self, guild: discord.Guild, member: Optional[discord.Member] = None):
guild_temp_actions = self.temp_actions.get(guild.id, {})
if not guild_temp_actions:
Expand Down
45 changes: 23 additions & 22 deletions warnsystem/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,10 @@ async def edit_button(self, interaction: discord.Interaction, button: Button):
return
await self.api.edit_case(interaction.guild, self.user, self.case_index + 1, new_reason)
await interaction.followup.edit_message(
"@original", content=_("The reason was successfully edited!\n"), embed=None, view=None
"@original",
content=_("The reason was successfully edited!\n"),
embed=None,
view=None,
)

@discord.ui.button(
Expand All @@ -216,32 +219,20 @@ async def edit_button(self, interaction: discord.Interaction, button: Button):
async def delete_button(self, interaction: discord.Interaction, button: Button):
guild = interaction.guild
embed = discord.Embed()
can_unmute = False
add_roles = False
if self.case["level"] == 2:
mute_role = guild.get_role(await self.ws.cache.get_mute_role(guild))
member = guild.get_member(self.user)
if member:
if mute_role and mute_role in member.roles:
can_unmute = True
add_roles = await self.ws.data.guild(guild).remove_roles()
description = _(
"Case #{number} deletion.\n**Click on the button to confirm your action.**"
).format(number=self.case_index + 1)
if can_unmute or add_roles:
description += _("\nNote: Deleting the case will also do the following:")
if can_unmute:
description += _("\n- unmute the member")
if add_roles:
description += _("\n- add all roles back to the member")
embed.description = description
response = await prompt_yes_or_no(self.bot, interaction, embed=embed, clear_after=False)
if response is False:
return
await self.api.delete_case(guild, self.user, self.case_index + 1) # does not starting at 0
self.list.deleted_cases.append(self.case_index)
await interaction.followup.edit_message(
"@original", content=_("The case was successfully deleted!"), embed=None, view=None
"@original",
content=_("The case was successfully deleted!"),
embed=None,
view=None,
)


Expand All @@ -255,7 +246,12 @@ async def format_page(self, menu: WarningsSelector, balls: List[dict]):


class WarningsSelector(Pages[menus.ListPageSource]):
def __init__(self, ctx: Context, user: Union[discord.Member, UnavailableMember], warnings: List[dict]):
def __init__(
self,
ctx: Context,
user: Union[discord.Member, UnavailableMember],
warnings: List[dict],
):
self.user = user
self.ws = cast("WarnSystem", ctx.bot.get_cog("WarnSystem"))
self.api: "API" = self.ws.api
Expand All @@ -268,7 +264,7 @@ def _get_label(self, level: int) -> Tuple[str, str]:
if level == 1:
return (_("Warning"), "⚠")
elif level == 2:
return (_("Mute"), "🔇")
return (_("Timeout"), "🔇")
elif level == 3:
return (_("Kick"), "👢")
elif level == 4:
Expand All @@ -295,10 +291,10 @@ def set_options(self, cases: List[dict]):
self.select_warning_menu.options = options

@discord.ui.select(placeholder="Select a warning to view it.")
async def select_warning_menu(self, interaction: discord.Interaction, item:discord.ui.Select):
async def select_warning_menu(self, interaction: discord.Interaction, item: discord.ui.Select):
warning_str = lambda level, plural: {
1: (_("Warning"), _("Warnings")),
2: (_("Mute"), _("Mutes")),
2: (_("Timeout"), _("Timeouts")),
3: (_("Kick"), _("Kicks")),
4: (_("Softban"), _("Softbans")),
5: (_("Ban"), _("Bans")),
Expand Down Expand Up @@ -338,6 +334,11 @@ async def select_warning_menu(self, interaction: discord.Interaction, item:disco
await interaction.response.send_message(
embed=embed,
view=WarningEditionView(
self.bot, self, user=self.user, case=case, case_index=i, disabled=not is_mod
self.bot,
self,
user=self.user,
case=case,
case_index=i,
disabled=not is_mod,
),
)
7 changes: 5 additions & 2 deletions warnsystem/context_menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ class ReasonEntry(Modal, title="Member warn"):
)

def __init__(
self, og_interaction: discord.Interaction["Red"], level: int, member: discord.Member
self,
og_interaction: discord.Interaction["Red"],
level: int,
member: discord.Member,
):
super().__init__()
self.og_interaction = og_interaction
Expand Down Expand Up @@ -114,7 +117,7 @@ async def warn_1(self, interaction: discord.Interaction["Red"], button: discord.
await self.warn(interaction, 1)

@button(
label="Mute",
label="Timeout",
style=discord.ButtonStyle.secondary,
emoji="\N{SPEAKER WITH CANCELLATION STROKE}",
)
Expand Down
8 changes: 6 additions & 2 deletions warnsystem/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ def non_lurker_members(self, members: List[discord.Member]):

def parse_arguments(self, arguments: str):
parser = NoExitParser(
description="Mass member selection in a server for WarnSystem.", add_help=False
description="Mass member selection in a server for WarnSystem.",
add_help=False,
)

parser.add_argument(
Expand Down Expand Up @@ -353,7 +354,10 @@ async def _role(
for role in _roles:
try:
roles.append(await RoleConverter().convert(self.ctx, role))
except (discord.errors.NotFound, discord.ext.commands.errors.BadArgument):
except (
discord.errors.NotFound,
discord.ext.commands.errors.BadArgument,
):
raise BadArgument(
_(
"Can't convert `{arg}` from `--{state}` into a "
Expand Down
13 changes: 1 addition & 12 deletions warnsystem/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
__all__ = [
"InvalidLevel",
"NotFound",
"MissingMuteRole",
"BadArgument",
"MissingPermissions",
"MemberTooHigh",
Expand Down Expand Up @@ -65,15 +64,6 @@ class UserNotFound(Exception):
pass


class MissingMuteRole(Exception):
"""
You requested a mute warn but the mute role doesn't exist. Call
:func:`~warnsystem.api.API.maybe_create_role` to fix this.
"""

pass


class BadArgument(Exception):
"""
The arguments provided for your request are wrong, check the types.
Expand Down Expand Up @@ -126,8 +116,7 @@ class LostPermissions(Exception):
"""
The bot lost a permission it had.

This can be the permission to send messages in the modlog channel or use\
the mute role.
This can be the permission to send messages in the modlog channel.
"""

pass
Expand Down
Loading
Loading