aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/__main__.py2
-rw-r--r--bot/cogs/bot.py2
-rw-r--r--bot/cogs/eval.py3
-rw-r--r--bot/cogs/moderation.py50
-rw-r--r--bot/cogs/reminders.py62
-rw-r--r--bot/cogs/superstarify.py (renamed from bot/cogs/hiphopify.py)48
-rw-r--r--bot/constants.py2
-rw-r--r--bot/rules/links.py16
-rw-r--r--bot/utils/scheduling.py55
-rw-r--r--config-default.yml6
10 files changed, 123 insertions, 123 deletions
diff --git a/bot/__main__.py b/bot/__main__.py
index f74b3545c..ab66492bb 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -61,7 +61,7 @@ bot.load_extension("bot.cogs.deployment")
bot.load_extension("bot.cogs.defcon")
bot.load_extension("bot.cogs.eval")
bot.load_extension("bot.cogs.fun")
-bot.load_extension("bot.cogs.hiphopify")
+bot.load_extension("bot.cogs.superstarify")
bot.load_extension("bot.cogs.information")
bot.load_extension("bot.cogs.moderation")
bot.load_extension("bot.cogs.off_topic_names")
diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py
index 846783e08..252695027 100644
--- a/bot/cogs/bot.py
+++ b/bot/cogs/bot.py
@@ -60,7 +60,7 @@ class Bot:
"""
embed = Embed(
- description="A utility bot designed just for the Python server! Try `!help()` for more info.",
+ description="A utility bot designed just for the Python server! Try `!help` for more info.",
url="https://gitlab.com/discord-python/projects/bot"
)
diff --git a/bot/cogs/eval.py b/bot/cogs/eval.py
index 421fa5b53..8261b0a3b 100644
--- a/bot/cogs/eval.py
+++ b/bot/cogs/eval.py
@@ -178,7 +178,8 @@ async def func(): # (None,) -> Any
async def internal_group(self, ctx):
"""Internal commands. Top secret!"""
- await ctx.invoke(self.bot.get_command("help"), "internal")
+ if not ctx.invoked_subcommand:
+ await ctx.invoke(self.bot.get_command("help"), "internal")
@internal_group.command(name='eval', aliases=('e',))
@with_role(Roles.admin, Roles.owner)
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
index 588962e29..9165fe654 100644
--- a/bot/cogs/moderation.py
+++ b/bot/cogs/moderation.py
@@ -1,7 +1,6 @@
import asyncio
import logging
import textwrap
-from typing import Dict
from aiohttp import ClientError
from discord import Colour, Embed, Guild, Member, Object, User
@@ -13,7 +12,7 @@ from bot.constants import Colours, Event, Icons, Keys, Roles, URLs
from bot.converters import InfractionSearchQuery
from bot.decorators import with_role
from bot.pagination import LinePaginator
-from bot.utils.scheduling import create_task
+from bot.utils.scheduling import Scheduler
from bot.utils.time import parse_rfc1123, wait_until
log = logging.getLogger(__name__)
@@ -21,7 +20,7 @@ log = logging.getLogger(__name__)
MODERATION_ROLES = Roles.owner, Roles.admin, Roles.moderator
-class Moderation:
+class Moderation(Scheduler):
"""
Rowboat replacement moderation tools.
"""
@@ -29,8 +28,8 @@ class Moderation:
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)
+ super().__init__()
@property
def mod_log(self) -> ModLog:
@@ -47,7 +46,7 @@ class Moderation:
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)
+ self.schedule_task(loop, infraction_object["id"], infraction_object)
# region: Permanent infractions
@@ -291,7 +290,7 @@ class Moderation:
infraction_expiration = infraction_object["expires_at"]
loop = asyncio.get_event_loop()
- self.schedule_expiration(loop, infraction_object)
+ self.schedule_task(loop, infraction_object["id"], infraction_object)
if reason is None:
result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}."
@@ -356,7 +355,7 @@ class Moderation:
infraction_expiration = infraction_object["expires_at"]
loop = asyncio.get_event_loop()
- self.schedule_expiration(loop, infraction_object)
+ self.schedule_task(loop, infraction_object["id"], infraction_object)
if reason is None:
result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}."
@@ -540,9 +539,9 @@ class Moderation:
infraction_object = response_object["infraction"]
# Re-schedule
- self.cancel_expiration(infraction_id)
+ self.cancel_task(infraction_id)
loop = asyncio.get_event_loop()
- self.schedule_expiration(loop, infraction_object)
+ self.schedule_task(loop, infraction_object["id"], infraction_object)
if duration is None:
await ctx.send(f":ok_hand: Updated infraction: marked as permanent.")
@@ -748,36 +747,7 @@ class Moderation:
max_size=1000
)
- 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 = create_task(loop, self._scheduled_expiration(infraction_object))
-
- 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):
+ async def _scheduled_task(self, infraction_object: dict):
"""
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,
@@ -794,7 +764,7 @@ class Moderation:
log.debug(f"Marking infraction {infraction_id} as inactive (expired).")
await self._deactivate_infraction(infraction_object)
- self.cancel_expiration(infraction_object["id"])
+ self.cancel_task(infraction_object["id"])
async def _deactivate_infraction(self, infraction_object):
"""
diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py
index 98d7942b3..f6ed111dc 100644
--- a/bot/cogs/reminders.py
+++ b/bot/cogs/reminders.py
@@ -14,7 +14,7 @@ from bot.constants import (
POSITIVE_REPLIES, Roles, URLs
)
from bot.pagination import LinePaginator
-from bot.utils.scheduling import create_task
+from bot.utils.scheduling import Scheduler
from bot.utils.time import humanize_delta, parse_rfc1123, wait_until
log = logging.getLogger(__name__)
@@ -24,16 +24,12 @@ WHITELISTED_CHANNELS = (Channels.bot,)
MAXIMUM_REMINDERS = 5
-# The scheduling parts of this cog are pretty much directly copied
-# from the moderation cog. I'll be working on making it more
-# webscale:tm: as soon as possible, because this is a mess :D
-class Reminders:
+class Reminders(Scheduler):
def __init__(self, bot: Bot):
self.bot = bot
-
self.headers = {"X-API-Key": Keys.site_api}
- self.reminder_tasks = {}
+ super().__init__()
async def on_ready(self):
# Get all the current reminders for re-scheduling
@@ -57,7 +53,7 @@ class Reminders:
await self.send_reminder(reminder, late)
else:
- self.schedule_reminder(loop, reminder)
+ self.schedule_task(loop, reminder["id"], reminder)
@staticmethod
async def _send_confirmation(ctx: Context, response: dict, on_success: str):
@@ -87,24 +83,7 @@ class Reminders:
await ctx.send(embed=embed)
return failed
- def schedule_reminder(self, loop: asyncio.AbstractEventLoop, reminder):
- """
- Schedule a reminder from the bot at the requested time.
-
- :param loop: the asyncio event loop
- :param reminder: the data of the reminder.
- """
-
- # Avoid duplicate schedules, just in case.
- reminder_id = reminder["id"]
- if reminder_id in self.reminder_tasks:
- return
-
- # Make a scheduled task and add it to the list
- task: asyncio.Task = create_task(loop, self._scheduled_reminder(reminder))
- self.reminder_tasks[reminder_id] = task
-
- async def _scheduled_reminder(self, reminder):
+ async def _scheduled_task(self, reminder: dict):
"""
A coroutine which sends the reminder once the time is reached.
@@ -120,27 +99,10 @@ class Reminders:
await self.send_reminder(reminder)
log.debug(f"Deleting reminder {reminder_id} (the user has been reminded).")
- await self._delete_reminder(reminder)
+ await self._delete_reminder(reminder_id)
# Now we can begone with it from our schedule list.
- self.cancel_reminder(reminder_id)
-
- def cancel_reminder(self, reminder_id: str):
- """
- Un-schedules a task to send a reminder.
-
- :param reminder_id: the ID of the reminder in question
- """
-
- task = self.reminder_tasks.get(reminder_id)
-
- if task is None:
- log.warning(f"Failed to unschedule {reminder_id}: no task found.")
- return
-
- task.cancel()
- log.debug(f"Unscheduled {reminder_id}.")
- del self.reminder_tasks[reminder_id]
+ self.cancel_task(reminder_id)
async def _delete_reminder(self, reminder_id: str):
"""
@@ -163,7 +125,7 @@ class Reminders:
)
# Now we can remove it from the schedule list
- self.cancel_reminder(reminder_id)
+ self.cancel_task(reminder_id)
async def _reschedule_reminder(self, reminder):
"""
@@ -174,8 +136,8 @@ class Reminders:
loop = asyncio.get_event_loop()
- self.cancel_reminder(reminder["id"])
- self.schedule_reminder(loop, reminder)
+ self.cancel_task(reminder["id"])
+ self.schedule_task(loop, reminder["id"], reminder)
async def send_reminder(self, reminder, late: relativedelta = None):
"""
@@ -291,7 +253,9 @@ class Reminders:
# If it worked, schedule the reminder.
if not failed:
loop = asyncio.get_event_loop()
- self.schedule_reminder(loop=loop, reminder=response_data["reminder"])
+ reminder = response_data["reminder"]
+
+ self.schedule_task(loop, reminder["id"], reminder)
@remind_group.command(name="list")
async def list_reminders(self, ctx: Context):
diff --git a/bot/cogs/hiphopify.py b/bot/cogs/superstarify.py
index 785aedca2..e1cfcc184 100644
--- a/bot/cogs/hiphopify.py
+++ b/bot/cogs/superstarify.py
@@ -16,7 +16,7 @@ from bot.decorators import with_role
log = logging.getLogger(__name__)
-class Hiphopify:
+class Superstarify:
"""
A set of commands to moderate terrible nicknames.
"""
@@ -30,7 +30,7 @@ class Hiphopify:
This event will trigger when someone changes their name.
At this point we will look up the user in our database and check
whether they are allowed to change their names, or if they are in
- hiphop-prison. If they are not allowed, we will change it back.
+ superstar-prison. If they are not allowed, we will change it back.
:return:
"""
@@ -39,11 +39,11 @@ class Hiphopify:
log.debug(
f"{before.display_name} is trying to change their nickname to {after.display_name}. "
- "Checking if the user is in hiphop-prison..."
+ "Checking if the user is in superstar-prison..."
)
response = await self.bot.http_session.get(
- URLs.site_hiphopify_api,
+ URLs.site_superstarify_api,
headers=self.headers,
params={"user_id": str(before.id)}
)
@@ -55,7 +55,7 @@ class Hiphopify:
return # Nick change was triggered by this event. Ignore.
log.debug(
- f"{after.display_name} is currently in hiphop-prison. "
+ f"{after.display_name} is currently in superstar-prison. "
f"Changing the nick back to {before.display_name}."
)
await after.edit(nick=response.get("forced_nick"))
@@ -63,23 +63,23 @@ class Hiphopify:
await after.send(
"You have tried to change your nickname on the **Python Discord** server "
f"from **{before.display_name}** to **{after.display_name}**, but as you "
- "are currently in hiphop-prison, you do not have permission to do so. "
+ "are currently in superstar-prison, you do not have permission to do so. "
"You will be allowed to change your nickname again at the following time:\n\n"
f"**{response.get('end_timestamp')}**."
)
except Forbidden:
log.warning(
- "The user tried to change their nickname while in hiphop-prison. "
+ "The user tried to change their nickname while in superstar-prison. "
"This led to the bot trying to DM the user to let them know they cannot do that, "
"but the user had either blocked the bot or disabled DMs, so it was not possible "
"to DM them, and a discord.errors.Forbidden error was incurred."
)
- @command(name='hiphopify', aliases=('force_nick', 'hh'))
+ @command(name='superstarify', aliases=('force_nick', 'ss'))
@with_role(Roles.admin, Roles.owner, Roles.moderator)
- async def hiphopify(self, ctx: Context, member: Member, duration: str, *, forced_nick: str = None):
+ async def superstarify(self, ctx: Context, member: Member, duration: str, *, forced_nick: str = None):
"""
- This command will force a random rapper name (like Lil' Wayne) to be the users
+ This command will force a random superstar name (like Taylor Swift) to be the user's
nickname for a specified duration. If a forced_nick is provided, it will use that instead.
:param ctx: Discord message context
@@ -89,7 +89,7 @@ class Hiphopify:
"""
log.debug(
- f"Attempting to hiphopify {member.display_name} for {duration}. "
+ f"Attempting to superstarify {member.display_name} for {duration}. "
f"forced_nick is set to {forced_nick}."
)
@@ -105,7 +105,7 @@ class Hiphopify:
params["forced_nick"] = forced_nick
response = await self.bot.http_session.post(
- URLs.site_hiphopify_api,
+ URLs.site_superstarify_api,
headers=self.headers,
json=params
)
@@ -114,7 +114,7 @@ class Hiphopify:
if "error_message" in response:
log.warning(
- "Encountered the following error when trying to hiphopify the user:\n"
+ "Encountered the following error when trying to superstarify the user:\n"
f"{response.get('error_message')}"
)
embed.colour = Colour.red()
@@ -142,7 +142,7 @@ class Hiphopify:
mod_log = self.bot.get_channel(Channels.modlog)
await mod_log.send(
f":middle_finger: {member.name}#{member.discriminator} (`{member.id}`) "
- f"has been hiphopified by **{ctx.author.name}**. Their new nickname is `{forced_nick}`. "
+ f"has been superstarified by **{ctx.author.name}**. Their new nickname is `{forced_nick}`. "
f"They will not be able to change their nickname again until **{end_time}**"
)
@@ -151,30 +151,30 @@ class Hiphopify:
await member.edit(nick=forced_nick)
await ctx.send(embed=embed)
- @command(name='unhiphopify', aliases=('release_nick', 'uhh'))
+ @command(name='unsuperstarify', aliases=('release_nick', 'uss'))
@with_role(Roles.admin, Roles.owner, Roles.moderator)
- async def unhiphopify(self, ctx: Context, member: Member):
+ async def unsuperstarify(self, ctx: Context, member: Member):
"""
This command will remove the entry from our database, allowing the user
to once again change their nickname.
:param ctx: Discord message context
- :param member: The member to unhiphopify
+ :param member: The member to unsuperstarify
"""
- log.debug(f"Attempting to unhiphopify the following user: {member.display_name}")
+ log.debug(f"Attempting to unsuperstarify the following user: {member.display_name}")
embed = Embed()
embed.colour = Colour.blurple()
response = await self.bot.http_session.delete(
- URLs.site_hiphopify_api,
+ URLs.site_superstarify_api,
headers=self.headers,
json={"user_id": str(member.id)}
)
response = await response.json()
- embed.description = "User has been released from hiphop-prison."
+ embed.description = "User has been released from superstar-prison."
embed.title = random.choice(POSITIVE_REPLIES)
if "error_message" in response:
@@ -182,14 +182,14 @@ class Hiphopify:
embed.title = random.choice(NEGATIVE_REPLIES)
embed.description = response.get("error_message")
log.warning(
- f"Error encountered when trying to unhiphopify {member.display_name}:\n"
+ f"Error encountered when trying to unsuperstarify {member.display_name}:\n"
f"{response}"
)
- log.debug(f"{member.display_name} was successfully released from hiphop-prison.")
+ log.debug(f"{member.display_name} was successfully released from superstar-prison.")
await ctx.send(embed=embed)
def setup(bot):
- bot.add_cog(Hiphopify(bot))
- log.info("Cog loaded: Hiphopify")
+ bot.add_cog(Superstarify(bot))
+ log.info("Cog loaded: Superstarify")
diff --git a/bot/constants.py b/bot/constants.py
index 5d5f4609f..2a9796cc5 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -398,7 +398,7 @@ class URLs(metaclass=YAMLGetter):
site_api: str
site_facts_api: str
site_clean_api: str
- site_hiphopify_api: str
+ site_superstarify_api: str
site_idioms_api: str
site_logs_api: str
site_logs_view: str
diff --git a/bot/rules/links.py b/bot/rules/links.py
index dfeb38c61..fa4043fcb 100644
--- a/bot/rules/links.py
+++ b/bot/rules/links.py
@@ -20,9 +20,19 @@ async def apply(
for msg in recent_messages
if msg.author == last_message.author
)
- total_links = sum(len(LINK_RE.findall(msg.content)) for msg in relevant_messages)
-
- if total_links > config['max']:
+ total_links = 0
+ messages_with_links = 0
+
+ for msg in relevant_messages:
+ total_matches = len(LINK_RE.findall(msg.content))
+ if total_matches:
+ messages_with_links += 1
+ total_links += total_matches
+
+ # Only apply the filter if we found more than one message with
+ # links to prevent wrongfully firing the rule on users posting
+ # e.g. an installation log of pip packages from GitHub.
+ if total_links > config['max'] and messages_with_links > 1:
return (
f"sent {total_links} links in {config['interval']}s",
(last_message.author,),
diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py
index f9b844046..ded6401b0 100644
--- a/bot/utils/scheduling.py
+++ b/bot/utils/scheduling.py
@@ -1,5 +1,60 @@
import asyncio
import contextlib
+import logging
+from abc import ABC, abstractmethod
+from typing import Dict
+
+log = logging.getLogger(__name__)
+
+
+class Scheduler(ABC):
+
+ def __init__(self):
+
+ self.cog_name = self.__class__.__name__ # keep track of the child cog's name so the logs are clear.
+ self.scheduled_tasks: Dict[str, asyncio.Task] = {}
+
+ @abstractmethod
+ async def _scheduled_task(self, task_object: dict):
+ """
+ A coroutine which handles the scheduling. This is added to the scheduled tasks,
+ and should wait the task duration, execute the desired code, and clean up the task.
+ For example, in Reminders this will wait for the reminder duration, send the reminder,
+ then make a site API request to delete the reminder from the database.
+
+ :param task_object:
+ """
+
+ def schedule_task(self, loop: asyncio.AbstractEventLoop, task_id: str, task_data: dict):
+ """
+ Schedules a task.
+ :param loop: the asyncio event loop
+ :param task_id: the ID of the task.
+ :param task_data: the data of the task, passed to `Scheduler._scheduled_expiration`.
+ """
+
+ if task_id in self.scheduled_tasks:
+ return
+
+ task: asyncio.Task = create_task(loop, self._scheduled_task(task_data))
+
+ self.scheduled_tasks[task_id] = task
+
+ def cancel_task(self, task_id: str):
+ """
+ Un-schedules a task.
+ :param task_id: the ID of the infraction in question
+ """
+
+ task = self.scheduled_tasks.get(task_id)
+
+ if task is None:
+ log.warning(f"{self.cog_name}: Failed to unschedule {task_id} (no task found).")
+ return
+
+ task.cancel()
+ log.debug(f"{self.cog_name}: Unscheduled {task_id}.")
+ del self.scheduled_tasks[task_id]
def create_task(loop: asyncio.AbstractEventLoop, coro_or_future):
diff --git a/config-default.yml b/config-default.yml
index 17aa69ead..0019d1688 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -129,7 +129,7 @@ guild:
filter:
# What do we filter?
- filter_zalgo: true
+ filter_zalgo: false
filter_invites: true
filter_domains: true
watch_words: true
@@ -218,7 +218,7 @@ urls:
site_bigbrother_api: !JOIN [*SCHEMA, *API, "/bot/bigbrother"]
site_docs_api: !JOIN [*SCHEMA, *API, "/bot/docs"]
site_facts_api: !JOIN [*SCHEMA, *API, "/bot/snake_facts"]
- site_hiphopify_api: !JOIN [*SCHEMA, *API, "/bot/hiphopify"]
+ site_superstarify_api: !JOIN [*SCHEMA, *API, "/bot/superstarify"]
site_idioms_api: !JOIN [*SCHEMA, *API, "/bot/snake_idioms"]
site_infractions: !JOIN [*SCHEMA, *API, "/bot/infractions"]
site_infractions_user: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}"]
@@ -288,7 +288,7 @@ anti_spam:
links:
interval: 10
- max: 20
+ max: 10
mentions:
interval: 10