From 2a83364494b480843b8ef8c7dfb667f78eb3403f Mon Sep 17 00:00:00 2001 From: Matthew Flegg Date: Fri, 29 Apr 2022 15:39:17 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=8C=9F=20bug=20fixes,=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/commands/misc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ext/commands/misc.py b/ext/commands/misc.py index 90bd2cb..5ec2039 100644 --- a/ext/commands/misc.py +++ b/ext/commands/misc.py @@ -63,14 +63,15 @@ async def meme(self, interaction: discord.Interaction): """ response = await self.client.session.get("https://meme-api.herokuapp.com/gimme") data = await response.json() + url = data['url'] meme_embed = discord.Embed( title="🎲 Found a Meme", - description=f"**`{data['title']}`**", + description=f"**[{data['title']}]({url})**", timestamp=datetime.datetime.utcnow(), color=self.client.theme, ) \ - .set_image(url=f"{data['url']}") \ + .set_image(url=f"{url}") \ .set_footer(text="❓ Try again? Use /meme.") await interaction.response.send_message(embed=meme_embed) @@ -87,7 +88,7 @@ async def poll(self, interaction: discord.Interaction, *, question: str): timestamp=datetime.datetime.utcnow(), color=self.client.theme, ) \ - .set_author(name=interaction.user.name, url=interaction.user.avatar.url) \ + .set_author(name=interaction.user.name, icon_url=interaction.user.avatar.url) \ .set_footer(text="Vote ✔️ Yes or ❌ No.") message = await interaction.channel.send(embed=poll_embed) From 13ed997722e79963ddbd12d180d8c8a2e20c6b6b Mon Sep 17 00:00:00 2001 From: Matthew Flegg Date: Fri, 29 Apr 2022 16:05:27 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=8C=9F=20added=20+=20improved=20/choo?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/commands/info.py | 10 +++++----- ext/commands/misc.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/ext/commands/info.py b/ext/commands/info.py index 0a5eea7..108cded 100644 --- a/ext/commands/info.py +++ b/ext/commands/info.py @@ -41,7 +41,7 @@ async def joined(self, interaction: discord.Interaction, *, member: Optional[dis .add_field(name="📅 Date", value=f"{date}.") \ .set_author(icon_url=member.avatar.url or None, name=member.name) - await interaction.response.send_message(embed=joined_embed, ephemeral=True) + await interaction.response.send_message(embed=joined_embed) @app_commands.command() @app_commands.describe(member="❓ The member to view the top role of.") @@ -61,7 +61,7 @@ async def toprole(self, interaction: discord.Interaction, *, member: Optional[di .add_field(name="🏷️ Role", value=f"*@{member.top_role.name}*") \ .set_author(icon_url=member.avatar.url or None, name=member.name) - await interaction.response.send_message(embed=top_role_embed, ephemeral=True) + await interaction.response.send_message(embed=top_role_embed) @app_commands.command() @app_commands.describe(member="❓ The member to view the permissions of.") @@ -84,7 +84,7 @@ async def perms(self, interaction: discord.Interaction, *, member: Optional[disc value="\u200b".join(f"`{perm}` " for perm, value in member.guild_permissions if value) or "..." ) - await interaction.response.send_message(embed=perms_embed, ephemeral=True) + await interaction.response.send_message(embed=perms_embed) @app_commands.command() @app_commands.describe(member="❓ The member to view the avatar of.") @@ -103,7 +103,7 @@ async def avatar(self, interaction: discord.Interaction, *, member: Optional[dis ) \ .set_image(url=member.avatar.url or None) - await interaction.response.send_message(embed=avatar_embed, ephemeral=True) + await interaction.response.send_message(embed=avatar_embed) @app_commands.command() async def servericon(self, interaction: discord.Interaction) -> None: @@ -118,7 +118,7 @@ async def servericon(self, interaction: discord.Interaction) -> None: ) \ .set_image(url=interaction.guild.icon.url) - await interaction.response.send_message(embed=server_icon_embed, ephemeral=True) + await interaction.response.send_message(embed=server_icon_embed) async def setup(client: core.DiscordClient) -> None: diff --git a/ext/commands/misc.py b/ext/commands/misc.py index 5ec2039..48f3bc4 100644 --- a/ext/commands/misc.py +++ b/ext/commands/misc.py @@ -3,6 +3,7 @@ information commands for BB.Bot. """ import datetime +import random import discord import humanize import twitchio @@ -31,7 +32,7 @@ async def twitch(self, interaction: discord.Interaction, *, broadcaster: str) -> if not streams: error_embed = utils.create_error_embed(f"The streamer **`{broadcaster}`** is not currently live.") - return await interaction.response.send_message(embed=error_embed) + return await interaction.response.send_message(embed=error_embed, ephemeral=True) stream: twitchio.Stream = streams[0] current_time = datetime.datetime.utcnow() @@ -55,6 +56,30 @@ async def twitch(self, interaction: discord.Interaction, *, broadcaster: str) -> .add_field(name="❓ Category", value=stream.game_name, inline=False) await interaction.response.send_message(embed=stream_embed) + + @app_commands.command() + @app_commands.describe(choices="❓ The choices to choose from, separated by commas.") + async def choose(self, interaction: discord.Interaction, *, choices: str): + """ + 🎲 Chooses a random option from a list of choices. + """ + choices = [x.strip() for x in choices.split(",")] + + if len(choices) <= 1: + error_embed = utils.create_error_embed("You need to give me at least 2 choices.") + return await interaction.response.send_message(embed=error_embed, ephemeral=True) + + numbered_choices = [f"**`{i + 1}`** — {x}" for i, x in enumerate(choices)] + + choice_embed = discord.Embed( + title="🎲 My Choice", + description=f"**`{random.choice(choices)}`**", + timestamp=datetime.datetime.utcnow(), + color=self.client.theme, + ) \ + .add_field(name="❗ Options Given", value="\n".join(numbered_choices)) + + await interaction.response.send_message(embed=choice_embed) @app_commands.command() async def meme(self, interaction: discord.Interaction): @@ -69,7 +94,7 @@ async def meme(self, interaction: discord.Interaction): title="🎲 Found a Meme", description=f"**[{data['title']}]({url})**", timestamp=datetime.datetime.utcnow(), - color=self.client.theme, + color=self.client.theme, ) \ .set_image(url=f"{url}") \ .set_footer(text="❓ Try again? Use /meme.") From dce35f042f5da52842c1a8e1f46aa4e3fe188a3d Mon Sep 17 00:00:00 2001 From: Matthew Flegg Date: Fri, 29 Apr 2022 16:20:43 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=8C=9F=20added=20/echo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/commands/misc.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/ext/commands/misc.py b/ext/commands/misc.py index 48f3bc4..ab4305d 100644 --- a/ext/commands/misc.py +++ b/ext/commands/misc.py @@ -107,6 +107,10 @@ async def poll(self, interaction: discord.Interaction, *, question: str): """ 🎲 Creates a simple yes or no poll for users to vote on. """ + if message is None: + error_embed = utils.create_error_embed("You need to ask a question.") + return await interaction.response.send_message(embed=error_embed) + poll_embed = discord.Embed( title="🎲 Poll", description=f"**`{question}`**", @@ -119,6 +123,26 @@ async def poll(self, interaction: discord.Interaction, *, question: str): message = await interaction.channel.send(embed=poll_embed) await message.add_reaction("✔️") await message.add_reaction("❌") + + @app_commands.command() + @app_commands.describe(message="❓ The phrase you want the bot to repeat.") + async def echo(self, interaction: discord.Interaction, *, message: str): + """ + 🎲 Repeats what you say. + """ + if message is None: + error_embed = utils.create_error_embed("You need to tell me what to say.") + return await interaction.response.send_message(embed=error_embed) + + echo_embed = discord.Embed( + title=f"🎲 Message", + description=f"**`{message}`**", + timestamp=datetime.datetime.utcnow(), + color=self.client.theme, + ) \ + .set_author(name=interaction.user.name, icon_url=interaction.user.avatar.url) \ + + await interaction.response.send_message(embed=echo_embed) From c4c33e215d3c8bef90f86ff75d88fa7a1f6761c4 Mon Sep 17 00:00:00 2001 From: Matthew Flegg Date: Fri, 29 Apr 2022 18:39:48 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=8C=9F=20added=20/youtube?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/commands/misc.py | 54 +++++++++++++++++++++++++++++++- ext/utils/functions.py | 63 +++++++++++++++++++++++++++++++++++-- ext/views/__init__.py | 4 +++ ext/views/base_view.py | 65 +++++++++++++++++++++++++++++++++++++++ ext/views/youtube_view.py | 57 ++++++++++++++++++++++++++++++++++ 5 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 ext/views/__init__.py create mode 100644 ext/views/base_view.py create mode 100644 ext/views/youtube_view.py diff --git a/ext/commands/misc.py b/ext/commands/misc.py index ab4305d..7b060e1 100644 --- a/ext/commands/misc.py +++ b/ext/commands/misc.py @@ -13,6 +13,8 @@ from discord import app_commands from discord.ext import commands +from ext.views.youtube_view import YoutubeView + class Misc(commands.Cog, name="Miscellaneous"): """ @@ -140,10 +142,60 @@ async def echo(self, interaction: discord.Interaction, *, message: str): timestamp=datetime.datetime.utcnow(), color=self.client.theme, ) \ - .set_author(name=interaction.user.name, icon_url=interaction.user.avatar.url) \ + .set_author(name=interaction.user.name, icon_url=interaction.user.avatar.url) await interaction.response.send_message(embed=echo_embed) + + @app_commands.command() + async def ping(self, interaction: discord.Interaction): + """ + 🎲 Shows the bot's current websocket latency. + """ + await interaction.response.defer() + + embed = discord.Embed( + title="🏓 Pong!", + description=f"⌛ Your ping is **{round(self.client.latency * 1000)}**ms.", + timestamp=datetime.datetime.utcnow(), + color=self.client.theme, + ) + + await interaction.followup.send(embed=embed) + + @app_commands.command() + @app_commands.describe(search="❓ The YouTube video to search for.") + async def youtube(self, interaction: discord.Interaction, *, search: str): + """ + 🎲 Searches for a video on youtube and sends the link. + """ + if search is None: + error_embed = utils.create_error_embed("You need to specify a search term.") + return await interaction.response.send_message(embed=error_embed) + + url = await utils.youtube_search_to_url(search) + title = await utils.youtube_url_to_title(url) + thumbnail = await utils.youtube_url_to_thumbnail(url) + + embed = discord.Embed( + title="🎲 Found a Video", + description=f"🔗 [{title}]({url})", + timestamp=datetime.datetime.utcnow(), + color=self.client.theme, + ) \ + .set_thumbnail(url=thumbnail) \ + .set_footer(text=f"❓ Follow the link above to view the video.") + + await interaction.response.send_message(embed=embed) + + embed = discord.Embed( + title="🎲 View in Discord?", + description="❓ Click View In Discord to view the video in this text channel.", + timestamp=datetime.datetime.utcnow(), + color=self.client.theme, + ) + view = YoutubeView(url, interaction) + view.message = await interaction.followup.send(embed=embed, view=view) async def setup(client: core.DiscordClient) -> None: diff --git a/ext/utils/functions.py b/ext/utils/functions.py index 6faa2fb..280f193 100644 --- a/ext/utils/functions.py +++ b/ext/utils/functions.py @@ -2,8 +2,18 @@ Module `functions` contains standalone utility functions to be used in BB.Bot extensions. """ +import functools +from typing import Any, Callable, List import discord import datetime +import requests +import re + +from bs4 import BeautifulSoup +from urllib import ( + parse, + request +) def create_error_embed(message: str) -> discord.Embed: @@ -21,5 +31,54 @@ def create_error_embed(message: str) -> discord.Embed: return discord.Embed( title=f"❌ An Error Occurred", description=f"🏷️ {message}", - timestamp=datetime.datetime.utcnow() - ) \ No newline at end of file + timestamp=datetime.datetime.utcnow(), + color=discord.Color.red(), + ) + + +async def youtube_search_to_url(search_query: str) -> str: + """ + Returns the first result in a Youtube search for a given + query. + + Params: + - search_query (str): The search terms to use. + + Returns: + - The URL of the first result found. + """ + query = parse.urlencode({"search_query": search_query}) + content = request.urlopen("http://www.youtube.com/results?" + query) + results = re.findall(r"watch\?v=(\S{11})", content.read().decode()) + return "https://www.youtube.com/watch?v=" + results[0] + + +async def youtube_url_to_title(url: str) -> str: + """ + Returns the title of a Youtube video, given a video's URL. + + Params: + - url (str): The URL of the Youtube video. + + Returns: + - The title of the Youtube video. + """ + reqs = requests.get(url) + soup = BeautifulSoup(reqs.text, "html.parser") + + return "".join([t for t in soup.find("title")]) + + +async def youtube_url_to_thumbnail(url: str) -> str: + """ + Returns the thumbnail of a Youtube video, given a video's URL. + + Params: + - url (str): The URL of the Youtube video. + + Returns: + - A URL for the thumbnail of the Youtube video. + """ + exp = r"^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*" + s = re.findall(exp, url)[0][-1] + return f"https://i.ytimg.com/vi/{s}/maxresdefault.jpg" \ No newline at end of file diff --git a/ext/views/__init__.py b/ext/views/__init__.py new file mode 100644 index 0000000..b65ad86 --- /dev/null +++ b/ext/views/__init__.py @@ -0,0 +1,4 @@ +""" +Module `views` contains user interface components used +in BB.Bot's extensions (commands or otherwise). +""" diff --git a/ext/views/base_view.py b/ext/views/base_view.py new file mode 100644 index 0000000..967613e --- /dev/null +++ b/ext/views/base_view.py @@ -0,0 +1,65 @@ +""" +Module `base_view` contains the `BaseView` class, which +provides common functionality. +""" +import discord + +from ext import utils +from typing import Any + +from discord import ( + ui, + app_commands +) + + +class BaseView(ui.View): + """ + Class `BaseView` is a subclass of `discord.ui.View` that + implements `interaction_check` and `disable_all_buttons`. + """ + def __init__(self, interaction: discord.Interaction, **kwargs: Any) -> None: + """ + Creates an instance of `discord.ui.View` that implements + an `interaction_check` and a `disable_all_buttons` method. + + Params: + - interaction (discord.Interaction): The interaction the command was invoked with. + - message (discord.Message): Pass the message the view is sent in. + - **kwargs (Any): Any keyword arguments that `discord.ui.View` accepts. + """ + super().__init__() + self.command_author_id = interaction.user.id + self.message: discord.Message = None + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + """ + Prevents users who weren't the command sender from interacting + with the view component. + + Params: + - interaction (discord.Interaction): The interaction used with the view. + + Returns: + - A `bool`. If false, the view will not execute any callbacks. + """ + if interaction.user.id == self.command_author_id: + return True + + error_embed = utils.create_error_embed("This isn't your interaction.") + await interaction.response.send_message(embed=error_embed, ephemeral=True) + return False + + async def disable_all_buttons(self) -> None: + """ + Disables all buttons that are attached to the view component. This + is usually done when a view should be closed, to prevent errors. + + Params: + - interaction (discord.Interaction): The interaction used with the view. + """ + for child in self.children: + child.disabled = True + + await self.message.edit(view=self) + self.stop() \ No newline at end of file diff --git a/ext/views/youtube_view.py b/ext/views/youtube_view.py new file mode 100644 index 0000000..398a006 --- /dev/null +++ b/ext/views/youtube_view.py @@ -0,0 +1,57 @@ +""" +Module `youtube_view` contains class `YoutubeView`, which provides +yes and no buttons for viewing a Youtube video in discord when prompted. +""" +import discord + +from discord import ui +from typing import Any +from .base_view import BaseView + + +class YoutubeView(BaseView): + """ + Class `YoutubeView` provides a set of yes or no buttons when + a user is asked if they want to view a video in discord. + """ + def __init__( + self, + youtube_url: str, + interaction: discord.Interaction, + **kwargs: Any + ) -> None: + """ + Creates a `YoutubeView` UI component that allows users to + view a video in discord, if they choose yes. + + Params: + - interaction (discord.Interaction): The interaction the command was invoked with. + - message (discord.Message): Pass the message the view is sent in. + - **kwargs (Any): Any keyword arguments that `discord.ui.View` accepts. + """ + super().__init__(interaction, **kwargs) + self.youtube_url = youtube_url + + @ui.button(label="View In Discord", style=discord.ButtonStyle.green, emoji="👍🏻") + async def view_in_discord( + self, interaction: discord.Interaction, _: discord.Button + ) -> None: + """ + Sends a viewable video embed to a user. + + Params: + - interaction (discord.Interaction): The button interaction. + """ + await interaction.response.send_message(f"**[Here's your link!]({self.youtube_url})**") + await self.disable_all_buttons() + + @ui.button(label="Close", style=discord.ButtonStyle.red, emoji="👎🏻") + async def close(self, interaction: discord.Interaction, _: discord.Button) -> None: + """ + Deletes the view. This is done when the view is no longer + needed, i.e, the user clicks close. + + Params: + - interaction (discord.Interaction): The button interaction. + """ + await self.message.delete() \ No newline at end of file