From 3819e56cd6616e619ca94db7f6c21466831df467 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Wed, 18 Aug 2021 21:12:32 +0200 Subject: [PATCH] :sparkles: Made the plex functions use buttons --- gwendolyn/cogs/event_cog.py | 5 + gwendolyn/funcs/other/plex.py | 240 +++++++++++++++----------- gwendolyn/gwendolyn_client.py | 270 +++++++++++++++--------------- gwendolyn/utils/event_handlers.py | 56 +++---- gwendolyn/utils/helper_classes.py | 40 ----- requirements.txt | 1 - 6 files changed, 305 insertions(+), 307 deletions(-) diff --git a/gwendolyn/cogs/event_cog.py b/gwendolyn/cogs/event_cog.py index 4737f8c..9fd379a 100644 --- a/gwendolyn/cogs/event_cog.py +++ b/gwendolyn/cogs/event_cog.py @@ -34,6 +34,11 @@ class EventCog(commands.Cog): """Handle when someone reacts to a message.""" await self.bot.event_handler.on_reaction_add(reaction, user) + @commands.Cog.listener() + async def on_component(self, ctx): + """Handle when someone reacts to a message.""" + await self.bot.event_handler.on_component(ctx) + def setup(bot): """Add the eventcog to the bot.""" diff --git a/gwendolyn/funcs/other/plex.py b/gwendolyn/funcs/other/plex.py index dc534e2..a30f878 100644 --- a/gwendolyn/funcs/other/plex.py +++ b/gwendolyn/funcs/other/plex.py @@ -1,11 +1,15 @@ """Plex integration with the bot.""" from math import floor, ceil import time -import json import asyncio import requests import imdb import discord +import xmltodict + +from discord_slash.utils.manage_components import (create_button, +create_actionrow) +from discord_slash.model import ButtonStyle class Plex(): """Container for Plex functions and commands.""" @@ -64,88 +68,102 @@ class Plex(): colour=0x00FF00 ) - message = await ctx.send(embed=embed) - - 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) - + buttons = [] if len(movies) == 1: - await message.add_reaction("✔️") + buttons.append(create_button( + style=ButtonStyle.green, + label="✓", + custom_id=f"plex:movie:{imdb_ids[0]}" + ) + ) else: 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=f"plex:movie:{imdb_ids[i]}" + ) + ) + + buttons.append(create_button( + style=ButtonStyle.red, + label="X", + custom_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): """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") - if edit_message: - await message.edit( - embed = None, - content = "Try searching for the IMDB id" - ) - else: - await message.channel.send("Try searching for the IMDB id") + message_text = "Try searching for the IMDB id" else: 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"] request_url = self.radarr_url+"movie/lookup/imdb?imdbId=tt"+imdb_id request_url += "&apiKey="+api_key response = requests.get(request_url) + + # Makes the dict used for the post request lookup_data = response.json() - post_data = {"qualityProfileId": 1, - "rootFolder_path" : self.movie_path, - "monitored" : True, - "addOptions": {"searchForMovie": True}} + post_data = { + "qualityProfileId": 1, + "rootFolderPath" : self.movie_path, + "monitored" : True, + "addOptions": {"searchForMovie": True} + } for key in ["tmdbId","title","titleSlug","images","year"]: post_data.update({key : lookup_data[key]}) + # Makes the post request response = requests.post( - url= self.radarr_url+"movie?apikey="+api_key, + url = self.radarr_url+"movie?apikey="+api_key, json = post_data ) + # Deciphers the response 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"] ) - 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: - 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'] ) - if edit_message: - await message.edit(embed = None, content = fail_text) - else: - await message.channel.send(fail_text) else: - if edit_message: - await message.edit( - embed = None, - content = "Something went wrong" - ) - else: - await message.channel.send("Something went wrong") self.bot.log(str(response.status_code)+" "+response.reason) + message_text = "Something went wrong", + + if edit_message: + await message.edit( + embed = None, + content = message_text, + components = [] + ) + else: + await message.channel.send(message_text) async def request_show(self, ctx, show_name): """Request a show for the Plex server.""" @@ -166,7 +184,7 @@ class Plex(): message_title = "**Is it any of these shows?**" message_text = "" - imdb_names = [] + imdb_ids = [] for i, show in enumerate(shows): try: @@ -176,10 +194,10 @@ class Plex(): message_text += "\n"+str(i+1)+") "+show["title"] except KeyError: message_text += "Error" - imdb_names.append(show["title"]) + imdb_ids.append(show.movieID) 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( @@ -188,42 +206,69 @@ class Plex(): colour=0x00FF00 ) - message = await ctx.send(embed=embed) - - 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) - + buttons = [] if len(shows) == 1: - await message.add_reaction("✔️") - else: - for i in range(len(shows)): - await message.add_reaction(["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣"][i]) - - 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" + buttons.append(create_button( + style=ButtonStyle.green, + label="✓", + custom_id=f"plex:show:{imdb_ids[0]}" + ) ) 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=f"plex:show:{imdb_ids[i]}" + ) + ) + + buttons.append(create_button( + style=ButtonStyle.red, + label="X", + custom_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"] 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 response = requests.get(request_url) + + # Makes the dict used for the post request lookup_data = response.json()[0] post_data = { "ProfileId" : 1, @@ -234,29 +279,32 @@ class Plex(): for key in ["tvdbId","title","titleSlug","images","seasons"]: post_data.update({key : lookup_data[key]}) - + # Makes the post request response = requests.post( url= self.sonarr_url+"series?apikey="+api_key, json = post_data ) + # Deciphers the response 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") + message_text = post_data["title"]+" successfully added to Plex" 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'] ) - await message.edit(embed = None, content = text) else: - await message.edit( - embed = None, - content = "Something went wrong" - ) self.bot.log(str(response.status_code)+" "+response.reason) + message_text = "Something went wrong" + + if edit_message: + await message.edit( + embed = None, + content = message_text, + components = [] + ) + else: + await message.channel.send(message_text) async def __generate_download_list(self, show_dm, show_movies, show_shows, episodes): @@ -364,7 +412,9 @@ class Plex(): movie_list = requests.get( self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"] ).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( self.radarr_url+"queue?apiKey="+self.credentials["radarr_key"] ).json() diff --git a/gwendolyn/gwendolyn_client.py b/gwendolyn/gwendolyn_client.py index ff4c683..850827a 100644 --- a/gwendolyn/gwendolyn_client.py +++ b/gwendolyn/gwendolyn_client.py @@ -1,135 +1,135 @@ -""" -Contains the Gwendolyn class, a subclass of the discord command bot. - -*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 discord # Used for discord.Intents and discord.Status -import discord_slash # Used to initialized SlashCommands object -import git # Used to pull when stopping - -from discord.ext import commands # Used to inherit from commands.bot -from pymongo import MongoClient # Used for database management -from gwendolyn.funcs import Money, StarWars, Games, Other, LookupFuncs -from gwendolyn.utils import (get_options, get_credentials, log_this, - DatabaseFuncs, EventHandler, ErrorHandler, - long_strings) - - -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) - """ - # pylint: disable=too-many-instance-attributes - - def __init__(self): - """Initialize the bot.""" - intents = discord.Intents.default() - intents.members = True - initiation_parameters = { - "command_prefix": " ", - "case_insensitive": True, - "intents": intents, - "status": discord.Status.dnd - } - super().__init__(**initiation_parameters) - - self._add_clients_and_options() - self._add_util_classes() - self._add_function_containers() - self._add_cogs() - - def _add_clients_and_options(self): - """Add all the client, option and credentials objects.""" - self.long_strings = long_strings() - self.options = get_options() - self.credentials = get_credentials() - finnhub_key = self.credentials["finnhub_key"] - self.finnhub_client = finnhub.Client(api_key=finnhub_key) - mongo_user = self.credentials["mongo_db_user"] - mongo_password = self.credentials["mongo_db_password"] - mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn" - mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority" - database_clint = MongoClient(mongo_url) - - if self.options["testing"]: - self.log("Testing mode") - self.database = database_clint["Gwendolyn-Test"] - else: - self.database = database_clint["Gwendolyn"] - - def _add_util_classes(self): - """Add all the classes used as utility.""" - self.database_funcs = DatabaseFuncs(self) - self.event_handler = EventHandler(self) - self.error_handler = ErrorHandler(self) - slash_parameters = { - "sync_commands": True, - "sync_on_cog_reload": True, - "override_type": True - } - self.slash = discord_slash.SlashCommand(self, **slash_parameters) - - def _add_function_containers(self): - """Add all the function containers used for commands.""" - self.star_wars = StarWars(self) - self.other = Other(self) - self.lookup_funcs = LookupFuncs(self) - self.games = Games(self) - self.money = Money(self) - - def _add_cogs(self): - """Load cogs.""" - for filename in os.listdir("./gwendolyn/cogs"): - if filename.endswith(".py"): - self.load_extension(f"gwendolyn.cogs.{filename[:-3]}") - - def log(self, messages, channel: str = "", level: int = 20): - """Log a message. Described in utils/util_functions.py.""" - log_this(messages, channel, level) - - 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) - - self.database_funcs.wipe_games() - if not self.options["testing"]: - git_client = git.cmd.Git("") - git_client.pull() - - self.log("Logging out", level=25) - await self.close() - else: - log_message = f"{ctx.author.display_name} tried to stop me!" - self.log(log_message, str(ctx.channel_id)) - await ctx.send(f"I don't think I will, {ctx.author.display_name}") - - async def defer(self, ctx: discord_slash.context.SlashContext): - """Send a "Gwendolyn is thinking" message to the user.""" - try: - await ctx.defer() - except discord_slash.error.AlreadyResponded: - self.log("defer failed") +""" +Contains the Gwendolyn class, a subclass of the discord command bot. + +*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 discord # Used for discord.Intents and discord.Status +import discord_slash # Used to initialized SlashCommands object +import git # Used to pull when stopping + +from discord.ext import commands # Used to inherit from commands.bot +from pymongo import MongoClient # Used for database management +from gwendolyn.funcs import Money, StarWars, Games, Other, LookupFuncs +from gwendolyn.utils import (get_options, get_credentials, log_this, + DatabaseFuncs, EventHandler, ErrorHandler, + long_strings) + + +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) + """ + # pylint: disable=too-many-instance-attributes + + def __init__(self): + """Initialize the bot.""" + intents = discord.Intents.default() + intents.members = True + initiation_parameters = { + "command_prefix": " ", + "case_insensitive": True, + "intents": intents, + "status": discord.Status.dnd + } + super().__init__(**initiation_parameters) + + self._add_clients_and_options() + self._add_util_classes() + self._add_function_containers() + self._add_cogs() + + def _add_clients_and_options(self): + """Add all the client, option and credentials objects.""" + self.long_strings = long_strings() + self.options = get_options() + self.credentials = get_credentials() + finnhub_key = self.credentials["finnhub_key"] + self.finnhub_client = finnhub.Client(api_key=finnhub_key) + mongo_user = self.credentials["mongo_db_user"] + mongo_password = self.credentials["mongo_db_password"] + mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn" + mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority" + database_clint = MongoClient(mongo_url) + + if self.options["testing"]: + self.log("Testing mode") + self.database = database_clint["Gwendolyn-Test"] + else: + self.database = database_clint["Gwendolyn"] + + def _add_util_classes(self): + """Add all the classes used as utility.""" + self.database_funcs = DatabaseFuncs(self) + self.event_handler = EventHandler(self) + self.error_handler = ErrorHandler(self) + slash_parameters = { + "sync_commands": True, + "sync_on_cog_reload": True, + "override_type": True + } + self.slash = discord_slash.SlashCommand(self, **slash_parameters) + + def _add_function_containers(self): + """Add all the function containers used for commands.""" + self.star_wars = StarWars(self) + self.other = Other(self) + self.lookup_funcs = LookupFuncs(self) + self.games = Games(self) + self.money = Money(self) + + def _add_cogs(self): + """Load cogs.""" + for filename in os.listdir("./gwendolyn/cogs"): + if filename.endswith(".py"): + self.load_extension(f"gwendolyn.cogs.{filename[:-3]}") + + def log(self, messages, channel: str = "", level: int = 20): + """Log a message. Described in utils/util_functions.py.""" + log_this(messages, channel, level) + + 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) + + self.database_funcs.wipe_games() + if not self.options["testing"]: + git_client = git.cmd.Git("") + git_client.pull() + + self.log("Logging out", level=25) + await self.close() + else: + log_message = f"{ctx.author.display_name} tried to stop me!" + self.log(log_message, str(ctx.channel_id)) + await ctx.send(f"I don't think I will, {ctx.author.display_name}") + + async def defer(self, ctx: discord_slash.context.SlashContext): + """Send a "Gwendolyn is thinking" message to the user.""" + try: + await ctx.defer() + except discord_slash.error.AlreadyResponded: + self.log("defer failed") diff --git a/gwendolyn/utils/event_handlers.py b/gwendolyn/utils/event_handlers.py index ca8093b..57579d6 100644 --- a/gwendolyn/utils/event_handlers.py +++ b/gwendolyn/utils/event_handlers.py @@ -15,7 +15,7 @@ import discord # Used to init discord.Game and discord.Status, as well from discord.ext import commands # Used to compare errors with command # errors -from discord_slash.context import SlashContext +from discord_slash.context import SlashContext, ComponentContext from gwendolyn.utils.util_functions import emoji_to_command @@ -72,13 +72,6 @@ class EventHandler(): 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 tests.connect_four_reaction_test(*reaction_test_parameters): @@ -86,34 +79,6 @@ class EventHandler(): params = [message, f"#{user.id}", column-1] await self.bot.games.connect_four.place_piece(*params) - if plex_data[0]: - plex_functions = self.bot.other.plex - if plex_data[1]: - movie_pick = emoji_to_command(reaction.emoji) - if movie_pick == "none": - imdb_id = None - else: - imdb_id = plex_data[2][movie_pick-1] - - if isinstance(channel, discord.DMChannel): - await message.delete() - await plex_functions.add_movie(message, imdb_id, False) - else: - await message.clear_reactions() - await plex_functions.add_movie(message, imdb_id) - else: - show_pick = emoji_to_command(reaction.emoji) - if show_pick == "none": - imdb_name = None - else: - imdb_name = plex_data[2][show_pick-1] - - 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") @@ -126,6 +91,25 @@ class EventHandler(): else: self.bot.log("Bot they didn't react with a valid guess") + async def on_component(self, ctx: ComponentContext): + info = ctx.custom_id.split(":") + channel = ctx.origin_message.channel + + if info[0] == "plex": + if info[1] == "movie": + await self.bot.other.plex.add_movie( + ctx.origin_message, + info[2], + not isinstance(channel, discord.DMChannel) + ) + + if info[1] == "show": + await self.bot.other.plex.add_show( + ctx.origin_message, + info[2], + not isinstance(channel, discord.DMChannel) + ) + class ErrorHandler(): """ diff --git a/gwendolyn/utils/helper_classes.py b/gwendolyn/utils/helper_classes.py index 8f9f535..4735987 100644 --- a/gwendolyn/utils/helper_classes.py +++ b/gwendolyn/utils/helper_classes.py @@ -6,7 +6,6 @@ Contains classes used for utilities. DatabaseFuncs() """ 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 re # Used in get_id @@ -206,45 +205,6 @@ class DatabaseFuncs(): 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): """Sync the slash commands with the discord API.""" collection = self.bot.database["last synced"] diff --git a/requirements.txt b/requirements.txt index 38e52fe..5b760a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,6 @@ lxml==4.6.3 more-itertools==8.8.0 multidict==5.1.0 Pillow==8.3.1 -pip==21.2.3 pymongo==3.12.0 requests==2.26.0 setuptools==57.4.0