mirror of
				https://gitlab.com/ddorn/tfjm-discord-bot.git
				synced 2025-11-04 16:02:27 +01:00 
			
		
		
		
	✨ CustomBot with full reload
This commit is contained in:
		@@ -1,4 +1,6 @@
 | 
			
		||||
import code
 | 
			
		||||
import sys
 | 
			
		||||
from importlib import reload
 | 
			
		||||
from pprint import pprint
 | 
			
		||||
 | 
			
		||||
import discord
 | 
			
		||||
@@ -8,13 +10,20 @@ from discord.ext.commands import Cog
 | 
			
		||||
from discord.utils import get
 | 
			
		||||
 | 
			
		||||
from src.constants import *
 | 
			
		||||
from src.core import CustomBot
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
COGS_SHORTCUTS = {"d": "dev", "ts": "teams", "t": "tirages", "m": "misc", "e": "errors"}
 | 
			
		||||
COGS_SHORTCUTS = {
 | 
			
		||||
    "d": "tirages",
 | 
			
		||||
    "e": "errors",
 | 
			
		||||
    "m": "misc",
 | 
			
		||||
    "t": "teams",
 | 
			
		||||
    "u": "src.utils",
 | 
			
		||||
    "v": "dev",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DevCog(Cog, name="Dev tools"):
 | 
			
		||||
    def __init__(self, bot: Bot):
 | 
			
		||||
    def __init__(self, bot: CustomBot):
 | 
			
		||||
        self.bot = bot
 | 
			
		||||
 | 
			
		||||
    @command(name="interrupt")
 | 
			
		||||
@@ -69,6 +78,11 @@ class DevCog(Cog, name="Dev tools"):
 | 
			
		||||
        possibles: `teams`, `tirages`, `dev`.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if name is None:
 | 
			
		||||
            self.bot.reload()
 | 
			
		||||
            await ctx.send(":tada: The bot was reloaded !")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        names = [name] if name else list(COGS_SHORTCUTS.values())
 | 
			
		||||
 | 
			
		||||
        for name in names:
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@ import discord
 | 
			
		||||
from discord.ext.commands import *
 | 
			
		||||
from discord.utils import maybe_coroutine
 | 
			
		||||
 | 
			
		||||
from src.errors import UnwantedCommand
 | 
			
		||||
 | 
			
		||||
from src.core import CustomBot
 | 
			
		||||
from src.errors import UnwantedCommand, TfjmError
 | 
			
		||||
 | 
			
		||||
# Global variable and function because I'm too lazy to make a metaclass
 | 
			
		||||
handlers = {}
 | 
			
		||||
@@ -29,6 +29,9 @@ def handles(error_type):
 | 
			
		||||
class ErrorsCog(Cog):
 | 
			
		||||
    """This cog defines all the handles for errors."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bot: CustomBot):
 | 
			
		||||
        self.bot = bot
 | 
			
		||||
 | 
			
		||||
    @Cog.listener()
 | 
			
		||||
    async def on_command_error(self, ctx: Context, error: CommandError):
 | 
			
		||||
        print(repr(error), file=sys.stderr)
 | 
			
		||||
@@ -47,10 +50,11 @@ class ErrorsCog(Cog):
 | 
			
		||||
            msg = await maybe_coroutine(handler, self, ctx, error)
 | 
			
		||||
 | 
			
		||||
        if msg:
 | 
			
		||||
            await ctx.send(msg)
 | 
			
		||||
            message = await ctx.send(msg)
 | 
			
		||||
            await self.bot.wait_for_bin(ctx.message.author, message)
 | 
			
		||||
 | 
			
		||||
    @handles(UnwantedCommand)
 | 
			
		||||
    async def on_unwanted_command(self, ctx, error):
 | 
			
		||||
    async def on_unwanted_command(self, ctx, error: UnwantedCommand):
 | 
			
		||||
        await ctx.message.delete()
 | 
			
		||||
        author: discord.Message
 | 
			
		||||
        await ctx.author.send(
 | 
			
		||||
@@ -59,14 +63,19 @@ class ErrorsCog(Cog):
 | 
			
		||||
            + "\nC'est pas grave, c'est juste pour ne pas encombrer "
 | 
			
		||||
            "le chat lors du tirage."
 | 
			
		||||
        )
 | 
			
		||||
        await ctx.author.send("Raison: " + error.original.msg)
 | 
			
		||||
        await ctx.author.send("Raison: " + error.msg)
 | 
			
		||||
 | 
			
		||||
    @handles(TfjmError)
 | 
			
		||||
    async def on_tfjm_error(self, ctx: Context, error: TfjmError):
 | 
			
		||||
        msg = await ctx.send(error.msg)
 | 
			
		||||
        await self.bot.wait_for_bin(ctx.author, msg)
 | 
			
		||||
 | 
			
		||||
    @handles(CommandInvokeError)
 | 
			
		||||
    async def on_command_invoke_error(self, ctx, error):
 | 
			
		||||
        specific_handler = handlers.get(type(error.original))
 | 
			
		||||
 | 
			
		||||
        if specific_handler:
 | 
			
		||||
            return await specific_handler(self, ctx, error)
 | 
			
		||||
            return await specific_handler(self, ctx, error.original)
 | 
			
		||||
 | 
			
		||||
        traceback.print_tb(error.original.__traceback__, file=sys.stderr)
 | 
			
		||||
        return (
 | 
			
		||||
@@ -91,4 +100,4 @@ class ErrorsCog(Cog):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def setup(bot):
 | 
			
		||||
    bot.add_cog(ErrorsCog())
 | 
			
		||||
    bot.add_cog(ErrorsCog(bot))
 | 
			
		||||
 
 | 
			
		||||
@@ -17,13 +17,15 @@ from discord.ext.commands import (
 | 
			
		||||
    Group,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from src import utils
 | 
			
		||||
from src.constants import *
 | 
			
		||||
from src.constants import Emoji
 | 
			
		||||
from src.utils import has_role
 | 
			
		||||
from src.core import CustomBot
 | 
			
		||||
from src.utils import has_role, start_time
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MiscCog(Cog, name="Divers"):
 | 
			
		||||
    def __init__(self, bot: Bot):
 | 
			
		||||
    def __init__(self, bot: CustomBot):
 | 
			
		||||
        self.bot = bot
 | 
			
		||||
        self.show_hidden = False
 | 
			
		||||
        self.verify_checks = True
 | 
			
		||||
@@ -55,6 +57,7 @@ class MiscCog(Cog, name="Divers"):
 | 
			
		||||
 | 
			
		||||
        await message.add_reaction(Emoji.JOY)
 | 
			
		||||
        await message.add_reaction(Emoji.SOB)
 | 
			
		||||
        await self.bot.wait_for_bin(ctx.message.author, message)
 | 
			
		||||
 | 
			
		||||
    @command(name="status")
 | 
			
		||||
    @commands.has_role(Role.CNO)
 | 
			
		||||
@@ -65,7 +68,7 @@ class MiscCog(Cog, name="Divers"):
 | 
			
		||||
        benevoles = [g for g in guild.members if has_role(g, Role.BENEVOLE)]
 | 
			
		||||
        participants = [g for g in guild.members if has_role(g, Role.PARTICIPANT)]
 | 
			
		||||
        no_role = [g for g in guild.members if g.top_role == guild.default_role]
 | 
			
		||||
        uptime = datetime.timedelta(seconds=round(time() - START_TIME))
 | 
			
		||||
        uptime = datetime.timedelta(seconds=round(time() - start_time()))
 | 
			
		||||
 | 
			
		||||
        infos = {
 | 
			
		||||
            "Bénévoles": len(benevoles),
 | 
			
		||||
 
 | 
			
		||||
@@ -7,13 +7,14 @@ from discord.ext.commands import Cog, Bot, group, Context
 | 
			
		||||
from discord.utils import get, find
 | 
			
		||||
 | 
			
		||||
from src.constants import *
 | 
			
		||||
from src.core import CustomBot
 | 
			
		||||
from src.utils import has_role
 | 
			
		||||
 | 
			
		||||
Team = namedtuple("Team", ["name", "trigram", "tournoi", "secret", "status"])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TeamsCog(Cog, name="Teams"):
 | 
			
		||||
    def __init__(self, bot: Bot):
 | 
			
		||||
    def __init__(self, bot: CustomBot):
 | 
			
		||||
        self.bot = bot
 | 
			
		||||
        self.teams = self.load_teams()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ from discord.ext.commands import group, Cog, Context
 | 
			
		||||
from discord.utils import get
 | 
			
		||||
 | 
			
		||||
from src.constants import *
 | 
			
		||||
from src.core import CustomBot
 | 
			
		||||
from src.errors import TfjmError, UnwantedCommand
 | 
			
		||||
 | 
			
		||||
__all__ = ["Tirage", "TirageCog"]
 | 
			
		||||
@@ -606,7 +607,7 @@ class TirageOrderPhase(OrderPhase):
 | 
			
		||||
 | 
			
		||||
class TirageCog(Cog, name="Tirages"):
 | 
			
		||||
    def __init__(self, bot):
 | 
			
		||||
        self.bot: commands.Bot = bot
 | 
			
		||||
        self.bot: CustomBot = bot
 | 
			
		||||
 | 
			
		||||
        # We retrieve the global variable.
 | 
			
		||||
        # We don't want tirages to be ust an attribute
 | 
			
		||||
@@ -627,13 +628,21 @@ class TirageCog(Cog, name="Tirages"):
 | 
			
		||||
        if channel in self.tirages:
 | 
			
		||||
            await self.tirages[channel].dice(ctx, n)
 | 
			
		||||
        else:
 | 
			
		||||
            if n == 0:
 | 
			
		||||
                raise TfjmError(f"Un dé sans faces ? Le concept m'intéresse...")
 | 
			
		||||
            if n < 1:
 | 
			
		||||
                raise TfjmError(f"Je ne peux pas lancer un dé à {n} faces, désolé.")
 | 
			
		||||
                raise TfjmError(
 | 
			
		||||
                    f"Je ne peux pas lancer un dé avec un "
 | 
			
		||||
                    f"nombre négatif faces, désolé."
 | 
			
		||||
                )
 | 
			
		||||
            if len(str(n)) > 1900:
 | 
			
		||||
                raise TfjmError(
 | 
			
		||||
                    "Oulà... Je sais que la taille ça ne compte pas, "
 | 
			
		||||
                    "mais là il est vraiment gros ton dé !"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            dice = random.randint(1, n)
 | 
			
		||||
            await ctx.send(
 | 
			
		||||
                f"Le dé à {n} face{'s' * (n > 1)} s'est arrêté sur... **{dice}**"
 | 
			
		||||
            )
 | 
			
		||||
            await ctx.send(f"{ctx.author.mention} : {Emoji.DICE} {dice}")
 | 
			
		||||
 | 
			
		||||
    @commands.command(
 | 
			
		||||
        name="random-problem",
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,8 @@ class Role:
 | 
			
		||||
class Emoji:
 | 
			
		||||
    JOY = "😂"
 | 
			
		||||
    SOB = "😭"
 | 
			
		||||
    BIN = "🗑️"
 | 
			
		||||
    DICE = "🎲"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class File:
 | 
			
		||||
@@ -71,3 +73,8 @@ class File:
 | 
			
		||||
with open(File.TOP_LEVEL / "data" / "problems") as f:
 | 
			
		||||
    PROBLEMS = f.read().splitlines()
 | 
			
		||||
MAX_REFUSE = len(PROBLEMS) - 4  # -5 usually but not in 2020 because of covid-19
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def setup(bot):
 | 
			
		||||
    # Just so we can reload the constants
 | 
			
		||||
    pass
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								src/core.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/core.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import sys
 | 
			
		||||
from importlib import reload
 | 
			
		||||
 | 
			
		||||
import psutil
 | 
			
		||||
from discord import User, Message, Reaction
 | 
			
		||||
from discord.ext.commands import Bot
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ["CustomBot"]
 | 
			
		||||
 | 
			
		||||
from discord.utils import get
 | 
			
		||||
 | 
			
		||||
from src.constants import Emoji
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomBot(Bot):
 | 
			
		||||
    """
 | 
			
		||||
    This is the same as a discord bot except
 | 
			
		||||
    for class reloading and it provides hints
 | 
			
		||||
    for the type checker about the modules
 | 
			
		||||
    that are added by extensions.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.__class__.__name__}:{hex(id(self.__class__))} obj at {hex(id(self))}"
 | 
			
		||||
 | 
			
		||||
    def reload(self):
 | 
			
		||||
        cls = self.__class__
 | 
			
		||||
        module_name = cls.__module__
 | 
			
		||||
        old_module = sys.modules[module_name]
 | 
			
		||||
 | 
			
		||||
        print("Trying to reload the bot.")
 | 
			
		||||
        try:
 | 
			
		||||
            # del sys.modules[module_name]
 | 
			
		||||
            module = reload(old_module)
 | 
			
		||||
            self.__class__ = getattr(module, cls.__name__, cls)
 | 
			
		||||
        except:
 | 
			
		||||
            print("Could not reload the bot :/")
 | 
			
		||||
            raise
 | 
			
		||||
        print("The bot has reloaded !")
 | 
			
		||||
 | 
			
		||||
    async def wait_for_bin(bot: Bot, user: User, *msgs: Message, timeout=300):
 | 
			
		||||
        """Wait for timeout seconds for `user` to delete the messages."""
 | 
			
		||||
 | 
			
		||||
        msgs = list(msgs)
 | 
			
		||||
 | 
			
		||||
        assert msgs, "No messages in wait_for_bin"
 | 
			
		||||
 | 
			
		||||
        for m in msgs:
 | 
			
		||||
            await m.add_reaction(Emoji.BIN)
 | 
			
		||||
 | 
			
		||||
        def check(reaction: Reaction, u):
 | 
			
		||||
            return (
 | 
			
		||||
                user == u
 | 
			
		||||
                and any(m.id == reaction.message.id for m in msgs)
 | 
			
		||||
                and str(reaction.emoji) == Emoji.BIN
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            while msgs:
 | 
			
		||||
                reaction, u = await bot.wait_for(
 | 
			
		||||
                    "reaction_add", check=check, timeout=timeout
 | 
			
		||||
                )
 | 
			
		||||
                the_msg = get(msgs, id=reaction.message.id)
 | 
			
		||||
                await the_msg.delete()
 | 
			
		||||
                msgs.remove(the_msg)
 | 
			
		||||
        except asyncio.TimeoutError:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        for m in msgs:
 | 
			
		||||
            await m.clear_reaction(Emoji.BIN)
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
#!/bin/python
 | 
			
		||||
 | 
			
		||||
from discord.ext import commands
 | 
			
		||||
 | 
			
		||||
from src.constants import *
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# We allow "! " to catch people that put a space in their commands.
 | 
			
		||||
# It must be in first otherwise "!" always match first and the space is not recognised
 | 
			
		||||
bot = commands.Bot(("! ", "!"))
 | 
			
		||||
from src.core import CustomBot
 | 
			
		||||
 | 
			
		||||
bot = CustomBot(("! ", "!"))
 | 
			
		||||
 | 
			
		||||
# Global variable to hold the tirages.
 | 
			
		||||
# We *want* it to be global so we can reload the tirages cog without
 | 
			
		||||
@@ -25,6 +26,7 @@ bot.load_extension("src.cogs.errors")
 | 
			
		||||
bot.load_extension("src.cogs.misc")
 | 
			
		||||
bot.load_extension("src.cogs.teams")
 | 
			
		||||
bot.load_extension("src.cogs.tirages")
 | 
			
		||||
bot.load_extension("src.utils")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								src/utils.py
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								src/utils.py
									
									
									
									
									
								
							@@ -1,6 +1,12 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
from typing import Sequence
 | 
			
		||||
 | 
			
		||||
import psutil
 | 
			
		||||
from discord import Message
 | 
			
		||||
from discord import Message, Member, User, Reaction
 | 
			
		||||
from discord.ext.commands import Context, Bot
 | 
			
		||||
from discord.utils import get
 | 
			
		||||
 | 
			
		||||
from src.constants import Emoji
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_role(member, role: str):
 | 
			
		||||
@@ -9,17 +15,9 @@ def has_role(member, role: str):
 | 
			
		||||
    return any(r.name == role for r in member.roles)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def send_and_bin(bot: Bot, ctx: Context, msg=None, *, embed=None):
 | 
			
		||||
    """Send a message and wait 5min for the author to delete it."""
 | 
			
		||||
 | 
			
		||||
    message: Message = await ctx.send(msg, embed=embed)
 | 
			
		||||
 | 
			
		||||
    await msg
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def start_time():
 | 
			
		||||
def start_time(self):
 | 
			
		||||
    return psutil.Process().create_time()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def setup(bot):
 | 
			
		||||
    bot.send_and_bin = send_and_bin
 | 
			
		||||
def setup(bot: Bot):
 | 
			
		||||
    pass
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user