From 8edf70e21c5807dee4454a08ba7f04b6dd8d1d5c Mon Sep 17 00:00:00 2001 From: Renovate Date: Tue, 17 Dec 2024 00:00:36 +0200 Subject: [PATCH 01/10] Update dependency libbot to v3.3.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0bd201d..d3ef55d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ requests>=2.32.2 aiofiles~=24.1.0 apscheduler>=3.10.0 async_pymongo==0.1.11 -libbot[speed,pycord]==3.2.3 +libbot[speed,pycord]==3.3.1 ujson~=5.10.0 WaifuPicsPython==0.2.0 \ No newline at end of file From 36d63e0240c521c0734c5858e5b1aa3c77b57a0d Mon Sep 17 00:00:00 2001 From: profitroll Date: Tue, 17 Dec 2024 22:14:06 +0100 Subject: [PATCH 02/10] Changed the Client structure --- classes/holo_bot.py | 18 +++++++++++++ cogs/admin.py | 8 +++--- cogs/analytics.py | 8 +++--- cogs/custom_channels.py | 8 +++--- cogs/data.py | 11 ++++---- cogs/fun.py | 8 +++--- cogs/logger.py | 8 +++--- cogs/utility.py | 57 +++++++++++++++++++++++++++++++++++++++ main.py | 60 +++++++++++------------------------------ modules/client.py | 10 ------- 10 files changed, 117 insertions(+), 79 deletions(-) create mode 100644 classes/holo_bot.py create mode 100644 cogs/utility.py delete mode 100644 modules/client.py diff --git a/classes/holo_bot.py b/classes/holo_bot.py new file mode 100644 index 0000000..182cff8 --- /dev/null +++ b/classes/holo_bot.py @@ -0,0 +1,18 @@ +from libbot.pycord.classes import PycordBot + + +class HoloBot(PycordBot): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + async def start(self, *args, **kwargs) -> None: + if self.scheduler is not None: + self.scheduler.start() + + await super().start(*args, **kwargs) + + async def close(self) -> None: + if self.scheduler is not None: + self.scheduler.shutdown() + + await super().close() diff --git a/cogs/admin.py b/cogs/admin.py index a8adbdf..d305ff7 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -14,9 +14,9 @@ from discord import ( from discord import utils as ds_utils from discord.ext import commands from libbot import config_get -from libbot.pycord.classes import PycordBot from libbot.sync import config_get as sync_config_get +from classes.holo_bot import HoloBot from enums import Color from modules.scheduler import scheduler from modules.utils_sync import guild_name @@ -28,8 +28,8 @@ logger = logging.getLogger(__name__) class Admin(commands.Cog): """Cog with utility commands for admins.""" - def __init__(self, client: PycordBot): - self.client: PycordBot = client + def __init__(self, client: HoloBot): + self.client: HoloBot = client # Disabled because warning functionality is temporarily not needed # @slash_command( @@ -236,5 +236,5 @@ class Admin(commands.Cog): ) -def setup(client: PycordBot) -> None: +def setup(client: HoloBot) -> None: client.add_cog(Admin(client)) diff --git a/cogs/analytics.py b/cogs/analytics.py index 1849e5d..1ca2068 100644 --- a/cogs/analytics.py +++ b/cogs/analytics.py @@ -3,16 +3,16 @@ from typing import Dict, List, Any from discord import Cog, Message from discord.ext import commands -from libbot.pycord.classes import PycordBot +from classes.holo_bot import HoloBot from modules.database import col_analytics logger = logging.getLogger(__name__) class Analytics(commands.Cog): - def __init__(self, client: PycordBot): - self.client: PycordBot = client + def __init__(self, client: HoloBot): + self.client: HoloBot = client @Cog.listener() async def on_message(self, message: Message) -> None: @@ -60,5 +60,5 @@ class Analytics(commands.Cog): ) -def setup(client: PycordBot) -> None: +def setup(client: HoloBot) -> None: client.add_cog(Analytics(client)) diff --git a/cogs/custom_channels.py b/cogs/custom_channels.py index e55053a..56746c6 100644 --- a/cogs/custom_channels.py +++ b/cogs/custom_channels.py @@ -7,9 +7,9 @@ from discord.abc import GuildChannel from discord.commands import SlashCommandGroup from discord.ext import commands from libbot import config_get -from libbot.pycord.classes import PycordBot from libbot.sync import config_get as sync_config_get +from classes.holo_bot import HoloBot from classes.holo_user import HoloUser from enums import Color from modules.database import col_users @@ -19,8 +19,8 @@ logger = logging.getLogger(__name__) class CustomChannels(commands.Cog): - def __init__(self, client: PycordBot): - self.client: PycordBot = client + def __init__(self, client: HoloBot): + self.client: HoloBot = client @commands.Cog.listener() async def on_guild_channel_delete(self, channel: GuildChannel) -> None: @@ -234,5 +234,5 @@ class CustomChannels(commands.Cog): ) -def setup(client: PycordBot) -> None: +def setup(client: HoloBot) -> None: client.add_cog(CustomChannels(client)) diff --git a/cogs/data.py b/cogs/data.py index 716f1b0..e5a68f2 100644 --- a/cogs/data.py +++ b/cogs/data.py @@ -1,4 +1,5 @@ import logging +from logging import Logger from os import makedirs from pathlib import Path from typing import Union, List, Dict, Any @@ -9,21 +10,21 @@ from discord import utils as ds_utils from discord.commands import SlashCommandGroup from discord.ext import commands from libbot import config_get -from libbot.pycord.classes import PycordBot from libbot.sync import config_get as sync_config_get from libbot.sync import json_write as sync_json_write +from classes.holo_bot import HoloBot from classes.holo_user import HoloUser from enums import Color from modules.database import col_users from modules.utils_sync import guild_name -logger = logging.getLogger(__name__) +logger: Logger = logging.getLogger(__name__) class Data(commands.Cog): - def __init__(self, client: PycordBot): - self.client: PycordBot = client + def __init__(self, client: HoloBot): + self.client: HoloBot = client data: SlashCommandGroup = SlashCommandGroup("data", "Керування даними користувачів") @@ -179,5 +180,5 @@ class Data(commands.Cog): ) -def setup(client: PycordBot) -> None: +def setup(client: HoloBot) -> None: client.add_cog(Data(client)) diff --git a/cogs/fun.py b/cogs/fun.py index a32adcd..f317114 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -3,9 +3,9 @@ import logging from discord import ApplicationContext, Embed, User, option, slash_command from discord.ext import commands from libbot import config_get -from libbot.pycord.classes import PycordBot from libbot.sync import config_get as sync_config_get +from classes.holo_bot import HoloBot from modules.utils_sync import guild_name from modules.waifu_pics import waifu_pics @@ -13,8 +13,8 @@ logger = logging.getLogger(__name__) class Fun(commands.Cog): - def __init__(self, client: PycordBot): - self.client: PycordBot = client + def __init__(self, client: HoloBot): + self.client: HoloBot = client @slash_command( name="action", @@ -54,5 +54,5 @@ class Fun(commands.Cog): await ctx.respond(embed=embed) -def setup(client: PycordBot) -> None: +def setup(client: HoloBot) -> None: client.add_cog(Fun(client)) diff --git a/cogs/logger.py b/cogs/logger.py index 7070188..c6fabbf 100644 --- a/cogs/logger.py +++ b/cogs/logger.py @@ -4,14 +4,14 @@ from discord import Member, Message, TextChannel from discord import utils as ds_utils from discord.ext import commands from libbot import config_get -from libbot.pycord.classes import PycordBot +from classes.holo_bot import HoloBot from modules.database import col_users class Logger(commands.Cog): - def __init__(self, client: PycordBot): - self.client: PycordBot = client + def __init__(self, client: HoloBot): + self.client: HoloBot = client @commands.Cog.listener() async def on_message(self, message: Message): @@ -65,5 +65,5 @@ class Logger(commands.Cog): await col_users.insert_one(document=user) -def setup(client: PycordBot) -> None: +def setup(client: HoloBot) -> None: client.add_cog(Logger(client)) diff --git a/cogs/utility.py b/cogs/utility.py new file mode 100644 index 0000000..a454392 --- /dev/null +++ b/cogs/utility.py @@ -0,0 +1,57 @@ +import logging +from logging import Logger + +from discord import Activity, ActivityType +from discord.ext import commands +from libbot import config_get + +from classes.holo_bot import HoloBot + +logger: Logger = logging.getLogger(__name__) + + +class Utility(commands.Cog): + def __init__(self, client: HoloBot): + self.client: HoloBot = client + + @commands.Cog.listener() + async def on_ready(self) -> None: + logger.info("Logged in as %s", self.client.user) + + activity_type: str = await config_get("type", "status") + activity_message: str = await config_get("message", "status") + + if activity_type == "playing": + await self.client.change_presence( + activity=Activity(type=ActivityType.playing, name=activity_message) + ) + elif activity_type == "watching": + await self.client.change_presence( + activity=Activity(type=ActivityType.watching, name=activity_message) + ) + elif activity_type == "listening": + await self.client.change_presence( + activity=Activity(type=ActivityType.listening, name=activity_message) + ) + elif activity_type == "streaming": + await self.client.change_presence( + activity=Activity(type=ActivityType.streaming, name=activity_message) + ) + elif activity_type == "competing": + await self.client.change_presence( + activity=Activity(type=ActivityType.competing, name=activity_message) + ) + elif activity_type == "custom": + await self.client.change_presence( + activity=Activity(type=ActivityType.custom, name=activity_message) + ) + else: + return + + logger.info( + "Set activity type to %s with message %s", activity_type, activity_message + ) + + +def setup(client: HoloBot) -> None: + client.add_cog(Utility(client)) diff --git a/main.py b/main.py index 50b4093..5beb79d 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,12 @@ import logging import sys from logging import Logger +from pathlib import Path -from discord import Activity, ActivityType -from libbot import config_get +from discord import LoginFailure, Intents from libbot.sync import config_get as sync_config_get -from modules.client import client +from classes.holo_bot import HoloBot from modules.scheduler import scheduler logging.basicConfig( @@ -25,53 +25,25 @@ except ImportError: pass -@client.event -async def on_ready() -> None: - logger.info("Logged in as %s", client.user) - - activity_type: str = await config_get("type", "status") - activity_message: str = await config_get("message", "status") - - if activity_type == "playing": - await client.change_presence( - activity=Activity(type=ActivityType.playing, name=activity_message) - ) - elif activity_type == "watching": - await client.change_presence( - activity=Activity(type=ActivityType.watching, name=activity_message) - ) - elif activity_type == "listening": - await client.change_presence( - activity=Activity(type=ActivityType.listening, name=activity_message) - ) - elif activity_type == "streaming": - await client.change_presence( - activity=Activity(type=ActivityType.streaming, name=activity_message) - ) - elif activity_type == "competing": - await client.change_presence( - activity=Activity(type=ActivityType.competing, name=activity_message) - ) - elif activity_type == "custom": - await client.change_presence( - activity=Activity(type=ActivityType.custom, name=activity_message) - ) - else: - return - - logger.info( - "Set activity type to %s with message %s", activity_type, activity_message - ) - - def main() -> None: + if not Path("config.json").exists(): + logger.error( + "Config file is missing: Make sure the configuration file 'config.json' is in place." + ) + sys.exit() + + intents: Intents = Intents().all() + client: HoloBot = HoloBot(intents=intents, scheduler=scheduler) + client.load_extension("cogs") try: - scheduler.start() client.run(sync_config_get("bot_token", "bot")) + except LoginFailure as exc: + logger.error("Provided bot token is invalid: %s", exc) except KeyboardInterrupt: - scheduler.shutdown() + logger.info("KeyboardInterrupt received: Shutting down gracefully.") + finally: sys.exit() diff --git a/modules/client.py b/modules/client.py deleted file mode 100644 index 926632b..0000000 --- a/modules/client.py +++ /dev/null @@ -1,10 +0,0 @@ -from discord import Intents -from libbot.pycord.classes import PycordBot - -from modules.scheduler import scheduler - -intents: Intents = Intents().all() - -intents.members = True - -client: PycordBot = PycordBot(intents=intents, scheduler=scheduler) From a753918432d9511777be6f247daec58792c06965 Mon Sep 17 00:00:00 2001 From: profitroll Date: Tue, 17 Dec 2024 22:29:03 +0100 Subject: [PATCH 03/10] Added experimental Docker instructions --- .dockerignore | 40 ++++++++++++++++++++++++++++++++++ Dockerfile | 27 +++++++++++++++++++++++ README.md | 59 +++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bf5b843 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,40 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md + +config.json +.renovaterc +**/.idea +**/.mypy_cache +validation \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6009219 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +ARG PYTHON_VERSION=3.12.8 +FROM python:${PYTHON_VERSION}-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + python -m pip install -r requirements.txt + +USER appuser + +COPY . . + +ENTRYPOINT ["python", "main.py"] \ No newline at end of file diff --git a/README.md b/README.md index 4a269e2..b7c196d 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,41 @@ Code style: black

-## Installation +## Installation from release -1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation/). -2. `git clone https://git.end-play.xyz/HoloUA/Discord.git` -3. `cd Discord` -4. Install Python 3.9+ (at least 3.11 is recommended) for your OS -5. `python3 -m pip install -r requirements.txt` -6. Run it with `python3 main.py` after configuring +1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation) +2. Install Python 3.9+ (3.11+ is recommended) +3. Download the [latest release](https://git.end-play.xyz/HoloUA/Discord/releases/latest)'s archive +4. Extract the archive +5. Navigate to the extracted folder and subfolder `Discord` in it +6. Create a virtual environment: + `python -m venv .venv` or `virtualenv .venv` +7. Activate the virtual environment: + Windows: `.venv\Scripts\activate.bat` + Linux/macOS: `.venv/bin/activate` +8. Install dependencies: + `python -m pip install -r requirements.txt` +9. Run the bot with `python main.py` after completing the [configuration](#Configuration) + +## Installation with Git + +1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation) +2. Install Python 3.9+ (3.11+ is recommended) +3. Clone the repository: + `git clone https://git.end-play.xyz/HoloUA/Discord.git` +4. `cd Discord` +5. Install dependencies: + `python -m pip install -r requirements.txt` +6. Run the bot with `python main.py` after completing the [configuration](#Configuration) ## Configuration There's a file `config_example.json` which contains default configuration -and should be used as a base config. +and should be used as a base config. -Copy this file to `config.json` and open it with any text editor of your liking. +Copy this file to `config.json` and open it with any text editor of your liking. -Modify the newly created configuration file to fit your needs. +Modify the newly created configuration file to fit your needs. Mandatory keys to modify: @@ -35,4 +53,23 @@ Mandatory keys to modify: - channels.* - roles.* -After all of that you're good to go! Happy using :) \ No newline at end of file +After all of that you're good to go! Happy using :) + +## Docker [Experimental] + +As an experiment, Docker deployment option has been added. + +### Building the image + +1. `git clone https://git.end-play.xyz/HoloUA/Discord.git` +2. `cd Discord` +3. `docker build -t holoua-discord .` + +### Starting the bot + +1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation) +2. Download + the [configuration example file](https://git.end-play.xyz/HoloUA/Discord/src/branch/main/config_example.json) and + store it somewhere you would like your bot to access it +3. Complete the [configuration](#Configuration) step for this file +4. `docker run -d -v /path/to/config.json:/app/config.json holoua-discord` \ No newline at end of file From 162898f5eb7559059fa99aaeb694499ef640a0d8 Mon Sep 17 00:00:00 2001 From: profitroll Date: Thu, 26 Dec 2024 19:12:50 +0100 Subject: [PATCH 04/10] WIP: libbot 4.0.0 adoption --- classes/holo_user.py | 2 +- cogs/admin.py | 7 +++---- cogs/custom_channels.py | 9 ++++----- cogs/data.py | 10 ++++------ cogs/fun.py | 7 +++---- cogs/logger.py | 2 +- cogs/utility.py | 2 +- main.py | 4 ++-- modules/database.py | 4 ++-- requirements.txt | 2 +- 10 files changed, 22 insertions(+), 27 deletions(-) diff --git a/classes/holo_user.py b/classes/holo_user.py index 3c72daf..0488bee 100644 --- a/classes/holo_user.py +++ b/classes/holo_user.py @@ -3,7 +3,7 @@ from typing import Any, Union, Dict from bson import ObjectId from discord import User, Member -from libbot import config_get +from libbot.utils import config_get from errors import UserNotFoundError from modules.database import col_warnings, sync_col_users, sync_col_warnings, col_users diff --git a/cogs/admin.py b/cogs/admin.py index d305ff7..50f9866 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -13,8 +13,7 @@ from discord import ( ) from discord import utils as ds_utils from discord.ext import commands -from libbot import config_get -from libbot.sync import config_get as sync_config_get +from libbot.utils import config_get from classes.holo_bot import HoloBot from enums import Color @@ -117,7 +116,7 @@ class Admin(commands.Cog): @slash_command( name="clear", description="Видалити деяку кількість повідомлень в каналі", - guild_ids=[sync_config_get("guild")], + guild_ids=[config_get("guild")], ) @option("amount", description="Кількість") @option("user", description="Користувач", default=None) @@ -182,7 +181,7 @@ class Admin(commands.Cog): @slash_command( name="reboot", description="Перезапустити бота", - guild_ids=[sync_config_get("guild")], + guild_ids=[config_get("guild")], ) async def reboot_cmd(self, ctx: ApplicationContext) -> None: await ctx.defer(ephemeral=True) diff --git a/cogs/custom_channels.py b/cogs/custom_channels.py index 56746c6..a8e5d66 100644 --- a/cogs/custom_channels.py +++ b/cogs/custom_channels.py @@ -6,8 +6,7 @@ from discord import utils as ds_utils from discord.abc import GuildChannel from discord.commands import SlashCommandGroup from discord.ext import commands -from libbot import config_get -from libbot.sync import config_get as sync_config_get +from libbot.utils import config_get from classes.holo_bot import HoloBot from classes.holo_user import HoloUser @@ -35,7 +34,7 @@ class CustomChannels(commands.Cog): @custom_channel_group.command( name="get", description="Отримати персональний текстовий канал", - guild_ids=[sync_config_get("guild")], + guild_ids=[config_get("guild")], ) @option("name", description="Назва каналу") @option("reactions", description="Дозволити реакції") @@ -122,7 +121,7 @@ class CustomChannels(commands.Cog): @custom_channel_group.command( name="edit", description="Змінити параметри особистого каналу", - guild_ids=[sync_config_get("guild")], + guild_ids=[config_get("guild")], ) @option("name", description="Назва каналу") @option("reactions", description="Дозволити реакції") @@ -167,7 +166,7 @@ class CustomChannels(commands.Cog): @custom_channel_group.command( name="remove", description="Відібрати канал, знищуючи його, та частково повернути кошти", - guild_ids=[sync_config_get("guild")], + guild_ids=[config_get("guild")], ) @option("confirm", description="Підтвердження операції") async def custom_channel_remove_cmd( diff --git a/cogs/data.py b/cogs/data.py index e5a68f2..f2c8d84 100644 --- a/cogs/data.py +++ b/cogs/data.py @@ -9,9 +9,7 @@ from discord import ApplicationContext, Embed, File, option, Role, TextChannel from discord import utils as ds_utils from discord.commands import SlashCommandGroup from discord.ext import commands -from libbot import config_get -from libbot.sync import config_get as sync_config_get -from libbot.sync import json_write as sync_json_write +from libbot.utils import config_get, json_write from classes.holo_bot import HoloBot from classes.holo_user import HoloUser @@ -31,7 +29,7 @@ class Data(commands.Cog): @data.command( name="export", description="Експортувати дані", - guild_ids=[sync_config_get("guild")], + guild_ids=[config_get("guild")], ) @option( "kind", description="Тип даних, які треба експортувати", choices=["Користувачі"] @@ -94,14 +92,14 @@ class Data(commands.Cog): } ) - sync_json_write(users, Path(f"tmp/{uuid}")) + json_write(users, Path(f"tmp/{uuid}")) await ctx.respond(file=File(Path(f"tmp/{uuid}"), filename="users.json")) @data.command( name="migrate", description="Мігрувати всіх користувачів до бази", - guild_ids=[sync_config_get("guild")], + guild_ids=[config_get("guild")], ) @option( "kind", description="Тип даних, які треба експортувати", choices=["Користувачі"] diff --git a/cogs/fun.py b/cogs/fun.py index f317114..98ff382 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -2,8 +2,7 @@ import logging from discord import ApplicationContext, Embed, User, option, slash_command from discord.ext import commands -from libbot import config_get -from libbot.sync import config_get as sync_config_get +from libbot.utils import config_get from classes.holo_bot import HoloBot from modules.utils_sync import guild_name @@ -19,12 +18,12 @@ class Fun(commands.Cog): @slash_command( name="action", description="Провести над користувачем РП дію", - guild_ids=[sync_config_get("guild")], + guild_ids=[config_get("guild")], ) @option( "type", description="Тип дії, яку хочете провести з користувачем", - choices=sync_config_get("actions").keys(), + choices=config_get("actions").keys(), ) @option("user", description="Користувач") async def action_cmd(self, ctx: ApplicationContext, type: str, user: User) -> None: diff --git a/cogs/logger.py b/cogs/logger.py index c6fabbf..b80eec3 100644 --- a/cogs/logger.py +++ b/cogs/logger.py @@ -3,7 +3,7 @@ from typing import Dict, Any, Union from discord import Member, Message, TextChannel from discord import utils as ds_utils from discord.ext import commands -from libbot import config_get +from libbot.utils import config_get from classes.holo_bot import HoloBot from modules.database import col_users diff --git a/cogs/utility.py b/cogs/utility.py index a454392..3cab98c 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -3,7 +3,7 @@ from logging import Logger from discord import Activity, ActivityType from discord.ext import commands -from libbot import config_get +from libbot.utils import config_get from classes.holo_bot import HoloBot diff --git a/main.py b/main.py index 5beb79d..6a1a038 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ from logging import Logger from pathlib import Path from discord import LoginFailure, Intents -from libbot.sync import config_get as sync_config_get +from libbot.utils import config_get from classes.holo_bot import HoloBot from modules.scheduler import scheduler @@ -38,7 +38,7 @@ def main() -> None: client.load_extension("cogs") try: - client.run(sync_config_get("bot_token", "bot")) + client.run(config_get("bot_token", "bot")) except LoginFailure as exc: logger.error("Provided bot token is invalid: %s", exc) except KeyboardInterrupt: diff --git a/modules/database.py b/modules/database.py index 8d2d0ae..f78d61a 100644 --- a/modules/database.py +++ b/modules/database.py @@ -1,12 +1,12 @@ from typing import Dict, Any from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase -from libbot.sync import config_get as sync_config_get +from libbot.utils import config_get from pymongo import MongoClient from pymongo.synchronous.collection import Collection from pymongo.synchronous.database import Database -db_config: Dict[str, Any] = sync_config_get("database") +db_config: Dict[str, Any] = config_get("database") con_string: str = ( "mongodb://{0}:{1}/{2}".format( diff --git a/requirements.txt b/requirements.txt index d3ef55d..cdebadf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ requests>=2.32.2 aiofiles~=24.1.0 apscheduler>=3.10.0 async_pymongo==0.1.11 -libbot[speed,pycord]==3.3.1 +libbot[speed,pycord]==4.0.0 ujson~=5.10.0 WaifuPicsPython==0.2.0 \ No newline at end of file From 0195706e92c15a4263a8885a3603fba69f50eb4c Mon Sep 17 00:00:00 2001 From: profitroll Date: Thu, 26 Dec 2024 19:18:02 +0100 Subject: [PATCH 05/10] PycordBot now handles scheduler on its own --- classes/holo_bot.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/classes/holo_bot.py b/classes/holo_bot.py index 182cff8..c2f4246 100644 --- a/classes/holo_bot.py +++ b/classes/holo_bot.py @@ -4,15 +4,3 @@ from libbot.pycord.classes import PycordBot class HoloBot(PycordBot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - - async def start(self, *args, **kwargs) -> None: - if self.scheduler is not None: - self.scheduler.start() - - await super().start(*args, **kwargs) - - async def close(self) -> None: - if self.scheduler is not None: - self.scheduler.shutdown() - - await super().close() From c54586940ef8d7f46e7b0a07ff61d62743e94362 Mon Sep 17 00:00:00 2001 From: profitroll Date: Thu, 26 Dec 2024 20:28:06 +0100 Subject: [PATCH 06/10] Added a fix in case typing extensions is missing --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index cdebadf..05effea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,6 @@ aiofiles~=24.1.0 apscheduler>=3.10.0 async_pymongo==0.1.11 libbot[speed,pycord]==4.0.0 +typing-extensions~=4.12.2 ujson~=5.10.0 WaifuPicsPython==0.2.0 \ No newline at end of file From 7b64f6938b52ca99c41f0664429d793d7c970672 Mon Sep 17 00:00:00 2001 From: profitroll Date: Thu, 26 Dec 2024 20:28:44 +0100 Subject: [PATCH 07/10] Added a nice comment explaining the sync call inside async function --- cogs/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cogs/data.py b/cogs/data.py index f2c8d84..d1fe76f 100644 --- a/cogs/data.py +++ b/cogs/data.py @@ -92,6 +92,8 @@ class Data(commands.Cog): } ) + # Temporary file must be written synchronously, + # otherwise it will not be there when ctx.respond() is be called json_write(users, Path(f"tmp/{uuid}")) await ctx.respond(file=File(Path(f"tmp/{uuid}"), filename="users.json")) From eed084cd91e502f7c07f083304a50bb3e9c5365d Mon Sep 17 00:00:00 2001 From: profitroll Date: Fri, 27 Dec 2024 00:18:54 +0100 Subject: [PATCH 08/10] Replaced legacy Union[] with new syntax --- classes/holo_user.py | 34 ++++++++++++++-------------------- cogs/admin.py | 9 ++++----- cogs/custom_channels.py | 10 ++++------ cogs/data.py | 10 +++++----- cogs/logger.py | 6 +++--- modules/utils_sync.py | 4 +--- 6 files changed, 31 insertions(+), 42 deletions(-) diff --git a/classes/holo_user.py b/classes/holo_user.py index 0488bee..0ebb2a9 100644 --- a/classes/holo_user.py +++ b/classes/holo_user.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Union, Dict +from typing import Any, Dict from bson import ObjectId from discord import User, Member @@ -12,11 +12,11 @@ logger = logging.getLogger(__name__) class HoloUser: - def __init__(self, user: Union[User, Member, int]) -> None: + def __init__(self, user: User | Member | int) -> None: """Get an object that has a proper binding between Discord ID and database ### Args: - * `user` (Union[User, Member, int]): Object from which ID can be extracted + * `user` (User | Member | int): Object from which ID can be extracted ### Raises: * `UserNotFoundError`: User with such ID does not seem to exist in database @@ -24,17 +24,15 @@ class HoloUser: self.id: int = user if not hasattr(user, "id") else user.id - jav_user: Union[Dict[str, Any], None] = sync_col_users.find_one( - {"user": self.id} - ) + jav_user: Dict[str, Any] | None = sync_col_users.find_one({"user": self.id}) if jav_user is None: raise UserNotFoundError(user=user, user_id=self.id) self.db_id: ObjectId = jav_user["_id"] - self.customrole: Union[int, None] = jav_user["customrole"] - self.customchannel: Union[int, None] = jav_user["customchannel"] + self.customrole: int | None = jav_user["customrole"] + self.customchannel: int | None = jav_user["customchannel"] self.warnings: int = self.warns() def warns(self) -> int: @@ -43,9 +41,7 @@ class HoloUser: ### Returns: * `int`: Number of warnings """ - warns: Union[Dict[str, Any], None] = sync_col_warnings.find_one( - {"user": self.id} - ) + warns: Dict[str, Any] | None = sync_col_warnings.find_one({"user": self.id}) return 0 if warns is None else warns["warns"] @@ -55,9 +51,7 @@ class HoloUser: ### Args: * `count` (int, optional): Count of warnings to be added. Defaults to 1. """ - warns: Union[Dict[str, Any], None] = await col_warnings.find_one( - {"user": self.id} - ) + warns: Dict[str, Any] | None = await col_warnings.find_one({"user": self.id}) if warns is not None: await col_warnings.update_one( @@ -88,11 +82,11 @@ class HoloUser: logger.info("Set attribute %s of user %s to %s", key, self.id, value) @staticmethod - async def is_moderator(member: Union[User, Member]) -> bool: + async def is_moderator(member: User | Member) -> bool: """Check if user is moderator or council member ### Args: - * `member` (Union[User, Member]): Member object + * `member` (User | Member): Member object ### Returns: `bool`: `True` if member is a moderator or member of council and `False` if not @@ -100,8 +94,8 @@ class HoloUser: if isinstance(member, User): return False - moderator_role: Union[int, None] = await config_get("moderators", "roles") - council_role: Union[int, None] = await config_get("council", "roles") + moderator_role: int | None = await config_get("moderators", "roles") + council_role: int | None = await config_get("council", "roles") for role in member.roles: if role.id in (moderator_role, council_role): @@ -110,11 +104,11 @@ class HoloUser: return False @staticmethod - async def is_council(member: Union[User, Member]) -> bool: + async def is_council(member: User | Member) -> bool: """Check if user is a member of council ### Args: - * `member` (Union[User, Member]): Member object + * `member` (User | Member): Member object ### Returns: `bool`: `True` if member is a member of council and `False` if not diff --git a/cogs/admin.py b/cogs/admin.py index 50f9866..2670b9b 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -1,6 +1,5 @@ import logging import sys -from typing import Union from discord import ( ApplicationContext, @@ -160,10 +159,10 @@ class Admin(commands.Cog): ) ) - mod_role: Union[Role, None] = ds_utils.get( + mod_role: Role | None = ds_utils.get( ctx.user.guild.roles, id=await config_get("moderators", "roles") ) - admin_chan: Union[TextChannel, None] = ds_utils.get( + admin_chan: TextChannel | None = ds_utils.get( ctx.user.guild.channels, id=await config_get("adminchat", "channels", "text"), ) @@ -216,10 +215,10 @@ class Admin(commands.Cog): ) ) - mod_role: Union[Role, None] = ds_utils.get( + mod_role: Role | None = ds_utils.get( ctx.user.guild.roles, id=await config_get("moderators", "roles") ) - admin_chan: Union[TextChannel, None] = ds_utils.get( + admin_chan: TextChannel | None = ds_utils.get( ctx.user.guild.channels, id=await config_get("adminchat", "channels", "text"), ) diff --git a/cogs/custom_channels.py b/cogs/custom_channels.py index a8e5d66..54db27d 100644 --- a/cogs/custom_channels.py +++ b/cogs/custom_channels.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, Union +from typing import Any, Dict from discord import ApplicationContext, Embed, option, TextChannel, Role from discord import utils as ds_utils @@ -108,9 +108,7 @@ class CustomChannels(commands.Cog): bots: Dict[str, Any] = await config_get("bots") for bot in bots: - role: Union[Role, None] = ds_utils.get( - ctx.user.guild.roles, id=bots[bot]["role"] - ) + role: Role | None = ds_utils.get(ctx.user.guild.roles, id=bots[bot]["role"]) if role is not None: await created_channel.set_permissions( @@ -131,7 +129,7 @@ class CustomChannels(commands.Cog): ) -> None: holo_user_ctx: HoloUser = HoloUser(ctx.user) - custom_channel: Union[TextChannel, None] = ds_utils.get( + custom_channel: TextChannel | None = ds_utils.get( ctx.guild.channels, id=holo_user_ctx.customchannel ) @@ -188,7 +186,7 @@ class CustomChannels(commands.Cog): await ctx.defer() - custom_channel: Union[TextChannel, None] = ds_utils.get( + custom_channel: TextChannel | None = ds_utils.get( ctx.guild.channels, id=holo_user_ctx.customchannel ) diff --git a/cogs/data.py b/cogs/data.py index d1fe76f..fc4e18f 100644 --- a/cogs/data.py +++ b/cogs/data.py @@ -2,7 +2,7 @@ import logging from logging import Logger from os import makedirs from pathlib import Path -from typing import Union, List, Dict, Any +from typing import List, Dict, Any from uuid import uuid4 from discord import ApplicationContext, Embed, File, option, Role, TextChannel @@ -54,10 +54,10 @@ class Data(commands.Cog): ) ) - mod_role: Union[Role, None] = ds_utils.get( + mod_role: Role | None = ds_utils.get( ctx.user.guild.roles, id=await config_get("moderators", "roles") ) - admin_chan: Union[TextChannel, None] = ds_utils.get( + admin_chan: TextChannel | None = ds_utils.get( ctx.user.guild.channels, id=await config_get("adminchat", "channels", "text"), ) @@ -126,10 +126,10 @@ class Data(commands.Cog): ) ) - mod_role: Union[Role, None] = ds_utils.get( + mod_role: Role | None = ds_utils.get( ctx.user.guild.roles, id=await config_get("moderators", "roles") ) - admin_chan: Union[TextChannel, None] = ds_utils.get( + admin_chan: TextChannel | None = ds_utils.get( ctx.user.guild.channels, id=await config_get("adminchat", "channels", "text"), ) diff --git a/cogs/logger.py b/cogs/logger.py index b80eec3..8a7f08e 100644 --- a/cogs/logger.py +++ b/cogs/logger.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, Union +from typing import Dict, Any from discord import Member, Message, TextChannel from discord import utils as ds_utils @@ -33,11 +33,11 @@ class Logger(commands.Cog): @commands.Cog.listener() async def on_member_join(self, member: Member) -> None: - welcome_chan: Union[TextChannel, None] = ds_utils.get( + welcome_chan: TextChannel | None = ds_utils.get( self.client.get_guild(await config_get("guild")).channels, id=await config_get("welcome", "channels", "text"), ) - rules_chan: Union[TextChannel, None] = ds_utils.get( + rules_chan: TextChannel | None = ds_utils.get( self.client.get_guild(await config_get("guild")).channels, id=await config_get("rules", "channels", "text"), ) diff --git a/modules/utils_sync.py b/modules/utils_sync.py index b5690f3..6d9c56e 100644 --- a/modules/utils_sync.py +++ b/modules/utils_sync.py @@ -1,9 +1,7 @@ -from typing import Union - from discord import Member, User -def guild_name(member: Union[Member, User]) -> str: +def guild_name(member: Member | User) -> str: if isinstance(member, User): return member.name From 6060a3df8323f22895a026e03100fb53426f6bc3 Mon Sep 17 00:00:00 2001 From: kku Date: Fri, 27 Dec 2024 20:16:30 +0100 Subject: [PATCH 09/10] Closes #18 --- cogs/logger.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cogs/logger.py b/cogs/logger.py index 8a7f08e..67b3bd1 100644 --- a/cogs/logger.py +++ b/cogs/logger.py @@ -1,6 +1,7 @@ +import logging from typing import Dict, Any -from discord import Member, Message, TextChannel +from discord import Member, Message, TextChannel, MessageType from discord import utils as ds_utils from discord.ext import commands from libbot.utils import config_get @@ -8,6 +9,8 @@ from libbot.utils import config_get from classes.holo_bot import HoloBot from modules.database import col_users +logger = logging.getLogger(__name__) + class Logger(commands.Cog): def __init__(self, client: HoloBot): @@ -31,6 +34,16 @@ class Logger(commands.Cog): await col_users.insert_one(document=user) + if ( + (message.type == MessageType.thread_created) + and (message.channel is not None) + and ( + await col_users.count_documents({"customchannel": message.channel.id}) + > 0 + ) + ): + await message.delete() + @commands.Cog.listener() async def on_member_join(self, member: Member) -> None: welcome_chan: TextChannel | None = ds_utils.get( @@ -42,6 +55,9 @@ class Logger(commands.Cog): id=await config_get("rules", "channels", "text"), ) + if welcome_chan is None: + logger.warning("Could not find a welcome channel by its id") + if ( (member != self.client.user) and (member.bot is False) From 9417951f55b9ac2c612c162864da731c3116fa5a Mon Sep 17 00:00:00 2001 From: kku Date: Fri, 27 Dec 2024 20:30:32 +0100 Subject: [PATCH 10/10] Removed legacy and improved documentation --- cogs/admin.py | 91 ++++------------------------------------- cogs/analytics.py | 4 ++ cogs/custom_channels.py | 11 +++++ cogs/data.py | 8 ++++ cogs/fun.py | 3 ++ cogs/logger.py | 2 + cogs/utility.py | 1 + 7 files changed, 37 insertions(+), 83 deletions(-) diff --git a/cogs/admin.py b/cogs/admin.py index 2670b9b..d6036aa 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -29,89 +29,6 @@ class Admin(commands.Cog): def __init__(self, client: HoloBot): self.client: HoloBot = client - # Disabled because warning functionality is temporarily not needed - # @slash_command( - # name="warning", - # description="Попередити юзера про порушення правил", - # guild_ids=[config_get_sync("guild")], - # ) - # @option("user", description="Користувач") - # @option("reason", description="Причина") - # async def warn_cmd( - # self, - # ctx: ApplicationContext, - # user: User, - # reason: str = "Не вказана", - # ): - # logging.info(f"User {ctx.user.id} warned {user.id} for {reason}") - # await ctx.defer() - # jav_user = HoloUser(user) - # if ctx.user.id in await config_get("admins"): - # logging.info( - # f"Moderator {guild_name(ctx.user)} warned {guild_name(user)} for {reason} (has {jav_user.warnings} warns)" - # ) - # if jav_user.warnings >= 5: - # logging.info( - # f"User {guild_name(user)} was banned due to a big amount of warns ({jav_user.warnings})" - # ) - # await user.send( - # embed=Embed( - # title="Перманентне блокування", - # description=f"Вас було заблоковано за неодноразове порушення правил сервера.", - # color=Color.fail, - # ) - # ) - # await user.ban(reason=reason) - # elif jav_user.warnings >= 2: - # logging.info( - # f"User {guild_name(user)} was muted due to a big amount of warns ({jav_user.warnings})" - # ) - # jav_user.warn(reason=reason) - # await user.send( - # embed=Embed( - # title="Тимчасове блокування", - # description=f"Причина: `{reason}`\n\nНа вашому рахунку вже {jav_user.warnings} попереджень. Вас було тимчасово заблоковано на **1 годину**.\n\nЯкщо Ви продовжите порушувати правила сервера – згодом Вас заблокують.", - # color=0xDED56B, - # ) - # ) - # await user.timeout_for(timedelta(hours=1), reason=reason) - # else: - # jav_user.warn() - - # await ctx.respond( - # embed=Embed( - # title="Попередження", - # description=f"{user.mention} Будь ласка, не порушуйте правила. Ви отримали попередження з причини `{reason}`.\n\nЯкщо Ви продовжите порушувати правила – це може призвести до блокування в спільноті.", - # color=0xDED56B, - # ) - # ) - # else: - # logging.warning( - # f"User {guild_name(ctx.user)} tried to use /warn but permission denied" - # ) - # await ctx.respond( - # embed=Embed( - # title="Відмовлено в доступі", - # description="Здається, це команда лише для модераторів", - # color=Color.fail, - # ) - # ) - # mod_role = ds_utils.get( - # ctx.user.guild.roles, id=await config_get("moderators", "roles") - # ) - # admin_chan = ds_utils.get( - # ctx.user.guild.channels, - # id=await config_get("adminchat", "channels", "text"), - # ) - # await admin_chan.send( - # content=f"{mod_role.mention}", - # embed=Embed( - # title="Неавторизований запит", - # description=f"Користувач {ctx.user.mention} запитав у каналі {ctx.channel.mention} команду, до якої не повинен мати доступу/бачити.", - # color=Color.fail, - # ), - # ) - @slash_command( name="clear", description="Видалити деяку кількість повідомлень в каналі", @@ -125,6 +42,10 @@ class Admin(commands.Cog): amount: int, user: User, ) -> None: + """Command /clear [] + + Removes last messages in the current channel. Optionally from a specific user. + """ if ctx.user.id in self.client.owner_ids: logging.info( "User %s removed %s message(s) in %s", @@ -183,6 +104,10 @@ class Admin(commands.Cog): guild_ids=[config_get("guild")], ) async def reboot_cmd(self, ctx: ApplicationContext) -> None: + """Command /reboot + + Stops the bot. Is called "reboot" because it's assumed that the bot has automatic restart. + """ await ctx.defer(ephemeral=True) if ctx.user.id in self.client.owner_ids: diff --git a/cogs/analytics.py b/cogs/analytics.py index 1ca2068..b3a27c7 100644 --- a/cogs/analytics.py +++ b/cogs/analytics.py @@ -16,11 +16,13 @@ class Analytics(commands.Cog): @Cog.listener() async def on_message(self, message: Message) -> None: + """Listener that collects analytical data (stickers, attachments, messages).""" if ( (message.author != self.client.user) and (message.author.bot is False) and (message.author.system is False) ): + # Handle stickers stickers: List[Dict[str, Any]] = [] for sticker in message.stickers: @@ -33,6 +35,7 @@ class Analytics(commands.Cog): } ) + # Handle attachments attachments: List[Dict[str, Any]] = [] for attachment in message.attachments: @@ -49,6 +52,7 @@ class Analytics(commands.Cog): } ) + # Insert entry into the database await col_analytics.insert_one( { "user": message.author.id, diff --git a/cogs/custom_channels.py b/cogs/custom_channels.py index 54db27d..6a1651b 100644 --- a/cogs/custom_channels.py +++ b/cogs/custom_channels.py @@ -42,6 +42,10 @@ class CustomChannels(commands.Cog): async def custom_channel_get_cmd( self, ctx: ApplicationContext, name: str, reactions: bool, threads: bool ) -> None: + """Command /customchannel get + + Command to create a custom channel for a user. + """ holo_user_ctx: HoloUser = HoloUser(ctx.user) # Return if the user is using the command outside of a guild @@ -127,6 +131,10 @@ class CustomChannels(commands.Cog): async def custom_channel_edit_cmd( self, ctx: ApplicationContext, name: str, reactions: bool, threads: bool ) -> None: + """Command /customchannel edit + + Command to change properties of a custom channel. + """ holo_user_ctx: HoloUser = HoloUser(ctx.user) custom_channel: TextChannel | None = ds_utils.get( @@ -170,6 +178,9 @@ class CustomChannels(commands.Cog): async def custom_channel_remove_cmd( self, ctx: ApplicationContext, confirm: bool = False ) -> None: + """Command /customchannel remove [] + + Command to remove a custom channel. Requires additional confirmation.""" holo_user_ctx: HoloUser = HoloUser(ctx.user) # Return if the user does not have a custom channel diff --git a/cogs/data.py b/cogs/data.py index fc4e18f..b5af890 100644 --- a/cogs/data.py +++ b/cogs/data.py @@ -35,6 +35,9 @@ class Data(commands.Cog): "kind", description="Тип даних, які треба експортувати", choices=["Користувачі"] ) async def data_export_cmd(self, ctx: ApplicationContext, kind: str) -> None: + """Command /data export + + Command to export specific kind of data.""" await ctx.defer() # Return if the user is not an owner and not in the council @@ -107,6 +110,11 @@ class Data(commands.Cog): "kind", description="Тип даних, які треба експортувати", choices=["Користувачі"] ) async def data_migrate_cmd(self, ctx: ApplicationContext, kind: str) -> None: + """Command /migrate + + Command to migrate specific kind of data. + + Migration of users in this case means creation of their DB entries.""" await ctx.defer() # Return if the user is not an owner and not in the council diff --git a/cogs/fun.py b/cogs/fun.py index 98ff382..bb2bc58 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -27,6 +27,9 @@ class Fun(commands.Cog): ) @option("user", description="Користувач") async def action_cmd(self, ctx: ApplicationContext, type: str, user: User) -> None: + """Command /action + + Command to perform some RP action on a user and send them a GIF.""" await ctx.defer() action: str = await config_get("category", "actions", type) diff --git a/cogs/logger.py b/cogs/logger.py index 67b3bd1..e16997a 100644 --- a/cogs/logger.py +++ b/cogs/logger.py @@ -18,6 +18,7 @@ class Logger(commands.Cog): @commands.Cog.listener() async def on_message(self, message: Message): + """Message listener. All actions on messages remain here for now.""" if ( (message.author != self.client.user) and (message.author.bot is False) @@ -46,6 +47,7 @@ class Logger(commands.Cog): @commands.Cog.listener() async def on_member_join(self, member: Member) -> None: + """Member join handler. All actions on member join remain here for now.""" welcome_chan: TextChannel | None = ds_utils.get( self.client.get_guild(await config_get("guild")).channels, id=await config_get("welcome", "channels", "text"), diff --git a/cogs/utility.py b/cogs/utility.py index 3cab98c..e828dd9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -16,6 +16,7 @@ class Utility(commands.Cog): @commands.Cog.listener() async def on_ready(self) -> None: + """Listener for the event when bot connects to Discord and becomes "ready".""" logger.info("Logged in as %s", self.client.user) activity_type: str = await config_get("type", "status")