diff options
40 files changed, 247 insertions, 188 deletions
| @@ -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 7283aae6d..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      """ 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 77f6eece5..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 @@ -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 b31db60d9..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,6 +17,7 @@ class Logging:      def __init__(self, bot: Bot):          self.bot = bot +    @Cog.listener()      async def on_ready(self):          log.info("Bot connected!") @@ -24,7 +25,10 @@ class Logging:          embed.set_author(              name="Python Bot",              url="https://github.com/python-discord/bot", -            icon_url="https://github.com/python-discord/branding/blob/master/logos/logo_circle/logo_circle.png" +            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 532a44f4d..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 @@ -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 5a61425be..cadc1bf92 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -3,9 +3,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 @@ -37,7 +37,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. @@ -69,21 +69,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 b540827bf..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): 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 b2e31db3e..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 @@ -18,7 +18,7 @@ log = logging.getLogger(__name__)  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 efbcda166..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 @@ -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/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): | 
