From 5e573141e6f8d66783c3a45160d202d5e1675382 Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 21 Jun 2021 23:13:18 +0200 Subject: [PATCH 001/120] Added function to user other units than days for mutes/bans --- moderation/mod/cog.py | 123 +++++++++++++++++++---------- moderation/mod/models.py | 12 +-- moderation/mod/translations/en.yml | 51 ++++++------ 3 files changed, 115 insertions(+), 71 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index e5aa02844..7fee766db 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -1,6 +1,7 @@ import re from datetime import datetime, timedelta -from typing import Optional, Union, List, Tuple +from dateutil.relativedelta import relativedelta +from typing import Optional, Union, List, Tuple, Generator from discord import Role, Guild, Member, Forbidden, HTTPException, User, Embed, NotFound, Message from discord.ext import commands, tasks @@ -36,17 +37,54 @@ t = t.mod +days_in_minutes = 60*60*24 + + class DurationConverter(Converter): async def convert(self, ctx, argument: str) -> Optional[int]: if argument.lower() in ("inf", "perm", "permanent", "-1", "∞"): return None - if (match := re.match(r"^(\d+)d?$", argument)) is None: + if (match := re.match(r"^(\d+y)?(\d+m)?(\d+w)?(\d+d)?(\d+h)?(\d+n)?$", argument)) is None: raise BadArgument(tg.invalid_duration) - if (days := int(match.group(1))) <= 0: + + minutes = ToMinutes.convert(*[ToMinutes.toint(match.group(i)) for i in range(1, 7)]) + + if minutes <= 0: raise BadArgument(tg.invalid_duration) - if days >= (1 << 31): + if minutes >= (1 << 31): raise BadArgument(t.invalid_duration_inf) - return days + return minutes + + +class ToMinutes: + @staticmethod + def toint(value: str) -> int: + return 0 if value is None else int(value[:-1]) + + @staticmethod + def convert(years: int, months: int, weeks: int, days: int, hours: int, minutes: int) -> int: + days += years * 365 + days += months * 30 + days += weeks * 7 + + td = timedelta(days=days, hours=hours, minutes=minutes) + return int(td.total_seconds() / 60) + + +def time_to_units(minutes: Union[int, float]) -> str: + rd = relativedelta(datetime.fromtimestamp(0) + timedelta(minutes=minutes), + datetime.fromtimestamp(0)) # Workaround that should be improved later + + def generator() -> Generator: + for unit in [t.times.years(cnt=rd.years), + t.times.months(cnt=rd.months), + t.times.days(cnt=rd.days), + t.times.hours(cnt=rd.hours), + t.times.minutes(cnt=rd.minutes)]: + if not str(unit).startswith("0"): + yield str(unit) + + return ", ".join(generator()) async def get_mute_role(guild: Guild) -> Role: @@ -96,7 +134,7 @@ async def send_to_changelog_mod( class ModCog(Cog, name="Mod Tools"): - CONTRIBUTORS = [Contributor.Defelo, Contributor.wolflu, Contributor.Florian] + CONTRIBUTORS = [Contributor.Defelo, Contributor.wolflu, Contributor.Florian, Contributor.LoC] async def on_ready(self): guild: Guild = self.bot.guilds[0] @@ -118,7 +156,7 @@ async def mod_loop(self): guild: Guild = self.bot.guilds[0] async for ban in await db.stream(filter_by(Ban, active=True)): - if ban.days != -1 and datetime.utcnow() >= ban.timestamp + timedelta(days=ban.days): + if ban.minutes != -1 and datetime.utcnow() >= ban.timestamp + timedelta(minutes=ban.minutes): await Ban.deactivate(ban.id) try: @@ -140,7 +178,7 @@ async def mod_loop(self): return async for mute in await db.stream(filter_by(Mute, active=True)): - if mute.days != -1 and datetime.utcnow() >= mute.timestamp + timedelta(days=mute.days): + if mute.minutes != -1 and datetime.utcnow() >= mute.timestamp + timedelta(minutes=mute.minutes): if member := guild.get_member(mute.member): await member.remove_roles(mute_role) else: @@ -190,17 +228,17 @@ async def count(cls): async def handle_get_user_status_entries(self, user_id: int) -> list[tuple[str, str]]: status = t.none if (ban := await db.get(Ban, member=user_id, active=True)) is not None: - if ban.days != -1: - expiry_date: datetime = ban.timestamp + timedelta(days=ban.days) - days_left = (expiry_date - datetime.utcnow()).days + 1 - status = t.status_banned_days(cnt=ban.days, left=days_left) + if ban.minutes != -1: + expiry_date: datetime = ban.timestamp + timedelta(minutes=ban.minutes) + time_left = time_to_units((expiry_date - datetime.utcnow()).total_seconds()/60 + 1) + status = t.status_banned_time(time_to_units(ban.minutes), time_left) else: status = t.status_banned elif (mute := await db.get(Mute, member=user_id, active=True)) is not None: - if mute.days != -1: - expiry_date: datetime = mute.timestamp + timedelta(days=mute.days) - days_left = (expiry_date - datetime.utcnow()).days + 1 - status = t.status_muted_days(cnt=mute.days, left=days_left) + if mute.minutes != -1: + expiry_date: datetime = mute.timestamp + timedelta(minutes=mute.minutes) + time_left = time_to_units((expiry_date - datetime.utcnow()).total_seconds()/60 + 1) + status = t.status_muted_time(time_to_units(mute.minutes), time_left) else: status = t.status_muted return [(t.active_sanctions, status)] @@ -221,10 +259,10 @@ async def handle_get_userlog_entries(self, user_id: int) -> list[tuple[datetime, async for mute in await db.stream(filter_by(Mute, member=user_id)): text = t.ulog.muted.upgrade if mute.is_upgrade else t.ulog.muted.first - if mute.days == -1: + if mute.minutes == -1: out.append((mute.timestamp, text.inf(f"<@{mute.mod}>", mute.reason))) else: - out.append((mute.timestamp, text.temp(f"<@{mute.mod}>", mute.reason, cnt=mute.days))) + out.append((mute.timestamp, text.temp(f"<@{mute.mod}>", time_to_units(mute.minutes), mute.reason))) if not mute.active and not mute.upgraded: if mute.unmute_mod is None: @@ -248,10 +286,10 @@ async def handle_get_userlog_entries(self, user_id: int) -> list[tuple[datetime, async for ban in await db.stream(filter_by(Ban, member=user_id)): text = t.ulog.banned.upgrade if ban.is_upgrade else t.ulog.banned.first - if ban.days == -1: + if ban.minutes == -1: out.append((ban.timestamp, text.inf(f"<@{ban.mod}>", ban.reason))) else: - out.append((ban.timestamp, text.temp(f"<@{ban.mod}>", ban.reason, cnt=ban.days))) + out.append((ban.timestamp, text.temp(f"<@{ban.mod}>", time_to_units(ban.minutes), ban.reason))) if not ban.active and not ban.upgraded: if ban.unban_mod is None: @@ -332,15 +370,17 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): @commands.command() @ModPermission.mute.check @guild_only() - async def mute(self, ctx: Context, user: UserMemberConverter, days: DurationConverter, *, reason: str): + async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConverter, *, reason: str): """ mute a user + time format: `ymwdhn` set days to `inf` for a permanent mute """ user: Union[Member, User] - days: Optional[int] + time: Optional[int] + minutes = time if len(reason) > 900: raise CommandError(t.reason_too_long) @@ -356,11 +396,11 @@ async def mute(self, ctx: Context, user: UserMemberConverter, days: DurationConv active_mutes: List[Mute] = await db.all(filter_by(Mute, active=True, member=user.id)) for mute in active_mutes: - if mute.days == -1: + if mute.minutes == -1: raise UserCommandError(user, t.already_muted) - ts = mute.timestamp + timedelta(days=mute.days) - if days is not None and datetime.utcnow() + timedelta(days=days) <= ts: + ts = mute.timestamp + timedelta(minutes=mute.minutes) + if minutes is not None and datetime.utcnow() + timedelta(minutes=minutes) <= ts: raise UserCommandError(user, t.already_muted) for mute in active_mutes: @@ -370,9 +410,9 @@ async def mute(self, ctx: Context, user: UserMemberConverter, days: DurationConv server_embed = Embed(title=t.mute, description=t.muted_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) - if days is not None: - await Mute.create(user.id, str(user), ctx.author.id, days, reason, bool(active_mutes)) - user_embed.description = t.muted(ctx.author.mention, ctx.guild.name, reason, cnt=days) + if minutes is not None: + await Mute.create(user.id, str(user), ctx.author.id, minutes, reason, bool(active_mutes)) + user_embed.description = t.muted(ctx.author.mention, ctx.guild.name, time_to_units(minutes), reason) await send_to_changelog_mod( ctx.guild, ctx.message, @@ -380,7 +420,7 @@ async def mute(self, ctx: Context, user: UserMemberConverter, days: DurationConv t.log_muted, user, reason, - duration=t.log_field.days(cnt=days), + duration=time_to_units(minutes), ) else: await Mute.create(user.id, str(user), ctx.author.id, -1, reason, bool(active_mutes)) @@ -392,7 +432,7 @@ async def mute(self, ctx: Context, user: UserMemberConverter, days: DurationConv t.log_muted, user, reason, - duration=t.log_field.days_infinity, + duration=t.log_field.infinity, ) try: @@ -486,17 +526,20 @@ async def ban( self, ctx: Context, user: UserMemberConverter, - ban_days: DurationConverter, + time: DurationConverter, delete_days: int, *, reason: str, ): """ ban a user + time format: `ymwdhn` set ban_days to `inf` for a permanent ban """ - ban_days: Optional[int] + time: Optional[int] + minutes = time + user: Union[Member, User] if not ctx.guild.me.guild_permissions.ban_members: @@ -514,11 +557,11 @@ async def ban( active_bans: List[Ban] = await db.all(filter_by(Ban, active=True, member=user.id)) for ban in active_bans: - if ban.days == -1: + if ban.minutes == -1: raise UserCommandError(user, t.already_banned) - ts = ban.timestamp + timedelta(days=ban.days) - if ban_days is not None and datetime.utcnow() + timedelta(days=ban_days) <= ts: + ts = ban.timestamp + timedelta(minutes=ban.minutes) + if minutes is not None and datetime.utcnow() + timedelta(minutes=minutes) <= ts: raise UserCommandError(user, t.already_banned) for ban in active_bans: @@ -530,9 +573,9 @@ async def ban( server_embed = Embed(title=t.ban, description=t.banned_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) - if ban_days is not None: - await Ban.create(user.id, str(user), ctx.author.id, ban_days, reason, bool(active_bans)) - user_embed.description = t.banned(ctx.author.mention, ctx.guild.name, reason, cnt=ban_days) + if minutes is not None: + await Ban.create(user.id, str(user), ctx.author.id, minutes, reason, bool(active_bans)) + user_embed.description = t.banned(ctx.author.mention, ctx.guild.name, time_to_units(minutes), reason) await send_to_changelog_mod( ctx.guild, ctx.message, @@ -540,7 +583,7 @@ async def ban( t.log_banned, user, reason, - duration=t.log_field.days(cnt=ban_days), + duration=time_to_units(minutes), ) else: await Ban.create(user.id, str(user), ctx.author.id, -1, reason, bool(active_bans)) @@ -552,7 +595,7 @@ async def ban( t.log_banned, user, reason, - duration=t.log_field.days_infinity, + duration=t.log_field.infinity, ) try: diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 9da14605c..5fa08358a 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -56,7 +56,7 @@ class Mute(db.Base): member_name: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) mod: Union[Column, int] = Column(BigInteger) timestamp: Union[Column, datetime] = Column(DateTime) - days: Union[Column, int] = Column(Integer) + minutes: Union[Column, int] = Column(Integer) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) active: Union[Column, bool] = Column(Boolean) deactivation_timestamp: Union[Column, Optional[datetime]] = Column(DateTime, nullable=True) @@ -66,13 +66,13 @@ class Mute(db.Base): is_upgrade: Union[Column, bool] = Column(Boolean) @staticmethod - async def create(member: int, member_name: str, mod: int, days: int, reason: str, is_upgrade: bool = False) -> Mute: + async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, is_upgrade: bool = False) -> Mute: row = Mute( member=member, member_name=member_name, mod=mod, timestamp=datetime.utcnow(), - days=days, + minutes=minutes, reason=reason, active=True, deactivation_timestamp=None, @@ -123,7 +123,7 @@ class Ban(db.Base): member_name: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) mod: Union[Column, int] = Column(BigInteger) timestamp: Union[Column, datetime] = Column(DateTime) - days: Union[Column, int] = Column(Integer) + minutes: Union[Column, int] = Column(Integer) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) active: Union[Column, bool] = Column(Boolean) deactivation_timestamp: Union[Column, Optional[datetime]] = Column(DateTime, nullable=True) @@ -133,13 +133,13 @@ class Ban(db.Base): is_upgrade: Union[Column, bool] = Column(Boolean) @staticmethod - async def create(member: int, member_name: str, mod: int, days: int, reason: str, is_upgrade: bool = False) -> Ban: + async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, is_upgrade: bool = False) -> Ban: row = Ban( member=member, member_name=member_name, mod=mod, timestamp=datetime.utcnow(), - days=days, + minutes=minutes, reason=reason, active=True, deactivation_timestamp=None, diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 80d1bba30..e754442aa 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -4,6 +4,23 @@ permissions: kick: kick a user ban: ban a user +times: + years: + one: "{cnt} year" + many: "{cnt} years" + months: + one: "{cnt} month" + many: "{cnt} months" + days: + one: "{cnt} day" + many: "{cnt} days" + hours: + one: "{cnt} hour" + many: "{cnt} hours" + minutes: + one: "{cnt} minute" + many: "{cnt} minutes" + log_field: member: ":bust_in_silhouette: Member" member_id: ":dna: Member ID" @@ -13,7 +30,7 @@ log_field: days: one: "{cnt} day" many: "{cnt} days" - days_infinity: "*Permanent*" + infinity: "*Permanent*" jump_url: "{} [[Jump]]({})" no_dm: ":warning: User does not allow direct messages." @@ -37,9 +54,7 @@ unmute: ":loud_sound: Mute" mute_role_not_set: User could not be muted because no mute role is configured. cannot_mute: This member cannot be muted. already_muted: User is already muted for the given period of time. -muted: - one: "{} just muted you on {} for {cnt} day:```\n{}\n```" - many: "{} just muted you on {} for {cnt} days:```\n{}\n```" +muted: "{} just muted you on {} for {}:```\n{}\n```" muted_inf: "{} just muted you on {}:```\n{}\n```" muted_response: "User muted successfully :white_check_mark:" log_muted: ":mute: Member muted" @@ -60,9 +75,7 @@ unban: ":white_check_mark: Ban" cannot_ban: This member cannot be banned. already_banned: This member is already banned for the given period of time. cannot_ban_permissions: User could not be banned because I don't have `ban_members` permission on this server. -banned: - one: "{} just banned you from {} for {cnt} day:```\n{}\n```" - many: "{} just banned you from {} for {cnt} days:```\n{}\n```" +banned: "{} just banned you from {} for {}:```\n{}\n```" banned_inf: "{} just banned you from {}:```\n{}\n```" banned_response: "User banned successfully :white_check_mark:" log_banned: ":no_entry: Member Banned" @@ -85,13 +98,9 @@ autokicks: active_sanctions: Active Punishments none: None status_banned: ":no_entry: User is banned from this server." -status_banned_days: - one: ":no_entry: User is banned for {cnt} day from this server ({left} left)." - many: ":no_entry: User is banned for {cnt} days from this server ({left} left)." +status_banned_time: ":no_entry: User is banned for {} from this server ({} left)." status_muted: ":mute: User is muted on this server." -status_muted_days: - one: ":mute: User is muted for {cnt} day on this server ({left} left)." - many: ":mute: User is muted for {cnt} days on this server ({left} left)." +status_muted_time: ":mute: User is muted for {} on this server ({} left)." ulog: reported: ":speech_balloon: **Reported** by {} because of `{}`" @@ -99,14 +108,10 @@ ulog: muted: first: - temp: - one: ":mute: **Muted** by {} for {cnt} day because of `{}`" - many: ":mute: **Muted** by {} for {cnt} days because of `{}`" + temp: ":mute: **Muted** by {} for {} because of `{}`" inf: ":mute: **Muted** permanently by {} because of `{}`" upgrade: - temp: - one: ":mute: **Mute upgraded** by {} to {cnt} day because of `{}`" - many: ":mute: **Mute upgraded** by {} to {cnt} days because of `{}`" + temp: ":mute: **Mute upgraded** by {} to {} because of `{}`" inf: ":mute: **Mute upgraded** to permanent by {} because of `{}`" unmuted: ":loud_sound: **Unmuted** by {} because of `{}`" unmuted_expired: ":loud_sound: **Unmuted** because the mute time has expired." @@ -116,14 +121,10 @@ ulog: banned: first: - temp: - one: ":no_entry: **Banned** by {} for {cnt} day because of `{}`" - many: ":no_entry: **Banned** by {} for {cnt} days because of `{}`" + temp: ":no_entry: **Banned** by {} for {} because of `{}`" inf: ":no_entry: **Banned** permanently by {} because of `{}`" upgrade: - temp: - one: ":no_entry: **Ban upgraded** by {} to {cnt} day because of `{}`" - many: ":no_entry: **Ban upgraded** by {} to {cnt} days because of `{}`" + temp: ":no_entry: **Ban upgraded** by {} to {} because of `{}`" inf: ":no_entry: **Ban upgraded** to permanent by {} because of `{}`" unbanned: ":white_check_mark: **Unbanned** by {} because of `{}`" unbanned_expired: ":white_check_mark: **Unbanned** because the ban time has expired." From dadf5f7aeade092ae81acbccde7407caae23f83b Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 21 Jun 2021 23:18:01 +0200 Subject: [PATCH 002/120] Removed unnecessary variable --- moderation/mod/cog.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 7fee766db..22df198cf 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -37,9 +37,6 @@ t = t.mod -days_in_minutes = 60*60*24 - - class DurationConverter(Converter): async def convert(self, ctx, argument: str) -> Optional[int]: if argument.lower() in ("inf", "perm", "permanent", "-1", "∞"): From 5d4df8bf25dd3ca6d5bb59c4d6780d1965206260 Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 21 Jun 2021 23:19:06 +0200 Subject: [PATCH 003/120] Enhanced code style --- moderation/mod/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 5fa08358a..31db12e7e 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -66,7 +66,8 @@ class Mute(db.Base): is_upgrade: Union[Column, bool] = Column(Boolean) @staticmethod - async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, is_upgrade: bool = False) -> Mute: + async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, + is_upgrade: bool = False) -> Mute: row = Mute( member=member, member_name=member_name, @@ -133,7 +134,8 @@ class Ban(db.Base): is_upgrade: Union[Column, bool] = Column(Boolean) @staticmethod - async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, is_upgrade: bool = False) -> Ban: + async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, + is_upgrade: bool = False) -> Ban: row = Ban( member=member, member_name=member_name, From 5d5682092cff66af54447ec95ef6fdf617b971f6 Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 21 Jun 2021 23:22:49 +0200 Subject: [PATCH 004/120] Added whitespaces around operators --- moderation/mod/cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 22df198cf..a167b890f 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -227,14 +227,14 @@ async def handle_get_user_status_entries(self, user_id: int) -> list[tuple[str, if (ban := await db.get(Ban, member=user_id, active=True)) is not None: if ban.minutes != -1: expiry_date: datetime = ban.timestamp + timedelta(minutes=ban.minutes) - time_left = time_to_units((expiry_date - datetime.utcnow()).total_seconds()/60 + 1) + time_left = time_to_units((expiry_date - datetime.utcnow()).total_seconds() / 60 + 1) status = t.status_banned_time(time_to_units(ban.minutes), time_left) else: status = t.status_banned elif (mute := await db.get(Mute, member=user_id, active=True)) is not None: if mute.minutes != -1: expiry_date: datetime = mute.timestamp + timedelta(minutes=mute.minutes) - time_left = time_to_units((expiry_date - datetime.utcnow()).total_seconds()/60 + 1) + time_left = time_to_units((expiry_date - datetime.utcnow()).total_seconds() / 60 + 1) status = t.status_muted_time(time_to_units(mute.minutes), time_left) else: status = t.status_muted From 8f036255285bd479c47b8f64f19e73b53b44cc89 Mon Sep 17 00:00:00 2001 From: LoC Date: Tue, 22 Jun 2021 21:36:55 +0200 Subject: [PATCH 005/120] Made reports available on private messages --- moderation/mod/cog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index a167b890f..6f6f12a1e 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -310,7 +310,6 @@ async def on_member_join(self, member: Member): await member.add_roles(mute_role) @commands.command() - @guild_only() async def report(self, ctx: Context, user: UserMemberConverter, *, reason: str): """ report a user From ddf2d4d97c8ed29c85055afa342defd0a4281b9d Mon Sep 17 00:00:00 2001 From: LoC Date: Tue, 22 Jun 2021 22:44:48 +0200 Subject: [PATCH 006/120] Added report, warn, kick, mute and ban db ids to ulog for mods --- information/user_info/cog.py | 7 ++- moderation/mod/cog.py | 97 ++++++++++++++++++++++++------ moderation/mod/translations/en.yml | 52 ++++++++++++---- 3 files changed, 121 insertions(+), 35 deletions(-) diff --git a/information/user_info/cog.py b/information/user_info/cog.py index 205c54a02..7ea36369f 100644 --- a/information/user_info/cog.py +++ b/information/user_info/cog.py @@ -160,7 +160,7 @@ async def userinfo(self, ctx: Context, user: Optional[Union[User, int]] = None): @commands.command(aliases=["userlog", "ulog"]) @optional_permissions(UserInfoPermission.view_userlog) - async def userlogs(self, ctx: Context, user: Optional[Union[User, int]] = None): + async def userlogs(self, ctx: Context, user: Optional[Union[User, int]] = None, show_ids: bool = False): """ show moderation log of a user """ @@ -169,6 +169,9 @@ async def userlogs(self, ctx: Context, user: Optional[Union[User, int]] = None): user, user_id, arg_passed = await get_user(ctx, user, UserInfoPermission.view_userlog) + if show_ids and not await UserInfoPermission.view_userlog.check_permissions(ctx.author): + raise CommandError(t.not_allowed) + out: list[tuple[datetime, str]] = [(snowflake_time(user_id), t.ulog.created)] join: Join @@ -199,7 +202,7 @@ async def userlogs(self, ctx: Context, user: Optional[Union[User, int]] = None): else: out.append((verification.timestamp, t.ulog.verification.revoked)) - responses = await get_userlog_entries(user_id) + responses = await get_userlog_entries(user_id, show_ids) for response in responses: out += response diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 6f6f12a1e..cb64aab90 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -241,41 +241,74 @@ async def handle_get_user_status_entries(self, user_id: int) -> list[tuple[str, return [(t.active_sanctions, status)] @get_userlog_entries.subscribe - async def handle_get_userlog_entries(self, user_id: int) -> list[tuple[datetime, str]]: + async def handle_get_userlog_entries(self, user_id: int, show_ids: bool) -> list[tuple[datetime, str]]: out: list[tuple[datetime, str]] = [] report: Report async for report in await db.stream(filter_by(Report, member=user_id)): - out.append((report.timestamp, t.ulog.reported(f"<@{report.reporter}>", report.reason))) + if show_ids: + out.append((report.timestamp, t.ulog.reported.id_on(f"<@{report.reporter}>", report.reason, report.id))) + else: + out.append((report.timestamp, t.ulog.reported.id_off(f"<@{report.reporter}>", report.reason))) warn: Warn async for warn in await db.stream(filter_by(Warn, member=user_id)): - out.append((warn.timestamp, t.ulog.warned(f"<@{warn.mod}>", warn.reason))) + if show_ids: + out.append((warn.timestamp, t.ulog.warned.id_on(f"<@{warn.mod}>", warn.reason, warn.id))) + else: + out.append((warn.timestamp, t.ulog.warned.id_off(f"<@{warn.mod}>", warn.reason))) mute: Mute async for mute in await db.stream(filter_by(Mute, member=user_id)): text = t.ulog.muted.upgrade if mute.is_upgrade else t.ulog.muted.first if mute.minutes == -1: - out.append((mute.timestamp, text.inf(f"<@{mute.mod}>", mute.reason))) + if show_ids: + out.append((mute.timestamp, text.inf.id_on(f"<@{mute.mod}>", mute.reason, mute.id))) + else: + out.append((mute.timestamp, text.inf.id_off(f"<@{mute.mod}>", mute.reason))) else: - out.append((mute.timestamp, text.temp(f"<@{mute.mod}>", time_to_units(mute.minutes), mute.reason))) + if show_ids: + out.append( + ( + mute.timestamp, + text.temp.id_on(f"<@{mute.mod}>", time_to_units(mute.minutes), mute.reason, mute.id) + ) + ) + else: + out.append( + ( + mute.timestamp, + text.temp.id_off(f"<@{mute.mod}>", time_to_units(mute.minutes), mute.reason) + ) + ) if not mute.active and not mute.upgraded: if mute.unmute_mod is None: out.append((mute.deactivation_timestamp, t.ulog.unmuted_expired)) else: - out.append( - ( - mute.deactivation_timestamp, - t.ulog.unmuted(f"<@{mute.unmute_mod}>", mute.unmute_reason), - ), - ) + if show_ids: + out.append( + ( + mute.deactivation_timestamp, + t.ulog.unmuted.id_on(f"<@{mute.unmute_mod}>", mute.unmute_reason, mute.id), + ), + ) + else: + out.append( + ( + mute.deactivation_timestamp, + t.ulog.unmuted.id_off(f"<@{mute.unmute_mod}>", mute.unmute_reason), + ), + ) kick: Kick async for kick in await db.stream(filter_by(Kick, member=user_id)): if kick.mod is not None: - out.append((kick.timestamp, t.ulog.kicked(f"<@{kick.mod}>", kick.reason))) + if show_ids: + out.append((kick.timestamp, t.ulog.kicked.id_on(f"<@{kick.mod}>", kick.reason, kick.id))) + else: + out.append((kick.timestamp, t.ulog.kicked.id_off(f"<@{kick.mod}>", kick.reason))) else: out.append((kick.timestamp, t.ulog.autokicked)) @@ -284,20 +317,44 @@ async def handle_get_userlog_entries(self, user_id: int) -> list[tuple[datetime, text = t.ulog.banned.upgrade if ban.is_upgrade else t.ulog.banned.first if ban.minutes == -1: - out.append((ban.timestamp, text.inf(f"<@{ban.mod}>", ban.reason))) + if show_ids: + out.append((ban.timestamp, text.inf.id_on(f"<@{ban.mod}>", ban.reason, ban.id))) + else: + out.append((ban.timestamp, text.inf.id_off(f"<@{ban.mod}>", ban.reason))) else: - out.append((ban.timestamp, text.temp(f"<@{ban.mod}>", time_to_units(ban.minutes), ban.reason))) + if show_ids: + out.append( + ( + ban.timestamp, + text.temp.id_on(f"<@{ban.mod}>", time_to_units(ban.minutes), ban.reason, ban.id) + ) + ) + else: + out.append( + ( + ban.timestamp, + text.temp.id_off(f"<@{ban.mod}>", time_to_units(ban.minutes), ban.reason) + ) + ) if not ban.active and not ban.upgraded: if ban.unban_mod is None: out.append((ban.deactivation_timestamp, t.ulog.unbanned_expired)) else: - out.append( - ( - ban.deactivation_timestamp, - t.ulog.unbanned(f"<@{ban.unban_mod}>", ban.unban_reason), - ), - ) + if show_ids: + out.append( + ( + ban.deactivation_timestamp, + t.ulog.unbanned.id_on(f"<@{ban.unban_mod}>", ban.unban_reason, ban.id), + ), + ) + else: + out.append( + ( + ban.deactivation_timestamp, + t.ulog.unbanned.id_off(f"<@{ban.unban_mod}>", ban.unban_reason), + ), + ) return out diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index e754442aa..159efe300 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -103,28 +103,54 @@ status_muted: ":mute: User is muted on this server." status_muted_time: ":mute: User is muted for {} on this server ({} left)." ulog: - reported: ":speech_balloon: **Reported** by {} because of `{}`" - warned: ":warning: **Warned** by {} because of `{}`" + reported: + id_off: ":speech_balloon: **Reported** by {} because of `{}`" + id_on: ":speech_balloon: **Reported** by {} because of `{}`\n`(ID: {})`" + warned: + id_off: ":warning: **Warned** by {} because of `{}`" + id_on: ":warning: **Warned** by {} because of `{}`\n`(ID: {})`" muted: first: - temp: ":mute: **Muted** by {} for {} because of `{}`" - inf: ":mute: **Muted** permanently by {} because of `{}`" + temp: + id_off: ":mute: **Muted** by {} for {} because of `{}`" + id_on: ":mute: **Muted** by {} for {} because of `{}`\n`(ID: {})`" + inf: + id_off: ":mute: **Muted** permanently by {} because of `{}`" + id_on: ":mute: **Muted** permanently by {} because of `{}`\n`(ID: {})`" upgrade: - temp: ":mute: **Mute upgraded** by {} to {} because of `{}`" - inf: ":mute: **Mute upgraded** to permanent by {} because of `{}`" - unmuted: ":loud_sound: **Unmuted** by {} because of `{}`" + temp: + id_off: ":mute: **Mute upgraded** by {} to {} because of `{}`" + id_on: ":mute: **Mute upgraded** by {} to {} because of `{}`\n`(ID: {})`" + inf: + id_off: ":mute: **Mute upgraded** to permanent by {} because of `{}`" + id_on: ":mute: **Mute upgraded** to permanent by {} because of `{}`\n`(ID: {})`" + unmuted: + id_off: ":loud_sound: **Unmuted** by {} because of `{}`" + id_on: ":loud_sound: **Unmuted** by {} because of `{}`\n`(ID: {})`" unmuted_expired: ":loud_sound: **Unmuted** because the mute time has expired." - kicked: ":x: **Kicked** by {} because of `{}`" + kicked: + id_off: ":x: **Kicked** by {} because of `{}`" + id_on: ":x: **Kicked** by {} because of `{}`\n`(ID: {})`" autokicked: ":hourglass: **Kicked** automatically." banned: first: - temp: ":no_entry: **Banned** by {} for {} because of `{}`" - inf: ":no_entry: **Banned** permanently by {} because of `{}`" + temp: + id_off: ":no_entry: **Banned** by {} for {} because of `{}`" + id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`" + inf: + id_off: ":no_entry: **Banned** permanently by {} because of `{}`" + id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`" upgrade: - temp: ":no_entry: **Ban upgraded** by {} to {} because of `{}`" - inf: ":no_entry: **Ban upgraded** to permanent by {} because of `{}`" - unbanned: ":white_check_mark: **Unbanned** by {} because of `{}`" + temp: + id_off: ":no_entry: **Ban upgraded** by {} to {} because of `{}`" + id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`" + inf: + id_off: ":no_entry: **Ban upgraded** to permanent by {} because of `{}`" + id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`" + unbanned: + id_off: ":white_check_mark: **Unbanned** by {} because of `{}`" + id_on: ":white_check_mark: **Unbanned** by {} because of `{}`\n`(ID: {})`" unbanned_expired: ":white_check_mark: **Unbanned** because the ban time has expired." From 19ee12ac70168f26b7641bd48e3c6a4b041c5d99 Mon Sep 17 00:00:00 2001 From: LoC Date: Tue, 22 Jun 2021 23:05:51 +0200 Subject: [PATCH 007/120] Fixed giving too many arguments to pubsub subscription --- information/user_info/cog.py | 2 +- moderation/invites/cog.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/information/user_info/cog.py b/information/user_info/cog.py index 7ea36369f..253d488d0 100644 --- a/information/user_info/cog.py +++ b/information/user_info/cog.py @@ -160,7 +160,7 @@ async def userinfo(self, ctx: Context, user: Optional[Union[User, int]] = None): @commands.command(aliases=["userlog", "ulog"]) @optional_permissions(UserInfoPermission.view_userlog) - async def userlogs(self, ctx: Context, user: Optional[Union[User, int]] = None, show_ids: bool = False): + async def userlogs(self, ctx: Context, user: Optional[Union[User, int]] = None, show_ids: Optional[bool] = False): """ show moderation log of a user """ diff --git a/moderation/invites/cog.py b/moderation/invites/cog.py index 9ce3e6c05..936d935bf 100644 --- a/moderation/invites/cog.py +++ b/moderation/invites/cog.py @@ -76,7 +76,7 @@ class InvitesCog(Cog, name="Allowed Discord Invites"): CONTRIBUTORS = [Contributor.Defelo, Contributor.wolflu, Contributor.TNT2k, Contributor.Florian] @get_userlog_entries.subscribe - async def handle_get_ulog_entries(self, user_id: int): + async def handle_get_ulog_entries(self, user_id: int, _): out = [] async for log in await db.stream(filter_by(InviteLog, applicant=user_id)): # type: InviteLog if log.approved: From ce407fe8243094c678db4cd03a3f1ce19104f486 Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 23 Jun 2021 12:37:15 +0200 Subject: [PATCH 008/120] Added function to attach an evidence image to reports --- moderation/mod/cog.py | 14 ++++++++++++-- moderation/mod/translations/en.yml | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index cb64aab90..bae6281e6 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -3,7 +3,7 @@ from dateutil.relativedelta import relativedelta from typing import Optional, Union, List, Tuple, Generator -from discord import Role, Guild, Member, Forbidden, HTTPException, User, Embed, NotFound, Message +from discord import Role, Guild, Member, Forbidden, HTTPException, User, Embed, NotFound, Message, Attachment from discord.ext import commands, tasks from discord.ext.commands import ( guild_only, @@ -100,6 +100,7 @@ async def send_to_changelog_mod( reason: str, *, duration: Optional[str] = None, + evidence: Optional[Attachment] = None, ): embed = Embed(title=title, colour=colour, timestamp=datetime.utcnow()) @@ -125,6 +126,9 @@ async def send_to_changelog_mod( if duration: embed.add_field(name=t.log_field.duration, value=duration, inline=True) + if evidence: + embed.add_field(name=t.log_field.evidence, value=t.image_link(evidence.filename, evidence.url), inline=True) + embed.add_field(name=t.log_field.reason, value=reason, inline=False) await send_to_changelog(guild, embed) @@ -382,11 +386,17 @@ async def report(self, ctx: Context, user: UserMemberConverter, *, reason: str): if user == ctx.author: raise UserCommandError(user, t.no_self_report) + if attachments := ctx.message.attachments: + evidence = attachments[0] + else: + evidence = None + await Report.create(user.id, str(user), ctx.author.id, reason) server_embed = Embed(title=t.report, description=t.reported_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) await reply(ctx, embed=server_embed) - await send_to_changelog_mod(ctx.guild, ctx.message, Colors.report, t.log_reported, user, reason) + await send_to_changelog_mod(ctx.guild, + ctx.message, Colors.report, t.log_reported, user, reason, evidence=evidence) @commands.command() @ModPermission.warn.check diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 159efe300..f407c3716 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -27,12 +27,12 @@ log_field: channel: ":compass: Channel" duration: ":clock2: Duration" reason: ":scroll: Reason" - days: - one: "{cnt} day" - many: "{cnt} days" infinity: "*Permanent*" + evidence: ":file_cabinet: Evidence image" + jump_url: "{} [[Jump]]({})" +image_link: "{} [[Go to image]]({})" no_dm: ":warning: User does not allow direct messages." reason_too_long: "Reason is too long. :pencil:" invalid_duration_inf: Invalid duration. Try `inf` instead. From aba7d8ce6ecd587c5e0fa599382c0cebd419756e Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 23 Jun 2021 12:48:22 +0200 Subject: [PATCH 009/120] Added evidence image to database on reports --- moderation/mod/cog.py | 2 +- moderation/mod/models.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index bae6281e6..d021542e7 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -391,7 +391,7 @@ async def report(self, ctx: Context, user: UserMemberConverter, *, reason: str): else: evidence = None - await Report.create(user.id, str(user), ctx.author.id, reason) + await Report.create(user.id, str(user), ctx.author.id, reason, evidence.url) server_embed = Embed(title=t.report, description=t.reported_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) await reply(ctx, embed=server_embed) diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 31db12e7e..00609ec5f 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -17,15 +17,17 @@ class Report(db.Base): reporter: Union[Column, int] = Column(BigInteger) timestamp: Union[Column, datetime] = Column(DateTime) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) + evidence: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) @staticmethod - async def create(member: int, member_name: str, reporter: int, reason: str) -> Report: + async def create(member: int, member_name: str, reporter: int, reason: str, evidence: Optional[str]) -> Report: row = Report( member=member, member_name=member_name, reporter=reporter, timestamp=datetime.utcnow(), reason=reason, + evidence=evidence, ) await db.add(row) return row From e65030a923ff4a1e06290b9beaa9a73c1289228a Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 23 Jun 2021 21:22:53 +0200 Subject: [PATCH 010/120] Added function to attach an evidence image to warns --- moderation/mod/cog.py | 17 ++++++++++++++--- moderation/mod/models.py | 11 +++++++++-- moderation/mod/translations/en.yml | 4 +++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index d021542e7..8c2d9442c 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -414,11 +414,22 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): if user == self.bot.user: raise UserCommandError(user, t.cannot_warn) + if attachments := ctx.message.attachments: + evidence = attachments[0] + else: + evidence = None + user_embed = Embed( title=t.warn, - description=t.warned(ctx.author.mention, ctx.guild.name, reason), colour=Colors.ModTools, ) + if evidence: + user_embed.description = t.warned.evidence(ctx.author.mention, ctx.guild.name, reason, + t.image_link(evidence.filename, evidence.url) + ) + else: + user_embed.description = t.warned.no_evidence(ctx.author.mention, ctx.guild.name, reason) + server_embed = Embed(title=t.warn, description=t.warned_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) try: @@ -426,9 +437,9 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): except (Forbidden, HTTPException): server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error - await Warn.create(user.id, str(user), ctx.author.id, reason) + await Warn.create(user.id, str(user), ctx.author.id, reason, evidence.url) await reply(ctx, embed=server_embed) - await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warned, user, reason) + await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warned, user, reason, evidence=evidence) @commands.command() @ModPermission.mute.check diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 00609ec5f..f9b37296d 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -42,10 +42,17 @@ class Warn(db.Base): mod: Union[Column, int] = Column(BigInteger) timestamp: Union[Column, datetime] = Column(DateTime) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) + evidence: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) @staticmethod - async def create(member: int, member_name: str, mod: int, reason: str) -> Warn: - row = Warn(member=member, member_name=member_name, mod=mod, timestamp=datetime.utcnow(), reason=reason) + async def create(member: int, member_name: str, mod: int, reason: str, evidence: Optional[str]) -> Warn: + row = Warn(member=member, + member_name=member_name, + mod=mod, + timestamp=datetime.utcnow(), + reason=reason, + evidence=evidence, + ) await db.add(row) return row diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index f407c3716..773f80f27 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -45,7 +45,9 @@ log_reported: ":speech_balloon: Member reported" warn: ":warning: Warn" cannot_warn: This member cannot be warned. -warned: "{} just warned you on {}:```\n{}\n```" +warned: + evidence: "{} just warned you on {}:```\n{}\n```Evidence: {}" + no_evidence: "{} just warned you on {}:```\n{}\n```" warned_response: "User warned successfully :white_check_mark:" log_warned: ":warning: Member warned" From 79f2cd1860a1d5f0003bd6e45bade628b5d1cb8c Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 23 Jun 2021 21:45:17 +0200 Subject: [PATCH 011/120] Added function to attach an evidence image to mutes --- moderation/mod/cog.py | 39 +++++++++++++++++++++++++----- moderation/mod/models.py | 4 ++- moderation/mod/translations/en.yml | 8 ++++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 8c2d9442c..de6060916 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -388,10 +388,12 @@ async def report(self, ctx: Context, user: UserMemberConverter, *, reason: str): if attachments := ctx.message.attachments: evidence = attachments[0] + evidence_url = evidence.url else: evidence = None + evidence_url = None - await Report.create(user.id, str(user), ctx.author.id, reason, evidence.url) + await Report.create(user.id, str(user), ctx.author.id, reason, evidence_url) server_embed = Embed(title=t.report, description=t.reported_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) await reply(ctx, embed=server_embed) @@ -416,8 +418,10 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): if attachments := ctx.message.attachments: evidence = attachments[0] + evidence_url = evidence.url else: evidence = None + evidence_url = None user_embed = Embed( title=t.warn, @@ -425,7 +429,7 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): ) if evidence: user_embed.description = t.warned.evidence(ctx.author.mention, ctx.guild.name, reason, - t.image_link(evidence.filename, evidence.url) + t.image_link(evidence.filename, evidence_url), ) else: user_embed.description = t.warned.no_evidence(ctx.author.mention, ctx.guild.name, reason) @@ -477,6 +481,13 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv if minutes is not None and datetime.utcnow() + timedelta(minutes=minutes) <= ts: raise UserCommandError(user, t.already_muted) + if attachments := ctx.message.attachments: + evidence = attachments[0] + evidence_url = evidence.url + else: + evidence = None + evidence_url = None + for mute in active_mutes: await Mute.upgrade(mute.id, ctx.author.id) @@ -485,8 +496,16 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv server_embed.set_author(name=str(user), icon_url=user.avatar_url) if minutes is not None: - await Mute.create(user.id, str(user), ctx.author.id, minutes, reason, bool(active_mutes)) - user_embed.description = t.muted(ctx.author.mention, ctx.guild.name, time_to_units(minutes), reason) + await Mute.create(user.id, str(user), ctx.author.id, minutes, reason, evidence_url, bool(active_mutes)) + if evidence: + user_embed.description = t.muted.evidence(ctx.author.mention, ctx.guild.name, time_to_units(minutes), + reason, t.image_link(evidence.filename, evidence_url), + ) + else: + user_embed.description = t.muted.no_evidence(ctx.author.mention, ctx.guild.name, time_to_units(minutes), + reason, + ) + await send_to_changelog_mod( ctx.guild, ctx.message, @@ -495,10 +514,17 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv user, reason, duration=time_to_units(minutes), + evidence=evidence, ) else: - await Mute.create(user.id, str(user), ctx.author.id, -1, reason, bool(active_mutes)) - user_embed.description = t.muted_inf(ctx.author.mention, ctx.guild.name, reason) + await Mute.create(user.id, str(user), ctx.author.id, -1, reason, evidence_url, bool(active_mutes)) + if evidence: + user_embed.description = t.muted_inf.evidence(ctx.author.mention, ctx.guild.name, reason, + t.image_link(evidence.filename, evidence_url), + ) + else: + user_embed.description = t.muted_inf.no_evidence(ctx.author.mention, ctx.guild.name, reason) + await send_to_changelog_mod( ctx.guild, ctx.message, @@ -507,6 +533,7 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv user, reason, duration=t.log_field.infinity, + evidence=evidence, ) try: diff --git a/moderation/mod/models.py b/moderation/mod/models.py index f9b37296d..b055729cb 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -67,6 +67,7 @@ class Mute(db.Base): timestamp: Union[Column, datetime] = Column(DateTime) minutes: Union[Column, int] = Column(Integer) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) + evidence: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) active: Union[Column, bool] = Column(Boolean) deactivation_timestamp: Union[Column, Optional[datetime]] = Column(DateTime, nullable=True) unmute_mod: Union[Column, Optional[int]] = Column(BigInteger, nullable=True) @@ -76,7 +77,7 @@ class Mute(db.Base): @staticmethod async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, - is_upgrade: bool = False) -> Mute: + evidence: Optional[str], is_upgrade: bool = False) -> Mute: row = Mute( member=member, member_name=member_name, @@ -84,6 +85,7 @@ async def create(member: int, member_name: str, mod: int, minutes: int, reason: timestamp=datetime.utcnow(), minutes=minutes, reason=reason, + evidence=evidence, active=True, deactivation_timestamp=None, unmute_mod=None, diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 773f80f27..325964e1a 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -56,8 +56,12 @@ unmute: ":loud_sound: Mute" mute_role_not_set: User could not be muted because no mute role is configured. cannot_mute: This member cannot be muted. already_muted: User is already muted for the given period of time. -muted: "{} just muted you on {} for {}:```\n{}\n```" -muted_inf: "{} just muted you on {}:```\n{}\n```" +muted: + evidence: "{} just muted you on {} for {}:```\n{}\n```Evidence: {}" + no_evidence: "{} just muted you on {} for {}:```\n{}\n```" +muted_inf: + evidence: "{} just muted you on {}:```\n{}\n```Evidence: {}" + no_evidence: "{} just muted you on {}:```\n{}\n```" muted_response: "User muted successfully :white_check_mark:" log_muted: ":mute: Member muted" log_unmuted_expired: "Unmuted automatically because the mute time for this user has expired." From e4415ddc409f1a6a3ed1f9571f9d8006d4fb94ec Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 23 Jun 2021 21:57:39 +0200 Subject: [PATCH 012/120] Added function to attach an evidence image to kicks --- moderation/mod/cog.py | 23 +++++++++++++++++++---- moderation/mod/models.py | 12 ++++++++++-- moderation/mod/translations/en.yml | 4 +++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index de6060916..fe1805777 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -595,14 +595,29 @@ async def kick(self, ctx: Context, member: Member, *, reason: str): if member.top_role >= ctx.guild.me.top_role or member.id == ctx.guild.owner_id: raise UserCommandError(member, t.cannot_kick) - await Kick.create(member.id, str(member), ctx.author.id, reason) - await send_to_changelog_mod(ctx.guild, ctx.message, Colors.kick, t.log_kicked, member, reason) + if attachments := ctx.message.attachments: + evidence = attachments[0] + evidence_url = evidence.url + else: + evidence = None + evidence_url = None + + await Kick.create(member.id, str(member), ctx.author.id, reason, evidence_url) + await send_to_changelog_mod(ctx.guild, ctx.message, Colors.kick, t.log_kicked, member, reason, + evidence=evidence) user_embed = Embed( title=t.kick, - description=t.kicked(ctx.author.mention, ctx.guild.name, reason), colour=Colors.ModTools, ) + + if evidence: + user_embed.description = t.kicked.evidence(ctx.author.mention, ctx.guild.name, reason, + t.image_link(evidence.filename, evidence_url), + ) + else: + user_embed.description = t.kicked.no_evidence(ctx.author.mention, ctx.guild.name, reason) + server_embed = Embed(title=t.kick, description=t.kicked_response, colour=Colors.ModTools) server_embed.set_author( name=str(member), @@ -615,7 +630,7 @@ async def kick(self, ctx: Context, member: Member, *, reason: str): server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error - await member.kick(reason=reason) + # await member.kick(reason=reason) await revoke_verification(member) await reply(ctx, embed=server_embed) diff --git a/moderation/mod/models.py b/moderation/mod/models.py index b055729cb..22170833c 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -119,10 +119,18 @@ class Kick(db.Base): mod: Union[Column, int] = Column(BigInteger) timestamp: Union[Column, datetime] = Column(DateTime) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) + evidence: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) @staticmethod - async def create(member: int, member_name: str, mod: Optional[int], reason: Optional[str]) -> Kick: - row = Kick(member=member, member_name=member_name, mod=mod, timestamp=datetime.utcnow(), reason=reason) + async def create(member: int, member_name: str, mod: Optional[int], reason: Optional[str], + evidence: Optional[str]) -> Kick: + row = Kick(member=member, + member_name=member_name, + mod=mod, + timestamp=datetime.utcnow(), + reason=reason, + evidence=evidence, + ) await db.add(row) return row diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 325964e1a..05a023f90 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -70,7 +70,9 @@ log_unmuted: ":loud_sound: Member unmuted" not_muted: User is not muted. kick: ":x: Kick" -kicked: "{} just kicked you from {}:```\n{}\n```" +kicked: + evidence: "{} just kicked you from {}:```\n{}\n```Evidence: {}" + no_evidence: "{} just kicked you from {}:```\n{}\n```" kicked_response: "User kicked successfully :white_check_mark:" log_kicked: ":x: Member kicked" cannot_kick: This member cannot be kicked. From 15c0c2a58beca16621416409608e8856b062905b Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 23 Jun 2021 22:17:33 +0200 Subject: [PATCH 013/120] Added function to attach an evidence image to bans --- moderation/mod/cog.py | 32 +++++++++++++++++++++++++----- moderation/mod/models.py | 4 +++- moderation/mod/translations/en.yml | 8 ++++++-- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index fe1805777..77917813b 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -630,7 +630,7 @@ async def kick(self, ctx: Context, member: Member, *, reason: str): server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error - # await member.kick(reason=reason) + await member.kick(reason=reason) await revoke_verification(member) await reply(ctx, embed=server_embed) @@ -685,13 +685,27 @@ async def ban( async for mute in await db.stream(filter_by(Mute, active=True, member=user.id)): await Mute.upgrade(mute.id, ctx.author.id) + if attachments := ctx.message.attachments: + evidence = attachments[0] + evidence_url = evidence.url + else: + evidence = None + evidence_url = None + user_embed = Embed(title=t.ban, colour=Colors.ModTools) server_embed = Embed(title=t.ban, description=t.banned_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) if minutes is not None: - await Ban.create(user.id, str(user), ctx.author.id, minutes, reason, bool(active_bans)) - user_embed.description = t.banned(ctx.author.mention, ctx.guild.name, time_to_units(minutes), reason) + await Ban.create(user.id, str(user), ctx.author.id, minutes, reason, evidence_url, bool(active_bans)) + if evidence: + user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, time_to_units(minutes), + reason, t.image_link(evidence.filename, evidence_url) + ) + else: + user_embed.description = t.banned.no_evidence(ctx.author.mention, ctx.guild.name, + time_to_units(minutes), reason + ) await send_to_changelog_mod( ctx.guild, ctx.message, @@ -700,10 +714,17 @@ async def ban( user, reason, duration=time_to_units(minutes), + evidence=evidence, ) else: - await Ban.create(user.id, str(user), ctx.author.id, -1, reason, bool(active_bans)) - user_embed.description = t.banned_inf(ctx.author.mention, ctx.guild.name, reason) + await Ban.create(user.id, str(user), ctx.author.id, -1, reason, evidence_url, bool(active_bans)) + if evidence: + user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, reason, + t.image_link(evidence.filename, evidence_url) + ) + else: + user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, reason) + await send_to_changelog_mod( ctx.guild, ctx.message, @@ -712,6 +733,7 @@ async def ban( user, reason, duration=t.log_field.infinity, + evidence=evidence ) try: diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 22170833c..97f2a9a19 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -145,6 +145,7 @@ class Ban(db.Base): timestamp: Union[Column, datetime] = Column(DateTime) minutes: Union[Column, int] = Column(Integer) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) + evidence: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) active: Union[Column, bool] = Column(Boolean) deactivation_timestamp: Union[Column, Optional[datetime]] = Column(DateTime, nullable=True) unban_reason: Union[Column, Optional[str]] = Column(Text(collation="utf8mb4_bin"), nullable=True) @@ -154,7 +155,7 @@ class Ban(db.Base): @staticmethod async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, - is_upgrade: bool = False) -> Ban: + evidence: Optional[str], is_upgrade: bool = False) -> Ban: row = Ban( member=member, member_name=member_name, @@ -162,6 +163,7 @@ async def create(member: int, member_name: str, mod: int, minutes: int, reason: timestamp=datetime.utcnow(), minutes=minutes, reason=reason, + evidence=evidence, active=True, deactivation_timestamp=None, unban_reason=None, diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 05a023f90..88635a24d 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -83,8 +83,12 @@ unban: ":white_check_mark: Ban" cannot_ban: This member cannot be banned. already_banned: This member is already banned for the given period of time. cannot_ban_permissions: User could not be banned because I don't have `ban_members` permission on this server. -banned: "{} just banned you from {} for {}:```\n{}\n```" -banned_inf: "{} just banned you from {}:```\n{}\n```" +banned: + evidence: "{} just banned you from {} for {}:```\n{}\n```Evidence: {}" + no_evidence: "{} just banned you from {} for {}:```\n{}\n```" +banned_inf: + evidence: "{} just banned you from {}:```\n{}\n```Evidence: {}" + no_evidence: "{} just banned you from {}:```\n{}\n```" banned_response: "User banned successfully :white_check_mark:" log_banned: ":no_entry: Member Banned" log_unbanned_expired: "Unbanned automatically because the ban time for this user has expired." From fdd45a0d1402cf499df6593c58fd71dc502a212c Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 23 Jun 2021 22:53:40 +0200 Subject: [PATCH 014/120] Added evidence images to user log --- moderation/mod/cog.py | 103 ++++++++++++++++++++++++----- moderation/mod/translations/en.yml | 30 +++++---- 2 files changed, 103 insertions(+), 30 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 77917813b..16c5af21c 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -91,6 +91,12 @@ async def get_mute_role(guild: Guild) -> Role: return mute_role +def show_evidence(evidence: Optional[str]) -> str: + if evidence: + return t.ulog.evidence(evidence) + return "" + + async def send_to_changelog_mod( guild: Guild, message: Optional[Message], @@ -251,39 +257,77 @@ async def handle_get_userlog_entries(self, user_id: int, show_ids: bool) -> list report: Report async for report in await db.stream(filter_by(Report, member=user_id)): if show_ids: - out.append((report.timestamp, t.ulog.reported.id_on(f"<@{report.reporter}>", report.reason, report.id))) + out.append( + (report.timestamp, + t.ulog.reported.id_on(f"<@{report.reporter}>", report.reason, report.id, + show_evidence(report.evidence) + ) + ) + ) else: - out.append((report.timestamp, t.ulog.reported.id_off(f"<@{report.reporter}>", report.reason))) + out.append( + (report.timestamp, + t.ulog.reported.id_off(f"<@{report.reporter}>", report.reason, show_evidence(report.evidence)) + ) + ) warn: Warn async for warn in await db.stream(filter_by(Warn, member=user_id)): if show_ids: - out.append((warn.timestamp, t.ulog.warned.id_on(f"<@{warn.mod}>", warn.reason, warn.id))) + out.append( + ( + warn.timestamp, + t.ulog.warned.id_on(f"<@{warn.mod}>", warn.reason, warn.id, show_evidence(warn.evidence)) + ) + ) else: - out.append((warn.timestamp, t.ulog.warned.id_off(f"<@{warn.mod}>", warn.reason))) + out.append( + ( + warn.timestamp, + t.ulog.warned.id_off(f"<@{warn.mod}>", warn.reason, show_evidence(warn.evidence)) + ) + ) mute: Mute async for mute in await db.stream(filter_by(Mute, member=user_id)): - text = t.ulog.muted.upgrade if mute.is_upgrade else t.ulog.muted.first + if mute.is_upgrade: + text = t.ulog.muted.upgrade + mute.evidence = None + else: + text = t.ulog.muted.first if mute.minutes == -1: if show_ids: - out.append((mute.timestamp, text.inf.id_on(f"<@{mute.mod}>", mute.reason, mute.id))) + out.append( + ( + mute.timestamp, + text.inf.id_on(f"<@{mute.mod}>", mute.reason, mute.id, show_evidence(mute.evidence)) + ) + ) else: - out.append((mute.timestamp, text.inf.id_off(f"<@{mute.mod}>", mute.reason))) + out.append( + ( + mute.timestamp, + text.inf.id_off(f"<@{mute.mod}>", mute.reason, show_evidence(mute.evidence)) + ) + ) else: if show_ids: out.append( ( mute.timestamp, - text.temp.id_on(f"<@{mute.mod}>", time_to_units(mute.minutes), mute.reason, mute.id) + text.temp.id_on(f"<@{mute.mod}>", time_to_units(mute.minutes), mute.reason, mute.id, + show_evidence(mute.evidence) + ) ) ) else: out.append( ( mute.timestamp, - text.temp.id_off(f"<@{mute.mod}>", time_to_units(mute.minutes), mute.reason) + text.temp.id_off(f"<@{mute.mod}>", time_to_units(mute.minutes), mute.reason, + show_evidence(mute.evidence) + ) ) ) @@ -310,34 +354,61 @@ async def handle_get_userlog_entries(self, user_id: int, show_ids: bool) -> list async for kick in await db.stream(filter_by(Kick, member=user_id)): if kick.mod is not None: if show_ids: - out.append((kick.timestamp, t.ulog.kicked.id_on(f"<@{kick.mod}>", kick.reason, kick.id))) + out.append( + ( + kick.timestamp, + t.ulog.kicked.id_on(f"<@{kick.mod}>", kick.reason, kick.id, show_evidence(kick.evidence)) + ) + ) else: - out.append((kick.timestamp, t.ulog.kicked.id_off(f"<@{kick.mod}>", kick.reason))) + out.append( + ( + kick.timestamp, + t.ulog.kicked.id_off(f"<@{kick.mod}>", kick.reason, show_evidence(kick.evidence)) + ) + ) else: out.append((kick.timestamp, t.ulog.autokicked)) ban: Ban async for ban in await db.stream(filter_by(Ban, member=user_id)): - text = t.ulog.banned.upgrade if ban.is_upgrade else t.ulog.banned.first + if ban.is_upgrade: + text = t.ulog.banned.upgrade + ban.evidence = None + else: + text = t.ulog.banned.first if ban.minutes == -1: if show_ids: - out.append((ban.timestamp, text.inf.id_on(f"<@{ban.mod}>", ban.reason, ban.id))) + out.append( + ( + ban.timestamp, + text.inf.id_on(f"<@{ban.mod}>", ban.reason, ban.id, show_evidence(ban.evidence)) + ) + ) else: - out.append((ban.timestamp, text.inf.id_off(f"<@{ban.mod}>", ban.reason))) + out.append( + ( + ban.timestamp, text.inf.id_off(f"<@{ban.mod}>", ban.reason) + ) + ) else: if show_ids: out.append( ( ban.timestamp, - text.temp.id_on(f"<@{ban.mod}>", time_to_units(ban.minutes), ban.reason, ban.id) + text.temp.id_on(f"<@{ban.mod}>", time_to_units(ban.minutes), ban.reason, ban.id, + show_evidence(ban.evidence) + ) ) ) else: out.append( ( ban.timestamp, - text.temp.id_off(f"<@{ban.mod}>", time_to_units(ban.minutes), ban.reason) + text.temp.id_off(f"<@{ban.mod}>", time_to_units(ban.minutes), ban.reason, + show_evidence(ban.evidence) + ) ) ) diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 88635a24d..ff0d4fda7 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -115,21 +115,23 @@ status_muted: ":mute: User is muted on this server." status_muted_time: ":mute: User is muted for {} on this server ({} left)." ulog: + evidence: "\n[[Evidence]]({})" + reported: - id_off: ":speech_balloon: **Reported** by {} because of `{}`" - id_on: ":speech_balloon: **Reported** by {} because of `{}`\n`(ID: {})`" + id_off: ":speech_balloon: **Reported** by {} because of `{}`{}" + id_on: ":speech_balloon: **Reported** by {} because of `{}`\n`(ID: {})`{}" warned: - id_off: ":warning: **Warned** by {} because of `{}`" - id_on: ":warning: **Warned** by {} because of `{}`\n`(ID: {})`" + id_off: ":warning: **Warned** by {} because of `{}`{}" + id_on: ":warning: **Warned** by {} because of `{}`\n`(ID: {})`{}" muted: first: temp: - id_off: ":mute: **Muted** by {} for {} because of `{}`" - id_on: ":mute: **Muted** by {} for {} because of `{}`\n`(ID: {})`" + id_off: ":mute: **Muted** by {} for {} because of `{}`{}" + id_on: ":mute: **Muted** by {} for {} because of `{}`\n`(ID: {})`{}" inf: - id_off: ":mute: **Muted** permanently by {} because of `{}`" - id_on: ":mute: **Muted** permanently by {} because of `{}`\n`(ID: {})`" + id_off: ":mute: **Muted** permanently by {} because of `{}`{}" + id_on: ":mute: **Muted** permanently by {} because of `{}`\n`(ID: {})`{}" upgrade: temp: id_off: ":mute: **Mute upgraded** by {} to {} because of `{}`" @@ -143,18 +145,18 @@ ulog: unmuted_expired: ":loud_sound: **Unmuted** because the mute time has expired." kicked: - id_off: ":x: **Kicked** by {} because of `{}`" - id_on: ":x: **Kicked** by {} because of `{}`\n`(ID: {})`" + id_off: ":x: **Kicked** by {} because of `{}`{}" + id_on: ":x: **Kicked** by {} because of `{}`\n`(ID: {})`{}" autokicked: ":hourglass: **Kicked** automatically." banned: first: temp: - id_off: ":no_entry: **Banned** by {} for {} because of `{}`" - id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`" + id_off: ":no_entry: **Banned** by {} for {} because of `{}`{}" + id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`{}" inf: - id_off: ":no_entry: **Banned** permanently by {} because of `{}`" - id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`" + id_off: ":no_entry: **Banned** permanently by {} because of `{}`{}" + id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`{}" upgrade: temp: id_off: ":no_entry: **Ban upgraded** by {} to {} because of `{}`" From 9615ecdfbeb9e56179d68558ca03159c73f25b0f Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 5 Jul 2021 22:26:20 +0200 Subject: [PATCH 015/120] Added function to edit warn reasons --- moderation/mod/cog.py | 64 +++++++++++++++++++++++++++++- moderation/mod/models.py | 6 +++ moderation/mod/translations/en.yml | 8 ++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 16c5af21c..c6103544e 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -14,12 +14,13 @@ ) from PyDrocsid.cog import Cog -from PyDrocsid.command import reply, UserCommandError +from PyDrocsid.command import reply, UserCommandError, confirm from PyDrocsid.converter import UserMemberConverter from PyDrocsid.database import db, filter_by, db_wrapper from PyDrocsid.settings import RoleSettings from PyDrocsid.translations import t from PyDrocsid.util import is_teamler +from PyDrocsid.config import Config from .colors import Colors from .models import Mute, Ban, Report, Warn, Kick from .permissions import ModPermission @@ -97,6 +98,16 @@ def show_evidence(evidence: Optional[str]) -> str: return "" +async def compare_mod_level(mod1: Member, mod2: Member) -> bool: + print(mod1) + print(mod2) + + lvl1 = await Config.PERMISSION_LEVELS.get_permission_level(mod1) + lvl2 = await Config.PERMISSION_LEVELS.get_permission_level(mod2) + + return lvl1.level > lvl2.level or mod1 == mod1.guild.owner + + async def send_to_changelog_mod( guild: Guild, message: Optional[Message], @@ -516,6 +527,57 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warned, user, reason, evidence=evidence) + @commands.command() + @ModPermission.warn.check + @guild_only() + async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): + """ + edit a warn + get the warn id from the users user log + """ + + warn = await db.get(Warn, id=warn_id) + if warn is None: + raise CommandError(t.no_warn) + + if not await compare_mod_level(ctx.author, ctx.guild.get_member(warn.mod)): + raise CommandError(tg.permission_denied) + + conf_embed = Embed( + title=t.confirmation, + description=t.confirm_warn_edit(warn.reason, reason), + color=Colors.ModTools + ) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + await Warn.edit(warn_id, ctx.author.id, reason) + + user = self.bot.get_user(warn.member) + + user_embed = Embed( + title=t.warn, + description=t.warn_edited(warn.reason, reason), + colour=Colors.ModTools, + ) + server_embed = Embed(title=t.warn, description=t.warn_edited_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + await reply(ctx, embed=server_embed) + await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warn_edited, user, reason) + @commands.command() @ModPermission.mute.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 97f2a9a19..0d452541f 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -56,6 +56,12 @@ async def create(member: int, member_name: str, mod: int, reason: str, evidence: await db.add(row) return row + @staticmethod + async def edit(warn_id: int, mod: int, new_reason: str): + row = await db.get(Warn, id=warn_id) + row.mod = mod + row.reason = new_reason + class Mute(db.Base): __tablename__ = "mute" diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index ff0d4fda7..3349ce15e 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -36,6 +36,9 @@ image_link: "{} [[Go to image]]({})" no_dm: ":warning: User does not allow direct messages." reason_too_long: "Reason is too long. :pencil:" invalid_duration_inf: Invalid duration. Try `inf` instead. +confirmation: Edit confirmation +edit_canceled: ":x: Edit canceled" +edit_confirmed: ":white_check_mark: Edit confirmed" report: ":speech_balloon: Report" cannot_report: This member cannot be reported. @@ -50,6 +53,11 @@ warned: no_evidence: "{} just warned you on {}:```\n{}\n```" warned_response: "User warned successfully :white_check_mark:" log_warned: ":warning: Member warned" +no_warn: No warn with this ID could be found +confirm_warn_edit: "Are you sure you want to change this warns reason from ```\n{}\n```to```\n{}\n```?" +warn_edited: "The warn reason has been changed from ```\n{}\n```to```\n{}\n```" +warn_edited_response: ":white_check_mark: The warn reason has been edited" +log_warn_edited: ":warning: Warn edited" mute: ":mute: Mute" unmute: ":loud_sound: Mute" From 1a01fa0b70c360752c5bd98943f722b3c15ecc31 Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 5 Jul 2021 22:28:00 +0200 Subject: [PATCH 016/120] Removed forgotten print statements --- moderation/mod/cog.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index c6103544e..451a0b855 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -99,9 +99,6 @@ def show_evidence(evidence: Optional[str]) -> str: async def compare_mod_level(mod1: Member, mod2: Member) -> bool: - print(mod1) - print(mod2) - lvl1 = await Config.PERMISSION_LEVELS.get_permission_level(mod1) lvl2 = await Config.PERMISSION_LEVELS.get_permission_level(mod2) From 8298a342a3738d3a410e261f32eb894e3d14cb93 Mon Sep 17 00:00:00 2001 From: LoC Date: Tue, 6 Jul 2021 11:03:32 +0200 Subject: [PATCH 017/120] Added function to edit mute reasons --- moderation/mod/cog.py | 60 ++++++++++++++++++++++++++++++ moderation/mod/models.py | 10 +++++ moderation/mod/translations/en.yml | 6 +++ 3 files changed, 76 insertions(+) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 451a0b855..aca1e5438 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -11,6 +11,7 @@ CommandError, Converter, BadArgument, + UserInputError, ) from PyDrocsid.cog import Cog @@ -674,6 +675,65 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv await reply(ctx, embed=server_embed) + @commands.group() + @ModPermission.mute.check + @guild_only() + async def edit_mute(self, ctx): + if ctx.invoked_subcommand is None: + raise UserInputError + + @edit_mute.command(name="reason") + async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): + """ + edit a mute + get the mute id from the users user log + """ + + mute = await db.get(Mute, id=mute_id) + if mute is None: + raise CommandError(t.no_warn) + + if not await compare_mod_level(ctx.author, ctx.guild.get_member(mute.mod)): + raise CommandError(tg.permission_denied) + + conf_embed = Embed( + title=t.confirmation, + description=t.confirm_mute_edit.reason(mute.reason, reason), + color=Colors.ModTools + ) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + await Mute.edit(mute_id, ctx.author.id, reason) + + user = self.bot.get_user(mute.member) + + user_embed = Embed( + title=t.warn, + description=t.mute_edited.reason(mute.reason, reason), + colour=Colors.ModTools, + ) + server_embed = Embed(title=t.mute, description=t.mute_edited_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + await reply(ctx, embed=server_embed) + await send_to_changelog_mod(ctx.guild, ctx.message, Colors.mute, t.log_mute_edited, user, reason) + + + + @commands.command() @ModPermission.mute.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 0d452541f..4007840fb 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -115,6 +115,16 @@ async def upgrade(ban_id: int, mod: int): mute = await Mute.deactivate(ban_id, mod) mute.upgraded = True + @staticmethod + async def edit(mute_id: int, mod: int, new_reason: Optional[str] = None, new_duration: Optional[int] = None): + row = await db.get(Mute, id=mute_id) + row.mod = mod + + if new_reason: + row.reason = new_reason + if new_duration: + row.minutes = new_duration + class Kick(db.Base): __tablename__ = "kick" diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 3349ce15e..ead4eadae 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -76,6 +76,12 @@ log_unmuted_expired: "Unmuted automatically because the mute time for this user unmuted_response: "User unmuted successfully :white_check_mark:" log_unmuted: ":loud_sound: Member unmuted" not_muted: User is not muted. +confirm_mute_edit: + reason: "Are you sure you want to change this mutes reason from ```\n{}\n```to```\n{}\n```?" +mute_edited: + reason: "The mute reason has been changed from ```\n{}\n```to```\n{}\n```" +mute_edited_response: ":white_check_mark: The mute reason has been edited" +log_mute_edited: ":mute: Mute edited" kick: ":x: Kick" kicked: From 199b7f51f283bb39366a43184e9df5b6ad75310f Mon Sep 17 00:00:00 2001 From: LoC Date: Tue, 6 Jul 2021 13:32:38 +0200 Subject: [PATCH 018/120] Added function to edit mute durations --- moderation/mod/cog.py | 123 ++++++++++++++++++++++++----- moderation/mod/models.py | 16 ++-- moderation/mod/translations/en.yml | 14 ++-- 3 files changed, 121 insertions(+), 32 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index aca1e5438..ffe255a87 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -299,8 +299,8 @@ async def handle_get_userlog_entries(self, user_id: int, show_ids: bool) -> list mute: Mute async for mute in await db.stream(filter_by(Mute, member=user_id)): - if mute.is_upgrade: - text = t.ulog.muted.upgrade + if mute.is_update: + text = t.ulog.muted.update mute.evidence = None else: text = t.ulog.muted.first @@ -340,7 +340,7 @@ async def handle_get_userlog_entries(self, user_id: int, show_ids: bool) -> list ) ) - if not mute.active and not mute.upgraded: + if not mute.active and not mute.updated: if mute.unmute_mod is None: out.append((mute.deactivation_timestamp, t.ulog.unmuted_expired)) else: @@ -541,6 +541,9 @@ async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): if not await compare_mod_level(ctx.author, ctx.guild.get_member(warn.mod)): raise CommandError(tg.permission_denied) + if len(reason) > 900: + raise CommandError(t.reason_too_long) + conf_embed = Embed( title=t.confirmation, description=t.confirm_warn_edit(warn.reason, reason), @@ -583,7 +586,7 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv """ mute a user time format: `ymwdhn` - set days to `inf` for a permanent mute + set time to `inf` for a permanent mute """ user: Union[Member, User] @@ -604,13 +607,8 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv await user.move_to(None) active_mutes: List[Mute] = await db.all(filter_by(Mute, active=True, member=user.id)) - for mute in active_mutes: - if mute.minutes == -1: - raise UserCommandError(user, t.already_muted) - - ts = mute.timestamp + timedelta(minutes=mute.minutes) - if minutes is not None and datetime.utcnow() + timedelta(minutes=minutes) <= ts: - raise UserCommandError(user, t.already_muted) + if active_mutes: + raise UserCommandError(user, t.already_muted) if attachments := ctx.message.attachments: evidence = attachments[0] @@ -619,15 +617,12 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv evidence = None evidence_url = None - for mute in active_mutes: - await Mute.upgrade(mute.id, ctx.author.id) - user_embed = Embed(title=t.mute, colour=Colors.ModTools) server_embed = Embed(title=t.mute, description=t.muted_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) if minutes is not None: - await Mute.create(user.id, str(user), ctx.author.id, minutes, reason, evidence_url, bool(active_mutes)) + await Mute.create(user.id, str(user), ctx.author.id, minutes, reason, evidence_url) if evidence: user_embed.description = t.muted.evidence(ctx.author.mention, ctx.guild.name, time_to_units(minutes), reason, t.image_link(evidence.filename, evidence_url), @@ -648,7 +643,7 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv evidence=evidence, ) else: - await Mute.create(user.id, str(user), ctx.author.id, -1, reason, evidence_url, bool(active_mutes)) + await Mute.create(user.id, str(user), ctx.author.id, -1, reason, evidence_url) if evidence: user_embed.description = t.muted_inf.evidence(ctx.author.mention, ctx.guild.name, reason, t.image_link(evidence.filename, evidence_url), @@ -679,6 +674,10 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv @ModPermission.mute.check @guild_only() async def edit_mute(self, ctx): + """ + edit a mute + """ + if ctx.invoked_subcommand is None: raise UserInputError @@ -691,11 +690,14 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): mute = await db.get(Mute, id=mute_id) if mute is None: - raise CommandError(t.no_warn) + raise CommandError(t.no_mute) if not await compare_mod_level(ctx.author, ctx.guild.get_member(mute.mod)): raise CommandError(tg.permission_denied) + if len(reason) > 900: + raise CommandError(t.reason_too_long) + conf_embed = Embed( title=t.confirmation, description=t.confirm_mute_edit.reason(mute.reason, reason), @@ -716,7 +718,7 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): user = self.bot.get_user(mute.member) user_embed = Embed( - title=t.warn, + title=t.mute, description=t.mute_edited.reason(mute.reason, reason), colour=Colors.ModTools, ) @@ -731,6 +733,91 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.mute, t.log_mute_edited, user, reason) + @edit_mute.command(name="duration") + async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time: DurationConverter): + """ + edit a mute duration + time format: `ymwdhn` + set time to `inf` for a permanent mute + """ + + user: Union[Member, User] + time: Optional[int] + minutes = time + + active_mutes: List[Mute] = await db.all(filter_by(Mute, active=True, member=user.id)) + + if not active_mutes: + raise CommandError(t.not_muted) + + mute = sorted(active_mutes, key=lambda active_mute: active_mute.timestamp)[0] + + if not await compare_mod_level(ctx.author, ctx.guild.get_member(mute.mod)): + raise CommandError(tg.permission_denied) + + conf_embed = Embed( + title=t.confirmation, + color=Colors.ModTools, + ) + + if minutes is None: + conf_embed.description = t.confirm_mute_edit.duration(time_to_units(mute.minutes), t.infinity) + else: + conf_embed.description = t.confirm_mute_edit.duration(time_to_units(mute.minutes), time_to_units(minutes)) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + for mute in active_mutes: + await Mute.update(mute.id, ctx.author.id) + + user_embed = Embed( + title=t.mute, + colour=Colors.ModTools, + ) + server_embed = Embed(title=t.mute, description=t.mute_edited_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + if minutes is not None: + await Mute.create(user.id, str(user), ctx.author.id, minutes, mute.reason, mute.evidence, True) + user_embed.description = t.mute_edited.duration(time_to_units(mute.minutes), time_to_units(minutes)) + await send_to_changelog_mod( + ctx.guild, + ctx.message, + Colors.mute, + t.log_mute_edited, + user, + mute.reason, + duration=time_to_units(minutes), + ) + + else: + await Mute.create(user.id, str(user), ctx.author.id, -1, mute.reason, mute.evidence, True) + user_embed.description = t.mute_edited.duration(time_to_units(mute.minutes), t.infinity) + await send_to_changelog_mod( + ctx.guild, + ctx.message, + Colors.mute, + t.log_mute_edited, + user, mute.reason, + duration=t.log_field.infinity, + ) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + await reply(ctx, embed=server_embed) + + + diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 4007840fb..6e03e96ad 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -78,12 +78,12 @@ class Mute(db.Base): deactivation_timestamp: Union[Column, Optional[datetime]] = Column(DateTime, nullable=True) unmute_mod: Union[Column, Optional[int]] = Column(BigInteger, nullable=True) unmute_reason: Union[Column, Optional[str]] = Column(Text(collation="utf8mb4_bin"), nullable=True) - upgraded: Union[Column, bool] = Column(Boolean, default=False) - is_upgrade: Union[Column, bool] = Column(Boolean) + updated: Union[Column, bool] = Column(Boolean, default=False) + is_update: Union[Column, bool] = Column(Boolean) @staticmethod async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, - evidence: Optional[str], is_upgrade: bool = False) -> Mute: + evidence: Optional[str], is_update: bool = False) -> Mute: row = Mute( member=member, member_name=member_name, @@ -96,7 +96,7 @@ async def create(member: int, member_name: str, mod: int, minutes: int, reason: deactivation_timestamp=None, unmute_mod=None, unmute_reason=None, - is_upgrade=is_upgrade, + is_update=is_update, ) await db.add(row) return row @@ -111,19 +111,17 @@ async def deactivate(mute_id: int, unmute_mod: int = None, reason: str = None) - return row @staticmethod - async def upgrade(ban_id: int, mod: int): + async def update(ban_id: int, mod: int): mute = await Mute.deactivate(ban_id, mod) - mute.upgraded = True + mute.updated = True @staticmethod - async def edit(mute_id: int, mod: int, new_reason: Optional[str] = None, new_duration: Optional[int] = None): + async def edit(mute_id: int, mod: int, new_reason: str): row = await db.get(Mute, id=mute_id) row.mod = mod if new_reason: row.reason = new_reason - if new_duration: - row.minutes = new_duration class Kick(db.Base): diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index ead4eadae..16f89e129 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -39,6 +39,7 @@ invalid_duration_inf: Invalid duration. Try `inf` instead. confirmation: Edit confirmation edit_canceled: ":x: Edit canceled" edit_confirmed: ":white_check_mark: Edit confirmed" +infinity: Permanent report: ":speech_balloon: Report" cannot_report: This member cannot be reported. @@ -76,10 +77,13 @@ log_unmuted_expired: "Unmuted automatically because the mute time for this user unmuted_response: "User unmuted successfully :white_check_mark:" log_unmuted: ":loud_sound: Member unmuted" not_muted: User is not muted. +no_mute: No mute with this ID could be found confirm_mute_edit: reason: "Are you sure you want to change this mutes reason from ```\n{}\n```to```\n{}\n```?" + duration: "Are you sure you want to change the mute duration from ```\n{}\n```to```\n{}\n```?" mute_edited: reason: "The mute reason has been changed from ```\n{}\n```to```\n{}\n```" + duration: "The mute duration has been changed from ```\n{}\n```to```\n{}\n```" mute_edited_response: ":white_check_mark: The mute reason has been edited" log_mute_edited: ":mute: Mute edited" @@ -146,13 +150,13 @@ ulog: inf: id_off: ":mute: **Muted** permanently by {} because of `{}`{}" id_on: ":mute: **Muted** permanently by {} because of `{}`\n`(ID: {})`{}" - upgrade: + update: temp: - id_off: ":mute: **Mute upgraded** by {} to {} because of `{}`" - id_on: ":mute: **Mute upgraded** by {} to {} because of `{}`\n`(ID: {})`" + id_off: ":mute: **Mute updated** by {} to {} because of `{}`" + id_on: ":mute: **Mute updated** by {} to {} because of `{}`\n`(ID: {})`" inf: - id_off: ":mute: **Mute upgraded** to permanent by {} because of `{}`" - id_on: ":mute: **Mute upgraded** to permanent by {} because of `{}`\n`(ID: {})`" + id_off: ":mute: **Mute updated** to permanent by {} because of `{}`" + id_on: ":mute: **Mute updated** to permanent by {} because of `{}`\n`(ID: {})`" unmuted: id_off: ":loud_sound: **Unmuted** by {} because of `{}`" id_on: ":loud_sound: **Unmuted** by {} because of `{}`\n`(ID: {})`" From c30138c9acd782f3773a30a6e3282c56fcb2e460 Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 7 Jul 2021 11:16:53 +0200 Subject: [PATCH 019/120] Added function to edit kick reasons --- moderation/mod/cog.py | 61 +++++++++++++++++++++++++++--- moderation/mod/models.py | 6 +++ moderation/mod/translations/en.yml | 5 +++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index ffe255a87..2f49925de 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -684,7 +684,7 @@ async def edit_mute(self, ctx): @edit_mute.command(name="reason") async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): """ - edit a mute + edit a mute reason get the mute id from the users user log """ @@ -816,11 +816,6 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time server_embed.colour = Colors.error await reply(ctx, embed=server_embed) - - - - - @commands.command() @ModPermission.mute.check @guild_only() @@ -912,6 +907,60 @@ async def kick(self, ctx: Context, member: Member, *, reason: str): await reply(ctx, embed=server_embed) + @commands.command() + @ModPermission.warn.check + @guild_only() + async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): + """ + edit a kick + get the kick id from the users user log + """ + + kick = await db.get(Kick, id=kick_id) + if kick is None: + raise CommandError(t.no_kick) + + if not await compare_mod_level(ctx.author, ctx.guild.get_member(kick.mod)): + raise CommandError(tg.permission_denied) + + if len(reason) > 900: + raise CommandError(t.reason_too_long) + + conf_embed = Embed( + title=t.confirmation, + description=t.confirm_kick_edit(kick.reason, reason), + color=Colors.ModTools + ) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + await Kick.edit(kick_id, ctx.author.id, reason) + + user = self.bot.get_user(kick.member) + + user_embed = Embed( + title=t.kick, + description=t.kick_edited(kick.reason, reason), + colour=Colors.ModTools, + ) + server_embed = Embed(title=t.kick, description=t.kick_edited_reponse, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + await reply(ctx, embed=server_embed) + await send_to_changelog_mod(ctx.guild, ctx.message, Colors.kick, t.log_kick_edited, user, reason) + @commands.command() @ModPermission.ban.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 6e03e96ad..342a9691e 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -148,6 +148,12 @@ async def create(member: int, member_name: str, mod: Optional[int], reason: Opti await db.add(row) return row + @staticmethod + async def edit(kick_id: int, mod: int, new_reason: str): + row = await db.get(Kick, id=kick_id) + row.mod = mod + row.reason = new_reason + class Ban(db.Base): __tablename__ = "ban" diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 16f89e129..3b2cb190c 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -95,6 +95,11 @@ kicked_response: "User kicked successfully :white_check_mark:" log_kicked: ":x: Member kicked" cannot_kick: This member cannot be kicked. cannot_kick_permissions: User could not be kicked because I don't have `kick_members` permission on this server. +no_kick: No kick with this ID could be found +confirm_kick_edit: "Are you sure you want to change this kicks reason from ```\n{}\n```to```\n{}\n```?" +kick_edited: "The kick reason has been changed from ```\n{}\n```to```\n{}\n```" +kick_edited_reponse: ":white_check_mark: The kick reason has been edited" +log_kick_edited: ":x: Kick edited" ban: ":no_entry: Ban" unban: ":white_check_mark: Ban" From 458d35a66faf8b78068b25b80dca98627a3fc310 Mon Sep 17 00:00:00 2001 From: LoC Date: Wed, 7 Jul 2021 11:37:16 +0200 Subject: [PATCH 020/120] Added function to edit ban reasons --- moderation/mod/cog.py | 69 ++++++++++++++++++++++++++++-- moderation/mod/models.py | 10 +++-- moderation/mod/translations/en.yml | 9 ++++ 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 2f49925de..2d9417a7a 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -713,8 +713,6 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): if msg: await msg.delete(delay=5) - await Mute.edit(mute_id, ctx.author.id, reason) - user = self.bot.get_user(mute.member) user_embed = Embed( @@ -725,6 +723,8 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): server_embed = Embed(title=t.mute, description=t.mute_edited_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) + await Mute.edit(mute_id, ctx.author.id, reason) + try: await user.send(embed=user_embed) except (Forbidden, HTTPException): @@ -1009,7 +1009,7 @@ async def ban( for ban in active_bans: await Ban.upgrade(ban.id, ctx.author.id) async for mute in await db.stream(filter_by(Mute, active=True, member=user.id)): - await Mute.upgrade(mute.id, ctx.author.id) + await Mute.update(mute.id, ctx.author.id) if attachments := ctx.message.attachments: evidence = attachments[0] @@ -1073,6 +1073,69 @@ async def ban( await reply(ctx, embed=server_embed) + @commands.group() + @ModPermission.mute.check + @guild_only() + async def edit_ban(self, ctx): + """ + edit a ban + """ + + if ctx.invoked_subcommand is None: + raise UserInputError + + @edit_ban.command(name="reason") + async def edit_ban_reason(self, ctx: Context, ban_id: int, *, reason: str): + """ + edit a ban reason + get the ban id from the users user log + """ + + ban = await db.get(Ban, id=ban_id) + if ban is None: + raise CommandError(t.no_ban) + + if not await compare_mod_level(ctx.author, ctx.guild.get_member(ban.mod)): + raise CommandError(tg.permission_denied) + + if len(reason) > 900: + raise CommandError(t.reason_too_long) + + conf_embed = Embed( + title=t.confirmation, + description=t.confirm_ban_edit.reason(ban.reason, reason), + color=Colors.ModTools + ) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + user = self.bot.get_user(ban.member) + + user_embed = Embed( + title=t.ban, + description=t.ban_edited.reason(ban.reason, reason), + colour=Colors.ModTools, + ) + server_embed = Embed(title=t.ban, description=t.ban_edited_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + await Ban.edit(ban_id, ctx.author.id, reason) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + await reply(ctx, embed=server_embed) + await send_to_changelog_mod(ctx.guild, ctx.message, Colors.ban, t.log_ban_edited, user, reason) + @commands.command() @ModPermission.ban.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 342a9691e..fb6a52b04 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -119,9 +119,7 @@ async def update(ban_id: int, mod: int): async def edit(mute_id: int, mod: int, new_reason: str): row = await db.get(Mute, id=mute_id) row.mod = mod - - if new_reason: - row.reason = new_reason + row.reason = new_reason class Kick(db.Base): @@ -206,3 +204,9 @@ async def deactivate(ban_id: int, unban_mod: int = None, unban_reason: str = Non async def upgrade(ban_id: int, mod: int): ban = await Ban.deactivate(ban_id, mod) ban.upgraded = True + + @staticmethod + async def edit(ban_id: int, mod: int, new_reason: str): + row = await db.get(Ban, id=ban_id) + row.mod = mod + row.reason = new_reason diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 3b2cb190c..150d54dff 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -119,6 +119,15 @@ not_banned: This member is not banned. unbanned_response: "User unbanned. :white_check_mark:" log_unbanned: ":white_check_mark: Member Unbanned" cannot_unban_permissions: User could not be unbanned because I don't have `ban_members` permission on this server. +no_ban: No ban with this ID could be found +confirm_ban_edit: + reason: "Are you sure you want to change this bans reason from ```\n{}\n```to```\n{}\n```?" + duration: "Are you sure you want to change the bans duration from ```\n{}\n```to```\n{}\n```?" +ban_edited: + reason: "The ban reason has been changed from ```\n{}\n```to```\n{}\n```" + duration: "The ban duration has been changed from ```\n{}\n```to```\n{}\n```" +ban_edited_response: ":white_check_mark: The ban reason has been edited" +log_ban_edited: ":no_entry: Ban edited" reported_cnt: ":speech_balloon: Reported" warned_cnt: ":warning: Warned" From ca83e225605bf56cb81cc1c6147e103a29fd78c2 Mon Sep 17 00:00:00 2001 From: LoC Date: Sun, 11 Jul 2021 22:57:50 +0200 Subject: [PATCH 021/120] Added a function to edit ban durations --- moderation/mod/cog.py | 110 ++++++++++++++++++++++++----- moderation/mod/models.py | 12 ++-- moderation/mod/translations/en.yml | 10 +-- 3 files changed, 103 insertions(+), 29 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 2d9417a7a..c84822f09 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -381,8 +381,8 @@ async def handle_get_userlog_entries(self, user_id: int, show_ids: bool) -> list ban: Ban async for ban in await db.stream(filter_by(Ban, member=user_id)): - if ban.is_upgrade: - text = t.ulog.banned.upgrade + if ban.is_update: + text = t.ulog.banned.update ban.evidence = None else: text = t.ulog.banned.first @@ -421,7 +421,7 @@ async def handle_get_userlog_entries(self, user_id: int, show_ids: bool) -> list ) ) - if not ban.active and not ban.upgraded: + if not ban.active and not ban.updated: if ban.unban_mod is None: out.append((ban.deactivation_timestamp, t.ulog.unbanned_expired)) else: @@ -998,18 +998,8 @@ async def ban( raise UserCommandError(user, t.cannot_ban) active_bans: List[Ban] = await db.all(filter_by(Ban, active=True, member=user.id)) - for ban in active_bans: - if ban.minutes == -1: - raise UserCommandError(user, t.already_banned) - - ts = ban.timestamp + timedelta(minutes=ban.minutes) - if minutes is not None and datetime.utcnow() + timedelta(minutes=minutes) <= ts: - raise UserCommandError(user, t.already_banned) - - for ban in active_bans: - await Ban.upgrade(ban.id, ctx.author.id) - async for mute in await db.stream(filter_by(Mute, active=True, member=user.id)): - await Mute.update(mute.id, ctx.author.id) + if active_bans: + raise UserCommandError(user, t.already_banned) if attachments := ctx.message.attachments: evidence = attachments[0] @@ -1023,7 +1013,7 @@ async def ban( server_embed.set_author(name=str(user), icon_url=user.avatar_url) if minutes is not None: - await Ban.create(user.id, str(user), ctx.author.id, minutes, reason, evidence_url, bool(active_bans)) + await Ban.create(user.id, str(user), ctx.author.id, minutes, reason, evidence_url, False) if evidence: user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, time_to_units(minutes), reason, t.image_link(evidence.filename, evidence_url) @@ -1043,7 +1033,7 @@ async def ban( evidence=evidence, ) else: - await Ban.create(user.id, str(user), ctx.author.id, -1, reason, evidence_url, bool(active_bans)) + await Ban.create(user.id, str(user), ctx.author.id, -1, reason, evidence_url, False) if evidence: user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, reason, t.image_link(evidence.filename, evidence_url) @@ -1068,7 +1058,7 @@ async def ban( server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error - await ctx.guild.ban(user, delete_message_days=delete_days, reason=reason) + # await ctx.guild.ban(user, delete_message_days=delete_days, reason=reason) await revoke_verification(user) await reply(ctx, embed=server_embed) @@ -1136,6 +1126,90 @@ async def edit_ban_reason(self, ctx: Context, ban_id: int, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.ban, t.log_ban_edited, user, reason) + @edit_ban.command(name="duration") + async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: DurationConverter): + """ + edit a ban duration + time format: `ymwdhn` + set time to `inf` for a permanent ban + """ + + user: Union[Member, User] + time: Optional[int] + minutes = time + + active_bans: List[Mute] = await db.all(filter_by(Ban, active=True, member=user.id)) + + if not active_bans: + raise CommandError(t.not_muted) + + ban = sorted(active_bans, key=lambda active_ban: active_ban.timestamp)[0] + + if not await compare_mod_level(ctx.author, ctx.guild.get_member(ban.mod)): + raise CommandError(tg.permission_denied) + + conf_embed = Embed( + title=t.confirmation, + color=Colors.ModTools, + ) + + if minutes is None: + conf_embed.description = t.confirm_ban_edit.duration(time_to_units(ban.minutes), t.infinity) + else: + conf_embed.description = t.confirm_ban_edit.duration(time_to_units(ban.minutes), time_to_units(minutes)) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + for ban in active_bans: + await ban.update(ban.id, ctx.author.id) + + user_embed = Embed( + title=t.ban, + colour=Colors.ModTools, + ) + server_embed = Embed(title=t.ban, description=t.ban_edited_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + if minutes is not None: + await Ban.create(user.id, str(user), ctx.author.id, minutes, ban.reason, ban.evidence, True) + user_embed.description = t.ban_edited.duration(time_to_units(ban.minutes), time_to_units(minutes)) + await send_to_changelog_mod( + ctx.guild, + ctx.message, + Colors.mute, + t.log_ban_edited, + user, + ban.reason, + duration=time_to_units(minutes), + ) + + else: + await Ban.create(user.id, str(user), ctx.author.id, -1, ban.reason, ban.evidence, True) + user_embed.description = t.ban_edited.duration(time_to_units(ban.minutes), t.infinity) + await send_to_changelog_mod( + ctx.guild, + ctx.message, + Colors.mute, + t.log_ban_edited, + user, + ban.reason, + duration=t.log_field.infinity, + ) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + await reply(ctx, embed=server_embed) + @commands.command() @ModPermission.ban.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index fb6a52b04..360f2ca0e 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -168,12 +168,12 @@ class Ban(db.Base): deactivation_timestamp: Union[Column, Optional[datetime]] = Column(DateTime, nullable=True) unban_reason: Union[Column, Optional[str]] = Column(Text(collation="utf8mb4_bin"), nullable=True) unban_mod: Union[Column, Optional[int]] = Column(BigInteger, nullable=True) - upgraded: Union[Column, bool] = Column(Boolean, default=False) - is_upgrade: Union[Column, bool] = Column(Boolean) + updated: Union[Column, bool] = Column(Boolean, default=False) + is_update: Union[Column, bool] = Column(Boolean) @staticmethod async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, - evidence: Optional[str], is_upgrade: bool = False) -> Ban: + evidence: Optional[str], is_update: bool = False) -> Ban: row = Ban( member=member, member_name=member_name, @@ -186,7 +186,7 @@ async def create(member: int, member_name: str, mod: int, minutes: int, reason: deactivation_timestamp=None, unban_reason=None, unban_mod=None, - is_upgrade=is_upgrade, + is_update=is_update, ) await db.add(row) return row @@ -201,9 +201,9 @@ async def deactivate(ban_id: int, unban_mod: int = None, unban_reason: str = Non return row @staticmethod - async def upgrade(ban_id: int, mod: int): + async def update(ban_id: int, mod: int): ban = await Ban.deactivate(ban_id, mod) - ban.upgraded = True + ban.updated = True @staticmethod async def edit(ban_id: int, mod: int, new_reason: str): diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 150d54dff..a8fe96dd7 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -189,13 +189,13 @@ ulog: inf: id_off: ":no_entry: **Banned** permanently by {} because of `{}`{}" id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`{}" - upgrade: + update: temp: - id_off: ":no_entry: **Ban upgraded** by {} to {} because of `{}`" - id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`" + id_off: ":no_entry: **Ban updated** by {} to {} because of `{}`" + id_on: ":no_entry: **Ban updated** by {} to {} because of `{}`\n`(ID: {})`" inf: - id_off: ":no_entry: **Ban upgraded** to permanent by {} because of `{}`" - id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`" + id_off: ":no_entry: **Ban updated** to permanent by {} because of `{}`" + id_on: ":no_entry: **Ban updated** to permanent by {} because of `{}`\n`(ID: {})`" unbanned: id_off: ":white_check_mark: **Unbanned** by {} because of `{}`" id_on: ":white_check_mark: **Unbanned** by {} because of `{}`\n`(ID: {})`" From 478c50d995b096189e8206458a3af25c7daf6e3a Mon Sep 17 00:00:00 2001 From: LoC Date: Sun, 11 Jul 2021 23:12:08 +0200 Subject: [PATCH 022/120] Fixed possibility to use senseless time periods for mute/ban edits --- moderation/mod/cog.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index c84822f09..2e3deed10 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -755,15 +755,23 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time if not await compare_mod_level(ctx.author, ctx.guild.get_member(mute.mod)): raise CommandError(tg.permission_denied) + if mute.minutes == minutes or (mute.minutes == -1 and minutes is None): + raise CommandError(t.already_muted) + conf_embed = Embed( title=t.confirmation, color=Colors.ModTools, ) + if mute.minutes == -1: + old_mute_minutes = t.infinity + else: + old_mute_minutes = time_to_units(mute.minutes) + if minutes is None: - conf_embed.description = t.confirm_mute_edit.duration(time_to_units(mute.minutes), t.infinity) + conf_embed.description = t.confirm_mute_edit.duration(old_mute_minutes, t.infinity) else: - conf_embed.description = t.confirm_mute_edit.duration(time_to_units(mute.minutes), time_to_units(minutes)) + conf_embed.description = t.confirm_mute_edit.duration(old_mute_minutes, time_to_units(minutes)) async with confirm(ctx, conf_embed) as (result, msg): if not result: @@ -1148,15 +1156,23 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: if not await compare_mod_level(ctx.author, ctx.guild.get_member(ban.mod)): raise CommandError(tg.permission_denied) + if ban.minutes == minutes or (ban.minutes == -1 and minutes is None): + raise CommandError(t.already_banned) + conf_embed = Embed( title=t.confirmation, color=Colors.ModTools, ) + if ban.minutes == -1: + old_ban_minutes = t.infinity + else: + old_ban_minutes = time_to_units(ban.minutes) + if minutes is None: - conf_embed.description = t.confirm_ban_edit.duration(time_to_units(ban.minutes), t.infinity) + conf_embed.description = t.confirm_ban_edit.duration(old_ban_minutes, t.infinity) else: - conf_embed.description = t.confirm_ban_edit.duration(time_to_units(ban.minutes), time_to_units(minutes)) + conf_embed.description = t.confirm_ban_edit.duration(old_ban_minutes, time_to_units(minutes)) async with confirm(ctx, conf_embed) as (result, msg): if not result: From d70eec7b487128057fcc35da4d0d2ed09aebd488 Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 12 Jul 2021 21:53:12 +0200 Subject: [PATCH 023/120] Added mod level to database for warns, mutes, kicks, bans --- moderation/mod/cog.py | 130 +++++++++++++++++++++++++++++++-------- moderation/mod/models.py | 31 +++++++--- 2 files changed, 126 insertions(+), 35 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 2e3deed10..431a53f3d 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -99,11 +99,13 @@ def show_evidence(evidence: Optional[str]) -> str: return "" -async def compare_mod_level(mod1: Member, mod2: Member) -> bool: - lvl1 = await Config.PERMISSION_LEVELS.get_permission_level(mod1) - lvl2 = await Config.PERMISSION_LEVELS.get_permission_level(mod2) +def get_mod_level(mod: Member): + lvl = await Config.PERMISSION_LEVELS.get_permission_level(mod) + return lvl.level - return lvl1.level > lvl2.level or mod1 == mod1.guild.owner + +async def compare_mod_level(mod: Member, mod_level: int) -> bool: + return get_mod_level(mod) > mod_level or mod == mod.guild.owner async def send_to_changelog_mod( @@ -212,7 +214,7 @@ async def mod_loop(self): @log_auto_kick.subscribe async def handle_log_auto_kick(self, member: Member): - await Kick.create(member.id, str(member), None, None) + await Kick.create(member.id, str(member), None, None, None, None) @get_user_info_entries.subscribe async def handle_get_user_stats_entries(self, user_id: int) -> list[tuple[str, str]]: @@ -521,7 +523,7 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): except (Forbidden, HTTPException): server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error - await Warn.create(user.id, str(user), ctx.author.id, reason, evidence.url) + await Warn.create(user.id, str(user), ctx.author.id, get_mod_level(ctx.author), reason, evidence.url) await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warned, user, reason, evidence=evidence) @@ -538,7 +540,7 @@ async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): if warn is None: raise CommandError(t.no_warn) - if not await compare_mod_level(ctx.author, ctx.guild.get_member(warn.mod)): + if not await compare_mod_level(ctx.author, warn.mod_level): raise CommandError(tg.permission_denied) if len(reason) > 900: @@ -559,7 +561,7 @@ async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): if msg: await msg.delete(delay=5) - await Warn.edit(warn_id, ctx.author.id, reason) + await Warn.edit(warn_id, ctx.author.id, get_mod_level(ctx.author), reason) user = self.bot.get_user(warn.member) @@ -622,7 +624,15 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv server_embed.set_author(name=str(user), icon_url=user.avatar_url) if minutes is not None: - await Mute.create(user.id, str(user), ctx.author.id, minutes, reason, evidence_url) + await Mute.create( + user.id, + str(user), + ctx.author.id, + get_mod_level(ctx.author), + minutes, + reason, + evidence_url, + ) if evidence: user_embed.description = t.muted.evidence(ctx.author.mention, ctx.guild.name, time_to_units(minutes), reason, t.image_link(evidence.filename, evidence_url), @@ -643,7 +653,15 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv evidence=evidence, ) else: - await Mute.create(user.id, str(user), ctx.author.id, -1, reason, evidence_url) + await Mute.create( + user.id, + str(user), + ctx.author.id, + get_mod_level(ctx.author), + -1, + reason, + evidence_url, + ) if evidence: user_embed.description = t.muted_inf.evidence(ctx.author.mention, ctx.guild.name, reason, t.image_link(evidence.filename, evidence_url), @@ -692,7 +710,7 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): if mute is None: raise CommandError(t.no_mute) - if not await compare_mod_level(ctx.author, ctx.guild.get_member(mute.mod)): + if not await compare_mod_level(ctx.author, mute.mod_level): raise CommandError(tg.permission_denied) if len(reason) > 900: @@ -723,7 +741,7 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): server_embed = Embed(title=t.mute, description=t.mute_edited_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) - await Mute.edit(mute_id, ctx.author.id, reason) + await Mute.edit(mute_id, ctx.author.id, get_mod_level(ctx.author), reason) try: await user.send(embed=user_embed) @@ -752,7 +770,7 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time mute = sorted(active_mutes, key=lambda active_mute: active_mute.timestamp)[0] - if not await compare_mod_level(ctx.author, ctx.guild.get_member(mute.mod)): + if not await compare_mod_level(ctx.author, mute.mod_level): raise CommandError(tg.permission_denied) if mute.minutes == minutes or (mute.minutes == -1 and minutes is None): @@ -793,7 +811,16 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time server_embed.set_author(name=str(user), icon_url=user.avatar_url) if minutes is not None: - await Mute.create(user.id, str(user), ctx.author.id, minutes, mute.reason, mute.evidence, True) + await Mute.create( + user.id, + str(user), + ctx.author.id, + get_mod_level(ctx.author), + minutes, + mute.reason, + mute.evidence, + True, + ) user_embed.description = t.mute_edited.duration(time_to_units(mute.minutes), time_to_units(minutes)) await send_to_changelog_mod( ctx.guild, @@ -806,7 +833,16 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time ) else: - await Mute.create(user.id, str(user), ctx.author.id, -1, mute.reason, mute.evidence, True) + await Mute.create( + user.id, + str(user), + ctx.author.id, + get_mod_level(ctx.author), + -1, + mute.reason, + mute.evidence, + True, + ) user_embed.description = t.mute_edited.duration(time_to_units(mute.minutes), t.infinity) await send_to_changelog_mod( ctx.guild, @@ -845,6 +881,9 @@ async def unmute(self, ctx: Context, user: UserMemberConverter, *, reason: str): await user.remove_roles(mute_role) async for mute in await db.stream(filter_by(Mute, active=True, member=user.id)): + if not await compare_mod_level(ctx.author, mute.mod_level): + raise CommandError(tg.permission_denied) + await Mute.deactivate(mute.id, ctx.author.id, reason) was_muted = True if not was_muted: @@ -882,7 +921,7 @@ async def kick(self, ctx: Context, member: Member, *, reason: str): evidence = None evidence_url = None - await Kick.create(member.id, str(member), ctx.author.id, reason, evidence_url) + await Kick.create(member.id, str(member), ctx.author.id, get_mod_level(ctx.author), reason, evidence_url) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.kick, t.log_kicked, member, reason, evidence=evidence) @@ -928,7 +967,7 @@ async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): if kick is None: raise CommandError(t.no_kick) - if not await compare_mod_level(ctx.author, ctx.guild.get_member(kick.mod)): + if not await compare_mod_level(ctx.author, kick.mod_level): raise CommandError(tg.permission_denied) if len(reason) > 900: @@ -949,7 +988,7 @@ async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): if msg: await msg.delete(delay=5) - await Kick.edit(kick_id, ctx.author.id, reason) + await Kick.edit(kick_id, ctx.author.id, get_mod_level(ctx.author), reason) user = self.bot.get_user(kick.member) @@ -1021,7 +1060,16 @@ async def ban( server_embed.set_author(name=str(user), icon_url=user.avatar_url) if minutes is not None: - await Ban.create(user.id, str(user), ctx.author.id, minutes, reason, evidence_url, False) + await Ban.create( + user.id, + str(user), + ctx.author.id, + get_mod_level(ctx.author), + minutes, + reason, + evidence_url, + False, + ) if evidence: user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, time_to_units(minutes), reason, t.image_link(evidence.filename, evidence_url) @@ -1041,7 +1089,16 @@ async def ban( evidence=evidence, ) else: - await Ban.create(user.id, str(user), ctx.author.id, -1, reason, evidence_url, False) + await Ban.create( + user.id, + str(user), + ctx.author.id, + get_mod_level(ctx.author), + -1, + reason, + evidence_url, + False, + ) if evidence: user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, reason, t.image_link(evidence.filename, evidence_url) @@ -1093,7 +1150,7 @@ async def edit_ban_reason(self, ctx: Context, ban_id: int, *, reason: str): if ban is None: raise CommandError(t.no_ban) - if not await compare_mod_level(ctx.author, ctx.guild.get_member(ban.mod)): + if not await compare_mod_level(ctx.author, ban.mod_level): raise CommandError(tg.permission_denied) if len(reason) > 900: @@ -1124,7 +1181,7 @@ async def edit_ban_reason(self, ctx: Context, ban_id: int, *, reason: str): server_embed = Embed(title=t.ban, description=t.ban_edited_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) - await Ban.edit(ban_id, ctx.author.id, reason) + await Ban.edit(ban_id, ctx.author.id, get_mod_level(ctx.author), reason) try: await user.send(embed=user_embed) @@ -1153,7 +1210,7 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: ban = sorted(active_bans, key=lambda active_ban: active_ban.timestamp)[0] - if not await compare_mod_level(ctx.author, ctx.guild.get_member(ban.mod)): + if not await compare_mod_level(ctx.author, ban.mod_level): raise CommandError(tg.permission_denied) if ban.minutes == minutes or (ban.minutes == -1 and minutes is None): @@ -1194,7 +1251,16 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: server_embed.set_author(name=str(user), icon_url=user.avatar_url) if minutes is not None: - await Ban.create(user.id, str(user), ctx.author.id, minutes, ban.reason, ban.evidence, True) + await Ban.create( + user.id, + str(user), + ctx.author.id, + get_mod_level(ctx.author), + minutes, + ban.reason, + ban.evidence, + True, + ) user_embed.description = t.ban_edited.duration(time_to_units(ban.minutes), time_to_units(minutes)) await send_to_changelog_mod( ctx.guild, @@ -1207,7 +1273,16 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: ) else: - await Ban.create(user.id, str(user), ctx.author.id, -1, ban.reason, ban.evidence, True) + await Ban.create( + user.id, + str(user), + ctx.author.id, + get_mod_level(ctx.author), + -1, + ban.reason, + ban.evidence, + True, + ) user_embed.description = t.ban_edited.duration(time_to_units(ban.minutes), t.infinity) await send_to_changelog_mod( ctx.guild, @@ -1249,8 +1324,11 @@ async def unban(self, ctx: Context, user: UserMemberConverter, *, reason: str): was_banned = False async for ban in await db.stream(filter_by(Ban, active=True, member=user.id)): - was_banned = True + if not await compare_mod_level(ctx.author, ban.mod_level): + raise CommandError(tg.permission_denied) + await Ban.deactivate(ban.id, ctx.author.id, reason) + was_banned = True if not was_banned: raise UserCommandError(user, t.not_banned) diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 360f2ca0e..973c45ee0 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -40,15 +40,18 @@ class Warn(db.Base): member: Union[Column, int] = Column(BigInteger) member_name: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) mod: Union[Column, int] = Column(BigInteger) + mod_level: Union[Column, int] = Column(Integer) timestamp: Union[Column, datetime] = Column(DateTime) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) evidence: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) @staticmethod - async def create(member: int, member_name: str, mod: int, reason: str, evidence: Optional[str]) -> Warn: + async def create(member: int, member_name: str, mod: int, mod_level: int, reason: str, + evidence: Optional[str]) -> Warn: row = Warn(member=member, member_name=member_name, mod=mod, + mod_level=mod_level, timestamp=datetime.utcnow(), reason=reason, evidence=evidence, @@ -57,9 +60,10 @@ async def create(member: int, member_name: str, mod: int, reason: str, evidence: return row @staticmethod - async def edit(warn_id: int, mod: int, new_reason: str): + async def edit(warn_id: int, mod: int, mod_level: int, new_reason: str): row = await db.get(Warn, id=warn_id) row.mod = mod + row.mod_level = mod_level row.reason = new_reason @@ -70,6 +74,7 @@ class Mute(db.Base): member: Union[Column, int] = Column(BigInteger) member_name: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) mod: Union[Column, int] = Column(BigInteger) + mod_level: Union[Column, int] = Column(Integer) timestamp: Union[Column, datetime] = Column(DateTime) minutes: Union[Column, int] = Column(Integer) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) @@ -82,12 +87,13 @@ class Mute(db.Base): is_update: Union[Column, bool] = Column(Boolean) @staticmethod - async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, + async def create(member: int, member_name: str, mod: int, mod_level: int, minutes: int, reason: str, evidence: Optional[str], is_update: bool = False) -> Mute: row = Mute( member=member, member_name=member_name, mod=mod, + mod_level=mod_level, timestamp=datetime.utcnow(), minutes=minutes, reason=reason, @@ -116,9 +122,10 @@ async def update(ban_id: int, mod: int): mute.updated = True @staticmethod - async def edit(mute_id: int, mod: int, new_reason: str): + async def edit(mute_id: int, mod: int, mod_level: int, new_reason: str): row = await db.get(Mute, id=mute_id) row.mod = mod + row.mod_level = mod_level row.reason = new_reason @@ -129,16 +136,18 @@ class Kick(db.Base): member: Union[Column, int] = Column(BigInteger) member_name: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) mod: Union[Column, int] = Column(BigInteger) + mod_level: Union[Column, int] = Column(Integer) timestamp: Union[Column, datetime] = Column(DateTime) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) evidence: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) @staticmethod - async def create(member: int, member_name: str, mod: Optional[int], reason: Optional[str], - evidence: Optional[str]) -> Kick: + async def create(member: int, member_name: str, mod: Optional[int], mod_level: Optional[int], + reason: Optional[str], evidence: Optional[str]) -> Kick: row = Kick(member=member, member_name=member_name, mod=mod, + mod_level=mod_level, timestamp=datetime.utcnow(), reason=reason, evidence=evidence, @@ -147,9 +156,10 @@ async def create(member: int, member_name: str, mod: Optional[int], reason: Opti return row @staticmethod - async def edit(kick_id: int, mod: int, new_reason: str): + async def edit(kick_id: int, mod: int, mod_level: int, new_reason: str): row = await db.get(Kick, id=kick_id) row.mod = mod + row.mod_level = mod_level row.reason = new_reason @@ -160,6 +170,7 @@ class Ban(db.Base): member: Union[Column, int] = Column(BigInteger) member_name: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) mod: Union[Column, int] = Column(BigInteger) + mod_level: Union[Column, int] = Column(Integer) timestamp: Union[Column, datetime] = Column(DateTime) minutes: Union[Column, int] = Column(Integer) reason: Union[Column, str] = Column(Text(collation="utf8mb4_bin")) @@ -172,12 +183,13 @@ class Ban(db.Base): is_update: Union[Column, bool] = Column(Boolean) @staticmethod - async def create(member: int, member_name: str, mod: int, minutes: int, reason: str, + async def create(member: int, member_name: str, mod: int, mod_level: int, minutes: int, reason: str, evidence: Optional[str], is_update: bool = False) -> Ban: row = Ban( member=member, member_name=member_name, mod=mod, + mod_level=mod_level, timestamp=datetime.utcnow(), minutes=minutes, reason=reason, @@ -206,7 +218,8 @@ async def update(ban_id: int, mod: int): ban.updated = True @staticmethod - async def edit(ban_id: int, mod: int, new_reason: str): + async def edit(ban_id: int, mod: int, mod_level: int, new_reason: str): row = await db.get(Ban, id=ban_id) row.mod = mod + row.mod_level = mod_level row.reason = new_reason From becc4e093b0931ade0ffe41cb17119dbd88705b9 Mon Sep 17 00:00:00 2001 From: LoC Date: Sat, 17 Jul 2021 11:25:04 +0200 Subject: [PATCH 024/120] Added setting for whether to send a user a concerning delete message --- moderation/mod/cog.py | 46 +++++++++++++++++++----------- moderation/mod/permissions.py | 1 + moderation/mod/settings.py | 5 ++++ moderation/mod/translations/en.yml | 14 ++++++++- 4 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 moderation/mod/settings.py diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 431a53f3d..a0eb3980d 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -25,6 +25,7 @@ from .colors import Colors from .models import Mute, Ban, Report, Warn, Kick from .permissions import ModPermission +from .settings import ModSettings from ...contributor import Contributor from ...pubsub import ( send_to_changelog, @@ -99,13 +100,13 @@ def show_evidence(evidence: Optional[str]) -> str: return "" -def get_mod_level(mod: Member): +async def get_mod_level(mod: Member): lvl = await Config.PERMISSION_LEVELS.get_permission_level(mod) return lvl.level async def compare_mod_level(mod: Member, mod_level: int) -> bool: - return get_mod_level(mod) > mod_level or mod == mod.guild.owner + return await get_mod_level(mod) > mod_level or mod == mod.guild.owner async def send_to_changelog_mod( @@ -452,6 +453,19 @@ async def on_member_join(self, member: Member): if await db.exists(filter_by(Mute, active=True, member=member.id)): await member.add_roles(mute_role) + @commands.command() + @ModPermission.modtools_write.check + async def send_delete_message(self, ctx: Context, send: bool): + """ + configure whether to send a warn/mute/kick/ban delete message to the concerned user + """ + + await ModSettings.send_delete_user_message.set(send) + embed = Embed(title=t.modtools, description=t.configured_send_delete_message[send], color=Colors.ModTools) + await reply(ctx, embed=embed) + + await send_to_changelog(ctx.guild, t.configured_send_delete_message[send]) + @commands.command() async def report(self, ctx: Context, user: UserMemberConverter, *, reason: str): """ @@ -523,7 +537,7 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): except (Forbidden, HTTPException): server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error - await Warn.create(user.id, str(user), ctx.author.id, get_mod_level(ctx.author), reason, evidence.url) + await Warn.create(user.id, str(user), ctx.author.id, await get_mod_level(ctx.author), reason, evidence.url) await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warned, user, reason, evidence=evidence) @@ -561,7 +575,7 @@ async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): if msg: await msg.delete(delay=5) - await Warn.edit(warn_id, ctx.author.id, get_mod_level(ctx.author), reason) + await Warn.edit(warn_id, ctx.author.id, await get_mod_level(ctx.author), reason) user = self.bot.get_user(warn.member) @@ -628,7 +642,7 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv user.id, str(user), ctx.author.id, - get_mod_level(ctx.author), + await get_mod_level(ctx.author), minutes, reason, evidence_url, @@ -657,7 +671,7 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv user.id, str(user), ctx.author.id, - get_mod_level(ctx.author), + await get_mod_level(ctx.author), -1, reason, evidence_url, @@ -741,7 +755,7 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): server_embed = Embed(title=t.mute, description=t.mute_edited_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) - await Mute.edit(mute_id, ctx.author.id, get_mod_level(ctx.author), reason) + await Mute.edit(mute_id, ctx.author.id, await get_mod_level(ctx.author), reason) try: await user.send(embed=user_embed) @@ -815,7 +829,7 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time user.id, str(user), ctx.author.id, - get_mod_level(ctx.author), + await get_mod_level(ctx.author), minutes, mute.reason, mute.evidence, @@ -837,7 +851,7 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time user.id, str(user), ctx.author.id, - get_mod_level(ctx.author), + await get_mod_level(ctx.author), -1, mute.reason, mute.evidence, @@ -921,7 +935,7 @@ async def kick(self, ctx: Context, member: Member, *, reason: str): evidence = None evidence_url = None - await Kick.create(member.id, str(member), ctx.author.id, get_mod_level(ctx.author), reason, evidence_url) + await Kick.create(member.id, str(member), ctx.author.id, await get_mod_level(ctx.author), reason, evidence_url) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.kick, t.log_kicked, member, reason, evidence=evidence) @@ -988,7 +1002,7 @@ async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): if msg: await msg.delete(delay=5) - await Kick.edit(kick_id, ctx.author.id, get_mod_level(ctx.author), reason) + await Kick.edit(kick_id, ctx.author.id, await get_mod_level(ctx.author), reason) user = self.bot.get_user(kick.member) @@ -1064,7 +1078,7 @@ async def ban( user.id, str(user), ctx.author.id, - get_mod_level(ctx.author), + await get_mod_level(ctx.author), minutes, reason, evidence_url, @@ -1093,7 +1107,7 @@ async def ban( user.id, str(user), ctx.author.id, - get_mod_level(ctx.author), + await get_mod_level(ctx.author), -1, reason, evidence_url, @@ -1181,7 +1195,7 @@ async def edit_ban_reason(self, ctx: Context, ban_id: int, *, reason: str): server_embed = Embed(title=t.ban, description=t.ban_edited_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.avatar_url) - await Ban.edit(ban_id, ctx.author.id, get_mod_level(ctx.author), reason) + await Ban.edit(ban_id, ctx.author.id, await get_mod_level(ctx.author), reason) try: await user.send(embed=user_embed) @@ -1255,7 +1269,7 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: user.id, str(user), ctx.author.id, - get_mod_level(ctx.author), + await get_mod_level(ctx.author), minutes, ban.reason, ban.evidence, @@ -1277,7 +1291,7 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: user.id, str(user), ctx.author.id, - get_mod_level(ctx.author), + await get_mod_level(ctx.author), -1, ban.reason, ban.evidence, diff --git a/moderation/mod/permissions.py b/moderation/mod/permissions.py index 06c8ebf38..5043d75b4 100644 --- a/moderation/mod/permissions.py +++ b/moderation/mod/permissions.py @@ -13,3 +13,4 @@ def description(self) -> str: mute = auto() kick = auto() ban = auto() + modtools_write = auto() diff --git a/moderation/mod/settings.py b/moderation/mod/settings.py new file mode 100644 index 000000000..2ac739ba0 --- /dev/null +++ b/moderation/mod/settings.py @@ -0,0 +1,5 @@ +from PyDrocsid.settings import Settings + + +class ModSettings(Settings): + send_delete_user_message = True diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index a8fe96dd7..452d6cc77 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -3,6 +3,7 @@ permissions: mute: mute a user kick: kick a user ban: ban a user + modtools_write: write mod tools configuration times: years: @@ -30,7 +31,10 @@ log_field: infinity: "*Permanent*" evidence: ":file_cabinet: Evidence image" - +modtools: Mod tools +configured_send_delete_message: + off: "Sending a message to a user after a warn/mute/kick/ban concerning him has been set to `off`" + on: "Sending a message to a user after a warn/mute/kick/ban concerning him has been set to `on`" jump_url: "{} [[Jump]]({})" image_link: "{} [[Go to image]]({})" no_dm: ":warning: User does not allow direct messages." @@ -41,12 +45,14 @@ edit_canceled: ":x: Edit canceled" edit_confirmed: ":white_check_mark: Edit confirmed" infinity: Permanent +# reports report: ":speech_balloon: Report" cannot_report: This member cannot be reported. no_self_report: You cannot report yourself. reported_response: "User reported successfully :white_check_mark:" log_reported: ":speech_balloon: Member reported" +# warns warn: ":warning: Warn" cannot_warn: This member cannot be warned. warned: @@ -59,7 +65,9 @@ confirm_warn_edit: "Are you sure you want to change this warns reason from ```\n warn_edited: "The warn reason has been changed from ```\n{}\n```to```\n{}\n```" warn_edited_response: ":white_check_mark: The warn reason has been edited" log_warn_edited: ":warning: Warn edited" +confirm_warn_delete: "Are you sure you want to delete this warning for *{}*?\n`(ID: {})`" +# mutes mute: ":mute: Mute" unmute: ":loud_sound: Mute" mute_role_not_set: User could not be muted because no mute role is configured. @@ -87,6 +95,7 @@ mute_edited: mute_edited_response: ":white_check_mark: The mute reason has been edited" log_mute_edited: ":mute: Mute edited" +# kicks kick: ":x: Kick" kicked: evidence: "{} just kicked you from {}:```\n{}\n```Evidence: {}" @@ -101,6 +110,7 @@ kick_edited: "The kick reason has been changed from ```\n{}\n```to```\n{}\n```" kick_edited_reponse: ":white_check_mark: The kick reason has been edited" log_kick_edited: ":x: Kick edited" +# bans ban: ":no_entry: Ban" unban: ":white_check_mark: Ban" cannot_ban: This member cannot be banned. @@ -139,6 +149,7 @@ autokicks: one: "(plus `{cnt}` autokick)" many: "(plus `{cnt}` autokicks)" +# user stats active_sanctions: Active Punishments none: None status_banned: ":no_entry: User is banned from this server." @@ -146,6 +157,7 @@ status_banned_time: ":no_entry: User is banned for {} from this server ({} left) status_muted: ":mute: User is muted on this server." status_muted_time: ":mute: User is muted for {} on this server ({} left)." +# user log ulog: evidence: "\n[[Evidence]]({})" From 288fb8ad3e2bb71d47ccec5b1712f43fe5dda1a1 Mon Sep 17 00:00:00 2001 From: LoC Date: Sat, 17 Jul 2021 12:15:11 +0200 Subject: [PATCH 025/120] Added a function to delete warns --- moderation/mod/cog.py | 54 +++++++++++++++++++++++++++++- moderation/mod/models.py | 5 +++ moderation/mod/translations/en.yml | 3 ++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index a0eb3980d..05eb3c280 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -463,7 +463,6 @@ async def send_delete_message(self, ctx: Context, send: bool): await ModSettings.send_delete_user_message.set(send) embed = Embed(title=t.modtools, description=t.configured_send_delete_message[send], color=Colors.ModTools) await reply(ctx, embed=embed) - await send_to_changelog(ctx.guild, t.configured_send_delete_message[send]) @commands.command() @@ -595,6 +594,59 @@ async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warn_edited, user, reason) + @commands.command() + @ModPermission.warn.check + @guild_only() + async def delete_warn(self, ctx: Context, warn_id: int): + """ + delete a warn + get the warn id from the users user log + """ + + warn = await db.get(Warn, id=warn_id) + if warn is None: + raise CommandError(t.no_warn) + + if not await compare_mod_level(ctx.author, warn.mod_level): + raise CommandError(tg.permission_denied) + + conf_embed = Embed( + title=t.confirmation, + description=t.confirm_warn_delete(warn.member_name, warn.id), + color=Colors.ModTools + ) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + await Warn.delete(warn_id) + + user = self.bot.get_user(warn.member) + server_embed = Embed(title=t.warn, description=t.warn_deleted_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + if await ModSettings.send_delete_user_message.get(): + user_embed = Embed( + title=t.warn, + description=t.warn_deleted(warn.reason), + colour=Colors.ModTools, + ) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + + await reply(ctx, embed=server_embed) + await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warn_deleted, user, warn.reason) + @commands.command() @ModPermission.mute.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 973c45ee0..65c292c1e 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -66,6 +66,11 @@ async def edit(warn_id: int, mod: int, mod_level: int, new_reason: str): row.mod_level = mod_level row.reason = new_reason + @staticmethod + async def delete(warn_id: int): + row = await db.get(Warn, id=warn_id) + await db.delete(row) + class Mute(db.Base): __tablename__ = "mute" diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 452d6cc77..6011a8d21 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -66,6 +66,9 @@ warn_edited: "The warn reason has been changed from ```\n{}\n```to```\n{}\n```" warn_edited_response: ":white_check_mark: The warn reason has been edited" log_warn_edited: ":warning: Warn edited" confirm_warn_delete: "Are you sure you want to delete this warning for *{}*?\n`(ID: {})`" +warn_deleted: "Your warn with the reason ```\n{}\n``` has been deleted" +warn_deleted_response: ":white_check_mark: The warn has been deleted" +log_warn_deleted: ":warning: Warn deleted" # mutes mute: ":mute: Mute" From abad9adc470746238790c522e943886f22801ef5 Mon Sep 17 00:00:00 2001 From: LoC Date: Sat, 17 Jul 2021 12:41:45 +0200 Subject: [PATCH 026/120] Added a function to delete mutes --- moderation/mod/cog.py | 86 ++++++++++++++++++++++++++++++ moderation/mod/models.py | 5 ++ moderation/mod/translations/en.yml | 6 +++ 3 files changed, 97 insertions(+) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 05eb3c280..9aa1dbd6a 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -926,6 +926,92 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time server_embed.colour = Colors.error await reply(ctx, embed=server_embed) + @commands.command() + @ModPermission.mute.check + @guild_only() + async def delete_mute(self, ctx: Context, mute_id: int): + """ + delete a warn + get the warn id from the users user log + """ + + mute = await db.get(Mute, id=mute_id) + if mute is None: + raise CommandError(t.no_mute) + + if not await compare_mod_level(ctx.author, mute.mod_level): + raise CommandError(tg.permission_denied) + + conf_embed = Embed( + title=t.confirmation, + description=t.confirm_mute_delete(mute.member_name, mute.id), + color=Colors.ModTools + ) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + active_mutes: List[Mute] = await db.all(filter_by(Mute, active=True, member=mute.member)) + + if len(active_mutes) == 1 and mute in active_mutes: + user = ctx.guild.get_member(mute.member) + if user is not None: + if (mute_role := get_mute_role(ctx.guild)) in user.roles: + await user.remove_roles(mute_role) + + user = self.bot.get_user(mute.member) + + await Mute.delete(mute_id) + + server_embed = Embed(title=t.mute, description=t.mute_deleted_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + if await ModSettings.send_delete_user_message.get(): + user_embed = Embed( + title=t.warn, + colour=Colors.ModTools, + ) + + if mute.minutes == -1: + user_embed.description = t.mute_deleted.inf(mute.reason) + else: + user_embed.description = t.mute_deleted.not_inf(time_to_units(mute.minutes), mute.reason) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + + await reply(ctx, embed=server_embed) + + if mute.minutes == -1: + await send_to_changelog_mod( + ctx.guild, + ctx.message, + Colors.mute, + t.log_mute_deleted, + user, + mute.reason, + duration=t.log_field_infinity, + ) + else: + await send_to_changelog_mod( + ctx.guild, + ctx.message, + Colors.mute, + t.log_mute_deleted, + user, + mute.reason, + duration=time_to_units(mute.minutes), + ) + @commands.command() @ModPermission.mute.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 65c292c1e..7dafebd46 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -133,6 +133,11 @@ async def edit(mute_id: int, mod: int, mod_level: int, new_reason: str): row.mod_level = mod_level row.reason = new_reason + @staticmethod + async def delete(mute_id: int): + row = await db.get(Mute, id=mute_id) + await db.delete(row) + class Kick(db.Base): __tablename__ = "kick" diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index 6011a8d21..cc5b75458 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -97,6 +97,12 @@ mute_edited: duration: "The mute duration has been changed from ```\n{}\n```to```\n{}\n```" mute_edited_response: ":white_check_mark: The mute reason has been edited" log_mute_edited: ":mute: Mute edited" +confirm_mute_delete: "Are you sure you want to delete this mute for *{}*?\n`(ID: {})`" +mute_deleted: + inf: "Your mute with the reason ```\n{}\n``` has been deleted" + not_inf: "Your mute for {} with the reason ```\n{}\n``` has been deleted" +mute_deleted_response: ":white_check_mark: The mute has been deleted" +log_mute_deleted: ":loud_sound: Mute deleted" # kicks kick: ":x: Kick" From ebbe3807bf6e9b2f10bd2225e6f313d7af0790a7 Mon Sep 17 00:00:00 2001 From: LoC Date: Sun, 18 Jul 2021 12:06:13 +0200 Subject: [PATCH 027/120] Added a function to delete kicks --- moderation/mod/cog.py | 53 ++++++++++++++++++++++++++++++ moderation/mod/models.py | 5 +++ moderation/mod/translations/en.yml | 4 +++ 3 files changed, 62 insertions(+) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 9aa1dbd6a..772496ae7 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -1160,6 +1160,59 @@ async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.kick, t.log_kick_edited, user, reason) + @commands.command() + @ModPermission.kick.check + @guild_only() + async def delete_kick(self, ctx: Context, kick_id: int): + """ + delete a warn + get the warn id from the users user log + """ + + kick = await db.get(Kick, id=kick_id) + if kick is None: + raise CommandError(t.no_kick) + + if not await compare_mod_level(ctx.author, kick.mod_level): + raise CommandError(tg.permission_denied) + + conf_embed = Embed( + title=t.confirmation, + description=t.confirm_kick_delete(kick.member_name, kick.id), + color=Colors.ModTools + ) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + await Kick.delete(kick_id) + + user = self.bot.get_user(kick.member) + server_embed = Embed(title=t.warn, description=t.kick_deleted_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + if await ModSettings.send_delete_user_message.get(): + user_embed = Embed( + title=t.kick, + description=t.kick_deleted(kick.reason), + colour=Colors.ModTools, + ) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + + await reply(ctx, embed=server_embed) + await send_to_changelog_mod(ctx.guild, ctx.message, Colors.kick, t.log_kick_deleted, user, kick.reason) + @commands.command() @ModPermission.ban.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index 7dafebd46..eb15e56af 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -172,6 +172,11 @@ async def edit(kick_id: int, mod: int, mod_level: int, new_reason: str): row.mod_level = mod_level row.reason = new_reason + @staticmethod + async def delete(kick_id: int): + row = await db.get(Kick, id=kick_id) + await db.delete(row) + class Ban(db.Base): __tablename__ = "ban" diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index cc5b75458..a0afb14ca 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -118,6 +118,10 @@ confirm_kick_edit: "Are you sure you want to change this kicks reason from ```\n kick_edited: "The kick reason has been changed from ```\n{}\n```to```\n{}\n```" kick_edited_reponse: ":white_check_mark: The kick reason has been edited" log_kick_edited: ":x: Kick edited" +confirm_kick_delete: "Are you sure you want to delete this kick of *{}*?\n`(ID: {})`" +kick_deleted: "Your kick with the reason ```\n{}\n``` has been deleted" +kick_deleted_response: ":white_check_mark: The kick has been deleted" +log_kick_deleted: ":x: Kick deleted" # bans ban: ":no_entry: Ban" From 70dea0cb94286a34b4fa6da7b35da43a4c7884f2 Mon Sep 17 00:00:00 2001 From: LoC Date: Sun, 18 Jul 2021 12:21:34 +0200 Subject: [PATCH 028/120] Added a function to delete bans --- moderation/mod/cog.py | 96 ++++++++++++++++++++++++++++-- moderation/mod/models.py | 5 ++ moderation/mod/translations/en.yml | 6 ++ 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 772496ae7..483914ec3 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -931,8 +931,8 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time @guild_only() async def delete_mute(self, ctx: Context, mute_id: int): """ - delete a warn - get the warn id from the users user log + delete a mute + get the mute id from the users user log """ mute = await db.get(Mute, id=mute_id) @@ -1165,8 +1165,8 @@ async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): @guild_only() async def delete_kick(self, ctx: Context, kick_id: int): """ - delete a warn - get the warn id from the users user log + delete a kick + get the kick id from the users user log """ kick = await db.get(Kick, id=kick_id) @@ -1506,6 +1506,94 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: server_embed.colour = Colors.error await reply(ctx, embed=server_embed) + @commands.command() + @ModPermission.ban.check + @guild_only() + async def delete_ban(self, ctx: Context, ban_id: int): + """ + delete a ban + get the ban id from the users user log + """ + + ban = await db.get(Ban, id=ban_id) + if ban is None: + raise CommandError(t.no_ban) + + if not await compare_mod_level(ctx.author, ban.mod_level): + raise CommandError(tg.permission_denied) + + conf_embed = Embed( + title=t.confirmation, + description=t.confirm_ban_delete(ban.member_name, ban.id), + color=Colors.ModTools + ) + + async with confirm(ctx, conf_embed) as (result, msg): + if not result: + conf_embed.description += "\n\n" + t.edit_canceled + return + + conf_embed.description += "\n\n" + t.edit_confirmed + if msg: + await msg.delete(delay=5) + + active_bans: List[Ban] = await db.all(filter_by(Ban, active=True, member=ban.member)) + + if len(active_bans) == 1 and ban in active_bans: + user = ctx.guild.get_member(ban.member) + if user is not None: + try: + await ctx.guild.unban(user, reason="Ban deleted") + except HTTPException: + pass + + user = self.bot.get_user(ban.member) + + await Ban.delete(ban_id) + + server_embed = Embed(title=t.mute, description=t.ban_deleted_response, colour=Colors.ModTools) + server_embed.set_author(name=str(user), icon_url=user.avatar_url) + + if await ModSettings.send_delete_user_message.get(): + user_embed = Embed( + title=t.ban, + colour=Colors.ModTools, + ) + + if ban.minutes == -1: + user_embed.description = t.ban_deleted.inf(ban.reason) + else: + user_embed.description = t.ban_deleted.not_inf(time_to_units(ban.minutes), ban.reason) + + try: + await user.send(embed=user_embed) + except (Forbidden, HTTPException): + server_embed.description = t.no_dm + "\n\n" + server_embed.description + server_embed.colour = Colors.error + + await reply(ctx, embed=server_embed) + + if ban.minutes == -1: + await send_to_changelog_mod( + ctx.guild, + ctx.message, + Colors.ban, + t.log_ban_deleted, + user, + ban.reason, + duration=t.log_field_infinity, + ) + else: + await send_to_changelog_mod( + ctx.guild, + ctx.message, + Colors.ban, + t.log_ban_deleted, + user, + ban.reason, + duration=time_to_units(ban.minutes), + ) + @commands.command() @ModPermission.ban.check @guild_only() diff --git a/moderation/mod/models.py b/moderation/mod/models.py index eb15e56af..26b0da50e 100644 --- a/moderation/mod/models.py +++ b/moderation/mod/models.py @@ -238,3 +238,8 @@ async def edit(ban_id: int, mod: int, mod_level: int, new_reason: str): row.mod = mod row.mod_level = mod_level row.reason = new_reason + + @staticmethod + async def delete(ban_id: int): + row = await db.get(Ban, id=ban_id) + await db.delete(row) diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index a0afb14ca..f662129a7 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -151,6 +151,12 @@ ban_edited: duration: "The ban duration has been changed from ```\n{}\n```to```\n{}\n```" ban_edited_response: ":white_check_mark: The ban reason has been edited" log_ban_edited: ":no_entry: Ban edited" +confirm_ban_delete: "Are you sure you want to delete this ban of *{}*?\n`(ID: {})`" +ban_deleted: + inf: "Your ban with the reason ```\n{}\n``` has been deleted" + not_inf: "Your ban for {} with the reason ```\n{}\n``` has been deleted" +ban_deleted_response: ":white_check_mark: The ban has been deleted" +log_ban_deleted: ":white_check_mark: Ban deleted" reported_cnt: ":speech_balloon: Reported" warned_cnt: ":warning: Warned" From 875d0c12ea58c7bed77865fa585599f2f9eea690 Mon Sep 17 00:00:00 2001 From: LoC Date: Sun, 18 Jul 2021 12:26:57 +0200 Subject: [PATCH 029/120] Fixed not comparing whether the original warn/mute/kick/ban creator is the editor/deleter --- moderation/mod/cog.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 483914ec3..0470231e8 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -553,7 +553,7 @@ async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): if warn is None: raise CommandError(t.no_warn) - if not await compare_mod_level(ctx.author, warn.mod_level): + if not await compare_mod_level(ctx.author, warn.mod_level) or not ctx.author.id == warn.mod: raise CommandError(tg.permission_denied) if len(reason) > 900: @@ -607,7 +607,7 @@ async def delete_warn(self, ctx: Context, warn_id: int): if warn is None: raise CommandError(t.no_warn) - if not await compare_mod_level(ctx.author, warn.mod_level): + if not await compare_mod_level(ctx.author, warn.mod_level) or not ctx.author.id == warn.mod: raise CommandError(tg.permission_denied) conf_embed = Embed( @@ -776,7 +776,7 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): if mute is None: raise CommandError(t.no_mute) - if not await compare_mod_level(ctx.author, mute.mod_level): + if not await compare_mod_level(ctx.author, mute.mod_level) or not ctx.author.id == mute.mod: raise CommandError(tg.permission_denied) if len(reason) > 900: @@ -836,7 +836,7 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time mute = sorted(active_mutes, key=lambda active_mute: active_mute.timestamp)[0] - if not await compare_mod_level(ctx.author, mute.mod_level): + if not await compare_mod_level(ctx.author, mute.mod_level) or not ctx.author.id == mute.mod: raise CommandError(tg.permission_denied) if mute.minutes == minutes or (mute.minutes == -1 and minutes is None): @@ -939,7 +939,7 @@ async def delete_mute(self, ctx: Context, mute_id: int): if mute is None: raise CommandError(t.no_mute) - if not await compare_mod_level(ctx.author, mute.mod_level): + if not await compare_mod_level(ctx.author, mute.mod_level) or not ctx.author.id == mute.mod: raise CommandError(tg.permission_denied) conf_embed = Embed( @@ -1119,7 +1119,7 @@ async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): if kick is None: raise CommandError(t.no_kick) - if not await compare_mod_level(ctx.author, kick.mod_level): + if not await compare_mod_level(ctx.author, kick.mod_level) or not ctx.author.id == kick.mod: raise CommandError(tg.permission_denied) if len(reason) > 900: @@ -1173,7 +1173,7 @@ async def delete_kick(self, ctx: Context, kick_id: int): if kick is None: raise CommandError(t.no_kick) - if not await compare_mod_level(ctx.author, kick.mod_level): + if not await compare_mod_level(ctx.author, kick.mod_level) or not ctx.author.id == kick.mod: raise CommandError(tg.permission_denied) conf_embed = Embed( @@ -1355,7 +1355,7 @@ async def edit_ban_reason(self, ctx: Context, ban_id: int, *, reason: str): if ban is None: raise CommandError(t.no_ban) - if not await compare_mod_level(ctx.author, ban.mod_level): + if not await compare_mod_level(ctx.author, ban.mod_level) or not ctx.author.id == ban.mod: raise CommandError(tg.permission_denied) if len(reason) > 900: @@ -1415,7 +1415,7 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: ban = sorted(active_bans, key=lambda active_ban: active_ban.timestamp)[0] - if not await compare_mod_level(ctx.author, ban.mod_level): + if not await compare_mod_level(ctx.author, ban.mod_level) or not ctx.author.id == ban.mod: raise CommandError(tg.permission_denied) if ban.minutes == minutes or (ban.minutes == -1 and minutes is None): @@ -1519,7 +1519,7 @@ async def delete_ban(self, ctx: Context, ban_id: int): if ban is None: raise CommandError(t.no_ban) - if not await compare_mod_level(ctx.author, ban.mod_level): + if not await compare_mod_level(ctx.author, ban.mod_level) or not ctx.author.id == ban.mod: raise CommandError(tg.permission_denied) conf_embed = Embed( From 26238a150e8296d2aa63336336a9f3ce56946ad9 Mon Sep 17 00:00:00 2001 From: LoC Date: Sun, 18 Jul 2021 12:35:46 +0200 Subject: [PATCH 030/120] Added mod comparison to unmutes/unbans --- moderation/mod/cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 0470231e8..5fa7844fd 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -1033,7 +1033,7 @@ async def unmute(self, ctx: Context, user: UserMemberConverter, *, reason: str): await user.remove_roles(mute_role) async for mute in await db.stream(filter_by(Mute, active=True, member=user.id)): - if not await compare_mod_level(ctx.author, mute.mod_level): + if not await compare_mod_level(ctx.author, mute.mod_level) or not ctx.author.id == mute.mod: raise CommandError(tg.permission_denied) await Mute.deactivate(mute.id, ctx.author.id, reason) @@ -1617,7 +1617,7 @@ async def unban(self, ctx: Context, user: UserMemberConverter, *, reason: str): was_banned = False async for ban in await db.stream(filter_by(Ban, active=True, member=user.id)): - if not await compare_mod_level(ctx.author, ban.mod_level): + if not await compare_mod_level(ctx.author, ban.mod_level) or not ctx.author.id == ban.mod: raise CommandError(tg.permission_denied) await Ban.deactivate(ban.id, ctx.author.id, reason) From a4c6966542364fbd575778a063a16568d5ba1c16 Mon Sep 17 00:00:00 2001 From: LoC Date: Sun, 18 Jul 2021 13:45:48 +0200 Subject: [PATCH 031/120] Added function to put bans initiated through discords ban system into the bots database --- moderation/mod/cog.py | 59 +++++++++++++++++++++++++++--- moderation/mod/translations/en.yml | 4 +- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 5fa7844fd..cf4f9d270 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -3,7 +3,20 @@ from dateutil.relativedelta import relativedelta from typing import Optional, Union, List, Tuple, Generator -from discord import Role, Guild, Member, Forbidden, HTTPException, User, Embed, NotFound, Message, Attachment +from discord import ( + Role, + Guild, + Member, + Forbidden, + HTTPException, + User, + Embed, + NotFound, + Message, + Attachment, + AuditLogAction, + AuditLogEntry, +) from discord.ext import commands, tasks from discord.ext.commands import ( guild_only, @@ -29,6 +42,7 @@ from ...contributor import Contributor from ...pubsub import ( send_to_changelog, + send_alert, log_auto_kick, get_userlog_entries, get_user_info_entries, @@ -401,7 +415,7 @@ async def handle_get_userlog_entries(self, user_id: int, show_ids: bool) -> list else: out.append( ( - ban.timestamp, text.inf.id_off(f"<@{ban.mod}>", ban.reason) + ban.timestamp, text.inf.id_off(f"<@{ban.mod}>", ban.reason, show_evidence(ban.evidence)) ) ) else: @@ -453,6 +467,41 @@ async def on_member_join(self, member: Member): if await db.exists(filter_by(Mute, active=True, member=member.id)): await member.add_roles(mute_role) + async def on_member_ban(self, guild: Guild, member: Member): + try: + entry: AuditLogEntry + async for entry in guild.audit_logs(limit=1, action=AuditLogAction.ban): + if entry.user == self.bot.user: + return + + if entry.reason: + await Ban.create( + entry.target.id, + str(entry.target), + entry.user.id, + await get_mod_level(entry.user), + -1, + entry.reason, + None, + False, + ) + + await send_to_changelog_mod( + guild, + None, + Colors.ban, + t.log_banned, + entry.target, + entry.reason, + duration=t.log_field.infinity, + ) + + else: + await send_alert(guild, t.alert_member_banned(str(entry.target), str(entry.user))) + + except Forbidden: + raise CommandError(t.cannot_fetch_audit_logs) + @commands.command() @ModPermission.modtools_write.check async def send_delete_message(self, ctx: Context, send: bool): @@ -1305,11 +1354,11 @@ async def ban( False, ) if evidence: - user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, reason, + user_embed.description = t.banned_inf.evidence(ctx.author.mention, ctx.guild.name, reason, t.image_link(evidence.filename, evidence_url) ) else: - user_embed.description = t.banned.evidence(ctx.author.mention, ctx.guild.name, reason) + user_embed.description = t.banned_inf.evidence(ctx.author.mention, ctx.guild.name, reason) await send_to_changelog_mod( ctx.guild, @@ -1328,7 +1377,7 @@ async def ban( server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error - # await ctx.guild.ban(user, delete_message_days=delete_days, reason=reason) + await ctx.guild.ban(user, delete_message_days=delete_days, reason=reason) await revoke_verification(user) await reply(ctx, embed=server_embed) diff --git a/moderation/mod/translations/en.yml b/moderation/mod/translations/en.yml index f662129a7..89d10dc9f 100644 --- a/moderation/mod/translations/en.yml +++ b/moderation/mod/translations/en.yml @@ -44,6 +44,7 @@ confirmation: Edit confirmation edit_canceled: ":x: Edit canceled" edit_confirmed: ":white_check_mark: Edit confirmed" infinity: Permanent +cannot_fetch_audit_logs: A ban initiated through the server couldn't be added to the database because I don't have `view_audit_log` permissions on this server. # reports report: ":speech_balloon: Report" @@ -157,6 +158,7 @@ ban_deleted: not_inf: "Your ban for {} with the reason ```\n{}\n``` has been deleted" ban_deleted_response: ":white_check_mark: The ban has been deleted" log_ban_deleted: ":white_check_mark: Ban deleted" +alert_member_banned: "The member {} has been banned through discords ban system by {}" reported_cnt: ":speech_balloon: Reported" warned_cnt: ":warning: Warned" @@ -219,7 +221,7 @@ ulog: id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`{}" inf: id_off: ":no_entry: **Banned** permanently by {} because of `{}`{}" - id_on: ":no_entry: **Banned** by {} for {} because of `{}`\n`(ID: {})`{}" + id_on: ":no_entry: **Banned** by permanently by {} because of `{}`\n`(ID: {})`{}" update: temp: id_off: ":no_entry: **Ban updated** by {} to {} because of `{}`" From ed72339793fa56c1211795429412da98dc9e6ca9 Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 19 Jul 2021 12:32:45 +0200 Subject: [PATCH 032/120] Updated get_userlog_entries pubsub documentation --- pubsub.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pubsub.md b/pubsub.md index dfcc57613..a4c3dc328 100644 --- a/pubsub.md +++ b/pubsub.md @@ -106,12 +106,13 @@ Subscriptions: Use this PubSub channel to get/provide log entries about a user for the user log command. ```python -async def get_userlog_entries(user_id: int) -> list[list[tuple[datetime, str]]] +async def get_userlog_entries(user_id: int, show_ids: bool) -> list[list[tuple[datetime, str]]] ``` Arguments: - `user_id`: The user id +- `show_ids`: whether to put ids from moderation events into the log entry (only relevant for the mod cog) Returns: A list of `(datetime, log_entry)` tuples From 5eb7b4bd82ce36745f7e38cd1a0c1b42809500ab Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 19 Jul 2021 12:42:20 +0200 Subject: [PATCH 033/120] Added command aliases --- moderation/mod/cog.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index cf4f9d270..925c4336e 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -589,7 +589,7 @@ async def warn(self, ctx: Context, user: UserMemberConverter, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warned, user, reason, evidence=evidence) - @commands.command() + @commands.command(aliases=["warn_edit"]) @ModPermission.warn.check @guild_only() async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): @@ -643,7 +643,7 @@ async def edit_warn(self, ctx: Context, warn_id: int, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.warn, t.log_warn_edited, user, reason) - @commands.command() + @commands.command(aliases=["warn_delete"]) @ModPermission.warn.check @guild_only() async def delete_warn(self, ctx: Context, warn_id: int): @@ -803,7 +803,7 @@ async def mute(self, ctx: Context, user: UserMemberConverter, time: DurationConv await reply(ctx, embed=server_embed) - @commands.group() + @commands.group(aliases=["mute_edit"]) @ModPermission.mute.check @guild_only() async def edit_mute(self, ctx): @@ -814,7 +814,7 @@ async def edit_mute(self, ctx): if ctx.invoked_subcommand is None: raise UserInputError - @edit_mute.command(name="reason") + @edit_mute.command(name="reason", aliases=["r"]) async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): """ edit a mute reason @@ -866,7 +866,7 @@ async def edit_mute_reason(self, ctx: Context, mute_id: int, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.mute, t.log_mute_edited, user, reason) - @edit_mute.command(name="duration") + @edit_mute.command(name="duration", aliases=["d"]) async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time: DurationConverter): """ edit a mute duration @@ -975,7 +975,7 @@ async def edit_mute_duration(self, ctx: Context, user: UserMemberConverter, time server_embed.colour = Colors.error await reply(ctx, embed=server_embed) - @commands.command() + @commands.command(aliases=["mute_delete"]) @ModPermission.mute.check @guild_only() async def delete_mute(self, ctx: Context, mute_id: int): @@ -1155,7 +1155,7 @@ async def kick(self, ctx: Context, member: Member, *, reason: str): await reply(ctx, embed=server_embed) - @commands.command() + @commands.command(aliases=["kick_edit"]) @ModPermission.warn.check @guild_only() async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): @@ -1209,7 +1209,7 @@ async def edit_kick(self, ctx: Context, kick_id: int, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.kick, t.log_kick_edited, user, reason) - @commands.command() + @commands.command(aliases=["kick_delete"]) @ModPermission.kick.check @guild_only() async def delete_kick(self, ctx: Context, kick_id: int): @@ -1382,7 +1382,7 @@ async def ban( await reply(ctx, embed=server_embed) - @commands.group() + @commands.group(aliases=["ban_edit"]) @ModPermission.mute.check @guild_only() async def edit_ban(self, ctx): @@ -1393,7 +1393,7 @@ async def edit_ban(self, ctx): if ctx.invoked_subcommand is None: raise UserInputError - @edit_ban.command(name="reason") + @edit_ban.command(name="reason", aliases=["r"]) async def edit_ban_reason(self, ctx: Context, ban_id: int, *, reason: str): """ edit a ban reason @@ -1445,7 +1445,7 @@ async def edit_ban_reason(self, ctx: Context, ban_id: int, *, reason: str): await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.ban, t.log_ban_edited, user, reason) - @edit_ban.command(name="duration") + @edit_ban.command(name="duration", aliases=["d"]) async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: DurationConverter): """ edit a ban duration @@ -1555,7 +1555,7 @@ async def edit_ban_duration(self, ctx: Context, user: UserMemberConverter, time: server_embed.colour = Colors.error await reply(ctx, embed=server_embed) - @commands.command() + @commands.command(aliases=["ban_delete"]) @ModPermission.ban.check @guild_only() async def delete_ban(self, ctx: Context, ban_id: int): From d6b73c19f744f9bcadce952b8093cacddee752d5 Mon Sep 17 00:00:00 2001 From: LoC Date: Mon, 19 Jul 2021 13:52:14 +0200 Subject: [PATCH 034/120] Added mod tools documentation --- moderation/mod/cog.py | 2 +- moderation/mod/documentation.md | 292 ++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 moderation/mod/documentation.md diff --git a/moderation/mod/cog.py b/moderation/mod/cog.py index 925c4336e..4b252b4fb 100644 --- a/moderation/mod/cog.py +++ b/moderation/mod/cog.py @@ -1277,7 +1277,7 @@ async def ban( """ ban a user time format: `ymwdhn` - set ban_days to `inf` for a permanent ban + set time to `inf` for a permanent ban """ time: Optional[int] diff --git a/moderation/mod/documentation.md b/moderation/mod/documentation.md new file mode 100644 index 000000000..31938b138 --- /dev/null +++ b/moderation/mod/documentation.md @@ -0,0 +1,292 @@ +# Mod Tools + +This cog contains commands for server moderation purposes. + +## `report` + +The .report command can be used to report misbehaviour of other server members. + +```css +.report +``` + +|Argument|Required|Description| +|:------:|:------:|:----------| +|`user`|:heavy_check_mark:|The user who should be reported| +|`reason`|:heavy_check_mark:|A description of the users misbehaviour| + +The reason cannot be longer than 900 characters. + +## `warn` + +The .warn command can be used to warn a member because of his misbehaviour. + +```css +.warn +``` + +|Argument|Required|Description| +|:------:|:------:|:----------| +|`user`|:heavy_check_mark:|The user who should be warned| +|`reason`|:heavy_check_mark:|A reason why the user is warned| + +The reason cannot be longer than 900 characters. + + +## `edit_warn` + +The .edit_warn command can be used to edit a warns reason. + +```css +.[edit_warn|warn_edit] +``` + +|Argument|Required|Description| +|:------:|:------:|:----------| +|`user`|:heavy_check_mark:|The id of the warn whose reason should be changed| +|`reason`|:heavy_check_mark:|The new warn reason| + +You can obtain the warn id from a users user log. +The reason cannot be longer than 900 characters. +To perform these changes, you need to be the moderator who created the original warn, have a higher moderation level or be the server owner. + +## `delete_warn` + +The .delete_warn command can be used to delete warns from the database. + +```css +.[delete_warn|warn_delete] +``` + +|Argument|Required|Description| +|:------:|:------:|:----------| +|`user`|:heavy_check_mark:|The id of the warn which should be deleted| + +You can obtain the warn id from a users user log. +To perform these changes, you need to be the moderator who created the original warn, have a higher moderation level or be the server owner. + +## `mute` + +The .mute command can be used to give a member a formerly configured mute role. + +```css +.mute