diff --git a/cogs/search.py b/cogs/search.py index a55c1eb..da6e426 100644 --- a/cogs/search.py +++ b/cogs/search.py @@ -1,106 +1,107 @@ +from inspect import currentframe import discord from discord.ext import commands -from utils import jisho +from utils import jisho, interactive -class JishoObject(): - def __init__(self, query, owner): - self.response = jisho.JishoResponse(query) - self.total_pages = self.response.entries - self.page = 0 - self.owner = owner +class SearchEmbed(interactive.InteractiveEmbed): + REACTIONS = { + "prev_page": "⬅️", + "next_page": "➡️" + } - def prev(self): - self.page -= 1 - if self.page < 0: - self.page = self.total_pages - 1 + def __init__(self, parent, ctx, response): + super(SearchEmbed, self).__init__(parent.bot, ctx, 60.0) + self.parent = parent + self.owner = self.ctx.author + self.response = response - def next(self): - self.page += 1 - if self.page >= self.total_pages: - self.page = 0 + self.current_page = 0 + + def prev_page(self): + self.current_page -= 1 + if self.current_page < 0: + self.current_page = self.response.size - 1 + + def next_page(self): + self.current_page += 1 + if self.current_page >= self.response.size: + self.current_page = 0 + + async def on_reaction(self, reaction, user): + if reaction.emoji == SearchEmbed.REACTIONS["prev_page"]: + self.prev_page() + await reaction.remove(user) + + if reaction.emoji == SearchEmbed.REACTIONS["next_page"]: + self.next_page() + await reaction.remove(user) + + async def add_navigation(self, message): + await message.add_reaction(SearchEmbed.REACTIONS["prev_page"]) + await message.add_reaction(SearchEmbed.REACTIONS["next_page"]) + + def make_embed(self): + node = self.response.nodes[self.current_page] + embed = discord.Embed( + title = node.japanese[0][0], + url = f"https://jisho.org/word/{node.slug}", + description = node.japanese[0][1], + colour = 0x56d926 + ) + + i = 1 + for sense in node.senses: + embed.add_field(name=f"{i}. {sense.fenglish_definitions}", value=sense.fparts_of_speech, inline=False) + i += 1 + + if len(node.japanese) > 1: + other = "" + for word, reading in node.japanese[1:]: + other += word + if reading != "": + other += f"【{reading}】" + other += "\n" + embed.add_field(name="Other forms", value=other) + + + embed.set_footer( + text = f"{node.ftags} \t\t {self.current_page + 1}/{self.response.size}" + ) + + return embed + + async def on_close(self): + self.parent.activeObjects.pop(self.ctx.channel.id) class Search(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.activeObject = None - self.latestMessage = 0 - - async def createEmbed(self): - if self.activeObject.total_pages == 0: - embed = discord.Embed( - title = "No search results", - description = "The search returned nothing. Did you make a typo?", - colour = 0x56d926 - ) - else: - response = self.activeObject.response - node = response.nodes[self.activeObject.page] - embed = discord.Embed( - title = node.japanese[0][0], - url = f"https://jisho.org/word/{node.slug}", - description = node.japanese[0][1], - colour = 0x56d926 - ) - - i = 1 - for sense in node.senses: - embed.add_field(name=f"{i}. {sense.fenglish_definitions}", value=sense.fparts_of_speech, inline=False) - i += 1 - - if len(node.japanese) > 1: - other = "" - for word, reading in node.japanese[1:]: - other += word - if reading != "": - other += f"【{reading}】" - other += "\n" - embed.add_field(name="Other forms", value=other) - - - embed.set_footer( - text = f"{node.ftags} \t\t {self.activeObject.page + 1}/{self.activeObject.total_pages}" - ) - - return embed - - @commands.Cog.listener() - async def on_reaction_add(self, reaction, user): - message = reaction.message - if message.id != self.latestMessage: - return - - if user == self.bot.user: - return - - if user.id != self.activeObject.owner: - return - - if reaction.me: - if reaction.emoji == "⬅️": - self.activeObject.prev() - await reaction.remove(user) - - if reaction.emoji == "➡️": - self.activeObject.next() - await reaction.remove(user) - - embed = await self.createEmbed() - await message.edit(embed=embed) - + self.activeObjects = {} @commands.command(name="search", description="Searches Jisho", usage="", aliases=["s"]) @commands.cooldown(1, 5) async def search(self, ctx: commands.Context, *, query: str = None): if query is None: + embed = discord.Embed( + title = "No search results", + description = "The search returned nothing. Did you make a typo?", + colour = 0x56d926 + ) + await ctx.send(embed=embed) return - self.activeObject = JishoObject(query, ctx.author.id) - embed = await self.createEmbed() - message = await ctx.send(embed=embed) - self.latestMessage = message.id - if self.activeObject.total_pages > 0: - await message.add_reaction("⬅️") - await message.add_reaction("➡️") + result = jisho.JishoResponse(query) + if result.size == 0: + embed = discord.Embed( + title = "No search results", + description = "The search returned nothing. Did you make a typo?", + colour = 0x56d926 + ) + await ctx.send(embed=embed) + return + + self.activeObjects[ctx.channel.id] = SearchEmbed(self, ctx, result) + await self.activeObjects[ctx.channel.id].show_embed() @search.error async def search_error(self, ctx, error): diff --git a/utils/interactive.py b/utils/interactive.py new file mode 100644 index 0000000..b8c8e75 --- /dev/null +++ b/utils/interactive.py @@ -0,0 +1,93 @@ +import discord +from discord.ext import commands +from abc import ABC, abstractmethod +import asyncio + +class InteractiveEmbed(ABC): + """ + This abstract base class can be used to create interactive embeds + """ + def __init__(self, bot, ctx, timeout): + """ + Sets up the embed with needed parameters + bot: The bot hosting this embed + ctx: The context that caused this embed + timeuout: The time until the embed times out (in seconds) + """ + self.bot = bot + self.ctx = ctx + self.message = None + self.timeout = timeout + + @abstractmethod + async def on_reaction(self, reaction, user): + """ + Gets called when the user interacted with the embed + """ + pass + + @abstractmethod + def make_embed(self): + """ + Creates and returns a new embed + """ + pass + + @abstractmethod + async def add_navigation(self, message): + """ + Adds the navigational emotes to the embed + """ + pass + + async def on_close(self): + """ + Can be overridden. Gets called when the embed is closed + """ + pass + + def additional_checks(self, reaction, user): + """ + Can be overridden. Additional checks to do before calling on_reaction() + """ + return True + + async def show_embed(self): + """ + Displays an embed / Creates an embed + """ + if self.message is None: + self.message = await self.ctx.send(embed=self.make_embed()) + await self.add_navigation(self.message) + + else: + await self.message.edit(embed=self.make_embed()) + + def check(reaction, user): + if self.message is None: + return False + + if user.id != self.ctx.author.id: + return False + + if reaction.message.id != self.message.id: + return False + + if not reaction.me: + return False + + return self.additional_checks(reaction, user) + + try: + reaction, user = await self.bot.wait_for("reaction_add", check=check, timeout=self.timeout) + await self.on_reaction(reaction, user) + await self.show_embed() + except asyncio.TimeoutError: + await self.close_embed() + + async def close_embed(self): + """ + Close this embed + """ + await self.message.clear_reactions() + await self.on_close() \ No newline at end of file diff --git a/utils/jisho.py b/utils/jisho.py index ccf2ec6..b72a777 100644 --- a/utils/jisho.py +++ b/utils/jisho.py @@ -64,6 +64,8 @@ class JishoResponse(): self.nodes = [] self.disassemble() + self.size = len(self.nodes) + def query(self): url = TEMPLATE_URL.format(urllib.parse.quote_plus(self.query_string)) r = requests.get(url, headers=HEADER)