aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Johannes Christ <[email protected]>2018-07-27 23:17:18 +0000
committerGravatar Johannes Christ <[email protected]>2018-07-27 23:17:18 +0000
commitc51715cee57bc3b1fe9ff0f1e93bb2ad5e18f81f (patch)
treefc0469dd3ec88cd72072131f30c6d206d3100334
parentIcon updates (diff)
parentRefactor moderation cog with namespace changes (diff)
Merge branch 'feature/rowboat-replacement' into 'master'
Rowboat moderation replacement See merge request python-discord/projects/bot!28
-rw-r--r--bot/__main__.py1
-rw-r--r--bot/cogs/moderation.py672
-rw-r--r--bot/constants.py6
-rw-r--r--bot/converters.py17
-rw-r--r--bot/pagination.py8
-rw-r--r--config-default.yml32
6 files changed, 722 insertions, 14 deletions
diff --git a/bot/__main__.py b/bot/__main__.py
index 4429c2a0d..b9e6001ac 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -60,6 +60,7 @@ bot.load_extension("bot.cogs.doc")
bot.load_extension("bot.cogs.eval")
bot.load_extension("bot.cogs.fun")
bot.load_extension("bot.cogs.hiphopify")
+bot.load_extension("bot.cogs.moderation")
bot.load_extension("bot.cogs.off_topic_names")
bot.load_extension("bot.cogs.snakes")
bot.load_extension("bot.cogs.snekbox")
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
new file mode 100644
index 000000000..fdb3b67f8
--- /dev/null
+++ b/bot/cogs/moderation.py
@@ -0,0 +1,672 @@
+import asyncio
+import datetime
+import logging
+from typing import Dict
+
+from aiohttp import ClientError
+from discord import Colour, Embed, Guild, Member, Object, User
+from discord.ext.commands import Bot, Context, command, group
+
+from bot import constants
+from bot.constants import Keys, Roles, URLs
+from bot.converters import InfractionSearchQuery
+from bot.decorators import with_role
+from bot.pagination import LinePaginator
+
+log = logging.getLogger(__name__)
+
+MODERATION_ROLES = Roles.owner, Roles.admin, Roles.moderator
+
+
+class Moderation:
+ """
+ Rowboat replacement moderation tools.
+ """
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.headers = {"X-API-KEY": Keys.site_api}
+ self.expiration_tasks: Dict[str, asyncio.Task] = {}
+ self._muted_role = Object(constants.Roles.muted)
+
+ async def on_ready(self):
+ # Schedule expiration for previous infractions
+ response = await self.bot.http_session.get(
+ URLs.site_infractions,
+ params={"dangling": "true"},
+ headers=self.headers
+ )
+ infraction_list = await response.json()
+ loop = asyncio.get_event_loop()
+ for infraction_object in infraction_list:
+ if infraction_object["expires_at"] is not None:
+ self.schedule_expiration(loop, infraction_object)
+
+ # region: Permanent infractions
+
+ @with_role(*MODERATION_ROLES)
+ @command(name="warn")
+ async def warn(self, ctx: Context, user: User, reason: str = None):
+ """
+ Create a warning infraction in the database for a user.
+ :param user: accepts user mention, ID, etc.
+ :param reason: the reason for the warning. Wrap in string quotes for multiple words.
+ """
+
+ try:
+ response = await self.bot.http_session.post(
+ URLs.site_infractions,
+ headers=self.headers,
+ json={
+ "type": "warning",
+ "reason": reason,
+ "user_id": str(user.id),
+ "actor_id": str(ctx.message.author.id)
+ }
+ )
+ except ClientError:
+ log.exception("There was an error adding an infraction.")
+ await ctx.send(":x: There was an error adding the infraction.")
+ return
+
+ response_object = await response.json()
+ if "error_code" in response_object:
+ await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}")
+ return
+
+ if reason is None:
+ result_message = f":ok_hand: warned {user.mention}."
+ else:
+ result_message = f":ok_hand: warned {user.mention} ({reason})."
+
+ await ctx.send(result_message)
+
+ @with_role(*MODERATION_ROLES)
+ @command(name="kick")
+ async def kick(self, ctx, user: Member, reason: str = None):
+ """
+ Kicks a user.
+ :param user: accepts user mention, ID, etc.
+ :param reason: the reason for the kick. Wrap in string quotes for multiple words.
+ """
+
+ try:
+ response = await self.bot.http_session.post(
+ URLs.site_infractions,
+ headers=self.headers,
+ json={
+ "type": "kick",
+ "reason": reason,
+ "user_id": str(user.id),
+ "actor_id": str(ctx.message.author.id)
+ }
+ )
+ except ClientError:
+ log.exception("There was an error adding an infraction.")
+ await ctx.send(":x: There was an error adding the infraction.")
+ return
+
+ response_object = await response.json()
+ if "error_code" in response_object:
+ await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}")
+ return
+
+ await user.kick(reason=reason)
+
+ if reason is None:
+ result_message = f":ok_hand: kicked {user.mention}."
+ else:
+ result_message = f":ok_hand: kicked {user.mention} ({reason})."
+
+ await ctx.send(result_message)
+
+ @with_role(*MODERATION_ROLES)
+ @command(name="ban")
+ async def ban(self, ctx: Context, user: User, reason: str = None):
+ """
+ Create a permanent ban infraction in the database for a user.
+ :param user: Accepts user mention, ID, etc.
+ :param reason: Wrap in quotes to make reason larger than one word.
+ """
+
+ try:
+ response = await self.bot.http_session.post(
+ URLs.site_infractions,
+ headers=self.headers,
+ json={
+ "type": "ban",
+ "reason": reason,
+ "user_id": str(user.id),
+ "actor_id": str(ctx.message.author.id)
+ }
+ )
+ except ClientError:
+ log.exception("There was an error adding an infraction.")
+ await ctx.send(":x: There was an error adding the infraction.")
+ return
+
+ response_object = await response.json()
+ if "error_code" in response_object:
+ await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}")
+ return
+
+ await ctx.guild.ban(user, reason=reason)
+
+ if reason is None:
+ result_message = f":ok_hand: permanently banned {user.mention}."
+ else:
+ result_message = f":ok_hand: permanently banned {user.mention} ({reason})."
+
+ await ctx.send(result_message)
+
+ @with_role(*MODERATION_ROLES)
+ @command(name="mute")
+ async def mute(self, ctx: Context, user: Member, reason: str = None):
+ """
+ Create a permanent mute infraction in the database for a user.
+ :param user: Accepts user mention, ID, etc.
+ :param reason: Wrap in quotes to make reason larger than one word.
+ """
+
+ try:
+ response = await self.bot.http_session.post(
+ URLs.site_infractions,
+ headers=self.headers,
+ json={
+ "type": "mute",
+ "reason": reason,
+ "user_id": str(user.id),
+ "actor_id": str(ctx.message.author.id)
+ }
+ )
+ except ClientError:
+ log.exception("There was an error adding an infraction.")
+ await ctx.send(":x: There was an error adding the infraction.")
+ return
+
+ response_object = await response.json()
+ if "error_code" in response_object:
+ await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}")
+ return
+
+ # add the mute role
+ await user.add_roles(self._muted_role, reason=reason)
+
+ if reason is None:
+ result_message = f":ok_hand: permanently muted {user.mention}."
+ else:
+ result_message = f":ok_hand: permanently muted {user.mention} ({reason})."
+
+ await ctx.send(result_message)
+
+ # endregion
+ # region: Temporary infractions
+
+ @with_role(*MODERATION_ROLES)
+ @command(name="tempmute")
+ async def tempmute(self, ctx: Context, user: Member, duration: str, reason: str = None):
+ """
+ Create a temporary mute infraction in the database for a user.
+ :param user: Accepts user mention, ID, etc.
+ :param duration: The duration for the temporary mute infraction
+ :param reason: Wrap in quotes to make reason larger than one word.
+ """
+
+ try:
+ response = await self.bot.http_session.post(
+ URLs.site_infractions,
+ headers=self.headers,
+ json={
+ "type": "mute",
+ "reason": reason,
+ "duration": duration,
+ "user_id": str(user.id),
+ "actor_id": str(ctx.message.author.id)
+ }
+ )
+ except ClientError:
+ log.exception("There was an error adding an infraction.")
+ await ctx.send(":x: There was an error adding the infraction.")
+ return
+
+ response_object = await response.json()
+ if "error_code" in response_object:
+ await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}")
+ return
+
+ await user.add_roles(self._muted_role, reason=reason)
+
+ infraction_object = response_object["infraction"]
+ infraction_expiration = infraction_object["expires_at"]
+
+ loop = asyncio.get_event_loop()
+ self.schedule_expiration(loop, infraction_object)
+
+ if reason is None:
+ result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}."
+ else:
+ result_message = f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})."
+
+ await ctx.send(result_message)
+
+ @with_role(*MODERATION_ROLES)
+ @command(name="tempban")
+ async def tempban(self, ctx, user: User, duration: str, reason: str = None):
+ """
+ Create a temporary ban infraction in the database for a user.
+ :param user: Accepts user mention, ID, etc.
+ :param duration: The duration for the temporary ban infraction
+ :param reason: Wrap in quotes to make reason larger than one word.
+ """
+
+ try:
+ response = await self.bot.http_session.post(
+ URLs.site_infractions,
+ headers=self.headers,
+ json={
+ "type": "ban",
+ "reason": reason,
+ "duration": duration,
+ "user_id": str(user.id),
+ "actor_id": str(ctx.message.author.id)
+ }
+ )
+ except ClientError:
+ log.exception("There was an error adding an infraction.")
+ await ctx.send(":x: There was an error adding the infraction.")
+ return
+
+ response_object = await response.json()
+ if "error_code" in response_object:
+ await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}")
+ return
+
+ guild: Guild = ctx.guild
+ await guild.ban(user, reason=reason)
+
+ infraction_object = response_object["infraction"]
+ infraction_expiration = infraction_object["expires_at"]
+
+ loop = asyncio.get_event_loop()
+ self.schedule_expiration(loop, infraction_object)
+
+ if reason is None:
+ result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}."
+ else:
+ result_message = f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})."
+
+ await ctx.send(result_message)
+
+ # endregion
+ # region: Remove infractions (un- commands)
+
+ @with_role(*MODERATION_ROLES)
+ @command(name="unmute")
+ async def unmute(self, ctx, user: Member):
+ """
+ Deactivates the active mute infraction for a user.
+ :param user: Accepts user mention, ID, etc.
+ """
+
+ try:
+ # check the current active infraction
+ response = await self.bot.http_session.get(
+ URLs.site_infractions_user_type_current.format(
+ user_id=user.id,
+ infraction_type="mute"
+ ),
+ headers=self.headers
+ )
+ response_object = await response.json()
+ if "error_code" in response_object:
+ await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}")
+ return
+
+ infraction_object = response_object["infraction"]
+ if infraction_object is None:
+ # no active infraction
+ await ctx.send(f":x: There is no active mute infraction for user {user.mention}.")
+ return
+
+ await self._deactivate_infraction(infraction_object)
+ if infraction_object["expires_at"] is not None:
+ self.cancel_expiration(infraction_object["id"])
+
+ await ctx.send(f":ok_hand: Un-muted {user.mention}.")
+ except Exception:
+ log.exception("There was an error removing an infraction.")
+ await ctx.send(":x: There was an error removing the infraction.")
+ return
+
+ @with_role(*MODERATION_ROLES)
+ @command(name="unban")
+ async def unban(self, ctx, user: User):
+ """
+ Deactivates the active ban infraction for a user.
+ :param user: Accepts user mention, ID, etc.
+ """
+
+ try:
+ # check the current active infraction
+ response = await self.bot.http_session.get(
+ URLs.site_infractions_user_type_current.format(
+ user_id=user.id,
+ infraction_type="ban"
+ ),
+ headers=self.headers
+ )
+ response_object = await response.json()
+ if "error_code" in response_object:
+ await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}")
+ return
+
+ infraction_object = response_object["infraction"]
+ if infraction_object is None:
+ # no active infraction
+ await ctx.send(f":x: There is no active ban infraction for user {user.mention}.")
+ return
+
+ await self._deactivate_infraction(infraction_object)
+ if infraction_object["expires_at"] is not None:
+ self.cancel_expiration(infraction_object["id"])
+
+ await ctx.send(f":ok_hand: Un-banned {user.mention}.")
+ except Exception:
+ log.exception("There was an error removing an infraction.")
+ await ctx.send(":x: There was an error removing the infraction.")
+ return
+
+ # endregion
+ # region: Edit infraction commands
+
+ @with_role(*MODERATION_ROLES)
+ @group(name='infraction', aliases=('infr',))
+ async def infraction_group(self, ctx: Context):
+ """Infraction manipulation commands."""
+
+ @with_role(*MODERATION_ROLES)
+ @infraction_group.group(name='edit')
+ async def infraction_edit_group(self, ctx: Context):
+ """Infraction editing commands."""
+
+ @with_role(*MODERATION_ROLES)
+ @infraction_edit_group.command(name="duration")
+ async def edit_duration(self, ctx, infraction_id: str, duration: str):
+ """
+ Sets the duration of the given infraction, relative to the time of updating.
+ :param infraction_id: the id (UUID) of the infraction
+ :param duration: the new duration of the infraction, relative to the time of updating. Use "permanent" to mark
+ the infraction as permanent.
+ """
+
+ try:
+ if duration == "permanent":
+ duration = None
+ # check the current active infraction
+ response = await self.bot.http_session.patch(
+ URLs.site_infractions,
+ json={
+ "id": infraction_id,
+ "duration": duration
+ },
+ headers=self.headers
+ )
+ response_object = await response.json()
+ if "error_code" in response_object or response_object.get("success") is False:
+ await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}")
+ return
+
+ infraction_object = response_object["infraction"]
+ # Re-schedule
+ self.cancel_expiration(infraction_id)
+ loop = asyncio.get_event_loop()
+ self.schedule_expiration(loop, infraction_object)
+
+ if duration is None:
+ await ctx.send(f":ok_hand: Updated infraction: marked as permanent.")
+ else:
+ await ctx.send(f":ok_hand: Updated infraction: set to expire on {infraction_object['expires_at']}.")
+
+ except Exception:
+ log.exception("There was an error updating an infraction.")
+ await ctx.send(":x: There was an error updating the infraction.")
+ return
+
+ @with_role(*MODERATION_ROLES)
+ @infraction_edit_group.command(name="reason")
+ async def edit_reason(self, ctx, infraction_id: str, reason: str):
+ """
+ Sets the reason of the given infraction.
+ :param infraction_id: the id (UUID) of the infraction
+ :param reason: the new reason of the infraction
+ """
+
+ try:
+ response = await self.bot.http_session.patch(
+ URLs.site_infractions,
+ json={
+ "id": infraction_id,
+ "reason": reason
+ },
+ headers=self.headers
+ )
+ response_object = await response.json()
+ if "error_code" in response_object or response_object.get("success") is False:
+ await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}")
+ return
+
+ await ctx.send(f":ok_hand: Updated infraction: set reason to \"{reason}\".")
+ except Exception:
+ log.exception("There was an error updating an infraction.")
+ await ctx.send(":x: There was an error updating the infraction.")
+ return
+
+ # endregion
+ # region: Search infractions
+
+ @with_role(*MODERATION_ROLES)
+ @infraction_group.command(name="search")
+ async def search(self, ctx, arg: InfractionSearchQuery):
+ """
+ Searches for infractions in the database.
+ :param arg: Either a user or a reason string. If a string, you can use the Re2 matching syntax.
+ """
+
+ if isinstance(arg, User):
+ user: User = arg
+ # get infractions for this user
+ try:
+ response = await self.bot.http_session.get(
+ URLs.site_infractions_user.format(
+ user_id=user.id
+ ),
+ headers=self.headers
+ )
+ infraction_list = await response.json()
+ except ClientError:
+ log.exception("There was an error fetching infractions.")
+ await ctx.send(":x: There was an error fetching infraction.")
+ return
+
+ if not infraction_list:
+ await ctx.send(f":warning: No infractions found for {user}.")
+ return
+
+ embed = Embed(
+ title=f"Infractions for {user} ({len(infraction_list)} total)",
+ colour=Colour.orange()
+ )
+
+ elif isinstance(arg, str):
+ # search by reason
+ try:
+ response = await self.bot.http_session.get(
+ URLs.site_infractions,
+ headers=self.headers,
+ params={"search": arg}
+ )
+ infraction_list = await response.json()
+ except ClientError:
+ log.exception("There was an error fetching infractions.")
+ await ctx.send(":x: There was an error fetching infraction.")
+ return
+
+ if not infraction_list:
+ await ctx.send(f":warning: No infractions matching `{arg}`.")
+ return
+
+ embed = Embed(
+ title=f"Infractions matching `{arg}` ({len(infraction_list)} total)",
+ colour=Colour.orange()
+ )
+
+ else:
+ await ctx.send(":x: Invalid infraction search query.")
+ return
+
+ await LinePaginator.paginate(
+ lines=(
+ self._infraction_to_string(infraction_object, show_user=isinstance(arg, str))
+ for infraction_object in infraction_list
+ ),
+ ctx=ctx,
+ embed=embed,
+ empty=True,
+ max_lines=3,
+ max_size=1000
+ )
+
+ # endregion
+ # region: Utility functions
+
+ def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict):
+ """
+ Schedules a task to expire a temporary infraction.
+ :param loop: the asyncio event loop
+ :param infraction_object: the infraction object to expire at the end of the task
+ """
+
+ infraction_id = infraction_object["id"]
+ if infraction_id in self.expiration_tasks:
+ return
+
+ task: asyncio.Task = asyncio.ensure_future(self._scheduled_expiration(infraction_object), loop=loop)
+
+ # Silently ignore exceptions in a callback (handles the CancelledError nonsense)
+ task.add_done_callback(_silent_exception)
+
+ self.expiration_tasks[infraction_id] = task
+
+ def cancel_expiration(self, infraction_id: str):
+ """
+ Un-schedules a task set to expire a temporary infraction.
+ :param infraction_id: the ID of the infraction in question
+ """
+
+ task = self.expiration_tasks.get(infraction_id)
+ if task is None:
+ log.warning(f"Failed to unschedule {infraction_id}: no task found.")
+ return
+ task.cancel()
+ log.debug(f"Unscheduled {infraction_id}.")
+ del self.expiration_tasks[infraction_id]
+
+ async def _scheduled_expiration(self, infraction_object):
+ """
+ A co-routine which marks an infraction as expired after the delay from the time of scheduling
+ to the time of expiration. At the time of expiration, the infraction is marked as inactive on the website,
+ and the expiration task is cancelled.
+ :param infraction_object: the infraction in question
+ """
+
+ infraction_id = infraction_object["id"]
+
+ # transform expiration to delay in seconds
+ expiration_datetime = parse_rfc1123(infraction_object["expires_at"])
+ delay = expiration_datetime - datetime.datetime.now(tz=datetime.timezone.utc)
+ delay_seconds = delay.total_seconds()
+
+ if delay_seconds > 1.0:
+ log.debug(f"Scheduling expiration for infraction {infraction_id} in {delay_seconds} seconds")
+ await asyncio.sleep(delay_seconds)
+
+ log.debug(f"Marking infraction {infraction_id} as inactive (expired).")
+ await self._deactivate_infraction(infraction_object)
+
+ self.cancel_expiration(infraction_object["id"])
+
+ async def _deactivate_infraction(self, infraction_object):
+ """
+ A co-routine which marks an infraction as inactive on the website. This co-routine does not cancel or
+ un-schedule an expiration task.
+ :param infraction_object: the infraction in question
+ """
+ guild: Guild = self.bot.get_guild(constants.Guild.id)
+ user_id = int(infraction_object["user"]["user_id"])
+ infraction_type = infraction_object["type"]
+
+ if infraction_type == "mute":
+ member: Member = guild.get_member(user_id)
+ if member:
+ # remove the mute role
+ await member.remove_roles(self._muted_role)
+ else:
+ log.warning(f"Failed to un-mute user: {user_id} (not found)")
+ elif infraction_type == "ban":
+ user: User = self.bot.get_user(user_id)
+ await guild.unban(user)
+
+ await self.bot.http_session.patch(
+ URLs.site_infractions,
+ headers=self.headers,
+ json={
+ "id": infraction_object["id"],
+ "active": False
+ }
+ )
+
+ def _infraction_to_string(self, infraction_object, show_user=False):
+ actor_id = int(infraction_object["actor"]["user_id"])
+ guild: Guild = self.bot.get_guild(constants.Guild.id)
+ actor = guild.get_member(actor_id)
+ active = infraction_object["active"] is True
+
+ lines = [
+ "**===============**" if active else "===============",
+ "Status: {0}".format("__**Active**__" if active else "Inactive"),
+ "Type: **{0}**".format(infraction_object["type"]),
+ "Reason: {0}".format(infraction_object["reason"] or "*None*"),
+ "Created: {0}".format(infraction_object["inserted_at"]),
+ "Expires: {0}".format(infraction_object["expires_at"] or "*Permanent*"),
+ "Actor: {0}".format(actor.mention if actor else actor_id),
+ "ID: `{0}`".format(infraction_object["id"]),
+ "**===============**" if active else "==============="
+ ]
+
+ if show_user:
+ user_id = int(infraction_object["user"]["user_id"])
+ user = self.bot.get_user(user_id)
+ lines.insert(1, "User: {0}".format(user.mention if user else user_id))
+
+ return "\n".join(lines)
+
+ # endregion
+
+
+RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
+
+
+def parse_rfc1123(time_str):
+ return datetime.datetime.strptime(time_str, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc)
+
+
+def _silent_exception(future):
+ try:
+ future.exception()
+ except Exception:
+ pass
+
+
+def setup(bot):
+ bot.add_cog(Moderation(bot))
+ log.info("Cog loaded: Moderation")
diff --git a/bot/constants.py b/bot/constants.py
index adfd5d014..205b09111 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -286,6 +286,7 @@ class Roles(metaclass=YAMLGetter):
moderator: int
owner: int
verified: int
+ muted: int
class Guild(metaclass=YAMLGetter):
@@ -341,6 +342,11 @@ class URLs(metaclass=YAMLGetter):
site_tags_api: str
site_user_api: str
site_user_complete_api: str
+ site_infractions: str
+ site_infractions_user: str
+ site_infractions_type: str
+ site_infractions_user_type_current: str
+ site_infractions_user_type: str
status: str
paste_service: str
diff --git a/bot/converters.py b/bot/converters.py
index 5637ab8b2..f18b2f6c7 100644
--- a/bot/converters.py
+++ b/bot/converters.py
@@ -4,7 +4,7 @@ from ssl import CertificateError
import discord
from aiohttp import AsyncResolver, ClientConnectorError, ClientSession, TCPConnector
-from discord.ext.commands import BadArgument, Converter
+from discord.ext.commands import BadArgument, Converter, UserConverter
from fuzzywuzzy import fuzz
from bot.constants import DEBUG_MODE, Keys, URLs
@@ -157,3 +157,18 @@ class ValidURL(Converter):
except ClientConnectorError:
raise BadArgument(f"Cannot connect to host with URL `{url}`.")
return url
+
+
+class InfractionSearchQuery(Converter):
+ """
+ A converter that checks if the argument is a Discord user, and if not, falls back to a string.
+ """
+
+ @staticmethod
+ async def convert(ctx, arg):
+ try:
+ user_converter = UserConverter()
+ user = await user_converter.convert(ctx, arg)
+ except Exception:
+ return arg
+ return user or arg
diff --git a/bot/pagination.py b/bot/pagination.py
index 49fae1e2e..9319a5b60 100644
--- a/bot/pagination.py
+++ b/bot/pagination.py
@@ -207,6 +207,8 @@ class LinePaginator(Paginator):
log.debug(f"Got first page reaction - changing to page 1/{len(paginator.pages)}")
+ embed.description = ""
+ await message.edit(embed=embed)
embed.description = paginator.pages[current_page]
if footer_text:
embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
@@ -220,6 +222,8 @@ class LinePaginator(Paginator):
log.debug(f"Got last page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
+ embed.description = ""
+ await message.edit(embed=embed)
embed.description = paginator.pages[current_page]
if footer_text:
embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
@@ -237,6 +241,8 @@ class LinePaginator(Paginator):
current_page -= 1
log.debug(f"Got previous page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
+ embed.description = ""
+ await message.edit(embed=embed)
embed.description = paginator.pages[current_page]
if footer_text:
@@ -256,6 +262,8 @@ class LinePaginator(Paginator):
current_page += 1
log.debug(f"Got next page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
+ embed.description = ""
+ await message.edit(embed=embed)
embed.description = paginator.pages[current_page]
if footer_text:
diff --git a/config-default.yml b/config-default.yml
index 84fa86a75..50505d4da 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -87,6 +87,7 @@ guild:
owner: 267627879762755584
verified: 352427296948486144
helpers: 267630620367257601
+ muted: 0
keys:
@@ -115,19 +116,24 @@ urls:
site: &DOMAIN "api.pythondiscord.com"
site_schema: &SCHEMA "https://"
- site_bigbrother_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/bigbrother"]
- site_docs_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/docs"]
- site_facts_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_facts"]
- site_hiphopify_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/hiphopify"]
- site_idioms_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_idioms"]
- site_names_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_names"]
- site_off_topic_names_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/off-topic-names"]
- site_quiz_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_quiz"]
- site_settings_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/settings"]
- site_special_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/special_snakes"]
- site_tags_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/tags"]
- site_user_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/users"]
- site_user_complete_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/users/complete"]
+ site_bigbrother_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/bigbrother"]
+ site_docs_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/docs"]
+ site_facts_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_facts"]
+ site_hiphopify_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/hiphopify"]
+ site_idioms_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_idioms"]
+ site_names_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_names"]
+ site_off_topic_names_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/off-topic-names"]
+ site_quiz_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_quiz"]
+ site_settings_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/settings"]
+ site_special_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/special_snakes"]
+ site_tags_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/tags"]
+ site_user_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/users"]
+ site_user_complete_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/users/complete"]
+ site_infractions: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions"]
+ site_infractions_user: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions/user/{user_id}"]
+ site_infractions_type: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions/type/{infraction_type}"]
+ site_infractions_by_id: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions/id/{infraction_id}"]
+ site_infractions_user_type_current: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions/user/{user_id}/{infraction_type}/current"]
# Env vars
deploy: !ENV "DEPLOY_URL"