From a2f8dfb0368d3921fde29a453b147eb2e77c241d Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Thu, 9 Nov 2023 17:23:54 +0530 Subject: [PATCH 01/15] Updating Contribution guidelines --- CONTRIBUTING.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 17 +++++++++++------ 2 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4bb1e29c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ + + +# Contributing + +## Setup Discord application +Create a new Discord application [here](https://discord.com/developers/applications) by clicking the `New application` button and name it whatever you want. + +![New application](https://cdn.discordapp.com/attachments/721750194797936823/794646477505822730/unknown.png) + +Go to the Bot section on the right-hand side and click on Add Bot. + +![Add Bot](https://cdn.discordapp.com/attachments/852867509765799956/853984486970359838/unknown.png) + +Copy the bot token (to be used in .env file when setting up project) + +![Token](https://cdn.discordapp.com/attachments/852867509765799956/853985222127124500/unknown.png) + + +To Invite the bot to your server go to Oauth2 select bot then select administrator and go to the link +![Invite Bot](https://cdn.discordapp.com/attachments/852867509765799956/853985694183850004/unknown.png) + + +## Project Setup & Installation + +1. Fork the repository to your own profile. +2. Setup postgresql DB +```postgresql://username:password@localhost/db_name``` +Replace username, password, db_name with appropriate values +3. Run migrations +```python cli.py migrate up``` +4. To install packages run:- + + ```pip install poetry``` + ```poetry install``` +5. Create a .env file and copy the contents of example.env, setup the env vars. + +6. Once above steps are done, run the bot using command + ```python cli.py``` +7. Feel free to join the server in case of any issues. + +## Guidelines + +Please keep the following guidelines in mind when contributing: + +- Follow the coding style and conventions used in the project. +- Write clear and concise commit messages. +- Test your changes thoroughly before submitting a pull request. +- Be respectful and constructive in all interactions with other contributors. diff --git a/README.md b/README.md index f3ed3df1..d6145d85 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ -# Discord-Bot -#### Discord bot for Tech With Tim's discord server. +
-## Contributing -* Fork the repository to your own profile, edit the code there. -* When you are done with a feature open a pull request. -* If you have any questions contact us in our discord server. +[![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](/LICENSE) +[![Discord](https://discord.com/api/guilds/501090983539245061/widget.png?style=shield)](https://discord.gg/E7bnXS2hpn) +
+ +# Discord-Bot +#### Discord bot for Tech With Tim's discord server. [![Invitation link](https://discord.com/api/guilds/501090983539245061/widget.png?style=banner3)](https://discord.gg/twt) + +# Contributing + +If you're looking to contribute , take a look at the [Contributing](README.md). If you're looking for things to do, check out our [issues](https://github.com/SylteA/Discord-Bot/issues). From 50f78eca64b406863c6e4d30be004d2fba30ea94 Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Thu, 9 Nov 2023 20:26:57 +0530 Subject: [PATCH 02/15] updating contribution --- CONTRIBUTING.md | 70 +++++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4bb1e29c..b172c7e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,48 +1,66 @@ +--- # Contributing -## Setup Discord application -Create a new Discord application [here](https://discord.com/developers/applications) by clicking the `New application` button and name it whatever you want. +## Setup Discord Application -![New application](https://cdn.discordapp.com/attachments/721750194797936823/794646477505822730/unknown.png) +Refer to the official discord.py [documentation](https://discordpy.readthedocs.io/en/stable/discord.html) to create a bot. -Go to the Bot section on the right-hand side and click on Add Bot. +## Project Setup & Installation -![Add Bot](https://cdn.discordapp.com/attachments/852867509765799956/853984486970359838/unknown.png) +1. Fork the repository to your own profile. +2. Set up a local PostgreSQL database or use [Docker](#Docker-Setup). -Copy the bot token (to be used in .env file when setting up project) + ```postgresql://username:password@localhost/db_name``` -![Token](https://cdn.discordapp.com/attachments/852867509765799956/853985222127124500/unknown.png) + Replace username, password, db_name with appropriate values. +3. Run migrations. -To Invite the bot to your server go to Oauth2 select bot then select administrator and go to the link -![Invite Bot](https://cdn.discordapp.com/attachments/852867509765799956/853985694183850004/unknown.png) + ```python cli.py migrate up``` +4. To install packages, run: -## Project Setup & Installation + ```bash + pip install poetry + poetry install + ``` + +5. Create a .env file and copy the contents of example.env, setting up the environment variables. + +6. Once the above steps are done, run the bot using the command: + + ```bash + python cli.py + ``` -1. Fork the repository to your own profile. -2. Setup postgresql DB -```postgresql://username:password@localhost/db_name``` -Replace username, password, db_name with appropriate values -3. Run migrations -```python cli.py migrate up``` -4. To install packages run:- - - ```pip install poetry``` - ```poetry install``` -5. Create a .env file and copy the contents of example.env, setup the env vars. - -6. Once above steps are done, run the bot using command - ```python cli.py``` 7. Feel free to join the server in case of any issues. -## Guidelines +## Docker-Setup -Please keep the following guidelines in mind when contributing: +1. Ensure you have Docker installed and set up on your system. Refer to the [Docker's official guide](https://docs.docker.com/get-started/overview/) if needed. +2. To run a PostgreSQL instance, execute: + + ```bash + docker compose up -d postgres + ``` + The `-d` flag runs the instance in detached mode. + +3. Run migrations. + + ```python cli.py migrate up``` +# Guidelines + +Please adhere to the following guidelines when contributing: - Follow the coding style and conventions used in the project. - Write clear and concise commit messages. - Test your changes thoroughly before submitting a pull request. - Be respectful and constructive in all interactions with other contributors. + +## Code Quality and Testing + +- Ensure your code follows best practices and is well-documented. +- Write unit tests for new features and ensure existing tests pass. +- Perform code reviews and address feedback from other contributors. From 96b5ab814bf857c2fcc94cae54388891d93ebe6d Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Thu, 9 Nov 2023 20:32:25 +0530 Subject: [PATCH 03/15] updating contribution --- CONTRIBUTING.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b172c7e1..ba639b8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,3 @@ - ---- - # Contributing ## Setup Discord Application From 0fd60c331fe93d3d6021e563dfc329a496d17e7d Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Sat, 11 Nov 2023 21:41:48 +0530 Subject: [PATCH 04/15] updating contribution --- CONTRIBUTING.md | 9 ++++----- README.md | 10 +--------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba639b8c..ccb432f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,16 +13,15 @@ Refer to the official discord.py [documentation](https://discordpy.readthedocs.i Replace username, password, db_name with appropriate values. -3. Run migrations. - - ```python cli.py migrate up``` - -4. To install packages, run: +3. To install packages, run: ```bash pip install poetry poetry install ``` +4. Once all the packages are installed, run migrations using: + + ```python cli.py migrate up``` 5. Create a .env file and copy the contents of example.env, setting up the environment variables. diff --git a/README.md b/README.md index d6145d85..23d34a8c 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,7 @@ -
- - -[![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](/LICENSE) -[![Discord](https://discord.com/api/guilds/501090983539245061/widget.png?style=shield)](https://discord.gg/E7bnXS2hpn) - -
- # Discord-Bot #### Discord bot for Tech With Tim's discord server. [![Invitation link](https://discord.com/api/guilds/501090983539245061/widget.png?style=banner3)](https://discord.gg/twt) # Contributing -If you're looking to contribute , take a look at the [Contributing](README.md). If you're looking for things to do, check out our [issues](https://github.com/SylteA/Discord-Bot/issues). +If you're looking to contribute , take a look at the [Contributing](CONTRIBUTING.md). If you're looking for things to do, check out our [issues](https://github.com/SylteA/Discord-Bot/issues). From 1be7ab6519e94c372592328f010b8b572d13e560 Mon Sep 17 00:00:00 2001 From: sarzz <45039724+sarzz2@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:04:35 +0530 Subject: [PATCH 05/15] Update CONTRIBUTING.md --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccb432f1..2fa05424 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,14 @@ Refer to the official discord.py [documentation](https://discordpy.readthedocs.io/en/stable/discord.html) to create a bot. +Also, please make sure that the scope application.commands is enabled. + +Make sure to enable intents as well—check the official guide for detailed instructions [here](https://discordpy.readthedocs.io/en/latest/intents.html). + +Screenshot 2023-11-14 at 12 51 00 PM + + + ## Project Setup & Installation 1. Fork the repository to your own profile. From 9f5b0a1acdc31e0585d6f0695a45c4bf90b8c71a Mon Sep 17 00:00:00 2001 From: sarzz <45039724+sarzz2@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:05:51 +0530 Subject: [PATCH 06/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23d34a8c..463d779c 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ # Contributing -If you're looking to contribute , take a look at the [Contributing](CONTRIBUTING.md). If you're looking for things to do, check out our [issues](https://github.com/SylteA/Discord-Bot/issues). +If you're interested in contributing, please review the [Contributing](CONTRIBUTING.md) section. For specific tasks and opportunities, explore our list of open [issues](https://github.com/SylteA/Discord-Bot/issues). From 3b19748d5d5c6e306d2dce3d2f9e8147237230b6 Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Tue, 14 Nov 2023 13:08:06 +0530 Subject: [PATCH 07/15] fixing lint --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fa05424..51dcdfa5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Refer to the official discord.py [documentation](https://discordpy.readthedocs.io/en/stable/discord.html) to create a bot. -Also, please make sure that the scope application.commands is enabled. +Also, please make sure that the scope application.commands is enabled. Make sure to enable intents as well—check the official guide for detailed instructions [here](https://discordpy.readthedocs.io/en/latest/intents.html). From eadf4ab9cfcd01dd5109403438edf29628c435a5 Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Tue, 14 Nov 2023 21:03:15 +0530 Subject: [PATCH 08/15] updating error webhook --- bot/core.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bot/core.py b/bot/core.py index f3a14134..66a89ae7 100644 --- a/bot/core.py +++ b/bot/core.py @@ -1,4 +1,5 @@ import datetime +import json import logging import os import traceback @@ -113,8 +114,12 @@ async def on_app_command_error(self, interaction: "InteractionType", error: app_ async def publish_error(self, interaction: "InteractionType", error: app_commands.AppCommandError) -> None: """Publishes the error to our error webhook.""" - content = "\n".join(traceback.format_exception(type(error), error, error.__traceback__)) - header = f"Ignored exception in command **{interaction.command.qualified_name}**" + content = "".join(traceback.format_exception(type(error), error, error.__traceback__)) + header = ( + f"Ignored exception in command **{interaction.command.qualified_name}** Invoked by **{interaction.user}**" + f"in channel **{interaction.channel.name}**" + ) + invoked_details_document = await paste.create(str(json.dumps(interaction.data, indent=4))) def wrap(code: str) -> str: code = code.replace("`", "\u200b`") @@ -129,6 +134,7 @@ def wrap(code: str) -> str: embed = discord.Embed( title=header, description=content, color=discord.Color.red(), timestamp=discord.utils.utcnow() ) + embed.add_field(name="Command Details: ", value=invoked_details_document.url, inline=True) await self.error_webhook.send(embed=embed) @tasks.loop(hours=24) From 84eece24d602555bf814d3e96e9de39f785923d7 Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Tue, 14 Nov 2023 21:25:00 +0530 Subject: [PATCH 09/15] updating json indent to 2 --- bot/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/core.py b/bot/core.py index 66a89ae7..da362fac 100644 --- a/bot/core.py +++ b/bot/core.py @@ -119,7 +119,7 @@ async def publish_error(self, interaction: "InteractionType", error: app_command f"Ignored exception in command **{interaction.command.qualified_name}** Invoked by **{interaction.user}**" f"in channel **{interaction.channel.name}**" ) - invoked_details_document = await paste.create(str(json.dumps(interaction.data, indent=4))) + invoked_details_document = await paste.create(str(json.dumps(interaction.data, indent=2))) def wrap(code: str) -> str: code = code.replace("`", "\u200b`") From e7297b70dfba996e0e3689745c97cccca2082862 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 14 Nov 2023 20:54:39 +0200 Subject: [PATCH 10/15] fixed cropping on shorts --- bot/extensions/youtube/tasks.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/bot/extensions/youtube/tasks.py b/bot/extensions/youtube/tasks.py index 978bfd2b..1f5cb766 100644 --- a/bot/extensions/youtube/tasks.py +++ b/bot/extensions/youtube/tasks.py @@ -1,13 +1,9 @@ -import asyncio import re import xml.etree.ElementTree as ET from datetime import datetime -from io import BytesIO import discord -import requests from discord.ext import commands, tasks -from PIL import Image from pydantic import BaseModel from bot import core @@ -40,26 +36,7 @@ def cog_unload(self) -> None: def channel(self) -> discord.TextChannel | None: return self.bot.get_channel(settings.notification.channel_id) - def crop_borders(self, url): - response = requests.get(url) - if response.status_code != 200: - raise Exception("Failed to download the image.") - - img = Image.open(BytesIO(response.content)) - width, height = img.size - - img_cropped = img.crop((0, 45, width, height - 45)) - - buf = BytesIO() - img_cropped.save(buf, format="PNG") - buf.seek(0) - return buf - async def send_notification(self, video: Video) -> None: - loop = asyncio.get_event_loop() - result = await loop.run_in_executor(None, self.crop_borders, video.thumbnail) - file = discord.File(fp=result, filename="thumbnail.png") - embed = discord.Embed( title=video.title, description=video.description.split("\n\n")[0], @@ -67,7 +44,7 @@ async def send_notification(self, video: Video) -> None: color=discord.Color.red(), timestamp=datetime.strptime(video.published, "%Y-%m-%dT%H:%M:%S%z"), ) - embed.set_image(url="attachment://thumbnail.png") + embed.set_image(url=video.thumbnail.replace("/hqdefault.jpg", "/mqdefault.jpg")) embed.set_author( name="Tech With Tim", url="https://www.youtube.com/c/TechWithTim", @@ -77,7 +54,6 @@ async def send_notification(self, video: Video) -> None: await self.channel.send( content=f"Hey <@&{settings.notification.role_id}>, **Tim** just posted a video! Go check it out!", - file=file, embed=embed, allowed_mentions=discord.AllowedMentions(roles=True), ) From 89c8cd8af8541dad8da0d51c5b82230fb9480f2e Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:20:40 +0200 Subject: [PATCH 11/15] Added checks for higher res thumbnails --- bot/extensions/youtube/tasks.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/bot/extensions/youtube/tasks.py b/bot/extensions/youtube/tasks.py index 1f5cb766..d2fd87cd 100644 --- a/bot/extensions/youtube/tasks.py +++ b/bot/extensions/youtube/tasks.py @@ -27,6 +27,7 @@ class YoutubeTasks(commands.Cog): def __init__(self, bot: core.DiscordBot): self.bot = bot self.video_links: list[str] = [] + self.update_thumbnail = [] self.check_for_new_videos.start() def cog_unload(self) -> None: @@ -37,6 +38,15 @@ def channel(self) -> discord.TextChannel | None: return self.bot.get_channel(settings.notification.channel_id) async def send_notification(self, video: Video) -> None: + update_thumbnail = False + max_res = video.thumbnail.replace("/hqdefault.jpg", "/maxresdefault.jpg") + async with http.session.get(max_res) as response: + if response.status == 200: + video.thumbnail = max_res + else: + update_thumbnail = True + video.thumbnail = video.thumbnail.replace("/hqdefault.jpg", "/mqdefault.jpg") + embed = discord.Embed( title=video.title, description=video.description.split("\n\n")[0], @@ -44,7 +54,7 @@ async def send_notification(self, video: Video) -> None: color=discord.Color.red(), timestamp=datetime.strptime(video.published, "%Y-%m-%dT%H:%M:%S%z"), ) - embed.set_image(url=video.thumbnail.replace("/hqdefault.jpg", "/mqdefault.jpg")) + embed.set_image(url=video.thumbnail) embed.set_author( name="Tech With Tim", url="https://www.youtube.com/c/TechWithTim", @@ -52,16 +62,30 @@ async def send_notification(self, video: Video) -> None: ) embed.set_footer(text="Uploaded", icon_url=self.bot.user.display_avatar.url) - await self.channel.send( + message = await self.channel.send( content=f"Hey <@&{settings.notification.role_id}>, **Tim** just posted a video! Go check it out!", embed=embed, allowed_mentions=discord.AllowedMentions(roles=True), ) + if update_thumbnail: + self.update_thumbnail.append((message.id, video.thumbnail)) + @tasks.loop(minutes=2) async def check_for_new_videos(self): """Check for new videos""" + for message_id, thumbnail_url in self.update_thumbnail: + async with http.session.get(thumbnail_url.replace("/mqdefault.jpg", "/maxresdefault.jpg")) as response: + if response.status == 404: + continue + + message = await self.channel.fetch_message(message_id) + embed = message.embeds[0] + embed.set_image(url=thumbnail_url) + await message.edit(embed=embed) + self.update_thumbnail.remove((message_id, thumbnail_url)) + url = "https://www.youtube.com/feeds/videos.xml?channel_id=UC4JX40jDee_tINbkjycV4Sg" async with http.session.get(url) as response: data = await response.text() From 77bc6f6a2d33801b262d786ed8aebe5b6bc76a63 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:41:15 +0200 Subject: [PATCH 12/15] code separation, head requests, check on startup --- bot/extensions/youtube/tasks.py | 38 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/bot/extensions/youtube/tasks.py b/bot/extensions/youtube/tasks.py index d2fd87cd..2750a697 100644 --- a/bot/extensions/youtube/tasks.py +++ b/bot/extensions/youtube/tasks.py @@ -40,7 +40,7 @@ def channel(self) -> discord.TextChannel | None: async def send_notification(self, video: Video) -> None: update_thumbnail = False max_res = video.thumbnail.replace("/hqdefault.jpg", "/maxresdefault.jpg") - async with http.session.get(max_res) as response: + async with http.session.head(max_res) as response: if response.status == 200: video.thumbnail = max_res else: @@ -71,12 +71,9 @@ async def send_notification(self, video: Video) -> None: if update_thumbnail: self.update_thumbnail.append((message.id, video.thumbnail)) - @tasks.loop(minutes=2) - async def check_for_new_videos(self): - """Check for new videos""" - + async def check_old_thumbnails(self): for message_id, thumbnail_url in self.update_thumbnail: - async with http.session.get(thumbnail_url.replace("/mqdefault.jpg", "/maxresdefault.jpg")) as response: + async with http.session.head(thumbnail_url.replace("/mqdefault.jpg", "/maxresdefault.jpg")) as response: if response.status == 404: continue @@ -86,6 +83,12 @@ async def check_for_new_videos(self): await message.edit(embed=embed) self.update_thumbnail.remove((message_id, thumbnail_url)) + @tasks.loop(minutes=2) + async def check_for_new_videos(self): + """Check for new videos""" + + await self.check_old_thumbnails() + url = "https://www.youtube.com/feeds/videos.xml?channel_id=UC4JX40jDee_tINbkjycV4Sg" async with http.session.get(url) as response: data = await response.text() @@ -111,13 +114,16 @@ async def check_for_new_videos(self): @check_for_new_videos.before_loop async def before_check(self): - if not self.video_links: - async for message in self.channel.history(limit=10): - if message.embeds: - self.video_links.append(message.embeds[0].url) - else: - match = YOUTUBE_URL.search(message.content) - if match: - self.video_links.append(match.group("url")) - - self.video_links.reverse() + if self.video_links: + return + + async for message in self.channel.history(limit=10): + if message.embeds: + embed = message.embeds[0] + self.video_links.append(embed.url) + if embed.image.url.endswith("/mqdefault.jpg"): + self.update_thumbnail.append((message.id, embed.image.url)) + else: + match = YOUTUBE_URL.search(message.content) + if match: + self.video_links.append(match.group("url")) From f16107d45099b507242906ff601607e0a77430a9 Mon Sep 17 00:00:00 2001 From: Sylte Date: Tue, 14 Nov 2023 21:26:30 +0100 Subject: [PATCH 13/15] Remove old tags.py file --- bot/cogs/tags.py | 534 ----------------------------------------------- 1 file changed, 534 deletions(-) delete mode 100644 bot/cogs/tags.py diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py deleted file mode 100644 index df9f8b0f..00000000 --- a/bot/cogs/tags.py +++ /dev/null @@ -1,534 +0,0 @@ -import asyncio -from datetime import datetime -from typing import TYPE_CHECKING, List, Literal - -import discord -from discord.ext import commands - -from bot.config import settings -from bot.models import Model, Tag -from utils.checks import is_admin, is_engineer_check, is_staff - -if TYPE_CHECKING: - from bot import Tim - -EMOJIS = [ - "\N{WHITE HEAVY CHECK MARK}", - "\N{CROSS MARK}", -] - - -class TagCommands(commands.Cog, name="Tags"): - def __init__(self, bot: "Tim"): - self.bot = bot - - @property - def log_channel(self): - return self.bot.get_channel(settings.tags.log_channel_id) - - def cog_check(self, ctx): - if ctx.guild is None: - return False - - return True - - @staticmethod - def log_embeds( - rtype: Literal["Create", "Delete", "Update", "Rename"], - tname: str, - before: str, - after: str, - author_id: int, - approve: bool = None, - approver: discord.Member = None, - ) -> List[discord.Embed]: - if approve is None: - color = discord.Color.blurple() - else: - color = discord.Color.green() if approve and rtype != "Delete" else discord.Color.red() - - fel = {None: " Request", False: " Denied", True: "d"} - embeds = [discord.Embed(title=f"Tag {rtype}{fel[approve]}", color=color)] - - if rtype in ("Create", "Delete"): - embeds[0].description = f"```Content```\n{after}" - elif rtype == "Update": - embeds[0].description = f"```Before```\n{before}" - embeds.append(discord.Embed(description=f"```After```\n{after}", color=color)) - else: # rtype = "Rename" - embeds[0].add_field(name="Before", value=before).add_field(name="After", value=after) - - embeds[-1].timestamp = datetime.utcnow() - - if rtype != "Rename": - embeds[-1].add_field(name="Tag's name", value=tname) - - embeds[-1].add_field( - name="Author", - value=f"<@{author_id}> ({author_id})", - inline=rtype != "Rename", - ) - - if approve is not None: - if rtype == "Delete": - action = "Deleted" - else: - action = "Approved" if approve else "Denied" - embeds[-1].set_footer(text=f"{action} by: {approver}") - - return embeds - - @staticmethod - async def notify(user, text): - try: - return await user.send(text) - except discord.Forbidden: - pass - - async def request(self, **kwargs): - embeds = self.log_embeds(**kwargs) - log = await self.log_channel.send(embeds=embeds) - - for emoji in EMOJIS: - await log.add_reaction(emoji) - - @commands.group(invoke_without_command=True) - async def tag(self, ctx, *, name: commands.clean_content): - """Main tag group.""" - name = name.lower() - tag = await Tag.fetch_tag(guild_id=ctx.guild.id, name=name) - - if tag is None: - await ctx.message.delete(delay=10.0) - message = await ctx.send("Could not find a tag with that name.") - return await message.delete(delay=10.0) - - await ctx.send(tag.text) - await Model.execute( - "UPDATE tags SET uses = uses + 1 WHERE guild_id = $1 AND name = $2", - ctx.guild.id, - name, - ) - - #################################################################################################################### - # Commands - #################################################################################################################### - - @tag.command() - async def info(self, ctx, *, name: commands.clean_content): - """Get information regarding the specified tag.""" - name = name.lower() - tag = await Tag.fetch_tag(guild_id=ctx.guild.id, name=name) - - if tag is None: - await ctx.message.delete(delay=10.0) - message = await ctx.send("Could not find a tag with that name.") - return await message.delete(delay=10.0) - - author = self.bot.get_user(tag.creator_id) - author = str(author) if isinstance(author, discord.User) else f"(ID: {tag.creator_id})" - text = f"Tag: {name}\n\n```prolog\nCreator: {author}\n Uses: {tag.uses}\n```" - await ctx.send(text) - - @tag.command() - @is_engineer_check() - async def create(self, ctx, name: commands.clean_content, *, text: commands.clean_content): - """Create a new tag.""" - name = name.lower() - - if len(name) > 32: - return await ctx.send("Tag name must be less than 32 characters.") - - if len(text) > 2000: - return await ctx.send("Tag text must be less than 2000 characters.") - - tag = await Tag.fetch_tag(guild_id=ctx.guild.id, name=name) - if tag is not None: - return await ctx.send("A tag with that name already exists.") - - kwargs = dict(rtype="Create", tname=name, before="", after=text, author_id=ctx.author.id) - - if is_staff(ctx.author): - tag = Tag( - bot=self.bot, - guild_id=ctx.guild.id, - creator_id=ctx.author.id, - name=name, - text=text, - ) - await tag.post() - await self.log_channel.send(embeds=self.log_embeds(**kwargs, approve=True, approver=ctx.author)) - - return await ctx.send("You have successfully created your tag.") - - await self.request(**kwargs) - return await ctx.reply("Tag creation request submitted.") - - @tag.command() - @is_engineer_check() - async def list(self, ctx, member: commands.MemberConverter = None): - """List your existing tags.""" - member = member or ctx.author - query = """SELECT name FROM tags WHERE guild_id = $1 AND creator_id = $2 ORDER BY name""" - records = await Model.fetch(query, ctx.guild.id, member.id) - if not records: - return await ctx.send("No tags found.") - - await ctx.send( - f"**{len(records)} tags by {'you' if member == ctx.author else str(member)} found on this server.**" - ) - - pager = commands.Paginator() - - for record in records: - pager.add_line(line=record["name"]) - - for page in pager.pages: - await ctx.send(page) - - @tag.command() - @commands.cooldown(1, 3600 * 24, commands.BucketType.user) - async def all(self, ctx: commands.Context): - """List all existing tags alphabetically ordered and sends them in DMs.""" - records = await Model.fetch("""SELECT name FROM tags WHERE guild_id = $1 ORDER BY name""", ctx.guild.id) - - if not records: - return await ctx.send("This server doesn't have any tags.") - - try: - await ctx.author.send(f"***{len(records)} tags found on this server.***") - except discord.Forbidden: - ctx.command.reset_cooldown(ctx) - return await ctx.send("Could not dm you...", delete_after=10) - - async def send_tags(): - pager = commands.Paginator() - - for record in records: - pager.add_line(line=record["name"]) - - for page in pager.pages: - await asyncio.sleep(1) - await ctx.author.send(page) - - asyncio.create_task(send_tags()) - - await ctx.send("Tags are being sent in DMs.") - - @tag.command() - @is_engineer_check() - async def edit(self, ctx, name: commands.clean_content, *, text: commands.clean_content): - """Edit a tag""" - name = name.lower() - - if len(text) > 2000: - return await ctx.send("Tag text must be less than 2000 characters.") - - tag = await Tag.fetch_tag(guild_id=ctx.guild.id, name=name) - - if tag is None: - await ctx.message.delete(delay=10.0) - message = await ctx.send("Could not find a tag with that name.") - return await message.delete(delay=10.0) - - if tag.creator_id != ctx.author.id: - if not is_admin(ctx.author): - return await ctx.send("You don't have permission to do that.") - - kwargs = dict( - rtype="Update", - tname=name, - before=tag.text, - after=text, - author_id=tag.creator_id, - ) - if is_staff(ctx.author): - await tag.update(text=text) - await self.log_channel.send(embeds=self.log_embeds(**kwargs, approve=True, approver=ctx.author)) - return await ctx.send("You have successfully edited your tag.") - - await self.request(**kwargs) - return await ctx.reply("Tag update request submitted.") - - @tag.command() - @is_engineer_check() - async def delete(self, ctx, *, name: commands.clean_content): - """Delete a tag.""" - name = name.lower() - tag = await Tag.fetch_tag(guild_id=ctx.guild.id, name=name) - - if tag is None: - await ctx.message.delete(delay=10.0) - message = await ctx.send("Could not find a tag with that name.") - return await message.delete(delay=10.0) - - if tag.creator_id != ctx.author.id: - if not is_staff(ctx.author): - return await ctx.send("You don't have permission to do that.") - - await tag.delete() - await ctx.send("You have successfully deleted your tag.") - - await self.log_channel.send( - embeds=self.log_embeds( - approve=True, - rtype="Delete", - tname=name, - before="", - after=tag.text, - author_id=tag.creator_id, - approver=ctx.author, - ) - ) - - @tag.command() - @commands.cooldown(1, 1, commands.BucketType.user) - async def search(self, ctx, *, term: str): - """Search for a tag given a search term. PostgreSQL syntax must be used for the search.""" - query = """SELECT name FROM tags WHERE guild_id = $1 AND name LIKE $2 LIMIT 10""" - records = await Model.fetch(query, ctx.guild.id, term) - - if not records: - return await ctx.send("No tags found that has the term in it's name", delete_after=10) - count = "Maximum of 10" if len(records) == 10 else len(records) - records = "\n".join([record["name"] for record in records]) - - await ctx.send(f"**{count} tags found with search term on this server.**```\n{records}\n```") - - @tag.command() - @is_engineer_check() - async def rename(self, ctx, name: commands.clean_content, *, new_name: commands.clean_content): - """Rename a tag.""" - name = name.lower() - new_name = new_name.lower() - - if len(new_name) > 32: - return await ctx.send("Tag name must be less than 32 characters.") - - tag = await Tag.fetch_tag(guild_id=ctx.guild.id, name=name) - - if tag is None: - await ctx.message.delete(delay=10.0) - message = await ctx.send("Could not find a tag with that name.") - return await message.delete(delay=10.0) - - if tag.creator_id != ctx.author.id: - if not is_admin(ctx.author): - return await ctx.send("You don't have permission to do that.") - - if await Tag.fetch_tag(guild_id=ctx.guild.id, name=new_name): - return await ctx.send("A tag with that name already exists.") - - kwargs = dict( - rtype="Rename", - tname="", - before=name, - after=new_name, - author_id=tag.creator_id, - ) - if is_staff(ctx.author): - await tag.rename(new_name=new_name) - - await self.log_channel.send(embeds=self.log_embeds(**kwargs, approve=True, approver=ctx.author)) - return await ctx.send("You have successfully renamed your tag.") - - await self.request(**kwargs) - return await ctx.reply("Tag update request submitted.") - - @tag.command() - @is_engineer_check() - async def append(self, ctx, name: commands.clean_content, *, text: commands.clean_content): - """Append some content to the end of a tag""" - name = name.lower() - - tag = await Tag.fetch_tag(guild_id=ctx.guild.id, name=name) - - if tag is None: - await ctx.message.delete(delay=10.0) - message = await ctx.send("Could not find a tag with that name.") - return await message.delete(delay=10.0) - - if tag.creator_id != ctx.author.id: - if not is_admin(ctx.author): - return await ctx.send("You don't have permission to do that.") - - new_text = tag.text + " " + text - - if len(new_text) > 2000: - return await ctx.send("Cannot append, content length will exceed discords maximum message length.") - - kwargs = dict( - rtype="Update", - tname=name, - before=tag.text, - after=new_text, - author_id=tag.creator_id, - ) - if is_staff(ctx.author): - await tag.update(text=new_text) - await self.log_channel.send(embeds=self.log_embeds(**kwargs, approve=True, approver=ctx.author)) - return await ctx.send("You have successfully appended to your tag content.") - - await self.request(**kwargs) - return await ctx.reply("Tag update request submitted.") - - @commands.Cog.listener() - async def on_raw_reaction_add(self, event: discord.RawReactionActionEvent): - if event.channel_id != settings.tags.log_channel_id: - return - - if event.member.bot: - return - - message = await self.bot.get_channel(event.channel_id).fetch_message(event.message_id) - - if not message.embeds: - return - - if str(event.emoji) not in EMOJIS: - return - - approved = str(event.emoji) == "\N{WHITE HEAVY CHECK MARK}" - - if message.embeds[0].title == "Tag Create Request": - await message.clear_reactions() - return self.bot.dispatch( - "tag_create_response", - message, - approved, - user=event.member, - ) - elif message.embeds[0].title == "Tag Update Request": - await message.clear_reactions() - return self.bot.dispatch( - "tag_update_response", - message, - approved, - user=event.member, - ) - elif message.embeds[0].title == "Tag Rename Request": - await message.clear_reactions() - return self.bot.dispatch( - "tag_rename_response", - message, - approved, - user=event.member, - ) - - #################################################################################################################### - # Listeners - #################################################################################################################### - - @commands.Cog.listener() - async def on_tag_rename_response(self, message: discord.Message, approved, user): - embed = message.embeds[0] - before, after = embed.fields[0].value, embed.fields[1].value - creator_id = int(embed.fields[-1].value.split("(")[-1][:-1]) - author = await self.bot.resolve_user(creator_id) - if approved: - tag = await Tag.fetch_tag(guild_id=message.guild.id, name=before) - - if tag is None: - # embed.title = "Tag Rename Failed" - # embed.colour = discord.Color.red() - # return message.edit(embed=embed) - return await message.delete() - - await tag.rename(new_name=after) - - await message.edit( - embeds=self.log_embeds( - rtype="Rename", - tname="", - before=before, - after=after, - author_id=author.id, - approver=user, - approve=approved, - ), - ) - await self.notify( - author, - f"Tag `{before}` renaming to `{after}` request has been {['deni', 'approv'][approved]}ed.", - ) - - @commands.Cog.listener() - async def on_tag_create_response(self, message: discord.Message, approved, user): - embed = message.embeds[0] - name, text = embed.fields[0].value, embed.description.split("\n", 1)[-1] - creator_id = int(embed.fields[-1].value.split("(")[-1][:-1]) - author = await self.bot.resolve_user(creator_id) - - if approved: - tag = Tag( - bot=self.bot, - guild_id=message.guild.id, - creator_id=creator_id, - name=name, - text=text, - ) - if await Tag.fetch_tag(guild_id=message.guild.id, name=name): - # embed.title = "Tag Create Failed" - # embed.colour = discord.Color.red() - # return await message.edit(embed=embed) - return await message.delete() - - await tag.post() - - await message.edit( - embeds=self.log_embeds( - rtype="Create", - tname=name, - before="", - after=text, - author_id=author.id, - approver=user, - approve=approved, - ), - ) - await self.notify( - author, - f"Tag `{name}` creating request has been {['deni', 'approv'][approved]}ed.", - ) - - @commands.Cog.listener() - async def on_tag_update_response(self, message: discord.Message, approved, user): - embeds = message.embeds - name = embeds[1].fields[0].value - before, after = ( - embeds[0].description.split("\n", 1)[-1], - embeds[1].description.split("\n", 1)[-1], - ) - creator_id = int(embeds[1].fields[-1].value.split("(")[-1][:-1]) - author = await self.bot.resolve_user(creator_id) - - if approved: - tag = await Tag.fetch_tag(guild_id=message.guild.id, name=name) - - if tag is None: - # embeds[0].title = "Tag Update Failed" - # embeds[0].colour = embeds[1].colour = discord.Color.red() - # return await message.edit(embeds=embeds) - return await message.delete() - - await tag.update(text=after) - - await message.edit( - embeds=self.log_embeds( - rtype="Update", - tname=name, - before=before, - after=after, - author_id=author.id, - approver=user, - approve=approved, - ), - ) - await self.notify( - author, - f"Tag `{name}` updating request has been {['deni', 'approv'][approved]}ed.", - ) - - -async def setup(bot): - await bot.add_cog(TagCommands(bot=bot)) From 8d0302e843d74040b73a8a6366450a485d3466d0 Mon Sep 17 00:00:00 2001 From: Sylte Date: Tue, 14 Nov 2023 22:03:13 +0100 Subject: [PATCH 14/15] Updated code --- bot/config.py | 14 ++-- bot/extensions/youtube/tasks.py | 125 +++++++++++++++++--------------- 2 files changed, 76 insertions(+), 63 deletions(-) diff --git a/bot/config.py b/bot/config.py index 989652cb..260dc16b 100644 --- a/bot/config.py +++ b/bot/config.py @@ -56,11 +56,6 @@ def val_func(cls, v): return json.loads(v) -class Notification(BaseModel): - channel_id: int - role_id: int - - class Postgres(BaseModel): max_pool_connections: int min_pool_connections: int @@ -100,6 +95,13 @@ class CustomRoles(BaseModel): divider_role_id: int +class YouTube(BaseModel): + channel_id: str + + text_channel_id: int + role_id: int + + class Settings(BaseSettings): aoc: AoC bot: Bot @@ -108,13 +110,13 @@ class Settings(BaseSettings): postgres: Postgres guild: Guild moderation: Moderation - notification: Notification # For tim's YouTube channel (currently unused) reaction_roles: ReactionRoles tags: Tags timathon: Timathon hastebin: Hastebin errors: ErrorHandling custom_roles: CustomRoles + youtube: YouTube class Config: env_file = ".env" diff --git a/bot/extensions/youtube/tasks.py b/bot/extensions/youtube/tasks.py index 2750a697..99f99a9a 100644 --- a/bot/extensions/youtube/tasks.py +++ b/bot/extensions/youtube/tasks.py @@ -11,6 +11,7 @@ from bot.services import http YOUTUBE_URL = re.compile(r"(?Phttps?://www\.youtube\.com/watch\?v=[\w-]+)") +RSS_FEED_BASE_URL = "https://www.youtube.com/feeds/videos.xml" class Video(BaseModel): @@ -27,7 +28,7 @@ class YoutubeTasks(commands.Cog): def __init__(self, bot: core.DiscordBot): self.bot = bot self.video_links: list[str] = [] - self.update_thumbnail = [] + self.old_thumbnails: list[tuple[discord.Message, str]] = [] self.check_for_new_videos.start() def cog_unload(self) -> None: @@ -35,61 +36,50 @@ def cog_unload(self) -> None: @property def channel(self) -> discord.TextChannel | None: - return self.bot.get_channel(settings.notification.channel_id) + return self.bot.get_channel(settings.youtube.text_channel_id) - async def send_notification(self, video: Video) -> None: - update_thumbnail = False - max_res = video.thumbnail.replace("/hqdefault.jpg", "/maxresdefault.jpg") - async with http.session.head(max_res) as response: - if response.status == 200: - video.thumbnail = max_res - else: - update_thumbnail = True - video.thumbnail = video.thumbnail.replace("/hqdefault.jpg", "/mqdefault.jpg") + @tasks.loop(minutes=2) + async def check_for_new_videos(self): + """Checks old thumbnails for higher resolution images and looks for new videos""" + await self.check_old_thumbnails() + await self.find_new_videos() - embed = discord.Embed( - title=video.title, - description=video.description.split("\n\n")[0], - url=video.link, - color=discord.Color.red(), - timestamp=datetime.strptime(video.published, "%Y-%m-%dT%H:%M:%S%z"), - ) - embed.set_image(url=video.thumbnail) - embed.set_author( - name="Tech With Tim", - url="https://www.youtube.com/c/TechWithTim", - icon_url=self.bot.user.display_avatar.url, - ) - embed.set_footer(text="Uploaded", icon_url=self.bot.user.display_avatar.url) + @check_for_new_videos.before_loop + async def before_check(self): + """Fetches the 10 last videos posted so we don't accidentally re-post it.""" + if self.video_links: + return - message = await self.channel.send( - content=f"Hey <@&{settings.notification.role_id}>, **Tim** just posted a video! Go check it out!", - embed=embed, - allowed_mentions=discord.AllowedMentions(roles=True), - ) + async for message in self.channel.history(limit=10): + if message.embeds: + embed = message.embeds[0] + self.video_links.append(embed.url) + if embed.image.url.endswith("/mqdefault.jpg"): + self.old_thumbnails.append((message, embed.image.url)) - if update_thumbnail: - self.update_thumbnail.append((message.id, video.thumbnail)) + else: + match = YOUTUBE_URL.search(message.content) + if match: + self.video_links.append(match.group("url")) async def check_old_thumbnails(self): - for message_id, thumbnail_url in self.update_thumbnail: - async with http.session.head(thumbnail_url.replace("/mqdefault.jpg", "/maxresdefault.jpg")) as response: - if response.status == 404: - continue + """Tries to fetch new thumbnails for any videos we've posted with a low resolution thumbnail.""" + for message, thumbnail_url in self.old_thumbnails: + max_resolution = thumbnail_url.replace("/mqdefault.jpg", "/maxresdefault.jpg") + + if not await self.image_exists(max_resolution): + continue - message = await self.channel.fetch_message(message_id) embed = message.embeds[0] embed.set_image(url=thumbnail_url) await message.edit(embed=embed) - self.update_thumbnail.remove((message_id, thumbnail_url)) - @tasks.loop(minutes=2) - async def check_for_new_videos(self): - """Check for new videos""" + self.old_thumbnails.remove((message, thumbnail_url)) - await self.check_old_thumbnails() + async def find_new_videos(self): + """Fetches most recent videos from rss feed and publishes any new videos.""" + url = RSS_FEED_BASE_URL + "?channel_id=" + settings.youtube.channel_id - url = "https://www.youtube.com/feeds/videos.xml?channel_id=UC4JX40jDee_tINbkjycV4Sg" async with http.session.get(url) as response: data = await response.text() tree = ET.fromstring(data) @@ -112,18 +102,39 @@ async def check_for_new_videos(self): self.video_links.append(video.link) await self.send_notification(video) - @check_for_new_videos.before_loop - async def before_check(self): - if self.video_links: - return + async def send_notification(self, video: Video) -> None: + """Sends an embed to discord with the new video.""" + max_resolution = video.thumbnail.replace("/mqdefault.jpg", "/maxresdefault.jpg") + use_max_resolution = await self.image_exists(max_resolution) - async for message in self.channel.history(limit=10): - if message.embeds: - embed = message.embeds[0] - self.video_links.append(embed.url) - if embed.image.url.endswith("/mqdefault.jpg"): - self.update_thumbnail.append((message.id, embed.image.url)) - else: - match = YOUTUBE_URL.search(message.content) - if match: - self.video_links.append(match.group("url")) + if use_max_resolution: + video.thumbnail = max_resolution + + embed = discord.Embed( + title=video.title, + description=video.description.split("\n\n")[0], + url=video.link, + color=discord.Color.red(), + timestamp=datetime.strptime(video.published, "%Y-%m-%dT%H:%M:%S%z"), + ) + embed.set_image(url=video.thumbnail) + embed.set_author( + name="Tech With Tim", + url="https://www.youtube.com/c/TechWithTim", + icon_url=self.bot.user.display_avatar.url, + ) + embed.set_footer(text="Uploaded", icon_url=self.bot.user.display_avatar.url) + + message = await self.channel.send( + content=f"Hey <@&{settings.youtube.role_id}>, **Tim** just posted a video! Go check it out!", + embed=embed, + allowed_mentions=discord.AllowedMentions(roles=True), + ) + + if not use_max_resolution: + self.old_thumbnails.append((message, video.thumbnail)) + + @staticmethod + async def image_exists(url: str): + async with http.session.head(url) as response: + return response.status == 200 From 98bdb349b9c9bb2cb281596c871ec6a509b28ee9 Mon Sep 17 00:00:00 2001 From: Sylte Date: Tue, 14 Nov 2023 22:08:18 +0100 Subject: [PATCH 15/15] Update example.env --- example.env | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/example.env b/example.env index efc11cf8..f0d5222f 100644 --- a/example.env +++ b/example.env @@ -65,5 +65,10 @@ TIMATHON__CHANNEL_ID=0 TIMATHON__PARTICIPANT_ROLE_ID=0 # --- Custom Roles -CUSTOM_ROLES__LOG_CHANNEL_ID = 0 -CUSTOM_ROLES__DIVIDER_ROLE_ID = 0 +CUSTOM_ROLES__LOG_CHANNEL_ID=0 +CUSTOM_ROLES__DIVIDER_ROLE_ID=0 + +# --- Youtube +YOUTUBE__CHANNEL_ID=UC4JX40jDee_tINbkjycV4Sg +YOUTUBE__TEXT_CHANNEL_ID=0 +YOUTUBE__ROLE_ID=0