Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 74 additions & 132 deletions bot/cogs/moderation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from bot.pagination import LinePaginator
from bot.utils.moderation import already_has_active_infraction, post_infraction
from bot.utils.scheduling import Scheduler, create_task
from bot.utils.time import wait_until
from bot.utils.time import INFRACTION_FORMAT, format_infraction, wait_until

log = logging.getLogger(__name__)

Expand All @@ -44,6 +44,15 @@ def proxy_user(user_id: str) -> Object:
return user


def permanent_duration(expires_at: str) -> str:
"""Only allow an expiration to be 'permanent' if it is a string."""
expires_at = expires_at.lower()
if expires_at != "permanent":
raise BadArgument
else:
return expires_at


UserTypes = Union[Member, User, proxy_user]


Expand Down Expand Up @@ -241,11 +250,7 @@ async def tempmute(self, ctx: Context, user: Member, duration: Duration, *, reas
reason=reason
)

infraction_expiration = (
datetime
.fromisoformat(infraction["expires_at"][:-1])
.strftime('%c')
)
infraction_expiration = format_infraction(infraction["expires_at"])

self.schedule_task(ctx.bot.loop, infraction["id"], infraction)

Expand Down Expand Up @@ -314,11 +319,7 @@ async def tempban(self, ctx: Context, user: UserTypes, duration: Duration, *, re
except Forbidden:
action_result = False

infraction_expiration = (
datetime
.fromisoformat(infraction["expires_at"][:-1])
.strftime('%c')
)
infraction_expiration = format_infraction(infraction["expires_at"])

self.schedule_task(ctx.bot.loop, infraction["id"], infraction)

Expand Down Expand Up @@ -505,11 +506,7 @@ async def shadow_tempmute(
self.mod_log.ignore(Event.member_update, user.id)
await user.add_roles(self._muted_role, reason=reason)

infraction_expiration = (
datetime
.fromisoformat(infraction["expires_at"][:-1])
.strftime('%c')
)
infraction_expiration = format_infraction(infraction["expires_at"])
self.schedule_task(ctx.bot.loop, infraction["id"], infraction)
await ctx.send(f":ok_hand: muted {user.mention} until {infraction_expiration}.")

Expand Down Expand Up @@ -562,11 +559,7 @@ async def shadow_tempban(
except Forbidden:
action_result = False

infraction_expiration = (
datetime
.fromisoformat(infraction["expires_at"][:-1])
.strftime('%c')
)
infraction_expiration = format_infraction(infraction["expires_at"])
self.schedule_task(ctx.bot.loop, infraction["id"], infraction)
await ctx.send(f":ok_hand: banned {user.mention} until {infraction_expiration}.")

Expand Down Expand Up @@ -745,121 +738,72 @@ async def infraction_group(self, ctx: Context) -> None:
await ctx.invoke(self.bot.get_command("help"), "infraction")

@with_role(*MODERATION_ROLES)
@infraction_group.group(name='edit', invoke_without_command=True)
async def infraction_edit_group(self, ctx: Context) -> None:
"""Infraction editing commands."""
await ctx.invoke(self.bot.get_command("help"), "infraction", "edit")

@with_role(*MODERATION_ROLES)
@infraction_edit_group.command(name="duration")
async def edit_duration(
self, ctx: Context,
infraction_id: int, expires_at: Union[Duration, str]
@infraction_group.command(name='edit')
async def infraction_edit(
self,
ctx: Context,
infraction_id: int,
expires_at: Union[Duration, permanent_duration, None],
Comment thread
MarkKoz marked this conversation as resolved.
*,
reason: str = None
) -> None:
"""
Sets the duration of the given infraction, relative to the time of updating.
Edit the duration and/or the reason of an infraction.

Duration strings are parsed per: http://strftime.org/, use "permanent" to mark the infraction as permanent.
Durations are relative to the time of updating.
Use "permanent" to mark the infraction as permanent.
"""
if isinstance(expires_at, str) and expires_at != 'permanent':
raise BadArgument(
"If `expires_at` is given as a non-datetime, "
"it must be `permanent`."
)
if expires_at == 'permanent':
expires_at = None

try:
previous_infraction = await self.bot.api_client.get(
'bot/infractions/' + str(infraction_id)
)

# check the current active infraction
infraction = await self.bot.api_client.patch(
'bot/infractions/' + str(infraction_id),
json={
'expires_at': (
expires_at.isoformat()
if expires_at is not None
else None
)
}
)

# Re-schedule
self.cancel_task(infraction['id'])
loop = asyncio.get_event_loop()
self.schedule_task(loop, infraction['id'], infraction)

if expires_at is None:
await ctx.send(f":ok_hand: Updated infraction: marked as permanent.")
else:
human_expiry = (
datetime
.fromisoformat(infraction['expires_at'][:-1])
.strftime('%c')
)
await ctx.send(
":ok_hand: Updated infraction: set to expire on "
f"{human_expiry}."
)

except Exception:
log.exception("There was an error updating an infraction.")
await ctx.send(":x: There was an error updating the infraction.")
return

# Get information about the infraction's user
user_id = infraction["user"]
user = ctx.guild.get_member(user_id)

if user:
member_text = f"{user.mention} (`{user.id}`)"
thumbnail = user.avatar_url_as(static_format="png")
if expires_at is None and reason is None:
# Unlike UserInputError, the error handler will show a specified message for BadArgument
raise BadArgument("Neither a new expiry nor a new reason was specified.")

# Retrieve the previous infraction for its information.
old_infraction = await self.bot.api_client.get(f'bot/infractions/{infraction_id}')

request_data = {}
confirm_messages = []
log_text = ""

if expires_at == "permanent":
request_data['expires_at'] = None
confirm_messages.append("marked as permanent")
elif expires_at is not None:
request_data['expires_at'] = expires_at.isoformat()
confirm_messages.append(f"set to expire on {expires_at.strftime(INFRACTION_FORMAT)}")
else:
member_text = f"`{user_id}`"
thumbnail = None
confirm_messages.append("expiry unchanged")

# The infraction's actor
actor_id = infraction["actor"]
actor = ctx.guild.get_member(actor_id) or f"`{actor_id}`"
if reason:
request_data['reason'] = reason
confirm_messages.append("set a new reason")
log_text += f"""
Previous reason: {old_infraction['reason']}
New reason: {reason}
""".rstrip()
else:
confirm_messages.append("reason unchanged")

await self.mod_log.send_log_message(
icon_url=Icons.pencil,
colour=Colour.blurple(),
title="Infraction edited",
thumbnail=thumbnail,
text=textwrap.dedent(f"""
Member: {member_text}
Actor: {actor}
Edited by: {ctx.message.author}
Previous expiry: {previous_infraction['expires_at']}
New expiry: {infraction['expires_at']}
""")
# Update the infraction
new_infraction = await self.bot.api_client.patch(
f'bot/infractions/{infraction_id}',
json=request_data,
)

@with_role(*MODERATION_ROLES)
@infraction_edit_group.command(name="reason")
async def edit_reason(self, ctx: Context, infraction_id: int, *, reason: str) -> None:
"""Edit the reason of the given infraction."""
try:
old_infraction = await self.bot.api_client.get(
'bot/infractions/' + str(infraction_id)
)
# Re-schedule infraction if the expiration has been updated
if 'expires_at' in request_data:
self.cancel_task(new_infraction['id'])
loop = asyncio.get_event_loop()
self.schedule_task(loop, new_infraction['id'], new_infraction)

updated_infraction = await self.bot.api_client.patch(
'bot/infractions/' + str(infraction_id),
json={'reason': reason}
)
await ctx.send(f":ok_hand: Updated infraction: set reason to \"{reason}\".")
log_text += f"""
Previous expiry: {old_infraction['expires_at'] or "Permanent"}
New expiry: {new_infraction['expires_at'] or "Permanent"}
""".rstrip()

except Exception:
log.exception("There was an error updating an infraction.")
await ctx.send(":x: There was an error updating the infraction.")
return
await ctx.send(f":ok_hand: Updated infraction: {' & '.join(confirm_messages)}")

# Get information about the infraction's user
user_id = updated_infraction['user']
user_id = new_infraction['user']
user = ctx.guild.get_member(user_id)

if user:
Expand All @@ -870,7 +814,7 @@ async def edit_reason(self, ctx: Context, infraction_id: int, *, reason: str) ->
thumbnail = None

# The infraction's actor
actor_id = updated_infraction['actor']
actor_id = new_infraction['actor']
actor = ctx.guild.get_member(actor_id) or f"`{actor_id}`"

await self.mod_log.send_log_message(
Expand All @@ -881,9 +825,7 @@ async def edit_reason(self, ctx: Context, infraction_id: int, *, reason: str) ->
text=textwrap.dedent(f"""
Member: {user_text}
Actor: {actor}
Edited by: {ctx.message.author}
Previous reason: {old_infraction['reason']}
New reason: {updated_infraction['reason']}
Edited by: {ctx.message.author}{log_text}
""")
)

Expand Down Expand Up @@ -1041,11 +983,11 @@ def _infraction_to_string(self, infraction_object: Dict[str, Union[str, int, boo
active = infraction_object["active"]
user_id = infraction_object["user"]
hidden = infraction_object["hidden"]
created = datetime.fromisoformat(infraction_object["inserted_at"][:-1]).strftime("%Y-%m-%d %H:%M")
created = format_infraction(infraction_object["inserted_at"])
if infraction_object["expires_at"] is None:
expires = "*Permanent*"
else:
expires = datetime.fromisoformat(infraction_object["expires_at"][:-1]).strftime("%Y-%m-%d %H:%M")
expires = format_infraction(infraction_object["expires_at"])

lines = textwrap.dedent(f"""
{"**===============**" if active else "==============="}
Expand Down Expand Up @@ -1076,7 +1018,7 @@ async def notify_infraction(
Returns a boolean indicator of whether the DM was successful.
"""
if isinstance(expires_at, datetime):
expires_at = expires_at.strftime('%c')
expires_at = expires_at.strftime(INFRACTION_FORMAT)

embed = Embed(
description=textwrap.dedent(f"""
Expand Down Expand Up @@ -1152,8 +1094,8 @@ async def log_notify_failure(self, target: str, actor: Member, infraction_type:

# endregion

@staticmethod
async def cog_command_error(ctx: Context, error: Exception) -> None:
# This cannot be static (must have a __func__ attribute).
async def cog_command_error(self, ctx: Context, error: Exception) -> None:
"""Send a notification to the invoking context on a Union failure."""
if isinstance(error, BadUnionArgument):
if User in error.converters:
Expand Down
11 changes: 3 additions & 8 deletions bot/cogs/superstarify/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
import random
from datetime import datetime

from discord import Colour, Embed, Member
from discord.errors import Forbidden
Expand All @@ -13,6 +12,7 @@
from bot.converters import Duration
from bot.decorators import with_role
from bot.utils.moderation import post_infraction
from bot.utils.time import format_infraction

log = logging.getLogger(__name__)
NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#wiki-toc-nickname-policy"
Expand Down Expand Up @@ -71,10 +71,7 @@ async def on_member_update(self, before: Member, after: Member) -> None:
f"Changing the nick back to {before.display_name}."
)
await after.edit(nick=forced_nick)
end_timestamp_human = (
datetime.fromisoformat(infraction['expires_at'][:-1])
.strftime('%c')
)
end_timestamp_human = format_infraction(infraction['expires_at'])

try:
await after.send(
Expand Down Expand Up @@ -113,9 +110,7 @@ async def on_member_join(self, member: Member) -> None:
[infraction] = active_superstarifies
forced_nick = get_nick(infraction['id'], member.id)
await member.edit(nick=forced_nick)
end_timestamp_human = (
datetime.fromisoformat(infraction['expires_at'][:-1]).strftime('%c')
)
end_timestamp_human = format_infraction(infraction['expires_at'])

try:
await member.send(
Expand Down
4 changes: 2 additions & 2 deletions bot/cogs/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ async def unsubscribe_command(self, ctx: Context, *_) -> None: # We don't actua
f"{ctx.author.mention} Unsubscribed from <#{Channels.announcements}> notifications."
)

@staticmethod
async def cog_command_error(ctx: Context, error: Exception) -> None:
# This cannot be static (must have a __func__ attribute).
async def cog_command_error(self, ctx: Context, error: Exception) -> None:
"""Check for & ignore any InChannelCheckFailure."""
if isinstance(error, InChannelCheckFailure):
error.handled = True
Expand Down
5 changes: 3 additions & 2 deletions bot/cogs/watchchannels/talentpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from bot.constants import Channels, Guild, Roles, Webhooks
from bot.decorators import with_role
from bot.pagination import LinePaginator
from bot.utils import time
from .watchchannel import WatchChannel, proxy_user

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -198,7 +199,7 @@ def _nomination_to_string(self, nomination_object: dict) -> str:
log.debug(active)
log.debug(type(nomination_object["inserted_at"]))

start_date = self._get_human_readable(nomination_object["inserted_at"])
start_date = time.format_infraction(nomination_object["inserted_at"])
if active:
lines = textwrap.dedent(
f"""
Expand All @@ -212,7 +213,7 @@ def _nomination_to_string(self, nomination_object: dict) -> str:
"""
)
else:
end_date = self._get_human_readable(nomination_object["ended_at"])
end_date = time.format_infraction(nomination_object["ended_at"])
lines = textwrap.dedent(
f"""
===============
Expand Down
Loading