🔀 change reactions into buttons

Add components
This commit is contained in:
Nikolaj Gade
2021-08-19 17:12:43 +02:00
committed by GitHub
12 changed files with 840 additions and 770 deletions

View File

@ -30,9 +30,9 @@ class EventCog(commands.Cog):
await self.bot.error_handler.on_error(method) await self.bot.error_handler.on_error(method)
@commands.Cog.listener() @commands.Cog.listener()
async def on_reaction_add(self, reaction, user): async def on_component(self, ctx):
"""Handle when someone reacts to a message.""" """Handle when someone reacts to a message."""
await self.bot.event_handler.on_reaction_add(reaction, user) await self.bot.event_handler.on_component(ctx)
def setup(bot): def setup(bot):

View File

@ -105,11 +105,6 @@ class ConnectFourCog(commands.Cog):
"""Start a game of connect four against Gwendolyn.""" """Start a game of connect four against Gwendolyn."""
await self.bot.games.connect_four.start(ctx, difficulty) await self.bot.games.connect_four.start(ctx, difficulty)
@cog_ext.cog_subcommand(**params["connect_four_surrender"])
async def connect_four_surrender(self, ctx):
"""Surrender the game of connect four."""
await self.bot.games.connect_four.surrender(ctx)
class HangmanCog(commands.Cog): class HangmanCog(commands.Cog):
"""Contains all the hangman commands.""" """Contains all the hangman commands."""
@ -118,16 +113,11 @@ class HangmanCog(commands.Cog):
"""Initialize the cog.""" """Initialize the cog."""
self.bot = bot self.bot = bot
@cog_ext.cog_subcommand(**params["hangman_start"]) @cog_ext.cog_slash(**params["hangman"])
async def hangman_start(self, ctx): async def hangman(self, ctx):
"""Start a game of hangman.""" """Start a game of hangman."""
await self.bot.games.hangman.start(ctx) await self.bot.games.hangman.start(ctx)
@cog_ext.cog_subcommand(**params["hangman_stop"])
async def hangman_stop(self, ctx):
"""Stop the current game of hangman."""
await self.bot.games.hangman.stop(ctx)
class HexCog(commands.Cog): class HexCog(commands.Cog):
"""Contains all the hex commands.""" """Contains all the hex commands."""

View File

@ -15,7 +15,13 @@ import discord # Used for typehints, discord.file and to check whether
# the opponent in ConnectFour.start is a discord.User # the opponent in ConnectFour.start is a discord.User
from PIL import Image, ImageDraw, ImageFont # Used by DrawConnectFour() from PIL import Image, ImageDraw, ImageFont # Used by DrawConnectFour()
from discord_slash.context import SlashContext # Used for typehints from discord_slash.context import SlashContext, ComponentContext # Used for
# typehints
from discord_slash.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.model import ButtonStyle
from gwendolyn.utils import encode_id
ROWCOUNT = 6 ROWCOUNT = 6
COLUMNCOUNT = 7 COLUMNCOUNT = 7
@ -37,7 +43,7 @@ class ConnectFour():
"""Initialize the class.""" """Initialize the class."""
self.bot = bot self.bot = bot
self.draw = DrawConnectFour(bot) self.draw = DrawConnectFour(bot)
self.database_funcs = self.bot.database_funcs self.get_name = self.bot.database_funcs.get_name
# pylint: disable=invalid-name # pylint: disable=invalid-name
self.AISCORES = { self.AISCORES = {
"middle": 3, "middle": 3,
@ -49,9 +55,41 @@ class ConnectFour():
"win": 10000, "win": 10000,
"avoid losing": 100 "avoid losing": 100
} }
self.REACTIONS = ["1", "2", "3", "4", "5", "6", "7"]
# pylint: enable=invalid-name # pylint: enable=invalid-name
def _encode_board_string(self, board: list):
string = [str(i) for row in board for i in row]
while len(string) > 0 and string[0] == "0":
string = string[1:]
if string == "":
string = "0"
dec = 0
for i, digit in enumerate(string[::-1]):
dec += (3**i)*int(digit)
return str(dec)
def _decode_board_string(self, board_string: str):
dec = int(board_string)
string = []
while dec:
string.append(str(dec % 3))
dec = dec // 3
while len(string) < ROWCOUNT * COLUMNCOUNT:
string.append("0")
string = string[::-1]
board = [
[int(x) for x in string[i*COLUMNCOUNT:i*COLUMNCOUNT+COLUMNCOUNT]]
for i in range(ROWCOUNT)]
return board
async def start(self, ctx: SlashContext, async def start(self, ctx: SlashContext,
opponent: Union[int, discord.User]): opponent: Union[int, discord.User]):
""" """
@ -68,23 +106,18 @@ class ConnectFour():
searches when minimaxing. searches when minimaxing.
""" """
await self.bot.defer(ctx) await self.bot.defer(ctx)
user = f"#{ctx.author.id}" user = ctx.author.id
channel = str(ctx.channel_id) channel = str(ctx.channel_id)
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
started_game = False started_game = False
can_start = True can_start = True
if game is not None: if isinstance(opponent, int):
send_message = self.bot.long_strings["Connect 4 going on"]
log_message = "There was already a game going on"
can_start = False
elif isinstance(opponent, int):
# Opponent is Gwendolyn # Opponent is Gwendolyn
if opponent in range(1, 6): if opponent in range(1, 6):
difficulty = int(opponent) difficulty = int(opponent)
difficulty_text = f" with difficulty {difficulty}" difficulty_text = f" with difficulty {difficulty}"
opponent = f"#{self.bot.user.id}" opponent = self.bot.user.id
else: else:
send_message = "Difficulty doesn't exist" send_message = "Difficulty doesn't exist"
log_message = "They challenged a difficulty that doesn't exist" log_message = "They challenged a difficulty that doesn't exist"
@ -96,7 +129,7 @@ class ConnectFour():
# It was Gwendolyn # It was Gwendolyn
difficulty = 3 difficulty = 3
difficulty_text = f" with difficulty {difficulty}" difficulty_text = f" with difficulty {difficulty}"
opponent = f"#{self.bot.user.id}" opponent = self.bot.user.id
else: else:
send_message = "You can't challenge a bot!" send_message = "You can't challenge a bot!"
log_message = "They tried to challenge a bot" log_message = "They tried to challenge a bot"
@ -104,7 +137,7 @@ class ConnectFour():
else: else:
# Opponent is another player # Opponent is another player
if ctx.author != opponent: if ctx.author != opponent:
opponent = f"#{opponent.id}" opponent = opponent.id
difficulty = 5 difficulty = 5
difficulty_text = "" difficulty_text = ""
else: else:
@ -117,26 +150,13 @@ class ConnectFour():
players = [user, opponent] players = [user, opponent]
random.shuffle(players) random.shuffle(players)
new_game = { self.draw.draw_image(channel, board, 0, [0,0], "", players)
"_id": channel,
"board": board,
"winner": 0,
"win direction": "",
"win coordinates": [0, 0],
"players": players,
"turn": 0,
"difficulty": difficulty
}
self.bot.database["connect 4 games"].insert_one(new_game) gwendolyn_turn = (players[0] == self.bot.user.id)
self.draw.draw_image(channel)
gwendolyn_turn = (players[0] == f"#{self.bot.user.id}")
started_game = True started_game = True
opponent_name = self.database_funcs.get_name(opponent) opponent_name = self.get_name(f"#{opponent}")
turn_name = self.database_funcs.get_name(players[0]) turn_name = self.get_name(f"#{players[0]}")
started_text = "Started game against {}{}.".format( started_text = "Started game against {}{}.".format(
opponent_name, opponent_name,
@ -147,84 +167,105 @@ class ConnectFour():
log_message = "They started a game" log_message = "They started a game"
self.bot.log(log_message) self.bot.log(log_message)
await ctx.send(send_message)
# Sets the whole game in motion # Sets the whole game in motion
if started_game: if started_game:
boards_path = "gwendolyn/resources/games/connect_four_boards/" boards_path = "gwendolyn/resources/games/connect_four_boards/"
file_path = f"{boards_path}board{ctx.channel_id}.png" file_path = f"{boards_path}board{ctx.channel_id}.png"
old_image = await ctx.channel.send(file=discord.File(file_path)) image_message = await ctx.send(
send_message, file=discord.File(file_path)
old_images_path = "gwendolyn/resources/games/old_images/" )
old_image_path = f"{old_images_path}connect_four{ctx.channel_id}"
with open(old_image_path, "w") as file_pointer:
file_pointer.write(str(old_image.id))
board_string = self._encode_board_string(board)
if gwendolyn_turn: if gwendolyn_turn:
await self._connect_four_ai(ctx) await self._connect_four_ai(
ctx, board_string, players, difficulty, image_message.id
)
else: else:
for reaction in self.REACTIONS: buttons = []
await old_image.add_reaction(reaction) for i in range(7):
custom_id = encode_id(
[
"connectfour",
"place",
str(players[0]),
board_string,
str(i),
str(players[0]),
str(players[1]),
str(difficulty),
str(image_message.id)
]
)
buttons.append(create_button(
style=ButtonStyle.blue,
label=str(i+1),
custom_id=custom_id,
disabled=(board[0][i] != 0)
))
async def place_piece(self, ctx: Union[SlashContext, discord.Message], custom_id = encode_id(
user: int, column: int): [
"connectfour",
"end",
str(players[0]),
str(players[1]),
str(image_message.id)
]
)
buttons.append(create_button(
style=ButtonStyle.red,
label="Surrender",
custom_id=custom_id
))
action_rows = []
for x in range(((len(buttons)-1)//4)+1):
row_buttons = buttons[
(x*4):(min(len(buttons),x*4+4))
]
action_rows.append(create_actionrow(*row_buttons))
await image_message.edit(
components=action_rows
)
else:
await ctx.send(send_message)
async def place_piece(self, ctx: ComponentContext, board_string: str,
column: int, players: list[int], difficulty: int,
placer: int, message_id: int):
""" """
Place a piece on the board. Place a piece on the board.
*Parameters* *Parameters*
------------
ctx: Union[SlashContext, discord.Message]
The context of the command/reaction. ctx can only be
SlashContext if the piece is placed by the bot
immediately after the player starts the game with a
command. Otherwise, ctx will be the message the player
reacted to when placing a piece.
user: int
The player-number of the player placing the piece.
column: int column: int
The column the player is placing the piece in. The column the player is placing the piece in.
""" """
channel = str(ctx.channel.id)
connect_four_games = self.bot.database["connect 4 games"] player_number = players.index(placer)+1
game = connect_four_games.find_one({"_id": channel}) user_name = self.get_name(f"#{placer}")
player_number = game["players"].index(user)+1
user_name = self.database_funcs.get_name(user)
placed_piece = False placed_piece = False
if game is None: board = self._decode_board_string(board_string)
send_message = "There's no game in this channel"
log_message = "There was no game in the channel"
else:
board = game["board"]
board = self._place_on_board(board, player_number, column) board = self._place_on_board(board, player_number, column)
if board is None: if board is None:
send_message = "There isn't any room in that column" send_message = "There isn't any room in that column"
log_message = "There wasn't any room in the column" log_message = "There wasn't any room in the column"
else: else:
updater = {"$set": {"board": board}} turn = player_number % 2
connect_four_games.update_one({"_id": channel}, updater)
turn = (game["turn"]+1) % 2
updater = {"$set": {"turn": turn}}
connect_four_games.update_one({"_id": channel}, updater)
self.bot.log("Checking for win") self.bot.log("Checking for win")
won, win_direction, win_coordinates = self._is_won(board) winner, win_direction, win_coordinates = self._is_won(board)
if won != 0: if winner != 0:
game_won = True game_won = True
updater = {"$set": {"winner": won}}
connect_four_games.update_one({"_id": channel}, updater)
updater = {"$set": {"win direction": win_direction}}
connect_four_games.update_one({"_id": channel}, updater)
updater = {"$set": {"win coordinates": win_coordinates}}
connect_four_games.update_one({"_id": channel}, updater)
send_message = "{} placed a piece in column {} and won. " send_message = "{} placed a piece in column {} and won. "
send_message = send_message.format(user_name, column+1) send_message = send_message.format(user_name, column+1)
log_message = f"{user_name} won" log_message = f"{user_name} won"
win_amount = int(game["difficulty"])**2+5 win_amount = difficulty**2+5
if game["players"][won-1] != f"#{self.bot.user.id}": if players[winner-1] != self.bot.user.id:
send_message += "Adding {} GwendoBucks to their account" send_message += "Adding {} GwendoBucks to their account"
send_message = send_message.format(win_amount) send_message = send_message.format(win_amount)
elif 0 not in board[0]: elif 0 not in board[0]:
@ -233,52 +274,95 @@ class ConnectFour():
log_message = "The game ended in a draw" log_message = "The game ended in a draw"
else: else:
game_won = False game_won = False
other_user_id = game["players"][turn] other_user_id = f"#{players[turn]}"
other_user_name = self.database_funcs.get_name( other_user_name = self.get_name(other_user_id)
other_user_id)
send_message = self.bot.long_strings["Connect 4 placed"] send_message = self.bot.long_strings["Connect 4 placed"]
format_parameters = [user_name, column+1, other_user_name] format_parameters = [user_name, column+1, other_user_name]
send_message = send_message.format(*format_parameters) send_message = send_message.format(*format_parameters)
log_message = "They placed the piece" log_message = "They placed the piece"
gwendolyn_turn = ( gwendolyn_turn = (players[turn] == self.bot.user.id)
game["players"][turn] == f"#{self.bot.user.id}")
placed_piece = True placed_piece = True
await ctx.channel.send(send_message)
self.bot.log(log_message) self.bot.log(log_message)
if placed_piece: if placed_piece:
self.draw.draw_image(channel) channel = str(ctx.channel)
self.draw.draw_image(
channel, board, winner, win_coordinates, win_direction, players)
old_images_path = "gwendolyn/resources/games/old_images/"
old_image_path = old_images_path + f"connect_four{channel}"
with open(old_image_path, "r") as file_pointer:
old_image_id = int(file_pointer.read())
old_image = await ctx.channel.fetch_message(old_image_id)
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
boards_path = "gwendolyn/resources/games/connect_four_boards/" boards_path = "gwendolyn/resources/games/connect_four_boards/"
file_path = boards_path + f"board{channel}.png" file_path = boards_path + f"board{channel}.png"
old_image = await ctx.channel.send(file=discord.File(file_path)) image_message = await ctx.channel.send(
send_message, file=discord.File(file_path))
if game_won: if game_won:
self._end_game(channel) self._end_game(winner, players, difficulty)
else: else:
with open(old_image_path, "w") as file_pointer: board_string = self._encode_board_string(board)
file_pointer.write(str(old_image.id))
if gwendolyn_turn: if gwendolyn_turn:
await self._connect_four_ai(ctx) await self._connect_four_ai(
ctx, board_string, players, difficulty, image_message.id
)
else: else:
for reaction in self.REACTIONS: buttons = []
await old_image.add_reaction(reaction) for i in range(7):
custom_id = encode_id(
[
"connectfour",
"place",
str(players[turn]),
board_string,
str(i),
str(players[0]),
str(players[1]),
str(difficulty),
str(image_message.id)
]
)
buttons.append(create_button(
style=ButtonStyle.blue,
label=str(i+1),
custom_id=custom_id,
disabled=(board[0][i] != 0)
))
async def surrender(self, ctx: SlashContext): custom_id = encode_id(
[
"connectfour",
"end",
str(players[0]),
str(players[1]),
str(difficulty),
str(image_message.id)
]
)
buttons.append(create_button(
style=ButtonStyle.red,
label="Surrender",
custom_id=custom_id
))
action_rows = []
for x in range(((len(buttons)-1)//4)+1):
row_buttons = buttons[
(x*4):(min(len(buttons),x*4+4))
]
action_rows.append(create_actionrow(*row_buttons))
await image_message.edit(
components=action_rows
)
else:
await ctx.channel.send(send_message)
old_message = await ctx.channel.fetch_message(message_id)
await old_message.delete()
async def surrender(self, ctx: ComponentContext, players: list,
difficulty: int, message_id: int):
""" """
Surrender a connect four game. Surrender a connect four game.
@ -287,38 +371,25 @@ class ConnectFour():
ctx: SlashContext ctx: SlashContext
The context of the command. The context of the command.
""" """
await self.bot.defer(ctx)
channel = str(ctx.channel_id)
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
if f"#{ctx.author.id}" in game["players"]: loser_index = players.index(ctx.author.id)
loser_index = game["players"].index(f"#{ctx.author.id}")
winner_index = (loser_index+1) % 2 winner_index = (loser_index+1) % 2
winner_id = game["players"][winner_index] winner_id = players[winner_index]
winner_name = self.database_funcs.get_name(winner_id) winner_name = self.get_name(f"#{winner_id}")
send_message = f"{ctx.author.display_name} surrenders." send_message = f"{ctx.author.display_name} surrenders."
send_message += f" This means {winner_name} is the winner." send_message += f" This means {winner_name} is the winner."
if winner_id != f"#{self.bot.user.id}": if winner_id != self.bot.user.id:
difficulty = int(game["difficulty"]) difficulty = int(difficulty)
reward = difficulty**2 + 5 reward = difficulty**2 + 5
send_message += f" Adding {reward} to their account" send_message += f" Adding {reward} to their account"
await ctx.send(send_message) await ctx.channel.send(send_message)
old_images_path = "gwendolyn/resources/games/old_images/"
old_image_path = old_images_path + f"connect_four{channel}"
with open(old_image_path, "r") as file_pointer:
old_image_id = int(file_pointer.read())
old_image = await ctx.channel.fetch_message(old_image_id)
if old_image is not None: self._end_game(winner_index+1, players, difficulty)
old_image = await ctx.channel.fetch_message(message_id)
await old_image.delete() await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self._end_game(channel)
else:
await ctx.send("You can't surrender when you're not a player")
def _place_on_board(self, board: dict, player: int, column: int): def _place_on_board(self, board: dict, player: int, column: int):
""" """
@ -355,17 +426,12 @@ class ConnectFour():
else: else:
return board return board
def _end_game(self, channel: str): def _end_game(self, winner: int, players: list, difficulty: int):
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
winner = game["winner"]
if winner != 0: if winner != 0:
if game["players"][winner-1] != f"#{self.bot.user.id}": if players[winner-1] != f"#{self.bot.user.id}":
difficulty = int(game["difficulty"]) difficulty = int(difficulty)
reward = difficulty**2 + 5 reward = difficulty**2 + 5
self.bot.money.addMoney(game["players"][winner-1], reward) self.bot.money.addMoney(f"#{players[winner-1]}", reward)
self.database_funcs.delete_game("connect 4 games", channel)
def _is_won(self, board: dict): def _is_won(self, board: dict):
won = 0 won = 0
@ -441,20 +507,19 @@ class ConnectFour():
return won, win_direction, win_coordinates return won, win_direction, win_coordinates
async def _connect_four_ai(self, ctx: SlashContext): async def _connect_four_ai(self,
ctx: Union[SlashContext, ComponentContext],
board_string: str, players: list,
difficulty: int, message_id: int):
def out_of_range(possible_scores: list): def out_of_range(possible_scores: list):
allowed_range = max(possible_scores)*(1-0.1) allowed_range = max(possible_scores)*(1-0.1)
more_than_one = len(possible_scores) != 1 more_than_one = len(possible_scores) != 1
return (min(possible_scores) <= allowed_range) and more_than_one return (min(possible_scores) <= allowed_range) and more_than_one
channel = str(ctx.channel.id)
self.bot.log("Figuring out best move") self.bot.log("Figuring out best move")
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
board = game["board"] board = self._decode_board_string(board_string)
player = game["players"].index(f"#{self.bot.user.id}")+1 player = players.index(self.bot.user.id)+1
difficulty = game["difficulty"]
scores = [-math.inf for _ in range(COLUMNCOUNT)] scores = [-math.inf for _ in range(COLUMNCOUNT)]
for column in range(COLUMNCOUNT): for column in range(COLUMNCOUNT):
@ -483,7 +548,15 @@ class ConnectFour():
best_columns = [i for i, x in enumerate(scores) if x == highest_score] best_columns = [i for i, x in enumerate(scores) if x == highest_score]
placement = random.choice(best_columns) placement = random.choice(best_columns)
await self.place_piece(ctx, f"#{self.bot.user.id}", placement) await self.place_piece(
ctx,
board_string,
placement,
players,
difficulty,
self.bot.user.id,
message_id
)
def _ai_calc_points(self, board: dict, player: int): def _ai_calc_points(self, board: dict, player: int):
score = 0 score = 0
@ -683,7 +756,7 @@ class DrawConnectFour():
def __init__(self, bot): def __init__(self, bot):
"""Initialize the class.""" """Initialize the class."""
self.bot = bot self.bot = bot
self.database_funcs = self.bot.database_funcs self.get_name = self.bot.database_funcs.get_name
win_bar_alpha = 160 win_bar_alpha = 160
font_path = "gwendolyn/resources/fonts/futura-bold.ttf" font_path = "gwendolyn/resources/fonts/futura-bold.ttf"
@ -729,19 +802,15 @@ class DrawConnectFour():
# pylint: enable=invalid-name # pylint: enable=invalid-name
# Draws the whole thing # Draws the whole thing
def draw_image(self, channel: str): def draw_image(self, channel: str, board: list, winner: int,
win_coordinates: list, win_direction: str, players: list):
""" """
Draw an image of the connect four board. Draw an image of the connect four board.
*Parameters* *Parameters*
------------ ------------
channel: str
The id of the channel the game is in.
""" """
self.bot.log("Drawing connect four board") self.bot.log("Drawing connect four board")
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
board = game["board"]
image_size = (self.WIDTH, self.HEIGHT+self.BOTTOMBORDER) image_size = (self.WIDTH, self.HEIGHT+self.BOTTOMBORDER)
background = Image.new("RGB", image_size, self.BACKGROUNDCOLOR) background = Image.new("RGB", image_size, self.BACKGROUNDCOLOR)
@ -751,10 +820,10 @@ class DrawConnectFour():
self._draw_pieces(drawer, board) self._draw_pieces(drawer, board)
if game["winner"] != 0: if winner != 0:
self._draw_win(background, game) self._draw_win(background, win_coordinates, win_direction)
self._draw_footer(drawer, game) self._draw_footer(drawer, players)
boards_path = "gwendolyn/resources/games/connect_four_boards/" boards_path = "gwendolyn/resources/games/connect_four_boards/"
background.save(boards_path + f"board{channel}.png") background.save(boards_path + f"board{channel}.png")
@ -897,7 +966,8 @@ class DrawConnectFour():
} }
drawer.ellipse(position, **piece_parameters) drawer.ellipse(position, **piece_parameters)
def _draw_win(self, background: Image, game: dict): def _draw_win(self, background: Image, win_coordinates: list,
win_direction: str):
""" """
Draw the bar that shows the winning pieces. Draw the bar that shows the winning pieces.
@ -908,10 +978,9 @@ class DrawConnectFour():
game: dict game: dict
The game data. The game data.
""" """
coordinates = game["win coordinates"]
start = self.BORDER + self.GRIDBORDER start = self.BORDER + self.GRIDBORDER
start_x = start + self.PIECEGRIDSIZE[0]*coordinates[1] start_x = start + self.PIECEGRIDSIZE[0]*win_coordinates[1]
start_y = start + self.PIECEGRIDSIZE[1]*coordinates[0] start_y = start + self.PIECEGRIDSIZE[1]*win_coordinates[0]
diagonal_length = ( diagonal_length = (
(math.sqrt( (math.sqrt(
(self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2+ (self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2+
@ -922,7 +991,7 @@ class DrawConnectFour():
size_ratio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0] size_ratio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0]
diagonal_angle = math.degrees(math.atan(size_ratio)) diagonal_angle = math.degrees(math.atan(size_ratio))
if game["win direction"] == "h": if win_direction == "h":
image_size = (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1]) image_size = (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1])
win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0)) win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
win_drawer = ImageDraw.Draw(win_bar) win_drawer = ImageDraw.Draw(win_bar)
@ -942,7 +1011,7 @@ class DrawConnectFour():
win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE)
win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE)
elif game["win direction"] == "v": elif win_direction == "v":
image_size = (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4) image_size = (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4)
win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0)) win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
win_drawer = ImageDraw.Draw(win_bar) win_drawer = ImageDraw.Draw(win_bar)
@ -962,7 +1031,7 @@ class DrawConnectFour():
win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE)
win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE)
elif game["win direction"] == "r": elif win_direction == "r":
image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length) image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length)
image_size = (image_width, self.PIECEGRIDSIZE[1]) image_size = (image_width, self.PIECEGRIDSIZE[1])
win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0)) win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
@ -1001,7 +1070,7 @@ class DrawConnectFour():
start_x -= 90 start_x -= 90
start_y -= 100 start_y -= 100
elif game["win direction"] == "l": elif win_direction == "l":
image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length) image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length)
image_size = (image_width, self.PIECEGRIDSIZE[1]) image_size = (image_width, self.PIECEGRIDSIZE[1])
win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0)) win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
@ -1040,16 +1109,16 @@ class DrawConnectFour():
win_bar_image = Image.new("RGBA", mask.size, color=self.WINBARCOLOR) win_bar_image = Image.new("RGBA", mask.size, color=self.WINBARCOLOR)
background.paste(win_bar_image, (start_x, start_y), mask) background.paste(win_bar_image, (start_x, start_y), mask)
def _draw_footer(self, drawer: ImageDraw, game: dict): def _draw_footer(self, drawer: ImageDraw, players: list):
if game["players"][0] == "Gwendolyn": if players[0] == "Gwendolyn":
player1 = "Gwendolyn" player1 = "Gwendolyn"
else: else:
player1 = self.database_funcs.get_name(game["players"][0]) player1 = self.get_name(f"#{players[0]}")
if game["players"][1] == "Gwendolyn": if players[1] == "Gwendolyn":
player2 = "Gwendolyn" player2 = "Gwendolyn"
else: else:
player2 = self.database_funcs.get_name(game["players"][1]) player2 = self.get_name(f"#{players[1]}")
circle_height = self.HEIGHT - self.BORDER circle_height = self.HEIGHT - self.BORDER
circle_height += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2 circle_height += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2

View File

@ -14,10 +14,16 @@ import math # Used by DrawHangman(), mainly for drawing circles
import random # Used to draw poorly import random # Used to draw poorly
import requests # Used for getting the word in Hangman.start() import requests # Used for getting the word in Hangman.start()
import discord # Used for discord.file and type hints import discord # Used for discord.file and type hints
import os
from discord_slash.context import SlashContext # Used for typehints from discord_slash.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.model import ButtonStyle
from discord_slash.context import SlashContext, ComponentContext
# Used for typehints
from PIL import ImageDraw, Image, ImageFont # Used to draw the image from PIL import ImageDraw, Image, ImageFont # Used to draw the image
from gwendolyn.utils import encode_id
class Hangman(): class Hangman():
""" """
@ -43,10 +49,10 @@ class Hangman():
APIPARAMS: dict APIPARAMS: dict
The parameters to pass to every api call. The parameters to pass to every api call.
""" """
self.__bot = bot self.bot = bot
self.__draw = DrawHangman(bot) self.__draw = DrawHangman(bot)
self.__API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name self.__API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name
api_key = self.__bot.credentials["wordnik_key"] api_key = self.bot.credentials["wordnik_key"]
self.__APIPARAMS = { # pylint: disable=invalid-name self.__APIPARAMS = { # pylint: disable=invalid-name
"hasDictionaryDef": True, "hasDictionaryDef": True,
"minCorpusCount": 5000, "minCorpusCount": 5000,
@ -68,70 +74,88 @@ class Hangman():
ctx: SlashContext ctx: SlashContext
The context of the command. The context of the command.
""" """
await self.__bot.defer(ctx) await self.bot.defer(ctx)
channel = str(ctx.channel_id)
user = f"#{ctx.author.id}"
game = self.__bot.database["hangman games"].find_one({"_id": channel})
user_name = self.__bot.database_funcs.get_name(user)
started_game = False
if game is None:
word = "-" word = "-"
while "-" in word or "." in word: while "-" in word or "." in word:
response = requests.get(self.__API_url, params=self.__APIPARAMS) response = requests.get(self.__API_url, params=self.__APIPARAMS)
word = list(response.json()[0]["word"].upper()) word = list(response.json()[0]["word"].upper())
self.__bot.log("Found the word \""+"".join(word)+"\"") self.bot.log("Found the word \""+"".join(word)+"\"")
guessed = [False] * len(word) guessed = [False] * len(word)
game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S") game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
new_game = {
"_id": channel,
"player": user,
"guessed letters": [],
"word": word,
"game ID": game_id,
"misses": 0,
"guessed": guessed
}
self.__bot.database["hangman games"].insert_one(new_game)
remaining_letters = list(string.ascii_uppercase) remaining_letters = list(string.ascii_uppercase)
self.__draw.draw_image(channel) self.__draw.draw_image(game_id, 0, word, guessed, [])
log_message = "Game started" send_message = f"{ctx.author.display_name} started a game of hangman."
send_message = f"{user_name} started game of hangman."
started_game = True
else:
log_message = "There was already a game going on"
send_message = self.__bot.long_strings["Hangman going on"]
self.__bot.log(log_message) self.bot.log("Game started")
await ctx.send(send_message)
if started_game:
boards_path = "gwendolyn/resources/games/hangman_boards/" boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{channel}.png" file_path = f"{boards_path}hangman_board{game_id}.png"
new_image = await ctx.channel.send(file=discord.File(file_path)) image_message = await ctx.send(
send_message,
file=discord.File(file_path)
)
blank_message = await ctx.channel.send("_ _") blank_message_one = await ctx.channel.send("_ _")
reaction_messages = { blank_message_two = await ctx.channel.send("_ _")
new_image: remaining_letters[:15],
blank_message: remaining_letters[15:]
}
old_messages = f"{new_image.id}\n{blank_message.id}" buttons = []
old_images_path = "gwendolyn/resources/games/old_images/" for letter in remaining_letters:
custom_id = encode_id([
"hangman",
"guess",
str(ctx.author.id),
letter,
"".join(word),
"",
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.blue,
label=letter,
custom_id=custom_id
))
with open(old_images_path+f"hangman{channel}", "w") as file_pointer: custom_id = encode_id([
file_pointer.write(old_messages) "hangman",
"end",
str(ctx.author.id),
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.red,
label="End game",
custom_id=custom_id
))
for message, letters in reaction_messages.items(): action_row_lists = []
for letter in letters: for i in range(((len(buttons)-1)//20)+1):
emoji = chr(ord(letter)+127397) action_rows = []
await message.add_reaction(emoji) available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
for x in range(((len(available_buttons)-1)//4)+1):
row_buttons = available_buttons[
(x*4):(min(len(available_buttons),x*4+4))
]
action_rows.append(create_actionrow(*row_buttons))
action_row_lists.append(action_rows)
async def stop(self, ctx: SlashContext):
await blank_message_one.edit(components=action_row_lists[0])
await blank_message_two.edit(components=action_row_lists[1])
async def stop(self, ctx: ComponentContext, game_id: str,
*messages: list[str]):
""" """
Stop the game of hangman. Stop the game of hangman.
@ -140,28 +164,20 @@ class Hangman():
ctx: SlashContext ctx: SlashContext
The context of the command. The context of the command.
""" """
channel = str(ctx.channel.id)
game = self.__bot.database["hangman games"].find_one({"_id": channel})
if game is None: self.bot.log("Deleting old messages")
await ctx.send("There's no game going on")
elif f"#{ctx.author.id}" != game["player"]:
await ctx.send("You can't end a game you're not in")
else:
self.__bot.database["hangman games"].delete_one({"_id": channel})
old_images_path = "gwendolyn/resources/games/old_images/"
with open(old_images_path+f"hangman{channel}", "r") as file_pointer: await ctx.channel.send("Hangman game stopped")
messages = file_pointer.read().splitlines() boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{game_id}.png"
os.remove(file_path)
for message in messages: for msg_id in messages:
old_message = await ctx.channel.fetch_message(int(message)) msg = await ctx.channel.fetch_message(msg_id)
self.__bot.log("Deleting old message") await msg.delete()
await old_message.delete()
await ctx.send("Game stopped") async def guess(self, ctx: ComponentContext, guess: str, word: str,
guessed_letters: str, game_id: str, *messages: list[str]):
async def guess(self, message: discord.Message, user: str, guess: str):
""" """
Guess a letter. Guess a letter.
@ -174,37 +190,23 @@ class Hangman():
guess: str guess: str
The guess. The guess.
""" """
channel = str(message.channel.id)
hangman_games = self.__bot.database["hangman games"]
game = hangman_games.find_one({"_id": channel})
game_exists = (game is not None) misses = 0
single_letter = (len(guess) == 1 and guess.isalpha()) guessed_letters += guess
new_guess = (guess not in game["guessed letters"]) guessed = [False for i in range(len(word))]
valid_guess = (game_exists and single_letter and new_guess) for guessed_letter in guessed_letters:
if valid_guess:
self.__bot.log("Guessed the letter")
correct_guess = 0 correct_guess = 0
for i, letter in enumerate(word):
for i, letter in enumerate(game["word"]): if guessed_letter == letter:
if guess == letter:
correct_guess += 1 correct_guess += 1
updater = {"$set": {f"guessed.{i}": True}} guessed[i] = True
hangman_games.update_one({"_id": channel}, updater)
if correct_guess == 0: if correct_guess == 0:
updater = {"$inc": {"misses": 1}} misses += 1
hangman_games.update_one({"_id": channel}, updater)
updater = {"$push": {"guessed letters": guess}}
hangman_games.update_one({"_id": channel}, updater)
remaining_letters = list(string.ascii_uppercase) remaining_letters = list(string.ascii_uppercase)
for letter in guessed_letters:
game = hangman_games.find_one({"_id": channel})
for letter in game["guessed letters"]:
remaining_letters.remove(letter) remaining_letters.remove(letter)
if correct_guess == 1: if correct_guess == 1:
@ -214,58 +216,87 @@ class Hangman():
send_message = "Guessed {}. There were {} {}s in the word." send_message = "Guessed {}. There were {} {}s in the word."
send_message = send_message.format(guess, correct_guess, guess) send_message = send_message.format(guess, correct_guess, guess)
self.__draw.draw_image(channel) self.__draw.draw_image(game_id, misses, word, guessed, guessed_letters)
if game["misses"] == 6: if misses == 6:
hangman_games.delete_one({"_id": channel}) send_message += self.bot.long_strings["Hangman lost game"]
send_message += self.__bot.long_strings["Hangman lost game"]
remaining_letters = [] remaining_letters = []
elif all(game["guessed"]): elif all(guessed):
hangman_games.delete_one({"_id": channel}) self.bot.money.addMoney(f"#{ctx.author_id}", 15)
self.__bot.money.addMoney(user, 15) send_message += self.bot.long_strings["Hangman guessed word"]
send_message += self.__bot.long_strings["Hangman guessed word"]
remaining_letters = [] remaining_letters = []
await message.channel.send(send_message)
old_images_path = "gwendolyn/resources/games/old_images/"
with open(old_images_path+f"hangman{channel}", "r") as file_pointer:
old_message_ids = file_pointer.read().splitlines()
for old_id in old_message_ids:
old_message = await message.channel.fetch_message(int(old_id))
self.__bot.log("Deleting old message")
await old_message.delete()
if remaining_letters != []:
boards_path = "gwendolyn/resources/games/hangman_boards/" boards_path = "gwendolyn/resources/games/hangman_boards/"
file_path = f"{boards_path}hangman_board{channel}.png" file_path = f"{boards_path}hangman_board{game_id}.png"
new_image = await message.channel.send(file=discord.File(file_path)) image_message = await ctx.channel.send(
send_message,
file=discord.File(file_path)
)
blank_message_one = await ctx.channel.send("_ _")
blank_message_two = await ctx.channel.send("_ _")
buttons = []
for letter in list(string.ascii_uppercase):
custom_id = encode_id([
"hangman",
"guess",
str(ctx.author.id),
letter,
word,
guessed_letters,
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.blue,
label=letter,
custom_id=custom_id,
disabled=(letter not in remaining_letters)
))
custom_id = encode_id([
"hangman",
"end",
str(ctx.author.id),
game_id,
str(image_message.id),
str(blank_message_one.id),
str(blank_message_two.id)
])
buttons.append(create_button(
style=ButtonStyle.red,
label="End game",
custom_id=custom_id
))
if len(remaining_letters) > 0: action_row_lists = []
if len(remaining_letters) > 15: for i in range(((len(buttons)-1)//20)+1):
blank_message = await message.channel.send("_ _") action_rows = []
reaction_messages = { available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
new_image: remaining_letters[:15], for x in range(((len(available_buttons)-1)//4)+1):
blank_message: remaining_letters[15:] row_buttons = available_buttons[
} (x*4):(min(len(available_buttons),x*4+4))
]
action_rows.append(create_actionrow(*row_buttons))
action_row_lists.append(action_rows)
await blank_message_one.edit(components=action_row_lists[0])
await blank_message_two.edit(components=action_row_lists[1])
else: else:
blank_message = "" boards_path = "gwendolyn/resources/games/hangman_boards/"
reaction_messages = {new_image: remaining_letters} file_path = f"{boards_path}hangman_board{game_id}.png"
await ctx.channel.send(send_message, file=discord.File(file_path))
os.remove(file_path)
for msg_id in messages:
msg = await ctx.channel.fetch_message(msg_id)
await msg.delete()
if blank_message != "":
old_messages = f"{new_image.id}\n{blank_message.id}"
else:
old_messages = str(new_image.id)
old_images_path = "gwendolyn/resources/games/old_images/"
old_image_path = old_images_path + f"hangman{channel}"
with open(old_image_path, "w") as file_pointer:
file_pointer.write(old_messages)
for message, letters in reaction_messages.items():
for letter in letters:
emoji = chr(ord(letter)+127397)
await message.add_reaction(emoji)
class DrawHangman(): class DrawHangman():
@ -355,9 +386,9 @@ class DrawHangman():
deviance_accuracy_x = 0 deviance_accuracy_x = 0
deviance_accuracy_y = 0 deviance_accuracy_y = 0
start = random.randint(-100, -80) start = random.randint(-100, -80)
degreess_amount = 360 + random.randint(-10, 30) degrees_amount = 360 + random.randint(-10, 30)
for degree in range(degreess_amount): for degree in range(degrees_amount):
deviance_x, deviance_accuracy_x = self.__deviate( deviance_x, deviance_accuracy_x = self.__deviate(
deviance_x, deviance_x,
deviance_accuracy_x, deviance_accuracy_x,
@ -589,7 +620,8 @@ class DrawHangman():
placed = True placed = True
return background return background
def draw_image(self, channel: str): def draw_image(self, game_id: str, misses: int, word: str,
guessed: list[bool], guessed_letters: str):
""" """
Draw a hangman Image. Draw a hangman Image.
@ -598,21 +630,20 @@ class DrawHangman():
channel: str channel: str
The id of the channel the game is in. The id of the channel the game is in.
""" """
self.__bot.log("Drawing hangman image", channel) self.__bot.log("Drawing hangman image")
game = self.__bot.database["hangman games"].find_one({"_id": channel})
random.seed(game["game ID"]) random.seed(game_id)
background = Image.open("gwendolyn/resources/paper.jpg") background = Image.open("gwendolyn/resources/paper.jpg")
gallow = self.__draw_gallows() gallow = self.__draw_gallows()
man = self.__draw_man(game["misses"], game["game ID"]) man = self.__draw_man(misses, game_id)
random.seed(game["game ID"]) random.seed(game_id)
letter_line_parameters = [game["word"], game["guessed"], game["misses"]] letter_line_parameters = [word, guessed, misses]
letter_lines = self.__draw_letter_lines(*letter_line_parameters) letter_lines = self.__draw_letter_lines(*letter_line_parameters)
random.seed(game["game ID"]) random.seed(game_id)
misses = self.__draw_misses(game["guessed letters"], game["word"]) misses = self.__draw_misses(list(guessed_letters), word)
background.paste(gallow, (100, 100), gallow) background.paste(gallow, (100, 100), gallow)
background.paste(man, (300, 210), man) background.paste(man, (300, 210), man)
@ -623,5 +654,6 @@ class DrawHangman():
misses_text_width = misses_text.size[0] misses_text_width = misses_text.size[0]
background.paste(misses_text, (850-misses_text_width//2, 50), misses_text) background.paste(misses_text, (850-misses_text_width//2, 50), misses_text)
board_path = f"gwendolyn/resources/games/hangman_boards/hangman_board{channel}.png" boards_path = "gwendolyn/resources/games/hangman_boards/"
background.save(board_path) image_path = f"{boards_path}hangman_board{game_id}.png"
background.save(image_path)

View File

@ -1,11 +1,17 @@
"""Plex integration with the bot.""" """Plex integration with the bot."""
from math import floor, ceil from math import floor, ceil
import time import time
import json
import asyncio import asyncio
import requests import requests
import imdb import imdb
import discord import discord
import xmltodict
from discord_slash.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.model import ButtonStyle
from gwendolyn.utils import encode_id
class Plex(): class Plex():
"""Container for Plex functions and commands.""" """Container for Plex functions and commands."""
@ -64,88 +70,102 @@ class Plex():
colour=0x00FF00 colour=0x00FF00
) )
message = await ctx.send(embed=embed) buttons = []
message_data = {"message_id":message.id,"imdb_ids":imdb_ids}
file_path = f"gwendolyn/resources/plex/old_message{ctx.channel.id}"
with open(file_path,"w") as file_pointer:
json.dump(message_data, file_pointer)
if len(movies) == 1: if len(movies) == 1:
await message.add_reaction("✔️") buttons.append(create_button(
style=ButtonStyle.green,
label="",
custom_id=encode_id(["plex", "movie", str(imdb_ids[0])])
)
)
else: else:
for i in range(len(movies)): for i in range(len(movies)):
await message.add_reaction(["1","2","3","4","5"][i]) buttons.append(
create_button(
style=ButtonStyle.blue,
label=str(i+1),
custom_id=encode_id(["plex", "movie", str(imdb_ids[i])])
)
)
buttons.append(create_button(
style=ButtonStyle.red,
label="X",
custom_id=encode_id(["plex", "movie", ""])
)
)
action_rows = []
for i in range(((len(buttons)-1)//5)+1):
action_rows.append(
create_actionrow(
*buttons[(i*5):(min(len(buttons),i*5+5))]
)
)
await ctx.send(embed=embed, components=action_rows)
await message.add_reaction("")
message = await ctx.channel.fetch_message(message.id)
if (message.content != "" and
not isinstance(ctx.channel, discord.DMChannel)):
await message.clear_reactions()
async def add_movie(self, message, imdb_id, edit_message = True): async def add_movie(self, message, imdb_id, edit_message = True):
"""Add a movie to Plex server.""" """Add a movie to Plex server."""
if imdb_id is None:
if not edit_message:
await message.delete()
if imdb_id == "":
self.bot.log("Did not find what the user was searching for") self.bot.log("Did not find what the user was searching for")
if edit_message: message_text = "Try searching for the IMDB id"
await message.edit(
embed = None,
content = "Try searching for the IMDB id"
)
else:
await message.channel.send("Try searching for the IMDB id")
else: else:
self.bot.log("Trying to add movie "+str(imdb_id)) self.bot.log("Trying to add movie "+str(imdb_id))
# Searches for the movie using the imdb id through Radarr
api_key = self.credentials["radarr_key"] api_key = self.credentials["radarr_key"]
request_url = self.radarr_url+"movie/lookup/imdb?imdbId=tt"+imdb_id request_url = self.radarr_url+"movie/lookup/imdb?imdbId=tt"+imdb_id
request_url += "&apiKey="+api_key request_url += "&apiKey="+api_key
response = requests.get(request_url) response = requests.get(request_url)
# Makes the dict used for the post request
lookup_data = response.json() lookup_data = response.json()
post_data = {"qualityProfileId": 1, post_data = {
"rootFolder_path" : self.movie_path, "qualityProfileId": 1,
"rootFolderPath" : self.movie_path,
"monitored" : True, "monitored" : True,
"addOptions": {"searchForMovie": True}} "addOptions": {"searchForMovie": True}
}
for key in ["tmdbId","title","titleSlug","images","year"]: for key in ["tmdbId","title","titleSlug","images","year"]:
post_data.update({key : lookup_data[key]}) post_data.update({key : lookup_data[key]})
# Makes the post request
response = requests.post( response = requests.post(
url= self.radarr_url+"movie?apikey="+api_key, url = self.radarr_url+"movie?apikey="+api_key,
json = post_data json = post_data
) )
# Deciphers the response
if response.status_code == 201: if response.status_code == 201:
success_message = "{} successfully added to Plex".format( self.bot.log("Added "+post_data["title"]+" to Plex")
message_text = "{} successfully added to Plex".format(
post_data["title"] post_data["title"]
) )
if edit_message:
await message.edit(
embed = None,
content = success_message
)
else:
await message.channel.send(success_message)
self.bot.log("Added "+post_data["title"]+" to Plex")
elif response.status_code == 400: elif response.status_code == 400:
fail_text = self.long_strings["Already on Plex"].format( self.bot.log("The movie was already on plex")
message_text = self.long_strings["Already on Plex"].format(
post_data['title'] post_data['title']
) )
if edit_message:
await message.edit(embed = None, content = fail_text)
else:
await message.channel.send(fail_text)
else: else:
self.bot.log(str(response.status_code)+" "+response.reason)
message_text = "Something went wrong",
if edit_message: if edit_message:
await message.edit( await message.edit(
embed = None, embed = None,
content = "Something went wrong" content = message_text,
components = []
) )
else: else:
await message.channel.send("Something went wrong") await message.channel.send(message_text)
self.bot.log(str(response.status_code)+" "+response.reason)
async def request_show(self, ctx, show_name): async def request_show(self, ctx, show_name):
"""Request a show for the Plex server.""" """Request a show for the Plex server."""
@ -166,7 +186,7 @@ class Plex():
message_title = "**Is it any of these shows?**" message_title = "**Is it any of these shows?**"
message_text = "" message_text = ""
imdb_names = [] imdb_ids = []
for i, show in enumerate(shows): for i, show in enumerate(shows):
try: try:
@ -176,10 +196,10 @@ class Plex():
message_text += "\n"+str(i+1)+") "+show["title"] message_text += "\n"+str(i+1)+") "+show["title"]
except KeyError: except KeyError:
message_text += "Error" message_text += "Error"
imdb_names.append(show["title"]) imdb_ids.append(show.movieID)
self.bot.log( self.bot.log(
f"Returning a list of {len(shows)} possible shows: {imdb_names}" f"Returning a list of {len(shows)} possible shows: {imdb_ids}"
) )
embed = discord.Embed( embed = discord.Embed(
@ -188,42 +208,69 @@ class Plex():
colour=0x00FF00 colour=0x00FF00
) )
message = await ctx.send(embed=embed) buttons = []
message_data = {"message_id":message.id,"imdb_names":imdb_names}
file_path = "gwendolyn/resources/plex/old_message"+str(ctx.channel.id)
with open(file_path,"w") as file_pointer:
json.dump(message_data, file_pointer)
if len(shows) == 1: if len(shows) == 1:
await message.add_reaction("✔️") buttons.append(create_button(
else: style=ButtonStyle.green,
for i in range(len(shows)): label="",
await message.add_reaction(["1","2","3","4","5"][i]) custom_id=encode_id(["plex", "show", str(imdb_ids[0])])
)
await message.add_reaction("")
message = await ctx.channel.fetch_message(message.id)
if message.content != "":
if not isinstance(ctx.channel, discord.DMChannel):
await message.clear_reactions()
async def add_show(self, message, imdb_name):
"""Add the requested show to Plex."""
if imdb_name is None:
self.bot.log("Did not find what the user was searching for")
await message.edit(
embed = None,
content = "Try searching for the IMDB id"
) )
else: else:
self.bot.log("Trying to add show "+str(imdb_name)) for i in range(len(shows)):
buttons.append(
create_button(
style=ButtonStyle.blue,
label=str(i+1),
custom_id=encode_id(["plex", "show", str(imdb_ids[i])])
)
)
buttons.append(create_button(
style=ButtonStyle.red,
label="X",
custom_id=encode_id(["plex", "show", ""])
)
)
action_rows = []
for i in range(((len(buttons)-1)//5)+1):
action_rows.append(
create_actionrow(
*buttons[(i*5):(min(len(buttons),i*5+5))]
)
)
await ctx.send(embed=embed, components=action_rows)
async def add_show(self, message, imdb_id, edit_message = True):
"""Add the requested show to Plex."""
if imdb_id == "":
self.bot.log("Did not find what the user was searching for")
message_text = "Try searching for the IMDB id"
else:
self.bot.log("Trying to add show "+str(imdb_id))
# Finds the tvdb id
tvdb_api_url = "https://thetvdb.com/api/"
tvdb_method = "GetSeriesByRemoteID.php"
tvdb_request_url = f"{tvdb_api_url}{tvdb_method}"
tvdb_id = xmltodict.parse(
requests.get(
tvdb_request_url+f"?imdbid=tt{imdb_id}",
headers = {"ContentType" : "application/json"}
).text
)['Data']['Series']['seriesid']
# Finds the rest of the information using Sonarr
api_key = self.credentials["sonarr_key"] api_key = self.credentials["sonarr_key"]
request_url = self.sonarr_url+"series/lookup?term=" request_url = self.sonarr_url+"series/lookup?term="
request_url += imdb_name.replace(" ","%20") request_url += f"tvdb:{tvdb_id}"
request_url += "&apiKey="+api_key request_url += "&apiKey="+api_key
response = requests.get(request_url) response = requests.get(request_url)
# Makes the dict used for the post request
lookup_data = response.json()[0] lookup_data = response.json()[0]
post_data = { post_data = {
"ProfileId" : 1, "ProfileId" : 1,
@ -234,29 +281,32 @@ class Plex():
for key in ["tvdbId","title","titleSlug","images","seasons"]: for key in ["tvdbId","title","titleSlug","images","seasons"]:
post_data.update({key : lookup_data[key]}) post_data.update({key : lookup_data[key]})
# Makes the post request
response = requests.post( response = requests.post(
url= self.sonarr_url+"series?apikey="+api_key, url= self.sonarr_url+"series?apikey="+api_key,
json = post_data json = post_data
) )
# Deciphers the response
if response.status_code == 201: if response.status_code == 201:
await message.edit(
embed = None,
content = post_data["title"]+" successfully added to Plex"
)
self.bot.log("Added a "+post_data["title"]+" to Plex") self.bot.log("Added a "+post_data["title"]+" to Plex")
message_text = post_data["title"]+" successfully added to Plex"
elif response.status_code == 400: elif response.status_code == 400:
text = self.long_strings["Already on Plex"].format( message_text = self.long_strings["Already on Plex"].format(
post_data['title'] post_data['title']
) )
await message.edit(embed = None, content = text)
else: else:
self.bot.log(str(response.status_code)+" "+response.reason)
message_text = "Something went wrong"
if edit_message:
await message.edit( await message.edit(
embed = None, embed = None,
content = "Something went wrong" content = message_text,
components = []
) )
self.bot.log(str(response.status_code)+" "+response.reason) else:
await message.channel.send(message_text)
async def __generate_download_list(self, show_dm, show_movies, show_shows, async def __generate_download_list(self, show_dm, show_movies, show_shows,
episodes): episodes):
@ -364,7 +414,9 @@ class Plex():
movie_list = requests.get( movie_list = requests.get(
self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"] self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
).json() ).json()
print(self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]) print(
self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
)
movie_queue = requests.get( movie_queue = requests.get(
self.radarr_url+"queue?apiKey="+self.credentials["radarr_key"] self.radarr_url+"queue?apiKey="+self.credentials["radarr_key"]
).json() ).json()

View File

@ -140,11 +140,6 @@
} }
] ]
}, },
"connect_four_surrender" : {
"base" : "connect_four",
"name" : "surrender",
"description" : "Surrender the game of connect four"
},
"downloading" : { "downloading" : {
"name" : "downloading", "name" : "downloading",
"description" : "See current downloads for Plex", "description" : "See current downloads for Plex",
@ -187,16 +182,10 @@
} }
] ]
}, },
"hangman_start" : { "hangman" : {
"base" : "hangman", "name" : "hangman",
"name" : "start",
"description" : "Start a game of hangman" "description" : "Start a game of hangman"
}, },
"hangman_stop" : {
"base" : "hangman",
"name" : "stop",
"description" : "Stop the current game of hangman"
},
"hello" : { "hello" : {
"name" : "hello", "name" : "hello",
"description" : "Greet Gwendolyn" "description" : "Greet Gwendolyn"

View File

@ -8,4 +8,5 @@ from .helper_classes import DatabaseFuncs
from .event_handlers import EventHandler, ErrorHandler from .event_handlers import EventHandler, ErrorHandler
from .util_functions import (get_params, log_this, cap, make_files, from .util_functions import (get_params, log_this, cap, make_files,
replace_multiple, emoji_to_command, long_strings, replace_multiple, emoji_to_command, long_strings,
sanitize, get_options, get_credentials) sanitize, get_options, get_credentials, encode_id,
decode_id)

View File

@ -7,16 +7,14 @@ Classes used to handle bot events and errors.
ErrorHandler ErrorHandler
""" """
import traceback # Used to get the traceback of errors import traceback # Used to get the traceback of errors
import sys # Used to get traceback when the specific error is not
# available
import discord # Used to init discord.Game and discord.Status, as well import discord # Used to init discord.Game and discord.Status, as well
# as compare errors to discord errors and as typehints # as compare errors to discord errors and as typehints
from discord.ext import commands # Used to compare errors with command from discord.ext import commands # Used to compare errors with command
# errors # errors
from discord_slash.context import SlashContext from discord_slash.context import SlashContext, ComponentContext
from gwendolyn.utils.util_functions import emoji_to_command from gwendolyn.utils.util_functions import decode_id
class EventHandler(): class EventHandler():
@ -63,68 +61,53 @@ class EventHandler():
log_message = f"{ctx.author.display_name} ran {full_command}" log_message = f"{ctx.author.display_name} ran {full_command}"
self.bot.log(log_message, str(ctx.channel_id), level=25) self.bot.log(log_message, str(ctx.channel_id), level=25)
async def on_reaction_add(self, reaction: discord.Reaction, async def on_component(self, ctx: ComponentContext):
user: discord.User): info = decode_id(ctx.custom_id)
"""Take action if the reaction is on a command message.""" self.bot.log(f"Component action with info {info}")
if not user.bot: channel = ctx.origin_message.channel
tests = self.bot.database_funcs
message = reaction.message
channel = message.channel
reacted_message = f"{user.display_name} reacted to a message"
self.bot.log(reacted_message, str(channel.id))
plex_data = tests.plex_reaction_test(message)
# plex_data is a list containing 3 elements: whether it was
# the add_show/add_movie 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).
reaction_test_parameters = [message, f"#{str(user.id)}"] if info[0].lower() == "plex":
if info[1].lower() == "movie":
await self.bot.other.plex.add_movie(
ctx.origin_message,
info[2],
not isinstance(channel, discord.DMChannel)
)
if tests.connect_four_reaction_test(*reaction_test_parameters): elif info[1].lower() == "show":
column = emoji_to_command(reaction.emoji) await self.bot.other.plex.add_show(
params = [message, f"#{user.id}", column-1] ctx.origin_message,
await self.bot.games.connect_four.place_piece(*params) info[2],
not isinstance(channel, discord.DMChannel)
)
if plex_data[0]: elif info[0].lower() == "hangman":
plex_functions = self.bot.other.plex if str(ctx.author_id) == info[2]:
if plex_data[1]: if info[1].lower() == "guess":
movie_pick = emoji_to_command(reaction.emoji) await self.bot.games.hangman.guess(ctx, *info[3:])
if movie_pick == "none": elif info[1].lower() == "end":
imdb_id = None await self.bot.games.hangman.stop(ctx, *info[3:])
else:
imdb_id = plex_data[2][movie_pick-1]
if isinstance(channel, discord.DMChannel): elif info[0].lower() == "connectfour":
await message.delete() connect_four = self.bot.games.connect_four
await plex_functions.add_movie(message, imdb_id, False) if info[1].lower() == "place" and str(ctx.author_id) == info[2]:
else: params = [
await message.clear_reactions() ctx,
await plex_functions.add_movie(message, imdb_id) info[3],
else: int(info[4]),
show_pick = emoji_to_command(reaction.emoji) [int(info[5]), int(info[6])],
if show_pick == "none": int(info[7]),
imdb_name = None ctx.author_id,
else: int(info[8])
imdb_name = plex_data[2][show_pick-1] ]
await connect_four.place_piece(*params)
if info[1].lower() == "end":
if str(ctx.author_id) in [info[2], info[3]]:
params = [
ctx, [int(info[2]), int(info[3])], info[4], info[5]
]
await connect_four.surrender(*params)
if isinstance(channel, discord.DMChannel):
await message.delete()
await plex_functions.add_show(message, imdb_name, False)
else:
await message.clear_reactions()
await plex_functions.add_show(message, imdb_name)
elif tests.hangman_reaction_test(*reaction_test_parameters):
self.bot.log("They reacted to the hangman message")
if ord(reaction.emoji) in range(127462, 127488):
# The range is letter-emojis
guess = chr(ord(reaction.emoji)-127397)
# 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(): class ErrorHandler():
@ -166,10 +149,6 @@ class ErrorHandler():
async def on_error(self, method: str): async def on_error(self, method: str):
"""Log when there's an error.""" """Log when there's an error."""
error_type = sys.exc_info()[0]
if error_type == discord.errors.NotFound:
self.bot.log("Deleted message before I could add all reactions")
else:
exception = traceback.format_exc() exception = traceback.format_exc()
exception_string = "".join(exception) exception_string = "".join(exception)

View File

@ -6,7 +6,6 @@ Contains classes used for utilities.
DatabaseFuncs() DatabaseFuncs()
""" """
import os # Used to test if files exist import os # Used to test if files exist
import json # Used to read the data about add_movie/add_show
import time # Used to test how long it's been since commands were synced import time # Used to test how long it's been since commands were synced
import re # Used in get_id import re # Used in get_id
@ -25,11 +24,6 @@ class DatabaseFuncs():
wipe_games() wipe_games()
connect_four_reaction_test(message: discord.Message, connect_four_reaction_test(message: discord.Message,
user: discord.User) -> bool user: discord.User) -> bool
hangman_reaction_test(message: discord.Message,
user: discord.User) -> bool
plex_reaction_test(message: discord.Message,
user: discord.User) -> bool, bool,
list
imdb_commands() imdb_commands()
""" """
@ -112,8 +106,7 @@ class DatabaseFuncs():
game_types = [ game_types = [
"trivia questions", "trivia questions",
"blackjack games", "blackjack games",
"connect 4 games", "connect 4 games"
"hangman games",
"hex games" "hex games"
] ]
for game_type in game_types: for game_type in game_types:
@ -164,87 +157,6 @@ class DatabaseFuncs():
return valid_reaction return valid_reaction
def hangman_reaction_test(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
file_path = f"gwendolyn/resources/games/old_images/hangman{channel.id}"
if os.path.isfile(file_path):
with open(file_path, "r") as file_pointer:
old_messages = file_pointer.read().splitlines()
else:
return False
game_message = False
for old_message in old_messages:
old_message_id = int(old_message)
if message.id == old_message_id:
database = self.bot.database["hangman games"]
channel_search = {"_id": str(channel.id)}
game = database.find_one(channel_search)
if user == game["player"]:
game_message = True
break
return game_message
def plex_reaction_test(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
old_messages_path = "gwendolyn/resources/plex/"
file_path = old_messages_path + f"old_message{str(channel.id)}"
if os.path.isfile(file_path):
with open(file_path, "r") as file_pointer:
data = json.load(file_pointer)
else:
return (False, None, None)
if data["message_id"] != message.id:
return (False, None, None)
if "imdb_ids" in data:
return_data = (True, True, data["imdb_ids"])
else:
return_data = (True, False, data["imdb_names"])
return return_data
async def imdb_commands(self): async def imdb_commands(self):
"""Sync the slash commands with the discord API.""" """Sync the slash commands with the discord API."""
collection = self.bot.database["last synced"] collection = self.bot.database["last synced"]

View File

@ -21,6 +21,17 @@ import logging # Used for logging
import os # Used by make_files() to check if files exist import os # Used by make_files() to check if files exist
import sys # Used to specify printing for logging import sys # Used to specify printing for logging
import imdb # Used to disable logging for the module import imdb # Used to disable logging for the module
import string
BASE_37 = ":" + string.digits + string.ascii_uppercase
BASE_128 = list(
string.digits +
string.ascii_letters +
"!#$€£¢¥¤&%()*+,-./;:<=>?@[]_{|}~ `¦§©®«»±µ·¿əʒ" +
"ÆØÅÐÉÈÊÇÑÖ" +
"æøåðéèêçñö"
)
# All of this is logging configuration # All of this is logging configuration
FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s" FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s"
@ -340,3 +351,39 @@ def emoji_to_command(emoji: str):
return_value = "" return_value = ""
return return_value return return_value
def encode_id(info: list):
letters = list(":".join(info))
dec = 0
for i, letter in enumerate(letters):
try:
dec += (37**i) * BASE_37.index(letter.upper())
except ValueError:
log_this(f"Could not encode letter {letter}", level=30)
custom_id = []
while dec:
custom_id.append(BASE_128[dec % 128])
dec = dec // 128
custom_id = ''.join(custom_id)
log_this(f"Encoded {info} to {custom_id}")
return custom_id
def decode_id(custom_id: str):
letters = list(custom_id)
dec = 0
for i, letter in enumerate(letters):
dec += (128**i) * BASE_128.index(letter)
info_string = []
while dec:
info_string.append(BASE_37[dec % 37])
dec = dec // 37
info = ''.join(info_string).split(':')
log_this(f"Decoded {custom_id} to be {info}")
return ''.join(info_string).split(':')

View File

@ -23,7 +23,6 @@ lxml==4.6.3
more-itertools==8.8.0 more-itertools==8.8.0
multidict==5.1.0 multidict==5.1.0
Pillow==8.3.1 Pillow==8.3.1
pip==21.2.3
pymongo==3.12.0 pymongo==3.12.0
requests==2.26.0 requests==2.26.0
setuptools==57.4.0 setuptools==57.4.0