diff options
Diffstat (limited to 'bot')
-rw-r--r-- | bot/constants.py | 1 | ||||
-rw-r--r-- | bot/exts/easter/april_fools_vids.py | 26 | ||||
-rw-r--r-- | bot/exts/evergreen/issues.py | 289 | ||||
-rw-r--r-- | bot/exts/evergreen/latex.py | 94 | ||||
-rw-r--r-- | bot/resources/easter/april_fools_vids.json | 263 | ||||
-rw-r--r-- | bot/resources/easter/easter_riddle.json | 8 |
6 files changed, 393 insertions, 288 deletions
diff --git a/bot/constants.py b/bot/constants.py index 1d35b3f1..a64882db 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -168,6 +168,7 @@ class Emojis: issue_closed = "<:IssueClosed:629695470570307614>" pull_request = "<:PROpen:629695470175780875>" pull_request_closed = "<:PRClosed:629695470519713818>" + pull_request_draft = "<:PRDraft:829755345425399848>" merge = "<:PRMerged:629695470570176522>" number_emojis = { diff --git a/bot/exts/easter/april_fools_vids.py b/bot/exts/easter/april_fools_vids.py index efe7e677..c7a3c014 100644 --- a/bot/exts/easter/april_fools_vids.py +++ b/bot/exts/easter/april_fools_vids.py @@ -1,36 +1,26 @@ import logging import random from json import load -from pathlib import Path from discord.ext import commands log = logging.getLogger(__name__) +with open("bot/resources/easter/april_fools_vids.json", encoding="utf-8") as f: + ALL_VIDS = load(f) + class AprilFoolVideos(commands.Cog): """A cog for April Fools' that gets a random April Fools' video from Youtube.""" - def __init__(self, bot: commands.Bot): - self.bot = bot - self.yt_vids = self.load_json() - self.youtubers = ['google'] # will add more in future - - @staticmethod - def load_json() -> dict: - """A function to load JSON data.""" - p = Path('bot/resources/easter/april_fools_vids.json') - with p.open(encoding="utf-8") as json_file: - all_vids = load(json_file) - return all_vids - @commands.command(name='fool') async def april_fools(self, ctx: commands.Context) -> None: """Get a random April Fools' video from Youtube.""" - random_youtuber = random.choice(self.youtubers) - category = self.yt_vids[random_youtuber] - random_vid = random.choice(category) - await ctx.send(f"Check out this April Fools' video by {random_youtuber}.\n\n{random_vid['link']}") + video = random.choice(ALL_VIDS) + + channel, url = video["channel"], video["url"] + + await ctx.send(f"Check out this April Fools' video by {channel}.\n\n{url}") def setup(bot: commands.Bot) -> None: diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 4a73d20b..bb6273bb 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -2,10 +2,10 @@ import logging import random import re import typing as t -from enum import Enum +from dataclasses import dataclass import discord -from discord.ext import commands, tasks +from discord.ext import commands from bot.constants import ( Categories, @@ -17,6 +17,8 @@ from bot.constants import ( Tokens, WHITELISTED_CHANNELS ) +from bot.utils.decorators import whitelist_override +from bot.utils.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -24,20 +26,20 @@ BAD_RESPONSE = { 404: "Issue/pull request not located! Please enter a valid number!", 403: "Rate limit has been hit! Please try again later!" } +REQUEST_HEADERS = { + "Accept": "application/vnd.github.v3+json" +} -MAX_REQUESTS = 10 -REQUEST_HEADERS = dict() +REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" +ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" +PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" -REPOS_API = "https://api.github.com/orgs/{org}/repos" if GITHUB_TOKEN := Tokens.github: REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" WHITELISTED_CATEGORIES = ( Categories.development, Categories.devprojects, Categories.media, Categories.staff ) -WHITELISTED_CHANNELS_ON_MESSAGE = ( - Channels.organisation, Channels.mod_meta, Channels.mod_tools, Channels.staff_voice -) CODE_BLOCK_RE = re.compile( r"^`([^`\n]+)`" # Inline codeblock @@ -45,12 +47,42 @@ CODE_BLOCK_RE = re.compile( re.DOTALL | re.MULTILINE ) +# Maximum number of issues in one message +MAXIMUM_ISSUES = 5 + +# Regex used when looking for automatic linking in messages +AUTOMATIC_REGEX = re.compile(r"((?P<org>.+?)\/)?(?P<repo>.+?)#(?P<number>.+?)") + + +@dataclass +class FoundIssue: + """Dataclass representing an issue found by the regex.""" + + organisation: t.Optional[str] + repository: str + number: str + + def __hash__(self) -> int: + return hash((self.organisation, self.repository, self.number)) + -class FetchIssueErrors(Enum): - """Errors returned in fetch issues.""" +@dataclass +class FetchError: + """Dataclass representing an error while fetching an issue.""" - value_error = "Numbers not found." - max_requests = "Max requests hit." + return_code: int + message: str + + +@dataclass +class IssueState: + """Dataclass representing the state of an issue.""" + + repository: str + number: int + url: str + title: str + emoji: str class Issues(commands.Cog): @@ -59,97 +91,96 @@ class Issues(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.repos = [] - self.get_pydis_repos.start() - - @tasks.loop(minutes=30) - async def get_pydis_repos(self) -> None: - """Get all python-discord repositories on github.""" - async with self.bot.http_session.get(REPOS_API.format(org="python-discord")) as resp: - if resp.status == 200: - data = await resp.json() - for repo in data: - self.repos.append(repo["full_name"].split("/")[1]) - self.repo_regex = "|".join(self.repos) - else: - log.debug(f"Failed to get latest Pydis repositories. Status code {resp.status}") @staticmethod - def check_in_block(message: discord.Message, repo_issue: str) -> bool: - """Check whether the <repo>#<issue> is in codeblocks.""" - block = re.findall(CODE_BLOCK_RE, message.content) - - if not block: - return False - elif "#".join(repo_issue.split(" ")) in "".join([*block[0]]): - return True - return False + def remove_codeblocks(message: str) -> str: + """Remove any codeblock in a message.""" + return re.sub(CODE_BLOCK_RE, "", message) async def fetch_issues( self, - numbers: set, + number: int, repository: str, user: str - ) -> t.Union[FetchIssueErrors, str, list]: - """Retrieve issue(s) from a GitHub repository.""" - links = [] - if not numbers: - return FetchIssueErrors.value_error - - if len(numbers) > MAX_REQUESTS: - return FetchIssueErrors.max_requests - - for number in numbers: - url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}" - merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge" - log.trace(f"Querying GH issues API: {url}") - async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: - json_data = await r.json() - - if r.status in BAD_RESPONSE: - log.warning(f"Received response {r.status} from: {url}") - return f"[{str(r.status)}] #{number} {BAD_RESPONSE.get(r.status)}" - - # The initial API request is made to the issues API endpoint, which will return information - # if the issue or PR is present. However, the scope of information returned for PRs differs - # from issues: if the 'issues' key is present in the response then we can pull the data we - # need from the initial API call. - if "issues" in json_data.get("html_url"): - if json_data.get("state") == "open": - icon_url = Emojis.issue - else: - icon_url = Emojis.issue_closed - - # If the 'issues' key is not contained in the API response and there is no error code, then - # we know that a PR has been requested and a call to the pulls API endpoint is necessary - # to get the desired information for the PR. + ) -> t.Union[IssueState, FetchError]: + """ + Retrieve an issue from a GitHub repository. + + Returns IssueState on success, FetchError on failure. + """ + url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) + pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) + log.trace(f"Querying GH issues API: {url}") + + async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: + json_data = await r.json() + + if r.status == 403: + if r.headers.get("X-RateLimit-Remaining") == "0": + log.info(f"Ratelimit reached while fetching {url}") + return FetchError(403, "Ratelimit reached, please retry in a few minutes.") + return FetchError(403, "Cannot access issue.") + elif r.status in (404, 410): + return FetchError(r.status, "Issue not found.") + elif r.status != 200: + return FetchError(r.status, "Error while fetching issue.") + + # The initial API request is made to the issues API endpoint, which will return information + # if the issue or PR is present. However, the scope of information returned for PRs differs + # from issues: if the 'issues' key is present in the response then we can pull the data we + # need from the initial API call. + if "issues" in json_data["html_url"]: + if json_data.get("state") == "open": + emoji = Emojis.issue else: - log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}") - async with self.bot.http_session.get(merge_url) as m: - if json_data.get("state") == "open": - icon_url = Emojis.pull_request - # When the status is 204 this means that the state of the PR is merged - elif m.status == 204: - icon_url = Emojis.merge - else: - icon_url = Emojis.pull_request_closed + emoji = Emojis.issue_closed + + # If the 'issues' key is not contained in the API response and there is no error code, then + # we know that a PR has been requested and a call to the pulls API endpoint is necessary + # to get the desired information for the PR. + else: + log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") + async with self.bot.http_session.get(pulls_url) as p: + pull_data = await p.json() + if pull_data["draft"]: + emoji = Emojis.pull_request_draft + elif pull_data["state"] == "open": + emoji = Emojis.pull_request + # When 'merged_at' is not None, this means that the state of the PR is merged + elif pull_data["merged_at"] is not None: + emoji = Emojis.merge + else: + emoji = Emojis.pull_request_closed - issue_url = json_data.get("html_url") - links.append([icon_url, f"[{repository}] #{number} {json_data.get('title')}", issue_url]) + issue_url = json_data.get("html_url") - return links + return IssueState(repository, number, issue_url, json_data.get('title', ''), emoji) @staticmethod - def get_embed(result: list, user: str = "python-discord", repository: str = "") -> discord.Embed: - """Get Response Embed.""" - description_list = ["{0} [{1}]({2})".format(*link) for link in result] + def format_embed( + results: t.List[t.Union[IssueState, FetchError]], + user: str, + repository: t.Optional[str] = None + ) -> discord.Embed: + """Take a list of IssueState or FetchError and format a Discord embed for them.""" + description_list = [] + + for result in results: + if isinstance(result, IssueState): + description_list.append(f"{result.emoji} [{result.title}]({result.url})") + elif isinstance(result, FetchError): + description_list.append(f":x: [{result.return_code}] {result.message}") + resp = discord.Embed( colour=Colours.bright_green, description='\n'.join(description_list) ) - resp.set_author(name="GitHub", url=f"https://github.com/{user}/{repository}") + embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" + resp.set_author(name="GitHub", url=embed_url) return resp + @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) @commands.command(aliases=("pr",)) async def issue( self, @@ -159,79 +190,79 @@ class Issues(commands.Cog): user: str = "python-discord" ) -> None: """Command to retrieve issue(s) from a GitHub repository.""" - if not ctx.guild or not( - ctx.channel.category.id in WHITELISTED_CATEGORIES - or ctx.channel.id in WHITELISTED_CHANNELS - ): - await ctx.send( - embed=discord.Embed( - title=random.choice(NEGATIVE_REPLIES), - description=( - "You can't run this command in this channel. " - f"Try again in <#{Channels.community_bot_commands}>" - ), - colour=discord.Colour.red() - ) - ) - return - - result = await self.fetch_issues(set(numbers), repository, user) + # Remove duplicates + numbers = set(numbers) - if result == FetchIssueErrors.value_error: - await ctx.invoke(self.bot.get_command('help'), 'issue') - - elif result == FetchIssueErrors.max_requests: + if len(numbers) > MAXIMUM_ISSUES: embed = discord.Embed( title=random.choice(ERROR_REPLIES), color=Colours.soft_red, - description=f"Too many issues/PRs! (maximum of {MAX_REQUESTS})" + description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" ) await ctx.send(embed=embed) + await invoke_help_command(ctx) - elif isinstance(result, list): - # Issue/PR format: emoji to show if open/closed/merged, number and the title as a singular link. - resp = self.get_embed(result, user, repository) - await ctx.send(embed=resp) - - elif isinstance(result, str): - await ctx.send(result) + results = [await self.fetch_issues(number, repository, user) for number in numbers] + await ctx.send(embed=self.format_embed(results, user, repository)) @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: - """Command to retrieve issue(s) from a GitHub repository using automatic linking if matching <repo>#<issue>.""" - # Ignore messages not in whitelisted categories / channels, only when in guild. - if message.guild and not ( - message.channel.category.id in WHITELISTED_CATEGORIES - or message.channel.id in WHITELISTED_CHANNELS_ON_MESSAGE - ): + """ + Automatic issue linking. + + Listener to retrieve issue(s) from a GitHub repository using automatic linking if matching <org>/<repo>#<issue>. + """ + # Ignore bots + if message.author.bot: return - message_repo_issue_map = re.findall(fr"({self.repo_regex})#(\d+)", message.content) + issues = [ + FoundIssue(*match.group("org", "repo", "number")) + for match in AUTOMATIC_REGEX.finditer(self.remove_codeblocks(message.content)) + ] links = [] - if message_repo_issue_map: + if issues: + # Block this from working in DMs if not message.guild: await message.channel.send( embed=discord.Embed( title=random.choice(NEGATIVE_REPLIES), description=( - "You can't retreive issues from DMs. " + "You can't retrieve issues from DMs. " f"Try again in <#{Channels.community_bot_commands}>" ), - colour=discord.Colour.red() + colour=Colours.soft_red ) ) return - for repo_issue in message_repo_issue_map: - if not self.check_in_block(message, " ".join(repo_issue)): - result = await self.fetch_issues({repo_issue[1]}, repo_issue[0], "python-discord") - if isinstance(result, list): - links.extend(result) + + log.trace(f"Found {issues = }") + # Remove duplicates + issues = set(issues) + + if len(issues) > MAXIMUM_ISSUES: + embed = discord.Embed( + title=random.choice(ERROR_REPLIES), + color=Colours.soft_red, + description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" + ) + await message.channel.send(embed=embed, delete_after=5) + return + + for repo_issue in issues: + result = await self.fetch_issues( + int(repo_issue.number), + repo_issue.repository, + repo_issue.organisation or "python-discord" + ) + if isinstance(result, IssueState): + links.append(result) if not links: return - resp = self.get_embed(links, "python-discord") + resp = self.format_embed(links, "python-discord") await message.channel.send(embed=resp) diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py new file mode 100644 index 00000000..c4a8597c --- /dev/null +++ b/bot/exts/evergreen/latex.py @@ -0,0 +1,94 @@ +import asyncio +import hashlib +import pathlib +import re +from concurrent.futures import ThreadPoolExecutor +from io import BytesIO + +import discord +import matplotlib.pyplot as plt +from discord.ext import commands + +# configure fonts and colors for matplotlib +plt.rcParams.update( + { + "font.size": 16, + "mathtext.fontset": "cm", # Computer Modern font set + "mathtext.rm": "serif", + "figure.facecolor": "36393F", # matches Discord's dark mode background color + "text.color": "white", + } +) + +FORMATTED_CODE_REGEX = re.compile( + r"(?P<delim>(?P<block>```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P<lang>[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P<code>.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)", # match the exact same delimiter from the start again + re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive +) + +CACHE_DIRECTORY = pathlib.Path("_latex_cache") +CACHE_DIRECTORY.mkdir(exist_ok=True) + + +class Latex(commands.Cog): + """Renders latex.""" + + @staticmethod + def _render(text: str, filepath: pathlib.Path) -> BytesIO: + """ + Return the rendered image if latex compiles without errors, otherwise raise a BadArgument Exception. + + Saves rendered image to cache. + """ + fig = plt.figure() + rendered_image = BytesIO() + fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") + + try: + plt.savefig(rendered_image, bbox_inches="tight", dpi=600) + except ValueError as e: + raise commands.BadArgument(str(e)) + + rendered_image.seek(0) + + with open(filepath, "wb") as f: + f.write(rendered_image.getbuffer()) + + return rendered_image + + @staticmethod + def _prepare_input(text: str) -> str: + text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ + + if match := FORMATTED_CODE_REGEX.match(text): + return match.group("code") + else: + return text + + @commands.command() + @commands.max_concurrency(1, commands.BucketType.guild, wait=True) + async def latex(self, ctx: commands.Context, *, text: str) -> None: + """Renders the text in latex and sends the image.""" + text = self._prepare_input(text) + query_hash = hashlib.md5(text.encode()).hexdigest() + image_path = CACHE_DIRECTORY.joinpath(f"{query_hash}.png") + async with ctx.typing(): + if image_path.exists(): + await ctx.send(file=discord.File(image_path)) + return + + with ThreadPoolExecutor() as pool: + image = await asyncio.get_running_loop().run_in_executor( + pool, self._render, text, image_path + ) + + await ctx.send(file=discord.File(image, "latex.png")) + + +def setup(bot: commands.Bot) -> None: + """Load the Latex Cog.""" + bot.add_cog(Latex(bot)) diff --git a/bot/resources/easter/april_fools_vids.json b/bot/resources/easter/april_fools_vids.json index b2cbd07b..e1e8c70a 100644 --- a/bot/resources/easter/april_fools_vids.json +++ b/bot/resources/easter/april_fools_vids.json @@ -1,133 +1,130 @@ -{ - "google": [ - { - "title": "Introducing Bad Joke Detector", - "link": "https://youtu.be/OYcv406J_J4" - }, - { - "title": "Introducing Google Cloud Hummus API - Find your Hummus!", - "link": "https://youtu.be/0_5X6N6DHyk" - }, - { - "title": "Introducing Google Play for Pets", - "link": "https://youtu.be/UmJ2NBHXTqo" - }, - { - "title": "Haptic Helpers: bringing you to your senses", - "link": "https://youtu.be/3MA6_21nka8" - }, - { - "title": "Introducing Google Wind", - "link": "https://youtu.be/QAwL0O5nXe0" - }, - { - "title": "Experience YouTube in #SnoopaVision", - "link": "https://youtu.be/DPEJB-FCItk" - }, - { - "title": "Introducing the self-driving bicycle in the Netherlands", - "link": "https://youtu.be/LSZPNwZex9s" - }, - { - "title": "Android Developer Story: The Guardian goes galactic with Android and Google Play", - "link": "https://youtu.be/dFrgNiweQDk" - }, - { - "title": "Introducing new delivery technology from Google Express", - "link": "https://youtu.be/F0F6SnbqUcE" - }, - { - "title": "Google Cardboard Plastic", - "link": "https://youtu.be/VkOuShXpoKc" - }, - { - "title": "Google Photos: Search your photos by emoji", - "link": "https://youtu.be/HQtGFBbwKEk" - }, - { - "title": "Introducing Google Actual Cloud Platform", - "link": "https://youtu.be/Cp10_PygJ4o" - }, - { - "title": "Introducing Dial-Up mode", - "link": "https://youtu.be/XTTtkisylQw" - }, - { - "title": "Smartbox by Inbox: the mailbox of tomorrow, today", - "link": "https://youtu.be/hydLZJXG3Tk" - }, - { - "title": "Introducing Coffee to the Home", - "link": "https://youtu.be/U2JBFlW--UU" - }, - { - "title": "Chrome for Android and iOS: Emojify the Web", - "link": "https://youtu.be/G3NXNnoGr3Y" - }, - { - "title": "Google Maps: Pokémon Challenge", - "link": "https://youtu.be/4YMD6xELI_k" - }, - { - "title": "Introducing Google Fiber to the Pole", - "link": "https://youtu.be/qcgWRpQP6ds" - }, - { - "title": "Introducing Gmail Blue", - "link": "https://youtu.be/Zr4JwPb99qU" - }, - { - "title": "Introducing Google Nose", - "link": "https://youtu.be/VFbYadm_mrw" - }, - { - "title": "Explore Treasure Mode with Google Maps", - "link": "https://youtu.be/_qFFHC0eIUc" - }, - { - "title": "YouTube's ready to select a winner", - "link": "https://youtu.be/H542nLTTbu0" - }, - { - "title": "A word about Gmail Tap", - "link": "https://youtu.be/Je7Xq9tdCJc" - }, - { - "title": "Introducing the Google Fiber Bar", - "link": "https://youtu.be/re0VRK6ouwI" - }, - { - "title": "Introducing Gmail Tap", - "link": "https://youtu.be/1KhZKNZO8mQ" - }, - { - "title": "Chrome Multitask Mode", - "link": "https://youtu.be/UiLSiqyDf4Y" - }, - { - "title": "Google Maps 8-bit for NES", - "link": "https://youtu.be/rznYifPHxDg" - }, - { - "title": "Being a Google Autocompleter", - "link": "https://youtu.be/blB_X38YSxQ" - }, - { - "title": "Introducing Gmail Motion", - "link": "https://youtu.be/Bu927_ul_X0" - }, - { - "title": "Introducing GeForce GTX G-Assist", - "link": "https://youtu.be/smM-Wdk2RLQ" - }, - { - "title": "The Hovering Mouse - Project McFly | Razer", - "link": "https://youtu.be/IlCx5gjAmqI" - }, - { - "title": "Be the Machine | Project Venom v2", - "link": "https://youtu.be/j8UJE7DoyJ8" - } - ] - -} +[ + { + "url": "https://youtu.be/OYcv406J_J4", + "channel": "google" + }, + { + "url": "https://youtu.be/0_5X6N6DHyk", + "channel": "google" + }, + { + "url": "https://youtu.be/UmJ2NBHXTqo", + "channel": "google" + }, + { + "url": "https://youtu.be/3MA6_21nka8", + "channel": "google" + }, + { + "url": "https://youtu.be/QAwL0O5nXe0", + "channel": "google" + }, + { + "url": "https://youtu.be/DPEJB-FCItk", + "channel": "google" + }, + { + "url": "https://youtu.be/LSZPNwZex9s", + "channel": "google" + }, + { + "url": "https://youtu.be/dFrgNiweQDk", + "channel": "google" + }, + { + "url": "https://youtu.be/F0F6SnbqUcE", + "channel": "google" + }, + { + "url": "https://youtu.be/VkOuShXpoKc", + "channel": "google" + }, + { + "url": "https://youtu.be/HQtGFBbwKEk", + "channel": "google" + }, + { + "url": "https://youtu.be/Cp10_PygJ4o", + "channel": "google" + }, + { + "url": "https://youtu.be/XTTtkisylQw", + "channel": "google" + }, + { + "url": "https://youtu.be/hydLZJXG3Tk", + "channel": "google" + }, + { + "url": "https://youtu.be/U2JBFlW--UU", + "channel": "google" + }, + { + "url": "https://youtu.be/G3NXNnoGr3Y", + "channel": "google" + }, + { + "url": "https://youtu.be/4YMD6xELI_k", + "channel": "google" + }, + { + "url": "https://youtu.be/qcgWRpQP6ds", + "channel": "google" + }, + { + "url": "https://youtu.be/Zr4JwPb99qU", + "channel": "google" + }, + { + "url": "https://youtu.be/VFbYadm_mrw", + "channel": "google" + }, + { + "url": "https://youtu.be/_qFFHC0eIUc", + "channel": "google" + }, + { + "url": "https://youtu.be/H542nLTTbu0", + "channel": "google" + }, + { + "url": "https://youtu.be/Je7Xq9tdCJc", + "channel": "google" + }, + { + "url": "https://youtu.be/re0VRK6ouwI", + "channel": "google" + }, + { + "url": "https://youtu.be/1KhZKNZO8mQ", + "channel": "google" + }, + { + "url": "https://youtu.be/UiLSiqyDf4Y", + "channel": "google" + }, + { + "url": "https://youtu.be/rznYifPHxDg", + "channel": "google" + }, + { + "url": "https://youtu.be/blB_X38YSxQ", + "channel": "google" + }, + { + "url": "https://youtu.be/Bu927_ul_X0", + "channel": "google" + }, + { + "url": "https://youtu.be/smM-Wdk2RLQ", + "channel": "nvidia" + }, + { + "url": "https://youtu.be/IlCx5gjAmqI", + "channel": "razer" + }, + { + "url": "https://youtu.be/j8UJE7DoyJ8", + "channel": "razer" + } +] diff --git a/bot/resources/easter/easter_riddle.json b/bot/resources/easter/easter_riddle.json index e93f6dad..f7eb63d8 100644 --- a/bot/resources/easter/easter_riddle.json +++ b/bot/resources/easter/easter_riddle.json @@ -64,14 +64,6 @@ "correct_answer": "A chocolate one" }, { - "question": "Where does the Easter Bunny get his eggs?", - "riddles": [ - "Not a bush or tree", - "Emoji for a body part" - ], - "correct_answer": "Eggplants" - }, - { "question": "Why did the Easter Bunny have to fire the duck?", "riddles": [ "Quack", |