🔀 Merging with master

This commit is contained in:
NikolajDanger
2021-04-15 15:16:56 +02:00
committed by Nikolaj
parent 420bc19387
commit 06b5d881ea
12 changed files with 729 additions and 259 deletions

4
.gitignore vendored
View File

@ -153,11 +153,11 @@ static
token.txt
credentials.txt
options.txt
resources/starWars/destinyPoints.txt
resources/star_wars/destinyPoints.txt
resources/bedreNetflix/
resources/games/hilo/
resources/games/blackjackTables/
resources/games/oldImages/
resources/games/old_images/
resources/games/connect4Boards/
resources/games/hexBoards/
resources/games/hangmanBoards/

View File

@ -1,63 +1,138 @@
import os, finnhub, platform, asyncio, discord
"""
Contains the Gwendolyn class, and runs it when run as script.
from discord.ext import commands
from discord_slash import SlashCommand
from pymongo import MongoClient
*Classes*
---------
Gwendolyn(discord.ext.commands.Bot)
"""
import os # Used for loading cogs in Gwendolyn.addCogs
import finnhub # Used to add a finhub client to the bot
import platform # Used to test if the bot is running on windows, in
# order to fix a bug with asyncio
import asyncio # used to set change the loop policy if the bot is
# running on windows
import discord # Used for discord.Intents and discord.Status
import discord_slash # Used to initialized SlashCommands object
from discord.ext import commands # Used to inherit from commands.bot
from pymongo import MongoClient # Used for database management
from funcs import Money, StarWars, Games, Other, LookupFuncs
from utils import Options, Credentials, logThis, makeFiles, databaseFuncs, EventHandler, ErrorHandler
from utils import (Options, Credentials, logThis, makeFiles, databaseFuncs,
EventHandler, ErrorHandler, longStrings)
class Gwendolyn(commands.Bot):
"""
A multifunctional Discord bot.
*Methods*
---------
log(messages: Union[str, list], channel: str = "",
level: int = 20)
stop(ctx: discord_slash.context.SlashContext)
defer(ctx: discord_slash.context.SlashContext)
"""
def __init__(self):
"""Initialize the bot."""
intents = discord.Intents.default()
intents.members = True
initParams = {
"command_prefix": " ",
"case_insensitive": True,
"intents": intents,
"status": discord.Status.dnd
}
super().__init__(**initParams)
self._addClientsAndOptions()
self._addUtilClasses()
self._addFunctionContainers()
self._addCogs()
def _addClientsAndOptions(self):
"""Add all the client, option and credentials objects."""
self.longStrings = longStrings()
self.options = Options()
self.credentials = Credentials()
self.finnhubClient = finnhub.Client(api_key = self.credentials.finnhubKey)
self.MongoClient = MongoClient(f"mongodb+srv://{self.credentials.mongoDBUser}:{self.credentials.mongoDBPassword}@gwendolyn.qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority")
finnhubKey = self.credentials.finnhubKey
self.finnhubClient = finnhub.Client(api_key=finnhubKey)
mongoUser = self.credentials.mongoDBUser
mongoPassword = self.credentials.mongoDBPassword
mongoLink = f"mongodb+srv://{mongoUser}:{mongoPassword}@gwendolyn"
mongoLink += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
dataBaseClient = MongoClient(mongoLink)
if self.options.testing:
self.log("Testing mode")
self.database = self.MongoClient["Gwendolyn-Test"]
self.database = dataBaseClient["Gwendolyn-Test"]
else:
self.database = self.MongoClient["Gwendolyn"]
self.database = dataBaseClient["Gwendolyn"]
def _addUtilClasses(self):
"""Add all the classes used as utility."""
self.databaseFuncs = databaseFuncs(self)
self.eventHandler = EventHandler(self)
self.errorHandler = ErrorHandler(self)
slashParams = {
"sync_commands": True,
"sync_on_cog_reload": True,
"override_type": True
}
self.slash = discord_slash.SlashCommand(self, **slashParams)
def _addFunctionContainers(self):
"""Add all the function containers used for commands."""
self.starWars = StarWars(self)
self.other = Other(self)
self.lookupFuncs = LookupFuncs(self)
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
def _addCogs(self):
"""Load cogs."""
for filename in os.listdir("./cogs"):
if filename.endswith(".py"):
self.load_extension(f"cogs.{filename[:-3]}")
super().__init__(command_prefix=" ", case_insensitive=True, intents = intents, status = discord.Status.dnd)
def log(self, messages, channel : str = "", level : int = 20):
def log(self, messages, channel: str = "", level: int = 20):
"""Log a message. Described in utils/utilFunctions.py."""
logThis(messages, channel, level)
async def stop(self, ctx):
async def stop(self, ctx: discord_slash.context.SlashContext):
"""
Stop the bot, and stop running games.
Only stops the bot if the user in ctx.author is one of the
admins given in options.txt.
*parameters*
------------
ctx: discord_slash.context.SlashContext
The context of the "/stop" slash command.
"""
if f"#{ctx.author.id}" in self.options.admins:
await ctx.send("Pulling git repo and restarting...")
await self.change_presence(status = discord.Status.offline)
await self.change_presence(status=discord.Status.offline)
self.databaseFuncs.stopServer()
self.databaseFuncs.wipeGames()
self.log("Logging out", level = 25)
self.log("Logging out", level=25)
await self.close()
else:
self.log(f"{ctx.author.display_name} tried to stop me! (error code 201)",str(ctx.channel_id))
await ctx.send(f"I don't think I will, {ctx.author.display_name} (error code 201)")
logMessage = f"{ctx.author.display_name} tried to stop me!"
self.log(logMessage, str(ctx.channel_id))
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
async def defer(self, ctx):
async def defer(self, ctx: discord_slash.context.SlashContext):
"""Send a "Gwendolyn is thinking" message to the user."""
try:
await ctx.defer()
except:
except discord_slash.error.AlreadyResponded:
self.log("defer failed")
if __name__ == "__main__":
if platform.system() == "Windows":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
@ -67,15 +142,9 @@ if __name__ == "__main__":
# Creates the Bot
bot = Gwendolyn()
bot.slash = SlashCommand(bot, sync_commands=True, sync_on_cog_reload=True, override_type=True)
#Loads cogs
for filename in os.listdir("./cogs"):
if filename.endswith(".py"):
bot.load_extension(f"cogs.{filename[:-3]}")
try:
# Runs the whole shabang
bot.run(bot.credentials.token)
except:
bot.log("Could not log in. Remember to write your bot token in the credentials.txt file")
except Exception:
bot.log(bot.longStrings["Can't log in"])

View File

@ -1,34 +1,40 @@
from discord.ext import commands
"""Contains the EventCog, which runs code for specific bot events."""
from discord.ext import commands # Has the cog class
class EventCog(commands.Cog):
"""Handles bot events."""
def __init__(self, bot):
"""Initialize the bot."""
self.bot = bot
self.bot.on_error = self.on_error
# Syncs commands, sets the game, and logs when the bot logs in
@commands.Cog.listener()
async def on_ready(self):
"""Log and set bot status when bot logs in."""
await self.bot.eventHandler.on_ready()
# Logs when user sends a command
@commands.Cog.listener()
async def on_slash_command(self, ctx):
"""Log when a slash command is run."""
await self.bot.eventHandler.on_slash_command(ctx)
# Logs if a command experiences an error
@commands.Cog.listener()
async def on_slash_command_error(self, ctx, error):
"""Log when a slash error occurs."""
await self.bot.errorHandler.on_slash_command_error(ctx, error)
# Logs if on error occurs
async def on_error(self, method, *args, **kwargs):
"""Log when an error occurs."""
await self.bot.errorHandler.on_error(method)
# If someone reacted to a message, checks if it's a reaction it's
# Gwendolyn has been waiting for, and then does something
@commands.Cog.listener()
async def on_reaction_add(self, reaction, user):
"""Handle when someone reacts to a message."""
await self.bot.eventHandler.on_reaction_add(reaction, user)
def setup(bot):
"""Add the eventcog to the bot."""
bot.add_cog(EventCog(bot))

View File

@ -1,155 +1,177 @@
from discord.ext import commands
from discord_slash import cog_ext
"""Contains all the cogs that deal with game commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from utils import getParams
from utils import getParams # pylint: disable=import-error
params = getParams()
class GamesCog(commands.Cog):
def __init__(self,bot):
"""Runs game stuff."""
"""Contains miscellaneous game commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
# Checks user balance
@cog_ext.cog_slash(**params["balance"])
async def balance(self, ctx):
"""Check user balance."""
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):
"""Give another user an amount of GwendoBucks."""
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"):
async def invest(self, ctx, parameters="check"):
"""Invest GwendoBucks in the stock market."""
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 = ""):
async def trivia(self, ctx, answer=""):
"""Run a game of trivia."""
await self.bot.games.trivia.triviaParse(ctx, answer)
class BlackjackCog(commands.Cog):
def __init__(self,bot):
"""Runs game stuff."""
"""Contains the blackjack commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
# Starts a game of blackjack
@cog_ext.cog_subcommand(**params["blackjackStart"])
async def blackjackStart(self, ctx):
"""Start a game of blackjack."""
await self.bot.games.blackjack.start(ctx)
@cog_ext.cog_subcommand(**params["blackjackBet"])
async def blackjackBet(self, ctx, bet):
"""Enter the game of blackjack with a bet."""
await self.bot.games.blackjack.playerDrawHand(ctx, bet)
@cog_ext.cog_subcommand(**params["blackjackStand"])
async def blackjackStand(self, ctx, hand = ""):
async def blackjackStand(self, ctx, hand=""):
"""Stand on your hand in blackjack."""
await self.bot.games.blackjack.stand(ctx, hand)
@cog_ext.cog_subcommand(**params["blackjackHit"])
async def blackjackHit(self, ctx, hand = 0):
async def blackjackHit(self, ctx, hand=0):
"""Hit on your hand in blackjack."""
await self.bot.games.blackjack.hit(ctx, hand)
@cog_ext.cog_subcommand(**params["blackjackDouble"])
async def blackjackDouble(self, ctx, hand = 0):
async def blackjackDouble(self, ctx, hand=0):
"""Double in blackjack."""
await self.bot.games.blackjack.double(ctx, hand)
@cog_ext.cog_subcommand(**params["blackjackSplit"])
async def blackjackSplit(self, ctx, hand = 0):
async def blackjackSplit(self, ctx, hand=0):
"""Split your hand in blackjack."""
await self.bot.games.blackjack.split(ctx, hand)
@cog_ext.cog_subcommand(**params["blackjackHilo"])
async def blackjackHilo(self, ctx):
"""Get the hilo value for the deck in blackjack."""
await self.bot.games.blackjack.hilo(ctx)
@cog_ext.cog_subcommand(**params["blackjackShuffle"])
async def blackjackShuffle(self, ctx):
"""Shuffle the blackjack game."""
await self.bot.games.blackjack.shuffle(ctx)
@cog_ext.cog_subcommand(**params["blackjackCards"])
async def blackjackCards(self, ctx):
"""Get the amount of cards left in the blackjack deck."""
await self.bot.games.blackjack.cards(ctx)
class ConnectFourCog(commands.Cog):
def __init__(self,bot):
"""Runs game stuff."""
"""Contains all the connect four commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
# Start a game of connect four against a user
@cog_ext.cog_subcommand(**params["connectFourStartUser"])
async def connectFourStartUser(self, ctx, user):
"""Start a game of connect four against another user."""
await self.bot.games.connectFour.start(ctx, user)
# Start a game of connect four against gwendolyn
@cog_ext.cog_subcommand(**params["connectFourStartGwendolyn"])
async def connectFourStartGwendolyn(self, ctx, difficulty = 3):
async def connectFourStartGwendolyn(self, ctx, difficulty=3):
"""Start a game of connect four against Gwendolyn."""
await self.bot.games.connectFour.start(ctx, difficulty)
# Stop the current game of connect four
@cog_ext.cog_subcommand(**params["connectFourSurrender"])
async def connectFourSurrender(self, ctx):
"""Surrender the game of connect four."""
await self.bot.games.connectFour.surrender(ctx)
class HangmanCog(commands.Cog):
def __init__(self,bot):
"""Runs game stuff."""
"""Contains all the hangman commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
# Starts a game of Hangman
@cog_ext.cog_subcommand(**params["hangmanStart"])
async def hangmanStart(self, ctx):
"""Start a game of hangman."""
await self.bot.games.hangman.start(ctx)
# Stops a game of Hangman
@cog_ext.cog_subcommand(**params["hangmanStop"])
async def hangmanStop(self, ctx):
"""Stop the current game of hangman."""
await self.bot.games.hangman.stop(ctx)
class HexCog(commands.Cog):
def __init__(self,bot):
"""Runs game stuff."""
self.bot = bot
"""Contains all the hex commands."""
def __init__(self, bot):
"""Initialize the cog."""
self.bot = bot
self.hex = self.bot.games.hex
# Start a game of Hex against another user
@cog_ext.cog_subcommand(**params["hexStartUser"])
async def hexStartUser(self, ctx, user):
await self.bot.games.hex.start(ctx, user)
"""Start a game of hex against another player."""
await self.hex.start(ctx, user)
# Start a game of Hex against Gwendolyn
@cog_ext.cog_subcommand(**params["hexStartGwendolyn"])
async def hexStartGwendolyn(self, ctx, difficulty = 2):
await self.bot.games.hex.start(ctx, difficulty)
async def hexStartGwendolyn(self, ctx, difficulty=2):
"""Start a game of hex against Gwendolyn."""
await self.hex.start(ctx, difficulty)
# Place a piece in the hex game
@cog_ext.cog_subcommand(**params["hexPlace"])
async def hexPlace(self, ctx, coordinates):
await self.bot.games.hex.placeHex(ctx, coordinates, f"#{ctx.author.id}")
"""Place a piece in the hex game."""
await self.hex.placeHex(ctx, coordinates, f"#{ctx.author.id}")
# Undo your last hex move
@cog_ext.cog_subcommand(**params["hexUndo"])
async def hexUndo(self, ctx):
await self.bot.games.hex.undo(ctx)
"""Undo your last hex move."""
await self.hex.undo(ctx)
# Perform a hex swap
@cog_ext.cog_subcommand(**params["hexSwap"])
async def hexSwap(self, ctx):
await self.bot.games.hex.swap(ctx)
"""Perform a hex swap."""
await self.hex.swap(ctx)
# Surrender the hex game
@cog_ext.cog_subcommand(**params["hexSurrender"])
async def hexSurrender(self, ctx):
await self.bot.games.hex.surrender(ctx)
"""Surrender the hex game."""
await self.hex.surrender(ctx)
def setup(bot):
"""Add all the cogs to the bot."""
bot.add_cog(GamesCog(bot))
bot.add_cog(BlackjackCog(bot))
bot.add_cog(ConnectFourCog(bot))
bot.add_cog(HangmanCog(bot))
bot.add_cog(HexCog(bot))
bot.add_cog(HexCog(bot))

View File

@ -1,24 +1,32 @@
from discord.ext import commands
from discord_slash import cog_ext
"""Contains the LookupCog, which deals with the lookup commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from utils import getParams
from utils import getParams # pylint: disable=import-error
params = getParams()
class LookupCog(commands.Cog):
"""Contains the lookup commands."""
def __init__(self, bot):
"""Runs lookup commands."""
"""Initialize the cog."""
self.bot = bot
# Looks up a spell
@cog_ext.cog_slash(**params["spell"])
async def spell(self, ctx, query):
"""Look up a spell."""
await self.bot.lookupFuncs.spellFunc(ctx, query)
# Looks up a monster
@cog_ext.cog_slash(**params["monster"])
async def monster(self, ctx, query):
"""Look up a monster."""
await self.bot.lookupFuncs.monsterFunc(ctx, query)
def setup(bot):
bot.add_cog(LookupCog(bot))
"""Add the cog to the bot."""
bot.add_cog(LookupCog(bot))

View File

@ -1,94 +1,99 @@
import discord, codecs, string, json
from discord.ext import commands
from discord_slash import cog_ext
"""Contains the MiscCog, which deals with miscellaneous commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from utils import getParams # pylint: disable=import-error
from utils import getParams # pylint: disable=import-error
params = getParams()
class MiscCog(commands.Cog):
"""Contains the miscellaneous commands."""
def __init__(self, bot):
"""Runs misc commands."""
"""Initialize the cog."""
self.bot = bot
self.bot.remove_command("help")
self.generators = bot.other.generators
self.bedreNetflix = bot.other.bedreNetflix
self.nerdShit = bot.other.nerdShit
# Sends the bot's latency
@cog_ext.cog_slash(**params["ping"])
async def ping(self, ctx):
"""Send the bot's latency."""
await ctx.send(f"Pong!\nLatency is {round(self.bot.latency * 1000)}ms")
# Restarts the bot
@cog_ext.cog_slash(**params["stop"])
async def stop(self, ctx):
"""Stop the bot."""
await self.bot.stop(ctx)
# Gets help for specific command
@cog_ext.cog_slash(**params["help"])
async def helpCommand(self, ctx, command = ""):
async def helpCommand(self, ctx, command=""):
"""Get help for commands."""
await self.bot.other.helpFunc(ctx, command)
# Lets you thank the bot
@cog_ext.cog_slash(**params["thank"])
async def thank(self, ctx):
"""Thank the bot."""
await ctx.send("You're welcome :blush:")
# Sends a friendly message
@cog_ext.cog_slash(**params["hello"])
async def hello(self, ctx):
"""Greet the bot."""
await self.bot.other.helloFunc(ctx)
# Rolls dice
@cog_ext.cog_slash(**params["roll"])
async def roll(self, ctx, dice = "1d20"):
async def roll(self, ctx, dice="1d20"):
"""Roll dice."""
await self.bot.other.rollDice(ctx, dice)
# Sends a random image
@cog_ext.cog_slash(**params["image"])
async def image(self, ctx):
"""Get a random image from Bing."""
await self.bot.other.imageFunc(ctx)
# Finds a random movie
@cog_ext.cog_slash(**params["movie"])
async def movie(self, ctx):
"""Get a random movie from the Plex server."""
await self.bot.other.movieFunc(ctx)
# Generates a random name
@cog_ext.cog_slash(**params["name"])
async def name(self, ctx):
"""Generate a random name."""
await self.generators.nameGen(ctx)
# Generates a random tavern name
@cog_ext.cog_slash(**params["tavern"])
async def tavern(self, ctx):
"""Generate a random tavern name."""
await self.generators.tavernGen(ctx)
# Finds a page on the Senkulpa wiki
@cog_ext.cog_slash(**params["wiki"])
async def wiki(self, ctx, wikiPage = ""):
async def wiki(self, ctx, wikiPage=""):
"""Get a page on a fandom wiki."""
await self.bot.other.findWikiPage(ctx, wikiPage)
#Searches for movie and adds it to Bedre Netflix
@cog_ext.cog_slash(**params["addMovie"])
async def addMovie(self, ctx, movie):
"""Search for a movie and add it to the Plex server."""
await self.bedreNetflix.requestMovie(ctx, movie)
#Searches for show and adds it to Bedre Netflix
@cog_ext.cog_slash(**params["addShow"])
async def addShow(self, ctx, show):
"""Search for a show and add it to the Plex server."""
await self.bedreNetflix.requestShow(ctx, show)
#Returns currently downloading torrents
@cog_ext.cog_slash(**params["downloading"])
async def downloading(self, ctx, parameters = "-d"):
async def downloading(self, ctx, parameters="-d"):
"""Get the current downloading torrents."""
await self.bedreNetflix.downloading(ctx, parameters)
#Looks up on Wolfram Alpha
@cog_ext.cog_slash(**params["wolf"])
async def wolf(self, ctx, query):
"""Perform a search on Wolfram Alpha."""
await self.nerdShit.wolfSearch(ctx, query)
def setup(bot):
"""Add the cog to the bot."""
bot.add_cog(MiscCog(bot))

View File

@ -1,37 +1,40 @@
import discord, string, json
"""Contains the StarWarsCog, which deals with Star Wars commands."""
from discord.ext import commands
from discord_slash import cog_ext
from utils import getParams
from utils import getParams # pylint: disable=import-error
params = getParams()
class starWarsCog(commands.Cog):
class StarWarsCog(commands.Cog):
"""Contains the Star Wars commands."""
def __init__(self, bot):
"""Runs star wars commands."""
"""Initialize the cog."""
self.bot = bot
# Rolls star wars dice
@cog_ext.cog_slash(**params["starWarsRoll"])
async def starWarsRoll(self, ctx, dice = ""):
async def starWarsRoll(self, ctx, dice=""):
"""Roll Star Wars dice."""
await self.bot.starWars.roll.parseRoll(ctx, dice)
# Controls destiny points
@cog_ext.cog_slash(**params["starWarsDestiny"])
async def starWarsDestiny(self, ctx, parameters = ""):
async def starWarsDestiny(self, ctx, parameters=""):
"""Control Star Wars destiny points."""
await self.bot.starWars.destiny.parseDestiny(ctx, parameters)
# Rolls for critical injuries
@cog_ext.cog_slash(**params["starWarsCrit"])
async def starWarsCrit(self, ctx, severity : int = 0):
async def starWarsCrit(self, ctx, severity: int = 0):
"""Roll for critical injuries."""
await self.bot.starWars.roll.critRoll(ctx, severity)
# Accesses and changes character sheet data with the parseChar function
# from funcs/starWarsFuncs/starWarsCharacter.py
@cog_ext.cog_slash(**params["starWarsCharacter"])
async def starWarsCharacter(self, ctx, parameters = ""):
async def starWarsCharacter(self, ctx, parameters=""):
"""Access and change Star Wars character sheet data."""
await self.bot.starWars.character.parseChar(ctx, parameters)
def setup(bot):
bot.add_cog(starWarsCog(bot))
"""Add the cog to the bot."""
bot.add_cog(StarWarsCog(bot))

View File

@ -0,0 +1,4 @@
{
"missing parameters" : "Missing command parameters. Try using `!help [command]` to find out how to use the command.",
"Can't log in": "Could not log in. Remember to write your bot token in the credentials.txt file"
}

View File

@ -1,7 +1,10 @@
"""A collections of utilities used by Gwendolyn and her functions"""
"""A collections of utilities used by Gwendolyn and her functions."""
__all__ = ["Options", "Credentials", "databaseFuncs", "EventHandler", "ErrorHandler", "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
from .utilFunctions import (getParams, logThis, cap, makeFiles,
replaceMultiple, emojiToCommand, longStrings)

View File

@ -1,13 +1,52 @@
import discord, traceback, discord_slash, sys
from discord.ext import commands
"""
Classes used to handle bot events and errors.
*Classes*
---------
EventHandler
ErrorHandler
"""
import discord # Used to init discord.Game and discord.Status, as well
# as compare errors to discord errors and as typehints
import traceback # Used to get the traceback of errors
import sys # Used to get traceback when the specific error is not
# available
from discord.ext import commands # Used to compare errors with command
# errors
from discord_slash.context import SlashContext
from utils.utilFunctions import emojiToCommand
from .utilFunctions import emojiToCommand
class EventHandler():
"""
Handles bot events.
*Methods*
---------
on_ready()
on_slash_command(ctx: discord_slash.context.SlashContext)
on_reaction_add(ctx: discord_slash.context.SlashContext)
"""
def __init__(self, bot):
"""Initialize the handler."""
self.bot = bot
async def on_slash_command(self, ctx):
async def on_ready(self):
"""Log and sets status when it logs in."""
await self.bot.databaseFuncs.syncCommands()
name = self.bot.user.name
userid = str(self.bot.user.id)
loggedInMessage = f"Logged in as {name}, {userid}"
self.bot.log(loggedInMessage, level=25)
game = discord.Game("Use /help for commands")
onlineStatus = discord.Status.online
await self.bot.change_presence(activity=game, status=onlineStatus)
async def on_slash_command(self, ctx: SlashContext):
"""Log when a slash command is given."""
if ctx.subcommand_name is not None:
subcommand = f" {ctx.subcommand_name} "
else:
@ -21,100 +60,117 @@ class EventHandler():
args = " ".join([str(i) for i in ctx.args])
fullCommand = f"/{ctx.command}{subcommand}{subcommandGroup}{args}"
logMessage = f"{ctx.author.display_name} ran {fullCommand}"
self.bot.log(logMessage, str(ctx.channel_id), level = 25)
self.bot.log(logMessage, str(ctx.channel_id), level=25)
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)
async def on_reaction_add(self, reaction : discord.Reaction, user):
if user.bot == False:
async def on_reaction_add(self, reaction: discord.Reaction,
user: discord.User):
"""Take action if the reaction is on a command message."""
if not user.bot:
tests = self.bot.databaseFuncs
message = reaction.message
channel = message.channel
self.bot.log(f"{user.display_name} reacted to a message",str(channel.id))
try:
connectFourTheirTurn, piece = self.bot.databaseFuncs.connectFourReactionTest(channel,message,"#"+str(user.id))
except:
connectFourTheirTurn = False
reactedMessage = f"{user.display_name} reacted to a message"
self.bot.log(reactedMessage, str(channel.id))
plexData = tests.bedreNetflixReactionTest(message)
# plexData is a list containing 3 elements: whether it was
# the addshow/addmovie command message the reaction was to
# (bool), whether it's a movie (bool) (if false, it's a
# show), and the imdb ids/names for the for the movies or
# shows listed in the message (list).
bedreNetflixMessage, addMovie, imdbIds = self.bot.databaseFuncs.bedreNetflixReactionTest(channel, message)
reactionTestParams = [message, f"#{str(user.id)}"]
if connectFourTheirTurn:
if tests.connectFourReactionTest(*reactionTestParams):
column = emojiToCommand(reaction.emoji)
await self.bot.games.connectFour.placePiece(message, f"#{user.id}", column-1)
elif bedreNetflixMessage and addMovie:
moviePick = emojiToCommand(reaction.emoji)
if moviePick == "none":
imdbID = None
else:
imdbID = imdbIds[moviePick-1]
params = [message, f"#{user.id}", column-1]
await self.bot.games.connectFour.placePiece(*params)
if isinstance(channel, discord.DMChannel):
await message.delete()
await self.bot.other.bedreNetflix.addMovie(message, imdbID, False)
else:
await message.clear_reactions()
await self.bot.other.bedreNetflix.addMovie(message, imdbID)
elif bedreNetflixMessage and not addMovie:
showPick = emojiToCommand(reaction.emoji)
if showPick == "none":
imdbName = None
else:
imdbName = imdbIds[showPick-1]
if plexData[0]:
plexFuncs = self.bot.other.bedreNetflix
if plexData[1]:
moviePick = emojiToCommand(reaction.emoji)
if moviePick == "none":
imdbID = None
else:
imdbID = plexData[2][moviePick-1]
if isinstance(channel, discord.DMChannel):
await message.delete()
await self.bot.other.bedreNetflix.addShow(message, imdbName, False)
if isinstance(channel, discord.DMChannel):
await message.delete()
await plexFuncs.addMovie(message, imdbID, False)
else:
await message.clear_reactions()
await plexFuncs.addMovie(message, imdbID)
else:
await message.clear_reactions()
await self.bot.other.bedreNetflix.addShow(message, imdbName)
showPick = emojiToCommand(reaction.emoji)
if showPick == "none":
imdbName = None
else:
imdbName = plexData[2][showPick-1]
elif self.bot.databaseFuncs.hangmanReactionTest(channel, message, f"#{user.id}"):
if isinstance(channel, discord.DMChannel):
await message.delete()
await plexFuncs.addShow(message, imdbName, False)
else:
await message.clear_reactions()
await plexFuncs.addShow(message, imdbName)
elif tests.hangmanReactionTest(*reactionTestParams):
self.bot.log("They reacted to the hangman message")
if ord(reaction.emoji) in range(127462,127488):
if ord(reaction.emoji) in range(127462, 127488):
# The range is letter-emojis
guess = chr(ord(reaction.emoji)-127397)
await self.bot.games.hangman.guess(message, f"#{user.id}", guess)
# Converts emoji to letter
params = [message, f"#{user.id}", guess]
await self.bot.games.hangman.guess(*params)
else:
self.bot.log("Bot they didn't react with a valid guess")
class ErrorHandler():
"""
Handles errors.
*Methods*
---------
on_slash_command_error(ctx: discord_slash.context.SlashContext,
error: Exception)
on_error(method: str)
"""
def __init__(self, bot):
"""Initialize the handler."""
self.bot = bot
async def on_slash_command_error(self, ctx, error):
async def on_slash_command_error(self, ctx: SlashContext,
error: Exception):
"""Log when there's a slash command."""
if isinstance(error, commands.CommandNotFound):
await ctx.send("That's not a command (error code 001)")
await ctx.send("That's not a command")
elif isinstance(error, discord.errors.NotFound):
self.bot.log("Deleted message before I could add all reactions")
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.")
self.bot.log(f"{error}", str(ctx.channel_id))
await ctx.send(self.bot.longStrings["missing parameters"])
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]
params = [type(error), error, error.__traceback__]
exception = traceback.format_exception(*params)
exceptionString = "".join(exception)
self.bot.log([f"exception in /{ctx.name}", f"{exceptionString}"],str(ctx.channel_id), 40)
logMessages = [f"exception in /{ctx.name}", f"{exceptionString}"]
self.bot.log(logMessages, str(ctx.channel_id), 40)
if isinstance(error, discord.errors.NotFound):
self.bot.log("Context is non-existant", level = 40)
self.bot.log("Context is non-existant", level=40)
else:
await ctx.send("Something went wrong (error code 000)")
async def on_error(self, method):
async def on_error(self, method: str):
"""Log when there's an error."""
errorType = sys.exc_info()[0]
if errorType == discord.errors.NotFound:
self.bot.log("Deleted message before I could add all reactions")
else:
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)
logMessages = [f"exception in {method}", f"{exceptionString}"]
self.bot.log(logMessages, level=40)

View File

@ -1,6 +1,44 @@
import re, git, os, json, time
"""
Contains classes used for utilities.
def sanitize(data : str, options : bool = False):
*Functions*
-----------
Sanitize(data: str, lowerCaseValue: bool = false) -> dict
*Classes*
---------
Options()
Credentials()
DatabaseFuncs()
"""
import re # Used in getID
import git # Used to pull when stopping
import os # Used to test if files exist
import json # Used to read the data about addmovie/addshow
import time # Used to test how long it's been since commands were synced
import discord # Used for type hints
def sanitize(data: str, lowerCaseValue: bool = False):
"""
Sanitize and create a dictionary from a string.
Each element is created from a line with a : in it. The key is left
of the :, the value is right of it.
*Parameters*
------------
data: str
The string to create a dict from.
lowerCaseValue: bool = False
Whether the value of each element should be lowercase.
*Returns*
---------
dct: dict
The sanitized dictionary of elements.
"""
data = data.splitlines()
dct = {}
for line in data:
@ -8,7 +46,7 @@ def sanitize(data : str, options : bool = False):
lineValues = line.split(":")
lineValues[0] = lineValues[0].lower()
lineValues[1] = lineValues[1].replace(" ", "")
if options:
if lowerCaseValue:
lineValues[1] = lineValues[1].lower()
if lineValues[0] in ["testing guild ids", "admins"]:
@ -23,18 +61,26 @@ def sanitize(data : str, options : bool = False):
return dct
class Options():
"""Contains the options for the bot."""
def __init__(self):
with open("options.txt","r") as f:
"""Initialize the options."""
with open("options.txt", "r") as f:
data = sanitize(f.read(), True)
self.testing = data["testing"]
self.guildIds = data["testing guild ids"]
self.admins = data["admins"]
class Credentials():
"""Contains the credentials for the bot and apis."""
def __init__(self):
with open("credentials.txt","r") as f:
"""Initialize the credentials."""
with open("credentials.txt", "r") as f:
data = sanitize(f.read())
self.token = data["bot token"]
@ -46,34 +92,99 @@ class Credentials():
self.radarrKey = data["radarr api key"]
self.sonarrKey = data["sonarr api key"]
class databaseFuncs():
"""
Manages database functions.
*Methods*
---------
getName(userID: str) -> str
getID(userName: str) -> str
deleteGame(gameType: str, channel: str)
wipeGames()
connectFourReactionTest(message: discord.Message,
user: discord.User) -> bool
hangmanReactionTest(message: discord.Message,
user: discord.User) -> bool
BedreNetflixReactionTest(message: discord.Message,
user: discord.User) -> bool, bool,
list
syncCommands()
"""
def __init__(self, bot):
"""Initialize the class."""
self.bot = bot
def getName(self, userID):
user = self.bot.database["users"].find_one({"_id":userID})
def getName(self, userID: str):
"""
Get the name of a user you have the # id of.
*Parameters:
------------
userID: str
The id of the user you want the name of. The format is
"#" + str(discord.User.id)
*Returns*
---------
userName: str
The name of the user. If the user couldn't be found,
returns the userID.
"""
user = self.bot.database["users"].find_one({"_id": userID})
if userID == f"#{self.bot.user.id}":
return "Gwendolyn"
elif user != None:
elif user is not None:
return user["user name"]
else:
self.bot.log(f"Couldn't find user {userID}")
return userID
def getID(self,userName):
user = self.bot.database["users"].find_one({"user name":re.compile(userName, re.IGNORECASE)})
def getID(self, userName: str):
"""
Get the id of a user you have the username of.
if user != None:
*Parameters:
------------
userName: str
The name of the user you want the id of.
*Returns*
---------
userID: str
The id of the user in the format "#" +
str(discord.User.id). If the user couldn't be found,
returns the userName.
"""
userSearch = {"user name": re.compile(userName, re.IGNORECASE)}
user = self.bot.database["users"].find_one(userSearch)
if user is not None:
return user["_id"]
else:
self.bot.log("Couldn't find user "+userName)
return None
def deleteGame(self, gameType, channel):
self.bot.database[gameType].delete_one({"_id":channel})
def deleteGame(self, gameType: str, channel: str):
"""
Remove a game from the database.
def stopServer(self):
*Parameters*
------------
gameType: str
The name of the collection the game is in, like
"hangman games", "blackjack games" etc.
channel: str
The channel id of the channel the game is on as a
string.
"""
self.bot.database[gameType].delete_one({"_id": channel})
def wipeGames(self):
"""Delete all running games and pull from git."""
self.bot.database["trivia questions"].delete_many({})
self.bot.database["blackjack games"].delete_many({})
self.bot.database["connect 4 games"].delete_many({})
@ -84,35 +195,80 @@ class databaseFuncs():
g = git.cmd.Git("")
g.pull()
def connectFourReactionTest(self,channel,message,user):
game = self.bot.database["connect 4 games"].find_one({"_id":str(channel.id)})
def connectFourReactionTest(self, message: discord.Message,
user: discord.User):
"""
Test if the given message is the current connect four game.
with open("resources/games/oldImages/connectFour"+str(channel.id), "r") as f:
Also tests if the given user is the one who's turn it is.
*Parameters*
------------
message: discord.Message
The message to test.
user: discord.User
The user to test.
*Returns*
---------
: bool
Whether the given message is the current connect four
game and if the user who reacted is the user who's turn
it is.
"""
channel = message.channel
channelSearch = {"_id": str(channel.id)}
game = self.bot.database["connect 4 games"].find_one(channelSearch)
filePath = f"resources/games/oldImages/connectFour{channel.id}"
with open(filePath, "r") as f:
oldImage = int(f.read())
if message.id == oldImage:
self.bot.log("They reacted to the connectFour game")
turn = game["turn"]
if user == game["players"][turn]:
return True, turn+1
return True
else:
self.bot.log("It wasn't their turn")
return False, 0
return False
else:
return False, 0
return False
def hangmanReactionTest(self, channel, message, user):
try:
with open("resources/games/oldImages/hangman"+str(channel.id), "r") as f:
def hangmanReactionTest(self, message: discord.Message,
user: discord.User):
"""
Test if the given message is the current hangman game.
Also tests if the given user is the one who's playing hangman.
*Parameters*
------------
message: discord.Message
The message to test.
user: discord.User
The user to test.
*Returns*
---------
: bool
Whether the given message is the current hangman game
and if the user who reacted is the user who's playing
hangman.
"""
channel = message.channel
filePath = f"resources/games/oldImages/hangman{channel.id}"
if os.path.isfile(filePath):
with open(filePath, "r") as f:
oldMessages = f.read().splitlines()
except:
else:
return False
gameMessage = False
for oldMessage in oldMessages:
oldMessageID = int(oldMessage)
if message.id == oldMessageID:
game = self.bot.database["hangman games"].find_one({"_id":str(channel.id)})
database = self.bot.database["hangman games"]
channelSearch = {"_id": str(channel.id)}
game = database.find_one(channelSearch)
if user == game["player"]:
gameMessage = True
@ -120,9 +276,30 @@ class databaseFuncs():
return gameMessage
def bedreNetflixReactionTest(self, channel, message):
if os.path.isfile(f"resources/bedreNetflix/oldMessage{str(channel.id)}"):
with open("resources/bedreNetflix/oldMessage"+str(channel.id),"r") as f:
def bedreNetflixReactionTest(self, message: discord.Message):
"""
Test if the given message is the response to a plex request.
*Parameters*
------------
message: discord.Message
The message to test.
*Returns*
---------
: bool
Whether the message is the response to a plex request.
: bool
Whether it was a movie request (false for a show
request)
: list
A list of ids or names of the shows or movies that
Gwendolyn presented after the request.
"""
channel = message.channel
filePath = f"resources/bedreNetflix/oldMessage{str(channel.id)}"
if os.path.isfile(filePath):
with open(filePath, "r") as f:
data = json.load(f)
else:
return False, None, None
@ -136,6 +313,7 @@ class databaseFuncs():
return False, None, None
async def syncCommands(self):
"""Sync the slash commands with the discord API."""
collection = self.bot.database["last synced"]
lastSynced = collection.find_one()
now = time.time()
@ -144,6 +322,6 @@ class databaseFuncs():
self.bot.log(f"Updating commands: {slashCommandList}")
await self.bot.slash.sync_all_commands()
idNumber = lastSynced["_id"]
queryFilter = {"_id" : idNumber}
update = {"$set" : {"last synced" : now}}
queryFilter = {"_id": idNumber}
update = {"$set": {"last synced": now}}
collection.update_one(queryFilter, update)

View File

@ -1,3 +1,18 @@
"""
Contains utility functions used by parts of the bot.
*Functions*
-----------
longstrings() -> dict
getParams() -> dict
logThis(messages: Union[str, list], channel: str = "",
level: int = 20)
cap(s: str) -> str
makeFiles()
replaceMultiple(mainString: str, toBeReplaced: list,
newString: str) -> str
emojiToCommand(emoji: str) -> str
"""
import json
import logging
import os
@ -5,22 +20,55 @@ import sys
import imdb
from .helperClasses import Options
# All of this is logging configuration
FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s"
PRINTFORMAT = "%(asctime)s - %(message)s"
DATEFORMAT = "%Y-%m-%d %H:%M:%S"
logging.addLevelName(25, "PRINT")
logging.basicConfig(format=FORMAT, datefmt=DATEFORMAT, level=logging.INFO, filename="gwendolyn.log")
loggingConfigParams = {
"format": FORMAT,
"datefmt": DATEFORMAT,
"level": logging.INFO,
"filename": "gwendolyn.log"
}
logging.basicConfig(**loggingConfigParams)
logger = logging.getLogger("Gwendolyn")
printer = logging.getLogger("printer")
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter(fmt = PRINTFORMAT, datefmt=DATEFORMAT))
handler.setFormatter(logging.Formatter(fmt=PRINTFORMAT, datefmt=DATEFORMAT))
printer.addHandler(handler)
printer.propagate = False
imdb._logging.setLevel("CRITICAL")
imdb._logging.setLevel("CRITICAL") # Basically disables imdbpy
# logging, since it's printed to the terminal.
def longStrings():
"""
Get the data from resources/longStrings.json.
*Returns*
---------
data: dict
The long strings and their keys.
"""
with open("resources/longStrings.json", "r") as f:
data = json.load(f)
return data
def getParams():
"""
Get the slash command parameters.
*Returns*
---------
params: dict
The parameters for every slash command.
"""
with open("resources/slashParameters.json", "r") as f:
params = json.load(f)
@ -32,8 +80,25 @@ def getParams():
return params
def logThis(messages, channel : str = "", level : int = 20):
channel = channel.replace("Direct Message with ","")
def logThis(messages, channel: str = "", level: int = 20):
"""
Log something in Gwendolyn's logs.
*Parameters*
------------
messages: Union[str, list]
A string or list of strings to be logged. If there are
multiple strings and the level is PRINT (25) or higher,
only the first string will be printed.
channel: str = ""
The channel the event to be logged occurred in. Will be
logged along with the message(s).
level: int = 20
The level to log the message(s) at. If PRINT (25) or
higher, the first message will be printed to the console.
"""
channel = channel.replace("Direct Message with ", "")
if type(messages) is str:
messages = [messages]
@ -41,9 +106,11 @@ def logThis(messages, channel : str = "", level : int = 20):
for x, msg in enumerate(messages):
if channel != "":
messages[x] = f"{msg} - ({channel})"
messages[x] = f"{msg} - ({channel})" # Adds channel to log
# messages
if len(messages) > 1:
if len(messages) > 1: # Tells user to check the log if there are
# more messages there
printMessage += " (details in log)"
if level >= 25:
@ -52,10 +119,24 @@ def logThis(messages, channel : str = "", level : int = 20):
for logMessage in messages:
logger.log(level, logMessage)
# Capitalizes all words except some of them
def cap(s):
no_caps_list = ["of","the"]
# Capitalizes a strink like a movie title
def cap(s: str):
"""
Capitalize a string like a movie title.
That means "of" and "the" are not capitalized.
*Parameters*
------------
s: str
The string to capitalized.
*Returns*
---------
res: str
The capitalized string.
"""
no_caps_list = ["of", "the"]
word_number = 0
lst = s.split()
res = ''
@ -67,22 +148,25 @@ def cap(s):
res = res[:-1]
return res
def makeFiles():
def makeJsonFile(path,content):
# Creates json file if it doesn't exist
if not os.path.isfile(path):
logThis(path.split("/")[-1]+" didn't exist. Making it now.")
with open(path,"w") as f:
json.dump(content,f,indent = 4)
def makeTxtFile(path,content):
# Creates txt file if it doesn't exist
def makeFiles():
"""Create all the files and directories needed by Gwendolyn."""
def makeJsonFile(path, content):
"""Create json file if it doesn't exist."""
if not os.path.isfile(path):
logThis(path.split("/")[-1]+" didn't exist. Making it now.")
with open(path,"w") as f:
with open(path, "w") as f:
json.dump(content, f, indent=4)
def makeTxtFile(path, content):
"""Create txt file if it doesn't exist."""
if not os.path.isfile(path):
logThis(path.split("/")[-1]+" didn't exist. Making it now.")
with open(path, "w") as f:
f.write(content)
def directory(path):
"""Create directory if it doesn't exist."""
if not os.path.isdir(path):
os.makedirs(path)
logThis("The "+path.split("/")[-1]+" directory didn't exist")
@ -90,27 +174,58 @@ def makeFiles():
with open("resources/startingFiles.json") as f:
data = json.load(f)
for path, content in data["json"].items():
makeJsonFile(path, content)
for path, content in data["txt"].items():
makeTxtFile(path, content)
for path in data["folder"]:
directory(path)
for path, content in data["json"].items():
makeJsonFile(path,content)
for path, content in data["txt"].items():
makeTxtFile(path,content)
def replaceMultiple(mainString: str, toBeReplaced: list, newString: str):
"""
Replace multiple substrings in a string with the same substring.
# Replaces multiple things with the same thing
def replaceMultiple(mainString, toBeReplaces, newString):
*Parameters*
------------
mainString: str
The string to replace substrings in.
toBeReplaced: list
The substrings to replace.
newString: str
The string to replace the substrings with.
*Returns*
---------
mainString: str
The string with the substrings replaced.
"""
# Iterate over the strings to be replaced
for elem in toBeReplaces :
for elem in toBeReplaced:
# Check if string is in the main string
if elem in mainString :
if elem in mainString:
# Replace the string
mainString = mainString.replace(elem, newString)
return mainString
def emojiToCommand(emoji):
def emojiToCommand(emoji: str):
"""
Convert emoji to text.
*Parameters*
------------
emoji: str
The emoji to decipher.
*Returns*
---------
: str
The deciphered string.
"""
if emoji == "1":
return 1
elif emoji == "2":
@ -131,4 +246,5 @@ def emojiToCommand(emoji):
return "none"
elif emoji == "✔️":
return 1
else: return ""
else:
return ""