aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Leon Sandøy <[email protected]>2019-09-16 21:37:49 +0200
committerGravatar GitHub <[email protected]>2019-09-16 21:37:49 +0200
commit2b4b0a55f76e1ed63ab6b3f9b6caf76f95c1cccd (patch)
tree07309446427c5f5ed4e88a3b29f5d188c6869dfc
parentImplement `!otn search`. Closes #408. (diff)
parentUpdate discord.py version to 1.2.3 (#433) (diff)
Merge branch 'master' into otn-search-command
-rw-r--r--Pipfile2
-rw-r--r--Pipfile.lock96
-rw-r--r--bot/__main__.py14
-rw-r--r--bot/cogs/alias.py4
-rw-r--r--bot/cogs/antispam.py16
-rw-r--r--bot/cogs/bot.py14
-rw-r--r--bot/cogs/clean.py4
-rw-r--r--bot/cogs/cogs.py12
-rw-r--r--bot/cogs/defcon.py14
-rw-r--r--bot/cogs/doc.py3
-rw-r--r--bot/cogs/error_handler.py5
-rw-r--r--bot/cogs/eval.py4
-rw-r--r--bot/cogs/filtering.py12
-rw-r--r--bot/cogs/free.py4
-rw-r--r--bot/cogs/help.py6
-rw-r--r--bot/cogs/information.py9
-rw-r--r--bot/cogs/jams.py2
-rw-r--r--bot/cogs/logging.py12
-rw-r--r--bot/cogs/moderation.py15
-rw-r--r--bot/cogs/modlog.py24
-rw-r--r--bot/cogs/off_topic_names.py14
-rw-r--r--bot/cogs/reddit.py7
-rw-r--r--bot/cogs/reminders.py5
-rw-r--r--bot/cogs/security.py4
-rw-r--r--bot/cogs/site.py19
-rw-r--r--bot/cogs/snekbox.py4
-rw-r--r--bot/cogs/superstarify/__init__.py8
-rw-r--r--bot/cogs/sync/cog.py11
-rw-r--r--bot/cogs/tags.py9
-rw-r--r--bot/cogs/token_remover.py5
-rw-r--r--bot/cogs/utils.py4
-rw-r--r--bot/cogs/verification.py13
-rw-r--r--bot/cogs/watchchannels/bigbrother.py4
-rw-r--r--bot/cogs/watchchannels/talentpool.py4
-rw-r--r--bot/cogs/watchchannels/watchchannel.py30
-rw-r--r--bot/cogs/wolfram.py4
-rw-r--r--bot/constants.py2
-rw-r--r--bot/patches/__init__.py6
-rw-r--r--bot/patches/message_edited_at.py32
-rw-r--r--bot/utils/__init__.py9
-rw-r--r--bot/utils/scheduling.py6
-rw-r--r--config-default.yml4
-rw-r--r--tests/cogs/test_security.py54
-rw-r--r--tests/cogs/test_token_remover.py133
-rw-r--r--tests/helpers.py10
-rw-r--r--tests/test_resources.py18
-rw-r--r--tox.ini2
47 files changed, 485 insertions, 208 deletions
diff --git a/Pipfile b/Pipfile
index 739507ac3..eaef3bd65 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
-discord-py = {git = "https://github.com/Rapptz/discord.py.git",extras = ["voice"],ref = "860d6a9ace8248dfeec18b8b159e7b757d9f56bb",editable = true}
+discord-py = "~=1.2"
aiodns = "*"
logmatic-python = "*"
aiohttp = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index f655943b4..3c98e2b93 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "987e3fc1840e8050f159daa9c23a2c67bd18d17914d4295eb469a42c778daa10"
+ "sha256": "c1933af105f88f5f2541b1796b92f91d1fcf7a1a947abfe1d8edb016710a56df"
},
"pipfile-spec": 6,
"requires": {
@@ -34,31 +34,31 @@
},
"aiohttp": {
"hashes": [
- "sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b",
- "sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08",
- "sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd",
- "sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac",
- "sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650",
- "sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa",
- "sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95",
- "sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330",
- "sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc",
- "sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b",
- "sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de",
- "sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4",
- "sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7",
- "sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b",
- "sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8",
- "sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd",
- "sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2",
- "sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698",
- "sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95",
- "sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6",
- "sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0",
- "sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07"
+ "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
+ "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
+ "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
+ "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
+ "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
+ "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
+ "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
+ "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
+ "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
+ "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
+ "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
+ "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
+ "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
+ "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
+ "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
+ "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
+ "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
+ "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
+ "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
+ "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
+ "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
+ "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
],
"index": "pypi",
- "version": "==3.4.4"
+ "version": "==3.5.4"
},
"aiormq": {
"hashes": [
@@ -167,12 +167,11 @@
"version": "==4.0.7"
},
"discord-py": {
- "editable": true,
- "extras": [
- "voice"
+ "hashes": [
+ "sha256:4684733fa137cc7def18087ae935af615212e423e3dbbe3e84ef01d7ae8ed17d"
],
- "git": "https://github.com/Rapptz/discord.py.git",
- "ref": "860d6a9ace8248dfeec18b8b159e7b757d9f56bb"
+ "index": "pypi",
+ "version": "==1.2.3"
},
"docutils": {
"hashes": [
@@ -375,7 +374,8 @@
},
"pycparser": {
"hashes": [
- "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
+ "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3",
+ "sha256:d51e69d7e2bda15beda04993a07d49598a09de7651375270ca60e234d10b7343"
],
"version": "==2.19"
},
@@ -386,42 +386,6 @@
],
"version": "==2.4.2"
},
- "pynacl": {
- "hashes": [
- "sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca",
- "sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512",
- "sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6",
- "sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776",
- "sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac",
- "sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b",
- "sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb",
- "sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98",
- "sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd",
- "sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2",
- "sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef",
- "sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f",
- "sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988",
- "sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b",
- "sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415",
- "sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2",
- "sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101",
- "sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0",
- "sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582",
- "sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a",
- "sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975",
- "sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1",
- "sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb",
- "sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45",
- "sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031",
- "sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9",
- "sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752",
- "sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0",
- "sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c",
- "sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053",
- "sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4"
- ],
- "version": "==1.2.1"
- },
"pyparsing": {
"hashes": [
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
diff --git a/bot/__main__.py b/bot/__main__.py
index b1a6a5fcd..f25693734 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -2,10 +2,11 @@ import asyncio
import logging
import socket
+import discord
from aiohttp import AsyncResolver, ClientSession, TCPConnector
-from discord import Game
from discord.ext.commands import Bot, when_mentioned_or
+from bot import patches
from bot.api import APIClient, APILoggingHandler
from bot.constants import Bot as BotConfig, DEBUG_MODE
@@ -14,9 +15,9 @@ log = logging.getLogger('bot')
bot = Bot(
command_prefix=when_mentioned_or(BotConfig.prefix),
- activity=Game(name="Commands: !help"),
+ activity=discord.Game(name="Commands: !help"),
case_insensitive=True,
- max_messages=10_000
+ max_messages=10_000,
)
# Global aiohttp session for all cogs
@@ -71,6 +72,11 @@ bot.load_extension("bot.cogs.utils")
bot.load_extension("bot.cogs.watchchannels")
bot.load_extension("bot.cogs.wolfram")
+# Apply `message_edited_at` patch if discord.py did not yet release a bug fix.
+if not hasattr(discord.message.Message, '_handle_edited_timestamp'):
+ patches.message_edited_at.apply_patch()
+
bot.run(BotConfig.token)
-bot.http_session.close() # Close the aiohttp session when the bot finishes running
+# This calls a coroutine, so it doesn't do anything at the moment.
+# bot.http_session.close() # Close the aiohttp session when the bot finishes running
diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py
index a44c47331..3d0c9d826 100644
--- a/bot/cogs/alias.py
+++ b/bot/cogs/alias.py
@@ -4,7 +4,7 @@ from typing import Union
from discord import Colour, Embed, Member, User
from discord.ext.commands import (
- Command, Context, clean_content, command, group
+ Cog, Command, Context, clean_content, command, group
)
from bot.cogs.watchchannels.watchchannel import proxy_user
@@ -14,7 +14,7 @@ from bot.pagination import LinePaginator
log = logging.getLogger(__name__)
-class Alias:
+class Alias(Cog):
"""
Aliases for more used commands
"""
diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py
index e980de364..7b97881fd 100644
--- a/bot/cogs/antispam.py
+++ b/bot/cogs/antispam.py
@@ -7,10 +7,9 @@ from operator import itemgetter
from typing import Dict, Iterable, List, Set
from discord import Colour, Member, Message, NotFound, Object, TextChannel
-from discord.ext.commands import Bot
+from discord.ext.commands import Bot, Cog
from bot import rules
-from bot.cogs.moderation import Moderation
from bot.cogs.modlog import ModLog
from bot.constants import (
AntiSpam as AntiSpamConfig, Channels,
@@ -95,7 +94,7 @@ class DeletionContext:
)
-class AntiSpam:
+class AntiSpam(Cog):
"""Cog that controls our anti-spam measures."""
def __init__(self, bot: Bot, validation_errors: bool) -> None:
@@ -113,6 +112,7 @@ class AntiSpam:
"""Allows for easy access of the ModLog cog."""
return self.bot.get_cog("ModLog")
+ @Cog.listener()
async def on_ready(self):
"""Unloads the cog and alerts admins if configuration validation failed."""
if self.validation_errors:
@@ -131,6 +131,7 @@ class AntiSpam:
self.bot.remove_cog(self.__class__.__name__)
return
+ @Cog.listener()
async def on_message(self, message: Message) -> None:
"""Applies the antispam rules to each received message."""
if (
@@ -152,7 +153,7 @@ class AntiSpam:
# Store history messages since `interval` seconds ago in a list to prevent unnecessary API calls.
earliest_relevant_at = datetime.utcnow() - timedelta(seconds=max_interval)
relevant_messages = [
- msg async for msg in message.channel.history(after=earliest_relevant_at, reverse=False)
+ msg async for msg in message.channel.history(after=earliest_relevant_at, oldest_first=False)
if not msg.author.bot
]
@@ -211,7 +212,12 @@ class AntiSpam:
# Since we're going to invoke the tempmute command directly, we need to manually call the converter.
dt_remove_role_after = await self.expiration_date_converter.convert(context, f"{remove_role_after}S")
- await context.invoke(Moderation.tempmute, member, dt_remove_role_after, reason=reason)
+ await context.invoke(
+ self.bot.get_command('tempmute'),
+ member,
+ dt_remove_role_after,
+ reason=reason
+ )
async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]) -> None:
"""Cleans the messages if cleaning is configured."""
diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py
index 4a0f208f4..e88b1d9b5 100644
--- a/bot/cogs/bot.py
+++ b/bot/cogs/bot.py
@@ -4,7 +4,7 @@ import re
import time
from discord import Embed, Message, RawMessageUpdateEvent
-from discord.ext.commands import Bot, Context, command, group
+from discord.ext.commands import Bot, Cog, Context, command, group
from bot.constants import (
Channels, Guild, MODERATION_ROLES,
@@ -16,7 +16,7 @@ from bot.utils.messages import wait_for_deletion
log = logging.getLogger(__name__)
-class Bot:
+class Bot(Cog):
"""
Bot information commands
"""
@@ -48,14 +48,14 @@ class Bot:
@group(invoke_without_command=True, name="bot", hidden=True)
@with_role(Roles.verified)
- async def bot_group(self, ctx: Context):
+ async def botinfo_group(self, ctx: Context):
"""
Bot informational commands
"""
await ctx.invoke(self.bot.get_command("help"), "bot")
- @bot_group.command(name='about', aliases=('info',), hidden=True)
+ @botinfo_group.command(name='about', aliases=('info',), hidden=True)
@with_role(Roles.verified)
async def about_command(self, ctx: Context):
"""
@@ -236,6 +236,7 @@ class Bot:
return msg.content[:3] in not_backticks
+ @Cog.listener()
async def on_message(self, msg: Message):
"""
Detect poorly formatted Python code and send the user
@@ -357,6 +358,7 @@ class Bot:
f"The message that was posted was:\n\n{msg.content}\n\n"
)
+ @Cog.listener()
async def on_raw_message_edit(self, payload: RawMessageUpdateEvent):
if (
# Checks to see if the message was called out by the bot
@@ -370,14 +372,14 @@ class Bot:
# Retrieve channel and message objects for use later
channel = self.bot.get_channel(int(payload.data.get("channel_id")))
- user_message = await channel.get_message(payload.message_id)
+ user_message = await channel.fetch_message(payload.message_id)
# Checks to see if the user has corrected their codeblock. If it's fixed, has_fixed_codeblock will be None
has_fixed_codeblock = self.codeblock_stripping(payload.data.get("content"), self.has_bad_ticks(user_message))
# If the message is fixed, delete the bot message and the entry from the id dictionary
if has_fixed_codeblock is None:
- bot_message = await channel.get_message(self.codeblock_message_ids[payload.message_id])
+ bot_message = await channel.fetch_message(self.codeblock_message_ids[payload.message_id])
await bot_message.delete()
del self.codeblock_message_ids[payload.message_id]
log.trace("User's incorrect code block has been fixed. Removing bot formatting message.")
diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py
index 1f3e1caa9..20c24dafc 100644
--- a/bot/cogs/clean.py
+++ b/bot/cogs/clean.py
@@ -4,7 +4,7 @@ import re
from typing import Optional
from discord import Colour, Embed, Message, User
-from discord.ext.commands import Bot, Context, group
+from discord.ext.commands import Bot, Cog, Context, group
from bot.cogs.modlog import ModLog
from bot.constants import (
@@ -16,7 +16,7 @@ from bot.decorators import with_role
log = logging.getLogger(__name__)
-class Clean:
+class Clean(Cog):
"""
A cog that allows messages to be deleted in
bulk, while applying various filters.
diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py
index ebdbf5ad8..ec497b966 100644
--- a/bot/cogs/cogs.py
+++ b/bot/cogs/cogs.py
@@ -2,7 +2,7 @@ import logging
import os
from discord import Colour, Embed
-from discord.ext.commands import Bot, Context, group
+from discord.ext.commands import Bot, Cog, Context, group
from bot.constants import (
Emojis, MODERATION_ROLES, Roles, URLs
@@ -15,7 +15,7 @@ log = logging.getLogger(__name__)
KEEP_LOADED = ["bot.cogs.cogs", "bot.cogs.modlog"]
-class Cogs:
+class Cogs(Cog):
"""
Cog management commands
"""
@@ -60,7 +60,7 @@ class Cogs:
embed.set_author(
name="Python Bot (Cogs)",
- url=URLs.gitlab_bot_repo,
+ url=URLs.github_bot_repo,
icon_url=URLs.bot_avatar
)
@@ -113,7 +113,7 @@ class Cogs:
embed.set_author(
name="Python Bot (Cogs)",
- url=URLs.gitlab_bot_repo,
+ url=URLs.github_bot_repo,
icon_url=URLs.bot_avatar
)
@@ -168,7 +168,7 @@ class Cogs:
embed.set_author(
name="Python Bot (Cogs)",
- url=URLs.gitlab_bot_repo,
+ url=URLs.github_bot_repo,
icon_url=URLs.bot_avatar
)
@@ -269,7 +269,7 @@ class Cogs:
embed.colour = Colour.blurple()
embed.set_author(
name="Python Bot (Cogs)",
- url=URLs.gitlab_bot_repo,
+ url=URLs.github_bot_repo,
icon_url=URLs.bot_avatar
)
diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py
index c67fa2807..8fab00712 100644
--- a/bot/cogs/defcon.py
+++ b/bot/cogs/defcon.py
@@ -2,10 +2,10 @@ import logging
from datetime import datetime, timedelta
from discord import Colour, Embed, Member
-from discord.ext.commands import Bot, Context, group
+from discord.ext.commands import Bot, Cog, Context, group
from bot.cogs.modlog import ModLog
-from bot.constants import Channels, Colours, Emojis, Event, Icons, Keys, Roles
+from bot.constants import Channels, Colours, Emojis, Event, Icons, Roles
from bot.decorators import with_role
log = logging.getLogger(__name__)
@@ -24,21 +24,23 @@ will be resolved soon. In the meantime, please feel free to peruse the resources
BASE_CHANNEL_TOPIC = "Python Discord Defense Mechanism"
-class Defcon:
+class Defcon(Cog):
"""Time-sensitive server defense mechanisms"""
days = None # type: timedelta
enabled = False # type: bool
def __init__(self, bot: Bot):
self.bot = bot
+ self.channel = None
self.days = timedelta(days=0)
- self.headers = {"X-API-KEY": Keys.site_api}
@property
def mod_log(self) -> ModLog:
return self.bot.get_cog("ModLog")
+ @Cog.listener()
async def on_ready(self):
+ self.channel = await self.bot.fetch_channel(Channels.defcon)
try:
response = await self.bot.api_client.get('bot/bot-settings/defcon')
data = response['data']
@@ -62,6 +64,7 @@ class Defcon:
await self.update_channel_topic()
+ @Cog.listener()
async def on_member_join(self, member: Member):
if self.enabled and self.days.days > 0:
now = datetime.utcnow()
@@ -278,8 +281,7 @@ class Defcon:
new_topic = f"{BASE_CHANNEL_TOPIC}\n(Status: Disabled)"
self.mod_log.ignore(Event.guild_channel_update, Channels.defcon)
- defcon_channel = self.bot.guilds[0].get_channel(Channels.defcon)
- await defcon_channel.edit(topic=new_topic)
+ await self.channel.edit(topic=new_topic)
def setup(bot: Bot):
diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py
index aa49b0c25..ebf2c1d65 100644
--- a/bot/cogs/doc.py
+++ b/bot/cogs/doc.py
@@ -120,12 +120,13 @@ class InventoryURL(commands.Converter):
return url
-class Doc:
+class Doc(commands.Cog):
def __init__(self, bot):
self.base_urls = {}
self.bot = bot
self.inventories = {}
+ @commands.Cog.listener()
async def on_ready(self):
await self.refresh_inventory()
diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py
index cfcba6f26..e2d8c3a8f 100644
--- a/bot/cogs/error_handler.py
+++ b/bot/cogs/error_handler.py
@@ -14,7 +14,7 @@ from discord.ext.commands import (
NoPrivateMessage,
UserInputError,
)
-from discord.ext.commands import Bot, Context
+from discord.ext.commands import Bot, Cog, Context
from bot.api import ResponseCodeError
from bot.constants import Channels
@@ -23,12 +23,13 @@ from bot.decorators import InChannelCheckFailure
log = logging.getLogger(__name__)
-class ErrorHandler:
+class ErrorHandler(Cog):
"""Handles errors emitted from commands."""
def __init__(self, bot: Bot):
self.bot = bot
+ @Cog.listener()
async def on_command_error(self, ctx: Context, e: CommandError):
command = ctx.command
parent = None
diff --git a/bot/cogs/eval.py b/bot/cogs/eval.py
index 8e97a35a2..c52c04df1 100644
--- a/bot/cogs/eval.py
+++ b/bot/cogs/eval.py
@@ -8,7 +8,7 @@ import traceback
from io import StringIO
import discord
-from discord.ext.commands import Bot, group
+from discord.ext.commands import Bot, Cog, group
from bot.constants import Roles
from bot.decorators import with_role
@@ -17,7 +17,7 @@ from bot.interpreter import Interpreter
log = logging.getLogger(__name__)
-class CodeEval:
+class CodeEval(Cog):
"""
Owner and admin feature that evaluates code
and returns the result to the channel.
diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
index 418297fc4..dc4de7ff1 100644
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -5,7 +5,7 @@ from typing import Optional, Union
import discord.errors
from dateutil.relativedelta import relativedelta
from discord import Colour, DMChannel, Member, Message, TextChannel
-from discord.ext.commands import Bot
+from discord.ext.commands import Bot, Cog
from bot.cogs.modlog import ModLog
from bot.constants import (
@@ -29,7 +29,7 @@ URL_RE = r"(https?://[^\s]+)"
ZALGO_RE = r"[\u0300-\u036F\u0489]"
-class Filtering:
+class Filtering(Cog):
"""
Filtering out invites, blacklisting domains,
and warning us of certain regular expressions
@@ -59,7 +59,7 @@ class Filtering:
"user_notification": Filter.notify_user_invites,
"notification_msg": (
f"Per Rule 10, your invite link has been removed. {_staff_mistake_str}\n\n"
- r"Our server rules can be found here: <https://pythondiscord.com/about/rules>"
+ r"Our server rules can be found here: <https://pythondiscord.com/pages/rules>"
)
},
"filter_domains": {
@@ -96,14 +96,16 @@ class Filtering:
def mod_log(self) -> ModLog:
return self.bot.get_cog("ModLog")
+ @Cog.listener()
async def on_message(self, msg: Message):
await self._filter_message(msg)
+ @Cog.listener()
async def on_message_edit(self, before: Message, after: Message):
if not before.edited_at:
delta = relativedelta(after.edited_at, before.created_at).microseconds
else:
- delta = None
+ delta = relativedelta(after.edited_at, before.edited_at).microseconds
await self._filter_message(after, delta)
async def _filter_message(self, msg: Message, delta: Optional[int] = None):
@@ -142,7 +144,7 @@ class Filtering:
# If the edit delta is less than 0.001 seconds, then we're probably dealing
# with a double filter trigger.
if delta is not None and delta < 100:
- return
+ continue
# Does the filter only need the message content or the full message?
if _filter["content_only"]:
diff --git a/bot/cogs/free.py b/bot/cogs/free.py
index fd6009bb8..92a9ca041 100644
--- a/bot/cogs/free.py
+++ b/bot/cogs/free.py
@@ -2,7 +2,7 @@ import logging
from datetime import datetime
from discord import Colour, Embed, Member, utils
-from discord.ext.commands import Context, command
+from discord.ext.commands import Cog, Context, command
from bot.constants import Categories, Channels, Free, STAFF_ROLES
from bot.decorators import redirect_output
@@ -15,7 +15,7 @@ RATE = Free.cooldown_rate
PER = Free.cooldown_per
-class Free:
+class Free(Cog):
"""Tries to figure out which help channels are free."""
PYTHON_HELP_ID = Categories.python_help
diff --git a/bot/cogs/help.py b/bot/cogs/help.py
index 20ed08f07..31e729003 100644
--- a/bot/cogs/help.py
+++ b/bot/cogs/help.py
@@ -6,7 +6,7 @@ from contextlib import suppress
from discord import Colour, Embed, HTTPException
from discord.ext import commands
-from discord.ext.commands import CheckFailure
+from discord.ext.commands import CheckFailure, Cog as DiscordCog
from fuzzywuzzy import fuzz, process
from bot import constants
@@ -107,7 +107,7 @@ class HelpSession:
self.query = ctx.bot
self.description = self.query.description
self.author = ctx.author
- self.destination = ctx.author if ctx.bot.pm_help else ctx.channel
+ self.destination = ctx.channel
# set the config for the session
self._cleanup = cleanup
@@ -649,7 +649,7 @@ class HelpSession:
await self.message.delete()
-class Help:
+class Help(DiscordCog):
"""
Custom Embed Pagination Help feature
"""
diff --git a/bot/cogs/information.py b/bot/cogs/information.py
index 320750a24..c4aff73b8 100644
--- a/bot/cogs/information.py
+++ b/bot/cogs/information.py
@@ -2,11 +2,9 @@ import logging
import textwrap
from discord import CategoryChannel, Colour, Embed, Member, TextChannel, VoiceChannel
-from discord.ext.commands import Bot, Context, command
+from discord.ext.commands import Bot, Cog, Context, command
-from bot.constants import (
- Channels, Emojis, Keys, MODERATION_ROLES, STAFF_ROLES
-)
+from bot.constants import Channels, Emojis, MODERATION_ROLES, STAFF_ROLES
from bot.decorators import InChannelCheckFailure, with_role
from bot.utils.checks import with_role_check
from bot.utils.time import time_since
@@ -14,7 +12,7 @@ from bot.utils.time import time_since
log = logging.getLogger(__name__)
-class Information:
+class Information(Cog):
"""
A cog with commands for generating embeds with
server information, such as server statistics
@@ -23,7 +21,6 @@ class Information:
def __init__(self, bot: Bot):
self.bot = bot
- self.headers = {"X-API-Key": Keys.site_api}
@with_role(*MODERATION_ROLES)
@command(name="roles")
diff --git a/bot/cogs/jams.py b/bot/cogs/jams.py
index bca1fb607..dd14111ce 100644
--- a/bot/cogs/jams.py
+++ b/bot/cogs/jams.py
@@ -10,7 +10,7 @@ from bot.decorators import with_role
log = logging.getLogger(__name__)
-class CodeJams:
+class CodeJams(commands.Cog):
"""
Manages the code-jam related parts of our server
"""
diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py
index 6b8462f3b..64bbed46e 100644
--- a/bot/cogs/logging.py
+++ b/bot/cogs/logging.py
@@ -1,7 +1,7 @@
import logging
from discord import Embed
-from discord.ext.commands import Bot
+from discord.ext.commands import Bot, Cog
from bot.constants import Channels, DEBUG_MODE
@@ -9,7 +9,7 @@ from bot.constants import Channels, DEBUG_MODE
log = logging.getLogger(__name__)
-class Logging:
+class Logging(Cog):
"""
Debug logging module
"""
@@ -17,14 +17,18 @@ class Logging:
def __init__(self, bot: Bot):
self.bot = bot
+ @Cog.listener()
async def on_ready(self):
log.info("Bot connected!")
embed = Embed(description="Connected!")
embed.set_author(
name="Python Bot",
- url="https://gitlab.com/discord-python/projects/bot",
- icon_url="https://gitlab.com/python-discord/branding/raw/master/logos/logo_circle/logo_circle.png"
+ url="https://github.com/python-discord/bot",
+ icon_url=(
+ "https://raw.githubusercontent.com/"
+ "python-discord/branding/master/logos/logo_circle/logo_circle_large.png"
+ )
)
if not DEBUG_MODE:
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
index fb791c933..c631dd69d 100644
--- a/bot/cogs/moderation.py
+++ b/bot/cogs/moderation.py
@@ -8,7 +8,7 @@ from discord import (
Colour, Embed, Forbidden, Guild, HTTPException, Member, NotFound, Object, User
)
from discord.ext.commands import (
- BadArgument, BadUnionArgument, Bot, Context, command, group
+ BadArgument, BadUnionArgument, Bot, Cog, Context, command, group
)
from bot import constants
@@ -28,7 +28,7 @@ INFRACTION_ICONS = {
"Kick": Icons.sign_out,
"Ban": Icons.user_ban
}
-RULES_URL = "https://pythondiscord.com/about/rules"
+RULES_URL = "https://pythondiscord.com/pages/rules"
APPEALABLE_INFRACTIONS = ("Ban", "Mute")
@@ -46,7 +46,7 @@ def proxy_user(user_id: str) -> Object:
UserTypes = Union[Member, User, proxy_user]
-class Moderation(Scheduler):
+class Moderation(Scheduler, Cog):
"""
Server moderation tools.
"""
@@ -60,6 +60,7 @@ class Moderation(Scheduler):
def mod_log(self) -> ModLog:
return self.bot.get_cog("ModLog")
+ @Cog.listener()
async def on_ready(self):
# Schedule expiration for previous infractions
infractions = await self.bot.api_client.get(
@@ -1348,7 +1349,7 @@ class Moderation(Scheduler):
"""
# sometimes `user` is a `discord.Object`, so let's make it a proper user.
- user = await self.bot.get_user_info(user.id)
+ user = await self.bot.fetch_user(user.id)
try:
await user.send(embed=embed)
@@ -1374,13 +1375,15 @@ class Moderation(Scheduler):
# endregion
- async def __error(self, ctx: Context, error) -> None:
+ @staticmethod
+ async def cog_command_error(ctx: Context, error) -> None:
if isinstance(error, BadUnionArgument):
if User in error.converters:
await ctx.send(str(error.errors[0]))
error.handled = True
- async def respect_role_hierarchy(self, ctx: Context, target: UserTypes, infr_type: str) -> bool:
+ @staticmethod
+ async def respect_role_hierarchy(ctx: Context, target: UserTypes, infr_type: str) -> bool:
"""
Check if the highest role of the invoking member is greater than that of the target member.
If this check fails, a warning is sent to the invoking ctx.
diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py
index 808ba667b..978646f46 100644
--- a/bot/cogs/modlog.py
+++ b/bot/cogs/modlog.py
@@ -11,7 +11,7 @@ from discord import (
RawMessageUpdateEvent, Role, TextChannel, User, VoiceChannel
)
from discord.abc import GuildChannel
-from discord.ext.commands import Bot
+from discord.ext.commands import Bot, Cog
from bot.constants import (
Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs
@@ -24,11 +24,11 @@ GUILD_CHANNEL = Union[CategoryChannel, TextChannel, VoiceChannel]
CHANNEL_CHANGES_UNSUPPORTED = ("permissions",)
CHANNEL_CHANGES_SUPPRESSED = ("_overwrites", "position")
-MEMBER_CHANGES_SUPPRESSED = ("activity", "status")
+MEMBER_CHANGES_SUPPRESSED = ("status", "activities", "_client_status")
ROLE_CHANGES_UNSUPPORTED = ("colour", "permissions")
-class ModLog:
+class ModLog(Cog, name="ModLog"):
"""
Logging for server events and staff actions
"""
@@ -122,6 +122,7 @@ class ModLog:
return await self.bot.get_context(log_message) # Optionally return for use with antispam
+ @Cog.listener()
async def on_guild_channel_create(self, channel: GUILD_CHANNEL):
if channel.guild.id != GuildConstant.id:
return
@@ -146,6 +147,7 @@ class ModLog:
await self.send_log_message(Icons.hash_green, Colour(Colours.soft_green), title, message)
+ @Cog.listener()
async def on_guild_channel_delete(self, channel: GUILD_CHANNEL):
if channel.guild.id != GuildConstant.id:
return
@@ -167,6 +169,7 @@ class ModLog:
title, message
)
+ @Cog.listener()
async def on_guild_channel_update(self, before: GUILD_CHANNEL, after: GuildChannel):
if before.guild.id != GuildConstant.id:
return
@@ -225,6 +228,7 @@ class ModLog:
"Channel updated", message
)
+ @Cog.listener()
async def on_guild_role_create(self, role: Role):
if role.guild.id != GuildConstant.id:
return
@@ -234,6 +238,7 @@ class ModLog:
"Role created", f"`{role.id}`"
)
+ @Cog.listener()
async def on_guild_role_delete(self, role: Role):
if role.guild.id != GuildConstant.id:
return
@@ -243,6 +248,7 @@ class ModLog:
"Role removed", f"{role.name} (`{role.id}`)"
)
+ @Cog.listener()
async def on_guild_role_update(self, before: Role, after: Role):
if before.guild.id != GuildConstant.id:
return
@@ -294,6 +300,7 @@ class ModLog:
"Role updated", message
)
+ @Cog.listener()
async def on_guild_update(self, before: Guild, after: Guild):
if before.id != GuildConstant.id:
return
@@ -343,6 +350,7 @@ class ModLog:
thumbnail=after.icon_url_as(format="png")
)
+ @Cog.listener()
async def on_member_ban(self, guild: Guild, member: Union[Member, User]):
if guild.id != GuildConstant.id:
return
@@ -358,6 +366,7 @@ class ModLog:
channel_id=Channels.modlog
)
+ @Cog.listener()
async def on_member_join(self, member: Member):
if member.guild.id != GuildConstant.id:
return
@@ -378,6 +387,7 @@ class ModLog:
channel_id=Channels.userlog
)
+ @Cog.listener()
async def on_member_remove(self, member: Member):
if member.guild.id != GuildConstant.id:
return
@@ -393,6 +403,7 @@ class ModLog:
channel_id=Channels.userlog
)
+ @Cog.listener()
async def on_member_unban(self, guild: Guild, member: User):
if guild.id != GuildConstant.id:
return
@@ -408,6 +419,7 @@ class ModLog:
channel_id=Channels.modlog
)
+ @Cog.listener()
async def on_member_update(self, before: Member, after: Member):
if before.guild.id != GuildConstant.id:
return
@@ -497,6 +509,7 @@ class ModLog:
channel_id=Channels.userlog
)
+ @Cog.listener()
async def on_message_delete(self, message: Message):
channel = message.channel
author = message.author
@@ -551,6 +564,7 @@ class ModLog:
channel_id=Channels.message_log
)
+ @Cog.listener()
async def on_raw_message_delete(self, event: RawMessageDeleteEvent):
if event.guild_id != GuildConstant.id or event.channel_id in GuildConstant.ignored:
return
@@ -590,6 +604,7 @@ class ModLog:
channel_id=Channels.message_log
)
+ @Cog.listener()
async def on_message_edit(self, before: Message, after: Message):
if (
not before.guild
@@ -663,10 +678,11 @@ class ModLog:
channel_id=Channels.message_log, timestamp_override=after.edited_at
)
+ @Cog.listener()
async def on_raw_message_edit(self, event: RawMessageUpdateEvent):
try:
channel = self.bot.get_channel(int(event.data["channel_id"]))
- message = await channel.get_message(event.message_id)
+ message = await channel.fetch_message(event.message_id)
except NotFound: # Was deleted before we got the event
return
diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py
index 8f5f9c2e5..cb8a03374 100644
--- a/bot/cogs/off_topic_names.py
+++ b/bot/cogs/off_topic_names.py
@@ -4,9 +4,9 @@ import logging
from datetime import datetime, timedelta
from discord import Colour, Embed
-from discord.ext.commands import BadArgument, Bot, Context, Converter, group
+from discord.ext.commands import BadArgument, Bot, Cog, Context, Converter, group
-from bot.constants import Channels, Keys, MODERATION_ROLES
+from bot.constants import Channels, MODERATION_ROLES
from bot.decorators import with_role
from bot.pagination import LinePaginator
@@ -38,7 +38,7 @@ class OffTopicName(Converter):
return argument.translate(table)
-async def update_names(bot: Bot, headers: dict):
+async def update_names(bot: Bot):
"""
The background updater task that performs a channel name update daily.
@@ -70,21 +70,21 @@ async def update_names(bot: Bot, headers: dict):
)
-class OffTopicNames:
+class OffTopicNames(Cog):
"""Commands related to managing the off-topic category channel names."""
def __init__(self, bot: Bot):
self.bot = bot
- self.headers = {"X-API-KEY": Keys.site_api}
self.updater_task = None
- def __cleanup(self):
+ def cog_unload(self):
if self.updater_task is not None:
self.updater_task.cancel()
+ @Cog.listener()
async def on_ready(self):
if self.updater_task is None:
- coro = update_names(self.bot, self.headers)
+ coro = update_names(self.bot)
self.updater_task = self.bot.loop.create_task(coro)
@group(name='otname', aliases=('otnames', 'otn'), invoke_without_command=True)
diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py
index b5bd26e3d..4c561b7e8 100644
--- a/bot/cogs/reddit.py
+++ b/bot/cogs/reddit.py
@@ -5,7 +5,7 @@ import textwrap
from datetime import datetime, timedelta
from discord import Colour, Embed, TextChannel
-from discord.ext.commands import Bot, Context, group
+from discord.ext.commands import Bot, Cog, Context, group
from bot.constants import Channels, ERROR_REPLIES, Reddit as RedditConfig, STAFF_ROLES
from bot.converters import Subreddit
@@ -15,7 +15,7 @@ from bot.pagination import LinePaginator
log = logging.getLogger(__name__)
-class Reddit:
+class Reddit(Cog):
"""
Track subreddit posts and show detailed statistics about them.
"""
@@ -279,8 +279,9 @@ class Reddit:
max_lines=15
)
+ @Cog.listener()
async def on_ready(self):
- self.reddit_channel = self.bot.get_channel(Channels.reddit)
+ self.reddit_channel = await self.bot.fetch_channel(Channels.reddit)
if self.reddit_channel is not None:
if self.new_posts_task is None:
diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py
index 03ea00de8..c6ae984ea 100644
--- a/bot/cogs/reminders.py
+++ b/bot/cogs/reminders.py
@@ -7,7 +7,7 @@ from operator import itemgetter
from dateutil.relativedelta import relativedelta
from discord import Colour, Embed
-from discord.ext.commands import Bot, Context, group
+from discord.ext.commands import Bot, Cog, Context, group
from bot.constants import Channels, Icons, NEGATIVE_REPLIES, POSITIVE_REPLIES, STAFF_ROLES
from bot.converters import ExpirationDate
@@ -22,12 +22,13 @@ WHITELISTED_CHANNELS = (Channels.bot,)
MAXIMUM_REMINDERS = 5
-class Reminders(Scheduler):
+class Reminders(Scheduler, Cog):
def __init__(self, bot: Bot):
self.bot = bot
super().__init__()
+ @Cog.listener()
async def on_ready(self):
# Get all the current reminders for re-scheduling
response = await self.bot.api_client.get(
diff --git a/bot/cogs/security.py b/bot/cogs/security.py
index 9523766af..e02e91530 100644
--- a/bot/cogs/security.py
+++ b/bot/cogs/security.py
@@ -1,11 +1,11 @@
import logging
-from discord.ext.commands import Bot, Context, NoPrivateMessage
+from discord.ext.commands import Bot, Cog, Context, NoPrivateMessage
log = logging.getLogger(__name__)
-class Security:
+class Security(Cog):
"""
Security-related helpers
"""
diff --git a/bot/cogs/site.py b/bot/cogs/site.py
index b5e63fb41..4d5b2e811 100644
--- a/bot/cogs/site.py
+++ b/bot/cogs/site.py
@@ -1,7 +1,7 @@
import logging
from discord import Colour, Embed
-from discord.ext.commands import Bot, Context, group
+from discord.ext.commands import Bot, Cog, Context, group
from bot.constants import Channels, STAFF_ROLES, URLs
from bot.decorators import redirect_output
@@ -12,7 +12,7 @@ log = logging.getLogger(__name__)
PAGES_URL = f"{URLs.site_schema}{URLs.site}/pages"
-class Site:
+class Site(Cog):
"""Commands for linking to different parts of the site."""
def __init__(self, bot: Bot):
@@ -46,15 +46,18 @@ class Site:
async def site_resources(self, ctx: Context):
"""Info about the site's Resources page."""
- url = f"{PAGES_URL}/resources"
+ learning_url = f"{PAGES_URL}/resources"
+ tools_url = f"{PAGES_URL}/tools"
- embed = Embed(title="Resources")
- embed.set_footer(text=url)
+ embed = Embed(title="Resources & Tools")
+ embed.set_footer(text=f"{learning_url} | {tools_url}")
embed.colour = Colour.blurple()
embed.description = (
- f"The [Resources page]({url}) on our website contains a "
+ f"The [Resources page]({learning_url}) on our website contains a "
"list of hand-selected goodies that we regularly recommend "
- "to both beginners and experts."
+ f"to both beginners and experts. The [Tools page]({tools_url}) "
+ "contains a couple of the most popular tools for programming in "
+ "Python."
)
await ctx.send(embed=embed)
@@ -111,7 +114,7 @@ class Site:
# Rules were not submitted. Return the default description.
rules_embed.description = (
"The rules and guidelines that apply to this community can be found on"
- " our [rules page](https://pythondiscord.com/about/rules). We expect"
+ f" our [rules page]({PAGES_URL}/rules). We expect"
" all members of the community to have read and understood these."
)
diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py
index c8705ac6f..d36c0795d 100644
--- a/bot/cogs/snekbox.py
+++ b/bot/cogs/snekbox.py
@@ -5,7 +5,7 @@ import textwrap
from signal import Signals
from typing import Optional, Tuple
-from discord.ext.commands import Bot, Context, command, guild_only
+from discord.ext.commands import Bot, Cog, Context, command, guild_only
from bot.constants import Channels, STAFF_ROLES, URLs
from bot.decorators import in_channel
@@ -36,7 +36,7 @@ RAW_CODE_REGEX = re.compile(
MAX_PASTE_LEN = 1000
-class Snekbox:
+class Snekbox(Cog):
"""
Safe evaluation of Python code using Snekbox
"""
diff --git a/bot/cogs/superstarify/__init__.py b/bot/cogs/superstarify/__init__.py
index cccd91304..e9743a2f5 100644
--- a/bot/cogs/superstarify/__init__.py
+++ b/bot/cogs/superstarify/__init__.py
@@ -4,7 +4,7 @@ from datetime import datetime
from discord import Colour, Embed, Member
from discord.errors import Forbidden
-from discord.ext.commands import Bot, Context, command
+from discord.ext.commands import Bot, Cog, Context, command
from bot.cogs.moderation import Moderation
from bot.cogs.modlog import ModLog
@@ -15,10 +15,10 @@ from bot.decorators import with_role
from bot.utils.moderation import post_infraction
log = logging.getLogger(__name__)
-NICKNAME_POLICY_URL = "https://pythondiscord.com/about/rules#nickname-policy"
+NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#wiki-toc-nickname-policy"
-class Superstarify:
+class Superstarify(Cog):
"""
A set of commands to moderate terrible nicknames.
"""
@@ -34,6 +34,7 @@ class Superstarify:
def modlog(self) -> ModLog:
return self.bot.get_cog("ModLog")
+ @Cog.listener()
async def on_member_update(self, before: Member, after: Member):
"""
This event will trigger when someone changes their name.
@@ -91,6 +92,7 @@ class Superstarify:
"to DM them, and a discord.errors.Forbidden error was incurred."
)
+ @Cog.listener()
async def on_member_join(self, member: Member):
"""
This event will trigger when someone (re)joins the server.
diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py
index ec6c5f447..9a3a48bba 100644
--- a/bot/cogs/sync/cog.py
+++ b/bot/cogs/sync/cog.py
@@ -3,7 +3,7 @@ from typing import Callable, Iterable
from discord import Guild, Member, Role
from discord.ext import commands
-from discord.ext.commands import Bot, Context
+from discord.ext.commands import Bot, Cog, Context
from bot import constants
from bot.api import ResponseCodeError
@@ -12,7 +12,7 @@ from bot.cogs.sync import syncers
log = logging.getLogger(__name__)
-class Sync:
+class Sync(Cog):
"""Captures relevant events and sends them to the site."""
# The server to synchronize events on.
@@ -29,6 +29,7 @@ class Sync:
def __init__(self, bot: Bot) -> None:
self.bot = bot
+ @Cog.listener()
async def on_ready(self) -> None:
"""Syncs the roles/users of the guild with the database."""
guild = self.bot.get_guild(self.SYNC_SERVER_ID)
@@ -47,6 +48,7 @@ class Sync:
f"deleted `{total_deleted}`."
)
+ @Cog.listener()
async def on_guild_role_create(self, role: Role) -> None:
"""Adds newly create role to the database table over the API."""
await self.bot.api_client.post(
@@ -60,10 +62,12 @@ class Sync:
}
)
+ @Cog.listener()
async def on_guild_role_delete(self, role: Role) -> None:
"""Deletes role from the database when it's deleted from the guild."""
await self.bot.api_client.delete(f'bot/roles/{role.id}')
+ @Cog.listener()
async def on_guild_role_update(self, before: Role, after: Role) -> None:
"""Syncs role with the database if any of the stored attributes were updated."""
if (
@@ -83,6 +87,7 @@ class Sync:
}
)
+ @Cog.listener()
async def on_member_join(self, member: Member) -> None:
"""
Adds a new user or updates existing user to the database when a member joins the guild.
@@ -118,6 +123,7 @@ class Sync:
# If we got `404`, the user is new. Create them.
await self.bot.api_client.post('bot/users', json=packed)
+ @Cog.listener()
async def on_member_remove(self, member: Member) -> None:
"""Updates the user information when a member leaves the guild."""
await self.bot.api_client.put(
@@ -132,6 +138,7 @@ class Sync:
}
)
+ @Cog.listener()
async def on_member_update(self, before: Member, after: Member) -> None:
"""Updates the user information if any of relevant attributes have changed."""
if (
diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py
index 7b1003148..8e9ba5da3 100644
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -2,9 +2,9 @@ import logging
import time
from discord import Colour, Embed
-from discord.ext.commands import Bot, Context, group
+from discord.ext.commands import Bot, Cog, Context, group
-from bot.constants import Channels, Cooldowns, Keys, MODERATION_ROLES, Roles
+from bot.constants import Channels, Cooldowns, MODERATION_ROLES, Roles
from bot.converters import TagContentConverter, TagNameConverter
from bot.decorators import with_role
from bot.pagination import LinePaginator
@@ -19,7 +19,7 @@ TEST_CHANNELS = (
)
-class Tags:
+class Tags(Cog):
"""
Save new tags and fetch existing tags.
"""
@@ -27,7 +27,6 @@ class Tags:
def __init__(self, bot: Bot):
self.bot = bot
self.tag_cooldowns = {}
- self.headers = {"Authorization": f"Token {Keys.site_api}"}
@group(name='tags', aliases=('tag', 't'), hidden=True, invoke_without_command=True)
async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None):
@@ -82,7 +81,7 @@ class Tags:
"time": time.time(),
"channel": ctx.channel.id
}
- await ctx.send(embed=Embed.from_data(tag['embed']))
+ await ctx.send(embed=Embed.from_dict(tag['embed']))
else:
tags = await self.bot.api_client.get('bot/tags')
diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py
index b2c4cd522..64bf126d6 100644
--- a/bot/cogs/token_remover.py
+++ b/bot/cogs/token_remover.py
@@ -6,7 +6,7 @@ import struct
from datetime import datetime
from discord import Colour, Message
-from discord.ext.commands import Bot
+from discord.ext.commands import Bot, Cog
from discord.utils import snowflake_time
from bot.cogs.modlog import ModLog
@@ -34,7 +34,7 @@ TOKEN_RE = re.compile(
)
-class TokenRemover:
+class TokenRemover(Cog):
"""Scans messages for potential discord.py bot tokens and removes them."""
def __init__(self, bot: Bot):
@@ -44,6 +44,7 @@ class TokenRemover:
def mod_log(self) -> ModLog:
return self.bot.get_cog("ModLog")
+ @Cog.listener()
async def on_message(self, msg: Message):
if msg.author.bot:
return
diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py
index 98208723a..08e77a24e 100644
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -5,7 +5,7 @@ from email.parser import HeaderParser
from io import StringIO
from discord import Colour, Embed
-from discord.ext.commands import AutoShardedBot, Context, command
+from discord.ext.commands import AutoShardedBot, Cog, Context, command
from bot.constants import Channels, STAFF_ROLES
from bot.decorators import in_channel
@@ -13,7 +13,7 @@ from bot.decorators import in_channel
log = logging.getLogger(__name__)
-class Utils:
+class Utils(Cog):
"""
A selection of utilities which don't have a clear category.
"""
diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py
index 6b42c9213..c42d4d67e 100644
--- a/bot/cogs/verification.py
+++ b/bot/cogs/verification.py
@@ -1,7 +1,7 @@
import logging
from discord import Message, NotFound, Object
-from discord.ext.commands import Bot, Context, command
+from discord.ext.commands import Bot, Cog, Context, command
from bot.cogs.modlog import ModLog
from bot.constants import Channels, Event, Roles
@@ -14,8 +14,8 @@ Hello! Welcome to the server, and thanks for verifying yourself!
For your records, these are the documents you accepted:
-`1)` Our rules, here: <https://pythondiscord.com/about/rules>
-`2)` Our privacy policy, here: <https://pythondiscord.com/about/privacy> - you can find information on how to have \
+`1)` Our rules, here: <https://pythondiscord.com/pages/rules>
+`2)` Our privacy policy, here: <https://pythondiscord.com/pages/privacy> - you can find information on how to have \
your information removed here as well.
Feel free to review them at any point!
@@ -28,7 +28,7 @@ If you'd like to unsubscribe from the announcement notifications, simply send `!
"""
-class Verification:
+class Verification(Cog):
"""
User verification and role self-management
"""
@@ -40,6 +40,7 @@ class Verification:
def mod_log(self) -> ModLog:
return self.bot.get_cog("ModLog")
+ @Cog.listener()
async def on_message(self, message: Message):
if message.author.bot:
return # They're a bot, ignore
@@ -152,13 +153,13 @@ class Verification:
)
@staticmethod
- async def __error(ctx: Context, error):
+ async def cog_command_error(ctx: Context, error):
if isinstance(error, InChannelCheckFailure):
# Do nothing; just ignore this error
error.handled = True
@staticmethod
- def __global_check(ctx: Context):
+ def bot_check(ctx: Context):
"""
Block any command within the verification channel that is not !accept.
"""
diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py
index e7b3d70bc..338b6c4ad 100644
--- a/bot/cogs/watchchannels/bigbrother.py
+++ b/bot/cogs/watchchannels/bigbrother.py
@@ -3,7 +3,7 @@ from collections import ChainMap
from typing import Union
from discord import User
-from discord.ext.commands import Context, group
+from discord.ext.commands import Cog, Context, group
from bot.constants import Channels, Roles, Webhooks
from bot.decorators import with_role
@@ -13,7 +13,7 @@ from .watchchannel import WatchChannel, proxy_user
log = logging.getLogger(__name__)
-class BigBrother(WatchChannel):
+class BigBrother(WatchChannel, Cog, name="Big Brother"):
"""Monitors users by relaying their messages to a watch channel to assist with moderation."""
def __init__(self, bot) -> None:
diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py
index 47d207d05..4452d7a59 100644
--- a/bot/cogs/watchchannels/talentpool.py
+++ b/bot/cogs/watchchannels/talentpool.py
@@ -4,7 +4,7 @@ from collections import ChainMap
from typing import Union
from discord import Color, Embed, Member, User
-from discord.ext.commands import Context, group
+from discord.ext.commands import Cog, Context, group
from bot.api import ResponseCodeError
from bot.constants import Channels, Guild, Roles, Webhooks
@@ -16,7 +16,7 @@ log = logging.getLogger(__name__)
STAFF_ROLES = Roles.owner, Roles.admin, Roles.moderator, Roles.helpers # <- In constants after the merge?
-class TalentPool(WatchChannel):
+class TalentPool(WatchChannel, Cog, name="Talentpool"):
"""Relays messages of helper candidates to a watch channel to observe them."""
def __init__(self, bot) -> None:
diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py
index 3a24e3f21..c34b0d5bb 100644
--- a/bot/cogs/watchchannels/watchchannel.py
+++ b/bot/cogs/watchchannels/watchchannel.py
@@ -3,20 +3,20 @@ import datetime
import logging
import re
import textwrap
-from abc import ABC, abstractmethod
+from abc import abstractmethod
from collections import defaultdict, deque
from dataclasses import dataclass
from typing import Optional
import discord
-from discord import Color, Embed, Message, Object, errors
-from discord.ext.commands import BadArgument, Bot, Context
+from discord import Color, Embed, HTTPException, Message, Object, errors
+from discord.ext.commands import BadArgument, Bot, Cog, Context
from bot.api import ResponseCodeError
from bot.cogs.modlog import ModLog
from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig, Icons
from bot.pagination import LinePaginator
-from bot.utils import messages
+from bot.utils import CogABCMeta, messages
from bot.utils.time import time_since
log = logging.getLogger(__name__)
@@ -47,7 +47,7 @@ class MessageHistory:
message_count: int = 0
-class WatchChannel(ABC):
+class WatchChannel(metaclass=CogABCMeta):
"""ABC with functionality for relaying users' messages to a certain channel."""
@abstractmethod
@@ -98,21 +98,14 @@ class WatchChannel(ABC):
"""Starts the watch channel by getting the channel, webhook, and user cache ready."""
await self.bot.wait_until_ready()
- # After updating d.py, this block can be replaced by `fetch_channel` with a try-except
- for attempt in range(1, self.retries+1):
- self.channel = self.bot.get_channel(self.destination)
- if self.channel is None:
- if attempt < self.retries:
- await asyncio.sleep(self.retry_delay)
- else:
- break
- else:
- self.log.error(f"Failed to retrieve the text channel with id {self.destination}")
+ try:
+ self.channel = await self.bot.fetch_channel(self.destination)
+ except HTTPException:
+ self.log.exception(f"Failed to retrieve the text channel with id `{self.destination}`")
- # `get_webhook_info` has been renamed to `fetch_webhook` in newer versions of d.py
try:
- self.webhook = await self.bot.get_webhook_info(self.webhook_id)
- except (discord.HTTPException, discord.NotFound, discord.Forbidden):
+ self.webhook = await self.bot.fetch_webhook(self.webhook_id)
+ except discord.HTTPException:
self.log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`")
if self.channel is None or self.webhook is None:
@@ -169,6 +162,7 @@ class WatchChannel(ABC):
return True
+ @Cog.listener()
async def on_message(self, msg: Message) -> None:
"""Queues up messages sent by watched users."""
if msg.author.id in self.watched_users:
diff --git a/bot/cogs/wolfram.py b/bot/cogs/wolfram.py
index 7dd613083..e88efa033 100644
--- a/bot/cogs/wolfram.py
+++ b/bot/cogs/wolfram.py
@@ -7,7 +7,7 @@ import discord
from dateutil.relativedelta import relativedelta
from discord import Embed
from discord.ext import commands
-from discord.ext.commands import BucketType, Context, check, group
+from discord.ext.commands import BucketType, Cog, Context, check, group
from bot.constants import Colours, STAFF_ROLES, Wolfram
from bot.pagination import ImagePaginator
@@ -163,7 +163,7 @@ async def get_pod_pages(ctx, bot, query: str) -> Optional[List[Tuple]]:
return pages
-class Wolfram:
+class Wolfram(Cog):
"""
Commands for interacting with the Wolfram|Alpha API.
"""
diff --git a/bot/constants.py b/bot/constants.py
index 4e14a85a8..d5b73bd1d 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -412,7 +412,7 @@ class URLs(metaclass=YAMLGetter):
# Misc endpoints
bot_avatar: str
deploy: str
- gitlab_bot_repo: str
+ github_bot_repo: str
status: str
# Site endpoints
diff --git a/bot/patches/__init__.py b/bot/patches/__init__.py
new file mode 100644
index 000000000..fd38ea8cf
--- /dev/null
+++ b/bot/patches/__init__.py
@@ -0,0 +1,6 @@
+"""Subpackage that contains patches for discord.py"""
+from . import message_edited_at
+
+__all__ = [
+ message_edited_at,
+]
diff --git a/bot/patches/message_edited_at.py b/bot/patches/message_edited_at.py
new file mode 100644
index 000000000..528373a9b
--- /dev/null
+++ b/bot/patches/message_edited_at.py
@@ -0,0 +1,32 @@
+"""
+# message_edited_at patch
+
+Date: 2019-09-16
+Author: Scragly
+Added by: Ves Zappa
+
+Due to a bug in our current version of discord.py (1.2.3), the edited_at timestamp of
+`discord.Messages` are not being handled correctly. This patch fixes that until a new
+release of discord.py is released (and we've updated to it).
+"""
+import logging
+
+from discord import message, utils
+
+log = logging.getLogger(__name__)
+
+
+def _handle_edited_timestamp(self, value):
+ """Helper function that takes care of parsing the edited timestamp."""
+ self._edited_timestamp = utils.parse_time(value)
+
+
+def apply_patch():
+ """Applies the `edited_at` patch to the `discord.message.Message` class."""
+ message.Message._handle_edited_timestamp = _handle_edited_timestamp
+ message.Message._HANDLERS['edited_timestamp'] = message.Message._handle_edited_timestamp
+ log.info("Patch applied: message_edited_at")
+
+
+if __name__ == "__main__":
+ apply_patch()
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index 4c99d50e8..d5ae0a7c5 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -1,3 +1,12 @@
+from abc import ABCMeta
+
+from discord.ext.commands import CogMeta
+
+
+class CogABCMeta(CogMeta, ABCMeta):
+ """Metaclass for ABCs meant to be implemented as Cogs."""
+ pass
+
class CaseInsensitiveDict(dict):
"""
diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py
index ded6401b0..f03865013 100644
--- a/bot/utils/scheduling.py
+++ b/bot/utils/scheduling.py
@@ -1,13 +1,15 @@
import asyncio
import contextlib
import logging
-from abc import ABC, abstractmethod
+from abc import abstractmethod
from typing import Dict
+from bot.utils import CogABCMeta
+
log = logging.getLogger(__name__)
-class Scheduler(ABC):
+class Scheduler(metaclass=CogABCMeta):
def __init__(self):
diff --git a/config-default.yml b/config-default.yml
index c9fc3b954..fd83e69a4 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -95,7 +95,7 @@ guild:
bot: 267659945086812160
checkpoint_test: 422077681434099723
defcon: 464469101889454091
- devlog: &DEVLOG 409308876241108992
+ devlog: &DEVLOG 622895325144940554
devtest: &DEVTEST 414574275865870337
help_0: 303906576991780866
help_1: 303906556754395136
@@ -273,7 +273,7 @@ urls:
# Misc URLs
bot_avatar: "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png"
- gitlab_bot_repo: "https://gitlab.com/python-discord/projects/bot"
+ github_bot_repo: "https://github.com/python-discord/bot"
anti_spam:
# Clean messages that violate a rule.
diff --git a/tests/cogs/test_security.py b/tests/cogs/test_security.py
new file mode 100644
index 000000000..1efb460fe
--- /dev/null
+++ b/tests/cogs/test_security.py
@@ -0,0 +1,54 @@
+import logging
+from unittest.mock import MagicMock
+
+import pytest
+from discord.ext.commands import NoPrivateMessage
+
+from bot.cogs import security
+
+
+def cog():
+ bot = MagicMock()
+ return security.Security(bot)
+
+
+def context():
+ return MagicMock()
+
+
+def test_check_additions(cog):
+ cog.bot.check.assert_any_call(cog.check_on_guild)
+ cog.bot.check.assert_any_call(cog.check_not_bot)
+
+
+def test_check_not_bot_for_humans(cog, context):
+ context.author.bot = False
+ assert cog.check_not_bot(context)
+
+
+def test_check_not_bot_for_robots(cog, context):
+ context.author.bot = True
+ assert not cog.check_not_bot(context)
+
+
+def test_check_on_guild_outside_of_guild(cog, context):
+ context.guild = None
+
+ with pytest.raises(NoPrivateMessage, match="This command cannot be used in private messages."):
+ cog.check_on_guild(context)
+
+
+def test_check_on_guild_on_guild(cog, context):
+ context.guild = "lemon's lemonade stand"
+ assert cog.check_on_guild(context)
+
+
+def test_security_cog_load(caplog):
+ bot = MagicMock()
+ security.setup(bot)
+ bot.add_cog.assert_called_once()
+ [record] = caplog.records
+ assert record.message == "Cog loaded: Security"
+ assert record.levelno == logging.INFO
diff --git a/tests/cogs/test_token_remover.py b/tests/cogs/test_token_remover.py
new file mode 100644
index 000000000..9d46b3a05
--- /dev/null
+++ b/tests/cogs/test_token_remover.py
@@ -0,0 +1,133 @@
+import asyncio
+from unittest.mock import MagicMock
+
+import pytest
+from discord import Colour
+
+from bot.cogs.token_remover import (
+ DELETION_MESSAGE_TEMPLATE,
+ TokenRemover,
+ setup as setup_cog,
+)
+from bot.constants import Channels, Colours, Event, Icons
+from tests.helpers import AsyncMock
+
+
+def token_remover():
+ bot = MagicMock()
+ bot.get_cog.return_value = MagicMock()
+ bot.get_cog.return_value.send_log_message = AsyncMock()
+ return TokenRemover(bot=bot)
+
+
+def message():
+ message = MagicMock()
+ message.author.__str__.return_value = 'lemon'
+ message.author.bot = False
+ message.author.avatar_url_as.return_value = 'picture-lemon.png'
+ message.author.id = 42
+ message.author.mention = '@lemon'
+ message.channel.send = AsyncMock()
+ message.channel.mention = '#lemonade-stand'
+ message.content = ''
+ message.delete = AsyncMock()
+ message.id = 555
+ return message
+
+
+ ('content', 'expected'),
+ (
+ ('MTIz', True), # 123
+ ('YWJj', False), # abc
+ )
+)
+def test_is_valid_user_id(content: str, expected: bool):
+ assert TokenRemover.is_valid_user_id(content) is expected
+
+
+ ('content', 'expected'),
+ (
+ ('DN9r_A', True), # stolen from dapi, thanks to the author of the 'token' tag!
+ ('MTIz', False), # 123
+ )
+)
+def test_is_valid_timestamp(content: str, expected: bool):
+ assert TokenRemover.is_valid_timestamp(content) is expected
+
+
+def test_mod_log_property(token_remover):
+ token_remover.bot.get_cog.return_value = 'lemon'
+ assert token_remover.mod_log == 'lemon'
+ token_remover.bot.get_cog.assert_called_once_with('ModLog')
+
+
+def test_ignores_bot_messages(token_remover, message):
+ message.author.bot = True
+ coroutine = token_remover.on_message(message)
+ assert asyncio.run(coroutine) is None
+
+
[email protected]('content', ('', 'lemon wins'))
+def test_ignores_messages_without_tokens(token_remover, message, content):
+ message.content = content
+ coroutine = token_remover.on_message(message)
+ assert asyncio.run(coroutine) is None
+
+
[email protected]('content', ('foo.bar.baz', 'x.y.'))
+def test_ignores_invalid_tokens(token_remover, message, content):
+ message.content = content
+ coroutine = token_remover.on_message(message)
+ assert asyncio.run(coroutine) is None
+
+
+ 'content, censored_token',
+ (
+ ('MTIz.DN9R_A.xyz', 'MTIz.DN9R_A.xxx'),
+ )
+)
+def test_censors_valid_tokens(
+ token_remover, message, content, censored_token, caplog
+):
+ message.content = content
+ coroutine = token_remover.on_message(message)
+ assert asyncio.run(coroutine) is None # still no rval
+
+ # asyncio logs some stuff about its reactor, discard it
+ [_, record] = caplog.records
+ assert record.message == (
+ "Censored a seemingly valid token sent by lemon (`42`) in #lemonade-stand, "
+ f"token was `{censored_token}`"
+ )
+
+ message.delete.assert_called_once_with()
+ message.channel.send.assert_called_once_with(
+ DELETION_MESSAGE_TEMPLATE.format(mention='@lemon')
+ )
+ token_remover.bot.get_cog.assert_called_with('ModLog')
+ message.author.avatar_url_as.assert_called_once_with(static_format='png')
+
+ mod_log = token_remover.bot.get_cog.return_value
+ mod_log.ignore.assert_called_once_with(Event.message_delete, message.id)
+ mod_log.send_log_message.assert_called_once_with(
+ icon_url=Icons.token_removed,
+ colour=Colour(Colours.soft_red),
+ title="Token removed!",
+ text=record.message,
+ thumbnail='picture-lemon.png',
+ channel_id=Channels.mod_alerts
+ )
+
+
+def test_setup(caplog):
+ bot = MagicMock()
+ setup_cog(bot)
+ [record] = caplog.records
+
+ bot.add_cog.assert_called_once()
+ assert record.message == "Cog loaded: TokenRemover"
diff --git a/tests/helpers.py b/tests/helpers.py
new file mode 100644
index 000000000..57c6fcc1a
--- /dev/null
+++ b/tests/helpers.py
@@ -0,0 +1,10 @@
+from unittest.mock import MagicMock
+
+
+__all__ = ('AsyncMock',)
+
+
+# TODO: Remove me on 3.8
+class AsyncMock(MagicMock):
+ async def __call__(self, *args, **kwargs):
+ return super(AsyncMock, self).__call__(*args, **kwargs)
diff --git a/tests/test_resources.py b/tests/test_resources.py
new file mode 100644
index 000000000..2b17aea64
--- /dev/null
+++ b/tests/test_resources.py
@@ -0,0 +1,18 @@
+import json
+import mimetypes
+from pathlib import Path
+from urllib.parse import urlparse
+
+
+def test_stars_valid():
+ """Validates that `bot/resources/stars.json` contains valid images."""
+
+ path = Path('bot', 'resources', 'stars.json')
+ content = path.read_text()
+ data = json.loads(content)
+
+ for url in data.values():
+ assert urlparse(url).scheme == 'https'
+
+ mimetype, _ = mimetypes.guess_type(url)
+ assert mimetype in ('image/jpeg', 'image/png')
diff --git a/tox.ini b/tox.ini
index c84827570..21097cd97 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[flake8]
max-line-length=120
-application_import_names=bot
+application_import_names=bot,tests
exclude=.cache,.venv
ignore=B311,W503,E226,S311,T000
import-order-style=pycharm