From b1380a9b25ef9963a8ad9ce8cb50a83abee739d9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:59:46 -0700 Subject: [PATCH 1/3] Replaces the google CSE search with Tavily --- config.default.yml | 2 +- techsupport_bot/commands/google.py | 239 ---------------------------- techsupport_bot/commands/search.py | 155 ++++++++++++++++++ techsupport_bot/commands/youtube.py | 111 +++++++++++++ techsupport_bot/core/http.py | 1 + 5 files changed, 268 insertions(+), 240 deletions(-) delete mode 100644 techsupport_bot/commands/google.py create mode 100644 techsupport_bot/commands/search.py create mode 100644 techsupport_bot/commands/youtube.py diff --git a/config.default.yml b/config.default.yml index c40a481e6..16baba1dd 100644 --- a/config.default.yml +++ b/config.default.yml @@ -39,12 +39,12 @@ api: dumpdbg: giphy: google: - google_cse: news: open_weather: openai: spotify_client: spotify_key: + tavily: wolfram: api_url: dumpdbg: diff --git a/techsupport_bot/commands/google.py b/techsupport_bot/commands/google.py deleted file mode 100644 index 259e222b3..000000000 --- a/techsupport_bot/commands/google.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Module for the google extension for the discord bot.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Self - -import munch -import ui -from core import auxiliary, cogs, extensionconfig -from discord.ext import commands - -if TYPE_CHECKING: - import bot - - -async def setup(bot: bot.TechSupportBot) -> None: - """Loading the Google plugin into the bot - - Args: - bot (bot.TechSupportBot): The bot object to register the cogs to - - Raises: - AttributeError: Raised if an API key is missing to prevent unusable commands from loading - """ - # Don't load without the API key - try: - if not bot.file_config.api.api_keys.google: - raise AttributeError("Googler was not loaded due to missing API key") - if not bot.file_config.api.api_keys.google_cse: - raise AttributeError("Googler was not loaded due to missing API key") - except AttributeError as exc: - raise AttributeError("Googler was not loaded due to missing API key") from exc - - config = extensionconfig.ExtensionConfig() - config.add( - key="max_responses", - datatype="int", - title="Max Responses", - description="The max amount of responses per embed page", - default=1, - ) - - await bot.add_cog(Googler(bot=bot)) - bot.add_extension_config("google", config) - - -class Googler(cogs.BaseCog): - """Class for the google extension for the discord bot. - - Attributes: - GOOGLE_URL (str): The API URL for google search - YOUTUBE_URL (str): The API URL for youtube search - ICON_URL (str): The google icon - """ - - GOOGLE_URL: str = "https://www.googleapis.com/customsearch/v1" - YOUTUBE_URL: str = ( - "https://www.googleapis.com/youtube/v3/search?part=id&maxResults=10" - ) - ICON_URL: str = ( - "https://upload.wikimedia.org/wikipedia/commons/thumb/c/" - + "c1/Google_%22G%22_logo.svg/768px-Google_%22G%22_logo.svg.png" - ) - - async def get_items( - self: Self, url: str, data: dict[str, str] - ) -> list[munch.Munch]: - """Calls the google API and retuns only the relevant section from the response - - Args: - url (str): The URL to query, either GOOGLE to YOUTUBE - data (dict[str, str]): The parameters required by the google API - - Returns: - list[munch.Munch]: The formatted list of items, ready to be processed and printed - """ - response = await self.bot.http_functions.http_call( - "get", url, params=data, use_cache=True - ) - return response.get("items") - - @commands.group( - aliases=["g", "G"], - brief="Executes a Google command", - description="Executes a Google command", - ) - async def google(self: Self, ctx: commands.Context) -> None: - """The bare .g/G command. This does nothing but generate the help message - - Args: - ctx (commands.Context): The context in which the command was run in - """ - return - - @auxiliary.with_typing - @commands.guild_only() - @google.command( - aliases=["s", "S"], - brief="Searches Google", - description="Returns the top Google search result", - usage="[query]", - ) - async def search(self: Self, ctx: commands.Context, *, query: str) -> None: - """The entry point for the URL search command - - Args: - ctx (commands.Context): The context in which the command was run in - query (str): The user inputted string to query google for - """ - data = { - "cx": self.bot.file_config.api.api_keys.google_cse, - "q": query, - "key": self.bot.file_config.api.api_keys.google, - } - - items = await self.get_items(self.GOOGLE_URL, data) - - if not items: - await auxiliary.send_deny_embed( - message=f"No search results found for: *{query}*", channel=ctx.channel - ) - return - - config = self.bot.guild_configs[str(ctx.guild.id)] - - embed = None - embeds = [] - if not getattr(ctx, "image_search", None): - field_counter = 1 - for index, item in enumerate(items): - link = item.get("link") - snippet = item.get("snippet", "
").replace("\n", "") - embed = ( - auxiliary.generate_basic_embed( - title=f"Results for {query}", url=self.ICON_URL - ) - if field_counter == 1 - else embed - ) - - embed.add_field(name=link, value=snippet, inline=False) - if ( - field_counter == config.extensions.google.max_responses.value - or index == len(items) - 1 - ): - embeds.append(embed) - field_counter = 1 - else: - field_counter += 1 - - await ui.PaginateView().send(ctx.channel, ctx.author, embeds) - - @auxiliary.with_typing - @commands.guild_only() - @google.command( - aliases=["i", "is", "I", "IS"], - brief="Searches Google Images", - description="Returns the top Google Images search result", - usage="[query]", - ) - async def images(self: Self, ctx: commands.Context, *, query: str) -> None: - """The entry point for the image search command - - Args: - ctx (commands.Context): The context in which the command was run in - query (str): The user inputted string to query google for - """ - data = { - "cx": self.bot.file_config.api.api_keys.google_cse, - "q": query, - "key": self.bot.file_config.api.api_keys.google, - "searchType": "image", - } - items = await self.get_items(self.GOOGLE_URL, data) - - if not items: - await auxiliary.send_deny_embed( - message=f"No image search results found for: *{query}*", - channel=ctx.channel, - ) - return - - embeds = [] - for item in items: - link = item.get("link") - if not link: - await auxiliary.send_deny_embed( - message=( - "I had an issue processing Google's response... try again" - " later!" - ), - channel=ctx.channel, - ) - return - embeds.append(link) - - await ui.PaginateView().send(ctx.channel, ctx.author, embeds) - - @auxiliary.with_typing - @commands.guild_only() - @commands.command( - aliases=["yt", "YT"], - brief="Searches YouTube", - description="Returns the top YouTube search result", - usage="[query]", - ) - async def youtube(self: Self, ctx: commands.Context, *, query: str) -> None: - """The entry point for the youtube search command - - Args: - ctx (commands.Context): The context in which the command was run in - query (str): The user inputted string to query google for - """ - items = await self.get_items( - self.YOUTUBE_URL, - data={ - "q": query, - "key": self.bot.file_config.api.api_keys.google, - "type": "video", - }, - ) - - if not items: - await auxiliary.send_deny_embed( - message=f"No video results found for: *{query}*", channel=ctx.channel - ) - return - - video_id = items[0].get("id", {}).get("videoId") - link = f"http://youtu.be/{video_id}" - - links = [] - for item in items: - video_id = item.get("id", {}).get("videoId") - link = f"http://youtu.be/{video_id}" if video_id else None - if link: - links.append(link) - - await ui.PaginateView().send(ctx.channel, ctx.author, links) diff --git a/techsupport_bot/commands/search.py b/techsupport_bot/commands/search.py new file mode 100644 index 000000000..2e72490a7 --- /dev/null +++ b/techsupport_bot/commands/search.py @@ -0,0 +1,155 @@ +"""Module for the google extension for the discord bot.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Self + +import discord +import munch +import ui +from core import auxiliary, cogs +from discord import app_commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the Search Engine plugin into the bot + + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to + + Raises: + AttributeError: Raised if an API key is missing to prevent unusable commands from loading + """ + # Don't load without the API key + try: + if not bot.file_config.api.api_keys.tavily: + raise AttributeError("WebSearcher was not loaded due to missing API key") + except AttributeError as exc: + raise AttributeError( + "WebSearcher was not loaded due to missing API key" + ) from exc + + await bot.add_cog(WebSearcher(bot=bot)) + +class WebSearcher(cogs.BaseCog): + """Class for the google extension for the discord bot. + + Attributes: + search (app_commands.Group): The group for the /search commands + """ + + + + search: app_commands.Group = app_commands.Group( + name="search", description="Command Group for the Search Extension" + ) + + async def make_reqest(self: Self, query: str) -> munch.Munch: + """This functions make a request to the Tavily API + This pulls the API key from the config and returns a munch.Munch result + + Args: + self (Self): _description_ + query (str): The string query passed in by the user + + Returns: + munch.Munch: The result from the tavily API + """ + api_url: str = "https://api.tavily.com/search" + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.bot.file_config.api.api_keys.tavily}", + } + json_data = { + "query": query, + "search_depth": "advanced", + "include_images": "true", + } + response = await self.bot.http_functions.http_call( + "post", + api_url, + headers=headers, + json=json_data, + ) + + return response + + @search.command( + name="text", + description="Returns the top Web search result", + extras={ + "usage": "[query]", + "module": "search", + }, + ) + async def websearch( + self: Self, interaction: discord.Interaction, query: str + ) -> None: + """This is the command for plaintext searches + + Args: + interaction (discord.Interaction): The interaction that called the command + query (str): The string query passed in by the user + """ + await interaction.response.defer() + response = self.make_request(query) + + embeds = [] + + for result in response.results[:10]: + embed = discord.Embed( + title=f"Search results for {query}", description=result.url + ) + embed.add_field( + name=result.title[:100], + value=result.content[:100], + ) + embed.color = discord.Color.blurple() + embeds.append(embed) + + if not embeds: + embed = auxiliary.prepare_deny_embed(f"No results returned for {query}") + await interaction.followup.send(embed=embed) + + view = ui.PaginateView() + await view.send(interaction.channel, interaction.user, embeds, interaction) + + @search.command( + name="images", + description="Returns the top Web search result", + extras={ + "usage": "[query]", + "module": "search", + }, + ) + async def websearch( + self: Self, interaction: discord.Interaction, query: str + ) -> None: + """This is the command for image searches + + Args: + interaction (discord.Interaction): The interaction that called the command + query (str): The string query passed in by the user + """ + await interaction.response.defer() + response = self.make_request(query) + + embeds = [] + + for result in response.images[:10]: + embed = discord.Embed(title=f"Search results for {query}") + embed.set_image(url=result) + embed.color = discord.Color.blurple() + embeds.append(embed) + + if not embeds: + embed = auxiliary.prepare_deny_embed(f"No results returned for {query}") + await interaction.followup.send(embed=embed) + + view = ui.PaginateView() + await view.send(interaction.channel, interaction.user, embeds, interaction) + diff --git a/techsupport_bot/commands/youtube.py b/techsupport_bot/commands/youtube.py new file mode 100644 index 000000000..a4bd91339 --- /dev/null +++ b/techsupport_bot/commands/youtube.py @@ -0,0 +1,111 @@ +"""Module for the google extension for the discord bot.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Self + +import discord +import munch +import ui +from core import auxiliary, cogs +from discord import app_commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the Youtube plugin into the bot + + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to + + Raises: + AttributeError: Raised if an API key is missing to prevent unusable commands from loading + """ + # Don't load without the API key + try: + if not bot.file_config.api.api_keys.google: + raise AttributeError( + "YoutubeSearcher was not loaded due to missing API key" + ) + except AttributeError as exc: + raise AttributeError( + "YoutubeSearcher was not loaded due to missing API key" + ) from exc + + await bot.add_cog(YoutubeSearcher(bot=bot)) + + +class YoutubeSearcher(cogs.BaseCog): + """Class for the google extension for the discord bot. + + Attributes: + YOUTUBE_URL (str): The API URL for youtube search + """ + + YOUTUBE_URL: str = ( + "https://www.googleapis.com/youtube/v3/search?part=id&maxResults=10" + ) + + async def get_items( + self: Self, url: str, data: dict[str, str] + ) -> list[munch.Munch]: + """Calls the google API and retuns only the relevant section from the response + + Args: + url (str): The URL to query, either GOOGLE to YOUTUBE + data (dict[str, str]): The parameters required by the google API + + Returns: + list[munch.Munch]: The formatted list of items, ready to be processed and printed + """ + response = await self.bot.http_functions.http_call( + "get", url, params=data, use_cache=True + ) + return response.get("items") + + @app_commands.command( + name="youtube", + description="Returns the top YouTube search result", + extras={ + "usage": "[query]", + "module": "youtube", + }, + ) + async def youtube(self: Self, interaction: discord.Interaction, query: str) -> None: + """The entry point for the youtube search command + + Args: + interaction (discord.Interaction): The context in which the command was run in + query (str): The user inputted string to query google for + """ + await interaction.response.defer() + items = await self.get_items( + self.YOUTUBE_URL, + data={ + "q": query, + "key": self.bot.file_config.api.api_keys.google, + "type": "video", + }, + ) + + if not items: + embed = auxiliary.prepare_deny_embed( + f"No video results found for: *{query}*" + ) + await interaction.followup.send(embed=embed) + return + + video_id = items[0].get("id", {}).get("videoId") + link = f"http://youtu.be/{video_id}" + + links = [] + for item in items: + video_id = item.get("id", {}).get("videoId") + link = f"http://youtu.be/{video_id}" if video_id else None + if link: + links.append(link) + + view = ui.PaginateView() + await view.send(interaction.channel, interaction.user, links, interaction) diff --git a/techsupport_bot/core/http.py b/techsupport_bot/core/http.py index ffe747174..e09d28a29 100644 --- a/techsupport_bot/core/http.py +++ b/techsupport_bot/core/http.py @@ -45,6 +45,7 @@ def __init__(self: Self, bot: bot.TechSupportBot) -> None: "api.urbandictionary.com": (2, 60), "api.openai.com": (3, 60), "www.googleapis.com": (5, 60), + "api.tavily.com": (5, 60), "ipinfo.io": (1, 30), "api.open-notify.org": (1, 60), "geocode.xyz": (1, 60), From 6df5d2ad429e9f45ed8d921e127dfaef8a7bd987 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:02:54 -0700 Subject: [PATCH 2/3] Formatting 1 --- techsupport_bot/commands/search.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/techsupport_bot/commands/search.py b/techsupport_bot/commands/search.py index 2e72490a7..c50c2136a 100644 --- a/techsupport_bot/commands/search.py +++ b/techsupport_bot/commands/search.py @@ -34,6 +34,7 @@ async def setup(bot: bot.TechSupportBot) -> None: await bot.add_cog(WebSearcher(bot=bot)) + class WebSearcher(cogs.BaseCog): """Class for the google extension for the discord bot. @@ -41,8 +42,6 @@ class WebSearcher(cogs.BaseCog): search (app_commands.Group): The group for the /search commands """ - - search: app_commands.Group = app_commands.Group( name="search", description="Command Group for the Search Extension" ) @@ -86,7 +85,7 @@ async def make_reqest(self: Self, query: str) -> munch.Munch: "module": "search", }, ) - async def websearch( + async def websearch_text( self: Self, interaction: discord.Interaction, query: str ) -> None: """This is the command for plaintext searches @@ -110,7 +109,7 @@ async def websearch( ) embed.color = discord.Color.blurple() embeds.append(embed) - + if not embeds: embed = auxiliary.prepare_deny_embed(f"No results returned for {query}") await interaction.followup.send(embed=embed) @@ -120,13 +119,13 @@ async def websearch( @search.command( name="images", - description="Returns the top Web search result", + description="Returns the top Web search image result", extras={ "usage": "[query]", "module": "search", }, ) - async def websearch( + async def websearch_image( self: Self, interaction: discord.Interaction, query: str ) -> None: """This is the command for image searches @@ -145,11 +144,10 @@ async def websearch( embed.set_image(url=result) embed.color = discord.Color.blurple() embeds.append(embed) - + if not embeds: embed = auxiliary.prepare_deny_embed(f"No results returned for {query}") await interaction.followup.send(embed=embed) view = ui.PaginateView() await view.send(interaction.channel, interaction.user, embeds, interaction) - From 39cde28632fae1a590f1ba0588531f17a9530f2e Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:04:18 -0700 Subject: [PATCH 3/3] Formatting 2 --- techsupport_bot/commands/search.py | 1 - 1 file changed, 1 deletion(-) diff --git a/techsupport_bot/commands/search.py b/techsupport_bot/commands/search.py index c50c2136a..622110914 100644 --- a/techsupport_bot/commands/search.py +++ b/techsupport_bot/commands/search.py @@ -51,7 +51,6 @@ async def make_reqest(self: Self, query: str) -> munch.Munch: This pulls the API key from the config and returns a munch.Munch result Args: - self (Self): _description_ query (str): The string query passed in by the user Returns: