diff --git a/Gwendolyn.py b/Gwendolyn.py index 47e7977..0494ec4 100644 --- a/Gwendolyn.py +++ b/Gwendolyn.py @@ -4,7 +4,7 @@ from discord.ext import commands from discord_slash import SlashCommand from pymongo import MongoClient from funcs import Money, StarWars, Games, Other, LookupFuncs -from utils import Options, Credentials, logThis, makeFiles, databaseFuncs +from utils import Options, Credentials, logThis, makeFiles, databaseFuncs, EventHandler, ErrorHandler class Gwendolyn(commands.Bot): def __init__(self): @@ -25,6 +25,8 @@ class Gwendolyn(commands.Bot): self.games = Games(self) self.money = Money(self) self.databaseFuncs = databaseFuncs(self) + self.eventHandler = EventHandler(self) + self.errorHandler = ErrorHandler(self) intents = discord.Intents.default() intents.members = True diff --git a/cogs/EventCog.py b/cogs/EventCog.py index d732ccd..34388b1 100644 --- a/cogs/EventCog.py +++ b/cogs/EventCog.py @@ -1,4 +1,4 @@ -import discord, traceback +import discord from discord.ext import commands class EventCog(commands.Cog): @@ -8,10 +8,7 @@ class EventCog(commands.Cog): # Syncs commands, sets the game, and logs when the bot logs in @commands.Cog.listener() async def on_ready(self): - await self.bot.databaseFuncs.syncCommands() - self.bot.log("Logged in as "+self.bot.user.name+", "+str(self.bot.user.id), level = 25) - game = discord.Game("Use /help for commands") - await self.bot.change_presence(activity=game, status = discord.Status.online) + await self.bot.eventHandler.on_ready() @commands.Cog.listener() async def on_disconnect(self): @@ -20,38 +17,18 @@ class EventCog(commands.Cog): # Logs when user sends a command @commands.Cog.listener() async def on_slash_command(self, ctx): - self.bot.log(f"{ctx.author.display_name} ran /{ctx.name}", str(ctx.channel_id), level = 25) + logMessage = f"{ctx.author.display_name} ran /{ctx.name}" + self.bot.log(logMessage, str(ctx.channel_id), level = 25) # Logs if a command experiences an error @commands.Cog.listener() async def on_slash_command_error(self, ctx, error): - if isinstance(error, commands.CommandNotFound): - await ctx.send("That's not a command (error code 001)") - elif isinstance(error,commands.errors.MissingRequiredArgument): - self.bot.log(f"{error}",str(ctx.channel_id)) - await ctx.send("Missing command parameters (error code 002). Try using `!help [command]` to find out how to use the command.") - else: - exception = traceback.format_exception(type(error), error, error.__traceback__) - stopAt = "\nThe above exception was the direct cause of the following exception:\n\n" - if stopAt in exception: - index = exception.index(stopAt) - exception = exception[:index] - - exceptionString = "".join(exception) - self.bot.log([f"exception in /{ctx.name}", f"{exceptionString}"],str(ctx.channel_id), 40) - await ctx.send("Something went wrong (error code 000)") + await self.bot.errorHandler.on_slash_command_error(ctx, error) # Logs if an error occurs @commands.Cog.listener() async def on_error(self, method): - exception = traceback.format_exc() - stopAt = "\nThe above exception was the direct cause of the following exception:\n\n" - if stopAt in exception: - index = exception.index(stopAt) - exception = exception[:index] - - exceptionString = "".join(exception) - self.bot.log([f"exception in /{method}", f"{exceptionString}"], level = 40) + await self.bot.errorHandler.on_error(method) def setup(bot): bot.add_cog(EventCog(bot)) diff --git a/cogs/GameCogs.py b/cogs/GameCogs.py index ed0ca78..d5aaf05 100644 --- a/cogs/GameCogs.py +++ b/cogs/GameCogs.py @@ -1,8 +1,6 @@ import discord, asyncio, json from discord.ext import commands from discord_slash import cog_ext -from discord_slash import SlashCommandOptionType as scot - from utils import getParams params = getParams() @@ -15,73 +13,22 @@ class GamesCog(commands.Cog): # Checks user balance @cog_ext.cog_slash(**params["balance"]) async def balance(self, ctx): - await ctx.defer() - response = self.bot.money.checkBalance("#"+str(ctx.author.id)) - if response == 1: - new_message = ctx.author.display_name + " has " + str(response) + " GwendoBuck" - else: - new_message = ctx.author.display_name + " has " + str(response) + " GwendoBucks" - await ctx.send(new_message) + await self.bot.money.sendBalance(ctx) # Gives another user an amount of GwendoBucks @cog_ext.cog_slash(**params["give"]) async def give(self, ctx, user, amount): - await ctx.defer() - username = user.display_name - if self.bot.databaseFuncs.getID(username) == None: - async for member in ctx.guild.fetch_members(limit=None): - if member.display_name.lower() == username.lower(): - username = member.display_name - userID = "#" + str(member.id) - self.bot.database["users"].insert_one({"_id":userID,"user name":username,"money":0}) - response = self.bot.money.giveMoney("#"+str(ctx.author.id),username,amount) - await ctx.send(response) + await self.bot.money.giveMoney(ctx, user, amount) # Invest GwendoBucks in the stock market @cog_ext.cog_slash(**params["invest"]) async def invest(self, ctx, parameters = "check"): - await ctx.defer() - response = self.bot.games.invest.parseInvest(parameters,"#"+str(ctx.author.id)) - if response.startswith("**"): - responses = response.split("\n") - em = discord.Embed(title=responses[0],description="\n".join(responses[1:]),colour=0x00FF00) - await ctx.send(embed=em) - else: - await ctx.send(response) + await self.bot.games.invest.parseInvest(ctx, parameters) # Runs a game of trivia @cog_ext.cog_slash(**params["trivia"]) async def trivia(self, ctx, answer = ""): - await ctx.defer() - if answer == "": - question, options, correctAnswer = self.bot.games.trivia.triviaStart(str(ctx.channel_id)) - if options != "": - results = "**"+question+"**\n" - for x, option in enumerate(options): - results += chr(x+97) + ") "+option+"\n" - - await ctx.send(results) - - await asyncio.sleep(60) - - self.bot.games.trivia.triviaCountPoints(str(ctx.channel_id)) - - self.bot.databaseFuncs.deleteGame("trivia questions",str(ctx.channel_id)) - - self.bot.log("Time's up for the trivia question",str(ctx.channel_id)) - await ctx.send("Time's up The answer was \""+chr(correctAnswer)+") "+options[correctAnswer-97]+"\". Anyone who answered that has gotten 1 GwendoBuck") - else: - await ctx.send(question, hidden=True) - - elif answer in ["a","b","c","d"]: - response = self.bot.games.trivia.triviaAnswer("#"+str(ctx.author.id),str(ctx.channel_id),answer) - if response.startswith("Locked in "): - await ctx.send(f"{ctx.author.display_name} answered {answer}") - else: - await ctx.send(response) - else: - self.bot.log("I didn't understand that (error code 1101)",str(ctx.channel_id)) - await ctx.send("I didn't understand that (error code 1101)") + await self.bot.games.trivia.triviaParse(ctx, answer) class BlackjackCog(commands.Cog): diff --git a/funcs/games/blackjack.py b/funcs/games/blackjack.py index 7fb9c02..eb7ce62 100644 --- a/funcs/games/blackjack.py +++ b/funcs/games/blackjack.py @@ -170,7 +170,6 @@ class Blackjack(): return hand, allStanding, preAllStanding - # When players try to hit def blackjackHit(self,channel,user,handNumber = 0): game = self.bot.database["blackjack games"].find_one({"_id":channel}) @@ -224,7 +223,6 @@ class Blackjack(): self.bot.log(user+" tried to hit without being in the game") return "You have to enter the game before you can hit" - # When players try to double down def blackjackDouble(self,channel,user,handNumber = 0): game = self.bot.database["blackjack games"].find_one({"_id":channel}) @@ -562,7 +560,6 @@ class Blackjack(): return finalWinnings - def calcWinnings(self,hand, dealerValue, topLevel, dealerBlackjack, dealerBusted): self.bot.log("Calculating winnings") reason = "" @@ -703,7 +700,7 @@ class Blackjack(): else: self.bot.log("Ending loop on round "+str(gameRound),str(channel.id)) - async def parseBlackjack(self,content, ctx): + async def parseBlackjack(self, content, ctx): # Blackjack shuffle variables blackjackMinCards = 50 blackjackDecks = 4 diff --git a/funcs/games/invest.py b/funcs/games/invest.py index 04a80dc..c34310a 100644 --- a/funcs/games/invest.py +++ b/funcs/games/invest.py @@ -1,3 +1,5 @@ +import discord + class Invest(): def __init__(self, bot): self.bot = bot @@ -103,35 +105,48 @@ class Invest(): else: return "no" - def parseInvest(self, content: str, user : str): - if content.startswith("check"): - commands = content.split(" ") + async def parseInvest(self, ctx, parameters): + try: + await ctx.defer() + except: + self.bot.log("Defer failed") + user = f"#{ctx.author.id}" + + if parameters.startswith("check"): + commands = parameters.split(" ") if len(commands) == 1: - return self.getPortfolio(user) + response = self.getPortfolio(user) else: price = self.getPrice(commands[1]) if price == 0: - return f"{commands[1].upper()} is not traded on the american market." + response = f"{commands[1].upper()} is not traded on the american market." else: price = f"{price:,}".replace(",",".") - return f"The current {commands[1].upper()} stock is valued at **{price}** GwendoBucks" + response = f"The current {commands[1].upper()} stock is valued at **{price}** GwendoBucks" - elif content.startswith("buy"): - commands = content.split(" ") + elif parameters.startswith("buy"): + commands = parameters.split(" ") if len(commands) == 3: - #try: - return self.buyStock(user,commands[1],int(commands[2])) - #except: - # return "The command must be given as \"/invest buy [stock] [amount of GwendoBucks to purchase with]\"" + response = self.buyStock(user,commands[1],int(commands[2])) else: - return "You must give both a stock name and an amount of gwendobucks you wish to spend." + response = "You must give both a stock name and an amount of gwendobucks you wish to spend." - elif content.startswith("sell"): - commands = content.split(" ") + elif parameters.startswith("sell"): + commands = parameters.split(" ") if len(commands) == 3: try: - return self.sellStock(user,commands[1],int(commands[2])) + response = self.sellStock(user,commands[1],int(commands[2])) except: - return "The command must be given as \"/invest sell [stock] [amount of GwendoBucks to sell stocks for]\"" + response = "The command must be given as \"/invest sell [stock] [amount of GwendoBucks to sell stocks for]\"" else: - return "You must give both a stock name and an amount of GwendoBucks you wish to sell stocks for." + response = "You must give both a stock name and an amount of GwendoBucks you wish to sell stocks for." + + else: + response = "Incorrect parameters" + + if response.startswith("**"): + responses = response.split("\n") + em = discord.Embed(title=responses[0],description="\n".join(responses[1:]),colour=0x00FF00) + await ctx.send(embed=em) + else: + await ctx.send(response) diff --git a/funcs/games/money.py b/funcs/games/money.py index f4d28e3..ef9458f 100644 --- a/funcs/games/money.py +++ b/funcs/games/money.py @@ -14,6 +14,15 @@ class Money(): return userData["money"] else: return 0 + async def sendBalance(self, ctx): + await ctx.defer() + response = self.checkBalance("#"+str(ctx.author.id)) + if response == 1: + new_message = ctx.author.display_name + " has " + str(response) + " GwendoBuck" + else: + new_message = ctx.author.display_name + " has " + str(response) + " GwendoBucks" + await ctx.send(new_message) + # Adds money to the account of a user def addMoney(self,user,amount): self.bot.log("adding "+str(amount)+" to "+user+"'s account") @@ -26,26 +35,39 @@ class Money(): self.database["users"].insert_one({"_id":user,"user name":self.bot.databaseFuncs.getName(user),"money":amount}) # Transfers money from one user to another - def giveMoney(self,user,targetUser,amount): - userData = self.database["users"].find_one({"_id":user}) - targetUser = self.bot.databaseFuncs.getID(targetUser) + async def giveMoney(self, ctx, user, amount): + try: + await ctx.defer() + except: + self.bot.log("Defer failed") + username = user.display_name + if self.bot.databaseFuncs.getID(username) == None: + async for member in ctx.guild.fetch_members(limit=None): + if member.display_name.lower() == username.lower(): + username = member.display_name + userID = "#" + str(member.id) + newUser = {"_id":userID,"user name":username,"money":0} + self.bot.database["users"].insert_one(newUser) + + userData = self.database["users"].find_one({"_id":f"#{ctx.author.id}"}) + targetUser = self.bot.databaseFuncs.getID(username) if amount > 0: if targetUser != None: if userData != None: if userData["money"] >= amount: - self.addMoney(user,-1 * amount) + self.addMoney(f"#{ctx.author.id}",-1 * amount) self.addMoney(targetUser,amount) - return "Transferred "+str(amount)+" GwendoBucks to "+self.bot.databaseFuncs.getName(targetUser) + await ctx.send(f"Transferred {amount} GwendoBucks to {username}") else: - self.bot.log("They didn't have enough GwendoBucks (error code 1223b)") - return "You don't have that many GwendoBucks (error code 1223b)" + self.bot.log("They didn't have enough GwendoBucks") + await ctx.send("You don't have that many GwendoBucks") else: - self.bot.log("They didn't have enough GwendoBucks (error code 1223a)") - return "You don't have that many GwendoBucks (error code 1223a)" + self.bot.log("They didn't have enough GwendoBucks") + await ctx.send("You don't have that many GwendoBuck") else: self.bot.log("They weren't in the system") - return "The target doesn't exist" + await ctx.send("The target doesn't exist") else: self.bot.log("They tried to steal") - return "Yeah, no. You can't do that" + await ctx.send("Yeah, no. You can't do that") diff --git a/funcs/games/trivia.py b/funcs/games/trivia.py index 3b75cdf..6c75fc6 100644 --- a/funcs/games/trivia.py +++ b/funcs/games/trivia.py @@ -1,6 +1,7 @@ import json import urllib import random +import asyncio class Trivia(): def __init__(self, bot): @@ -83,3 +84,38 @@ class Trivia(): self.bot.log("Couldn't find the question (error code 1102)") return None + + async def triviaParse(self, ctx, answer): + try: + await ctx.defer() + except: + self.bot.log("defer failed") + if answer == "": + question, options, correctAnswer = self.triviaStart(str(ctx.channel_id)) + if options != "": + results = "**"+question+"**\n" + for x, option in enumerate(options): + results += chr(x+97) + ") "+option+"\n" + + await ctx.send(results) + + await asyncio.sleep(60) + + self.triviaCountPoints(str(ctx.channel_id)) + + self.bot.databaseFuncs.deleteGame("trivia questions",str(ctx.channel_id)) + + self.bot.log("Time's up for the trivia question",str(ctx.channel_id)) + await ctx.send("Time's up The answer was \""+chr(correctAnswer)+") "+options[correctAnswer-97]+"\". Anyone who answered that has gotten 1 GwendoBuck") + else: + await ctx.send(question, hidden=True) + + elif answer in ["a","b","c","d"]: + response = self.triviaAnswer("#"+str(ctx.author.id), str(ctx.channel_id), answer) + if response.startswith("Locked in "): + await ctx.send(f"{ctx.author.display_name} answered {answer})") + else: + await ctx.send(response) + else: + self.bot.log("I didn't understand that (error code 1101)",str(ctx.channel_id)) + await ctx.send("I didn't understand that (error code 1101)") diff --git a/project-guidelines.md b/project-guidelines.md index c351765..852ae7d 100644 --- a/project-guidelines.md +++ b/project-guidelines.md @@ -84,7 +84,7 @@ All the Python code should follow the [PEP 8 guidelines](https://www.python.org/ Code called by a command should not have `try` and `except` statements. All errors should be raised to the `on_command_error()` or `Command.error()` functions, where they can be dealt with. ## Cogs -The `Command` methods in cogs should only exist to perform small tasks or call code from elsewhere in the Gwendolyn code. Therefore, a single `Command` method should not contain more than 3 lines of code and should not use any modules other than `Discord.py` and the Gwendolyn modules. +The `Command` methods in cogs should only exist to perform small tasks or call code from elsewhere in the Gwendolyn code. Therefore, a single `Command` method should not contain more than 3 lines of code and should not use any modules other than `Discord.py` and the Gwendolyn modules. If a cog method calls a function in Gwendolyn, ctx must be passed, and the function should handle sending messages. ## Codebase Management ### Folders @@ -102,11 +102,11 @@ Things you should know about the logging: + The function can take either a list of strings or a string as its first parameter. If the parameter is a string, it is converted to a list of 1 string. + The first string in the list is printed. All strings in the list are logged to the log-file. + If the list is longer than 1 string, `(details in log)` is added to the printed string. -+ The level parameter is 20 by default, which means the level is `INFO`. 40 corresponds to a level of `ERROR`, and 10 corresponds to a level of `DEBUG`. Only use these levels when logging. -+ Logs of level `DEBUG` are not printed. -+ Logs of level `ERROR` should only be created in the `on_command_error()` or `Command.error()` functions. ++ The level parameter is 20 by default, which means the level is `INFO`. 40 corresponds to a level of `ERROR`, and 25 corresponds to `print`. ++ Logs of level `INFO` are not printed. ++ Logs of level `ERROR` should only be created in the `on_command_error()`, `on_error()` or `Command.error()` functions. ### Logging rules 1. Never call the `logThis()` function from `/utils/utilFuncs/`. Always call `bot.log`. -1. The `on_slash_command()` and `on_ready()` events are the only times log should be called at level 20.`DEBUG` level logs should be used for all other logging. +1. The `on_slash_command()` and `on_ready()` events are the only times log should be called at level 25. `INFO` level logs should be used for all other logging. 1. Always provide the channel id if available. Although you shouldn't pass the channel id to a function purely to use it in logs. diff --git a/utils/__init__.py b/utils/__init__.py index 4598629..7d2a46c 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,6 +1,7 @@ """A collections of utilities used by Gwendolyn and her functions""" -__all__ = ["Options", "Credentials", "databaseFuncs", "getParams", "logThis", "cap", "makeFiles", "replaceMultiple", "emojiToCommand"] +__all__ = ["Options", "Credentials", "databaseFuncs", "EventHandler", "ErrorHandler", "getParams", "logThis", "cap", "makeFiles", "replaceMultiple", "emojiToCommand"] from .helperClasses import Options, Credentials, databaseFuncs +from .eventHandlers import EventHandler, ErrorHandler from .utilFunctions import getParams, logThis, cap, makeFiles, replaceMultiple, emojiToCommand \ No newline at end of file diff --git a/utils/eventHandlers.py b/utils/eventHandlers.py new file mode 100644 index 0000000..289b411 --- /dev/null +++ b/utils/eventHandlers.py @@ -0,0 +1,43 @@ +import discord, traceback +from discord.ext import commands + +class EventHandler(): + def __init__(self, bot): + self.bot = bot + + async def on_ready(self): + await self.bot.databaseFuncs.syncCommands() + self.bot.log("Logged in as "+self.bot.user.name+", "+str(self.bot.user.id), level = 25) + game = discord.Game("Use /help for commands") + await self.bot.change_presence(activity=game, status = discord.Status.online) + +class ErrorHandler(): + def __init__(self, bot): + self.bot = bot + + async def on_slash_command_error(self, ctx, error): + if isinstance(error, commands.CommandNotFound): + await ctx.send("That's not a command (error code 001)") + elif isinstance(error,commands.errors.MissingRequiredArgument): + self.bot.log(f"{error}",str(ctx.channel_id)) + await ctx.send("Missing command parameters (error code 002). Try using `!help [command]` to find out how to use the command.") + else: + exception = traceback.format_exception(type(error), error, error.__traceback__) + stopAt = "\nThe above exception was the direct cause of the following exception:\n\n" + if stopAt in exception: + index = exception.index(stopAt) + exception = exception[:index] + + exceptionString = "".join(exception) + self.bot.log([f"exception in /{ctx.name}", f"{exceptionString}"],str(ctx.channel_id), 40) + await ctx.send("Something went wrong (error code 000)") + + async def on_error(self, method): + exception = traceback.format_exc() + stopAt = "\nThe above exception was the direct cause of the following exception:\n\n" + if stopAt in exception: + index = exception.index(stopAt) + exception = exception[:index] + + exceptionString = "".join(exception) + self.bot.log([f"exception in /{method}", f"{exceptionString}"], level = 40) \ No newline at end of file