4 Commits

Author SHA1 Message Date
6087349622 Merge pull request 'v0.1.0-rc.4' (#19) from dev into main
Reviewed-on: #19
2024-12-27 21:31:46 +02:00
3010dc02bc Merge pull request 'v0.1.0-rc.3' (#15) from dev into main
Reviewed-on: #15
2024-12-16 22:08:22 +02:00
4afcbc93d5 Merge pull request 'Fixed minor issues' (#6) from dev into main
Reviewed-on: #6
2024-06-23 13:14:08 +03:00
72ccaa04a4 Merge pull request 'v0.1.0-rc.1' (#5) from dev into main
Reviewed-on: #5
2024-06-23 13:06:12 +03:00
15 changed files with 67 additions and 283 deletions

View File

@@ -24,4 +24,4 @@ USER appuser
COPY . . COPY . .
ENTRYPOINT ["python", "main.py", "--migrate"] ENTRYPOINT ["python", "main.py"]

View File

@@ -10,7 +10,7 @@
## Installation from release ## Installation from release
1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation) 1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation)
2. Install Python 3.11+ 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 3. Download the [latest release](https://git.end-play.xyz/HoloUA/Discord/releases/latest)'s archive
4. Extract the archive 4. Extract the archive
5. Navigate to the extracted folder and subfolder `Discord` in it 5. Navigate to the extracted folder and subfolder `Discord` in it
@@ -19,38 +19,20 @@
7. Activate the virtual environment: 7. Activate the virtual environment:
Windows: `.venv\Scripts\activate.bat` Windows: `.venv\Scripts\activate.bat`
Linux/macOS: `.venv/bin/activate` Linux/macOS: `.venv/bin/activate`
8. Install the dependencies: 8. Install dependencies:
`python -m pip install -r requirements.txt` `python -m pip install -r requirements.txt`
9. Run the bot with `python main.py` after completing the [configuration](#Configuration) 9. Run the bot with `python main.py` after completing the [configuration](#Configuration)
## Installation with Git ## Installation with Git
1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation) 1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation)
2. Install Python 3.11+ 2. Install Python 3.9+ (3.11+ is recommended)
3. Clone the repository: 3. Clone the repository:
`git clone https://git.end-play.xyz/HoloUA/Discord.git` `git clone https://git.end-play.xyz/HoloUA/Discord.git`
4. `cd Discord` 4. `cd Discord`
5. Create a virtual environment: 5. Install dependencies:
`python -m venv .venv` or `virtualenv .venv`
6. Activate the virtual environment:
Windows: `.venv\Scripts\activate.bat`
Linux/macOS: `.venv/bin/activate`
7. Install the dependencies:
`python -m pip install -r requirements.txt` `python -m pip install -r requirements.txt`
8. Run the bot with `python main.py` after completing the [configuration](#Configuration) 6. Run the bot with `python main.py` after completing the [configuration](#Configuration)
## Upgrading with Git
1. Go to the bot's directory
2. `git pull`
3. Activate the virtual environment:
Windows: `.venv\Scripts\activate.bat`
Linux/macOS: `.venv/bin/activate`
4. Update the dependencies:
`python -m pip install -r requirements.txt`
5. First start after the upgrade must initiate the migration:
`python main.py --migrate`
6. Now the bot is up to date and the next run will not require `--migrate` anymore
## Configuration ## Configuration

View File

@@ -1,79 +1,51 @@
import logging import logging
from logging import Logger
from typing import Any, Dict from typing import Any, Dict
from bson import ObjectId from bson import ObjectId
from discord import User, Member from discord import User, Member
from libbot.utils import config_get from libbot.utils import config_get
from pymongo.results import InsertOneResult
from errors import UserNotFoundError from errors import UserNotFoundError
from modules.database import col_warnings, col_users from modules.database import col_warnings, sync_col_users, sync_col_warnings, col_users
logger: Logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class HoloUser: class HoloUser:
def __init__( def __init__(self, user: User | Member | int) -> None:
self,
_id: ObjectId,
id: int,
custom_role: int | None,
custom_channel: int | None,
) -> None:
self._id: ObjectId = _id
self.id: int = id
self.custom_role: int | None = custom_role
self.custom_channel: int | None = custom_channel
@classmethod
async def from_user(
cls, user: User | Member, allow_creation: bool = True
) -> "HoloUser":
"""Get an object that has a proper binding between Discord ID and database """Get an object that has a proper binding between Discord ID and database
### Args: ### Args:
* `user` (User | Member): Object from which an ID can be extracted * `user` (User | Member | int): Object from which ID can be extracted
### Raises: ### Raises:
* `UserNotFoundError`: User with such ID does not seem to exist in database * `UserNotFoundError`: User with such ID does not seem to exist in database
""" """
db_entry: Dict[str, Any] | None = await col_users.find_one({"user": user.id})
if db_entry is None: self.id: int = user if not hasattr(user, "id") else user.id
if not allow_creation:
raise UserNotFoundError(user=user, user_id=user.id)
db_entry = { jav_user: Dict[str, Any] | None = sync_col_users.find_one({"user": self.id})
"user": user.id,
"custom_role": None,
"custom_channel": None,
}
insert_result: InsertOneResult = await col_users.insert_one(db_entry) if jav_user is None:
raise UserNotFoundError(user=user, user_id=self.id)
db_entry["_id"] = insert_result.inserted_id() self.db_id: ObjectId = jav_user["_id"]
db_entry["id"] = db_entry.pop("user") self.customrole: int | None = jav_user["customrole"]
self.customchannel: int | None = jav_user["customchannel"]
self.warnings: int = self.warns()
return cls(**db_entry) def warns(self) -> int:
@classmethod
async def from_id(cls, user_id: int) -> "HoloUser":
return NotImplemented
async def get_warnings(self) -> int:
"""Get number of warnings user has """Get number of warnings user has
### Returns: ### Returns:
* `int`: Number of warnings * `int`: Number of warnings
""" """
warns: Dict[str, Any] | None = await 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"] return 0 if warns is None else warns["warns"]
async def warn(self, count: int = 1, reason: str = "Not provided") -> None: async def warn(self, count=1, reason: str = "Not provided") -> None:
"""Warn and add count to warns number """Warn and add count to warns number
### Args: ### Args:
@@ -83,7 +55,7 @@ class HoloUser:
if warns is not None: if warns is not None:
await col_warnings.update_one( await col_warnings.update_one(
{"_id": self._id}, {"_id": self.db_id},
{"$set": {"warns": warns["warns"] + count}}, {"$set": {"warns": warns["warns"] + count}},
) )
else: else:
@@ -91,8 +63,8 @@ class HoloUser:
logger.info("User %s was warned %s times due to: %s", self.id, count, reason) logger.info("User %s was warned %s times due to: %s", self.id, count, reason)
async def _set(self, key: str, value: Any) -> None: async def set(self, key: str, value: Any) -> None:
"""Set attribute data and save it into the database """Set attribute data and save it into database
### Args: ### Args:
* `key` (str): Attribute to be changed * `key` (str): Attribute to be changed
@@ -104,44 +76,11 @@ class HoloUser:
setattr(self, key, value) setattr(self, key, value)
await col_users.update_one( await col_users.update_one(
{"_id": self._id}, {"$set": {key: value}}, upsert=True {"_id": self.db_id}, {"$set": {key: value}}, upsert=True
) )
logger.info("Set attribute %s of user %s to %s", key, self.id, value) logger.info("Set attribute %s of user %s to %s", key, self.id, value)
async def _remove(self, key: str) -> None:
"""Remove attribute data and save it into the database
### Args:
* `key` (str): Attribute to be removed
"""
if not hasattr(self, key):
raise AttributeError()
setattr(self, key, None)
await col_users.update_one(
{"_id": self._id}, {"$unset": {key: None}}, upsert=True
)
logger.info("Removed attribute %s of user %s", key, self.id)
async def set_custom_channel(self, channel_id: int) -> None:
await self._set("custom_channel", channel_id)
async def set_custom_role(self, role_id: int) -> None:
await self._set("custom_role", role_id)
async def remove_custom_channel(self) -> None:
await self._remove("custom_channel")
async def remove_custom_role(self) -> None:
await self._remove("custom_role")
async def purge(self) -> None:
"""Completely remove user data from database. Will not remove transactions logs and warnings."""
await col_users.delete_one({"_id": self._id})
@staticmethod @staticmethod
async def is_moderator(member: User | Member) -> bool: async def is_moderator(member: User | Member) -> bool:
"""Check if user is moderator or council member """Check if user is moderator or council member
@@ -184,3 +123,8 @@ class HoloUser:
return True return True
return False return False
# def purge(self) -> None:
# """Completely remove data from database. Will not remove transactions logs and warnings."""
# col_users.delete_one(filter={"_id": self.db_id})
# self.unauthorize()

View File

@@ -1,6 +1,5 @@
import logging import logging
import sys import sys
from logging import Logger
from discord import ( from discord import (
ApplicationContext, ApplicationContext,
@@ -21,7 +20,7 @@ from modules.scheduler import scheduler
from modules.utils_sync import guild_name from modules.utils_sync import guild_name
from modules.waifu_pics import waifu_pics from modules.waifu_pics import waifu_pics
logger: Logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Admin(commands.Cog): class Admin(commands.Cog):

View File

@@ -1,5 +1,4 @@
import logging import logging
from logging import Logger
from typing import Dict, List, Any from typing import Dict, List, Any
from discord import Cog, Message from discord import Cog, Message
@@ -8,7 +7,7 @@ from discord.ext import commands
from classes.holo_bot import HoloBot from classes.holo_bot import HoloBot
from modules.database import col_analytics from modules.database import col_analytics
logger: Logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Analytics(commands.Cog): class Analytics(commands.Cog):

View File

@@ -1,5 +1,4 @@
import logging import logging
from logging import Logger
from typing import Any, Dict from typing import Any, Dict
from discord import ApplicationContext, Embed, option, TextChannel, Role from discord import ApplicationContext, Embed, option, TextChannel, Role
@@ -15,7 +14,7 @@ from enums import Color
from modules.database import col_users from modules.database import col_users
from modules.utils_sync import guild_name from modules.utils_sync import guild_name
logger: Logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CustomChannels(commands.Cog): class CustomChannels(commands.Cog):
@@ -25,7 +24,7 @@ class CustomChannels(commands.Cog):
@commands.Cog.listener() @commands.Cog.listener()
async def on_guild_channel_delete(self, channel: GuildChannel) -> None: async def on_guild_channel_delete(self, channel: GuildChannel) -> None:
await col_users.find_one_and_update( await col_users.find_one_and_update(
{"custom_channel": channel.id}, {"$set": {"custom_channel": None}} {"customchannel": channel.id}, {"$set": {"customchannel": None}}
) )
custom_channel_group: SlashCommandGroup = SlashCommandGroup( custom_channel_group: SlashCommandGroup = SlashCommandGroup(
@@ -47,7 +46,7 @@ class CustomChannels(commands.Cog):
Command to create a custom channel for a user. Command to create a custom channel for a user.
""" """
holo_user_ctx: HoloUser = await HoloUser.from_user(ctx.user) holo_user_ctx: HoloUser = HoloUser(ctx.user)
# Return if the user is using the command outside of a guild # Return if the user is using the command outside of a guild
if not hasattr(ctx.author, "guild"): if not hasattr(ctx.author, "guild"):
@@ -62,7 +61,7 @@ class CustomChannels(commands.Cog):
return return
# Return if the user already has a custom channel # Return if the user already has a custom channel
if holo_user_ctx.custom_channel is not None: if holo_user_ctx.customchannel is not None:
await ctx.defer(ephemeral=True) await ctx.defer(ephemeral=True)
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
@@ -80,7 +79,7 @@ class CustomChannels(commands.Cog):
reason=f"Користувач {guild_name(ctx.user)} отримав власний приватний канал", reason=f"Користувач {guild_name(ctx.user)} отримав власний приватний канал",
category=ds_utils.get( category=ds_utils.get(
ctx.author.guild.categories, ctx.author.guild.categories,
id=await config_get("custom_channels", "categories"), id=await config_get("customchannels", "categories"),
), ),
) )
@@ -100,7 +99,7 @@ class CustomChannels(commands.Cog):
manage_channels=True, manage_channels=True,
) )
await holo_user_ctx.set_custom_channel(created_channel.id) await holo_user_ctx.set("customchannel", created_channel.id)
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
@@ -136,10 +135,10 @@ class CustomChannels(commands.Cog):
Command to change properties of a custom channel. Command to change properties of a custom channel.
""" """
holo_user_ctx: HoloUser = await HoloUser.from_user(ctx.user) holo_user_ctx: HoloUser = HoloUser(ctx.user)
custom_channel: TextChannel | None = ds_utils.get( custom_channel: TextChannel | None = ds_utils.get(
ctx.guild.channels, id=holo_user_ctx.custom_channel ctx.guild.channels, id=holo_user_ctx.customchannel
) )
# Return if the channel was not found # Return if the channel was not found
@@ -182,10 +181,10 @@ class CustomChannels(commands.Cog):
"""Command /customchannel remove [<confirm>] """Command /customchannel remove [<confirm>]
Command to remove a custom channel. Requires additional confirmation.""" Command to remove a custom channel. Requires additional confirmation."""
holo_user_ctx: HoloUser = await HoloUser.from_user(ctx.user) holo_user_ctx: HoloUser = HoloUser(ctx.user)
# Return if the user does not have a custom channel # Return if the user does not have a custom channel
if holo_user_ctx.custom_channel is None: if holo_user_ctx.customchannel is None:
await ctx.defer(ephemeral=True) await ctx.defer(ephemeral=True)
await ctx.respond( await ctx.respond(
embed=Embed( embed=Embed(
@@ -199,7 +198,7 @@ class CustomChannels(commands.Cog):
await ctx.defer() await ctx.defer()
custom_channel: TextChannel | None = ds_utils.get( custom_channel: TextChannel | None = ds_utils.get(
ctx.guild.channels, id=holo_user_ctx.custom_channel ctx.guild.channels, id=holo_user_ctx.customchannel
) )
# Return if the channel was not found # Return if the channel was not found
@@ -211,7 +210,7 @@ class CustomChannels(commands.Cog):
color=Color.FAIL, color=Color.FAIL,
) )
) )
await holo_user_ctx.remove_custom_channel() await holo_user_ctx.set("customchannel", None)
return return
# Return if the confirmation is missing # Return if the confirmation is missing
@@ -227,7 +226,7 @@ class CustomChannels(commands.Cog):
await custom_channel.delete(reason="Власник запросив видалення") await custom_channel.delete(reason="Власник запросив видалення")
await holo_user_ctx.remove_custom_channel() await holo_user_ctx.set("customchannel", None)
try: try:
await ctx.respond( await ctx.respond(

View File

@@ -1,5 +1,4 @@
import logging import logging
from logging import Logger
from discord import ApplicationContext, Embed, User, option, slash_command from discord import ApplicationContext, Embed, User, option, slash_command
from discord.ext import commands from discord.ext import commands
@@ -9,7 +8,7 @@ from classes.holo_bot import HoloBot
from modules.utils_sync import guild_name from modules.utils_sync import guild_name
from modules.waifu_pics import waifu_pics from modules.waifu_pics import waifu_pics
logger: Logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Fun(commands.Cog): class Fun(commands.Cog):

View File

@@ -1,5 +1,4 @@
import logging import logging
from logging import Logger
from typing import Dict, Any from typing import Dict, Any
from discord import Member, Message, TextChannel, MessageType from discord import Member, Message, TextChannel, MessageType
@@ -10,7 +9,7 @@ from libbot.utils import config_get
from classes.holo_bot import HoloBot from classes.holo_bot import HoloBot
from modules.database import col_users from modules.database import col_users
logger: Logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Logger(commands.Cog): class Logger(commands.Cog):
@@ -40,22 +39,11 @@ class Logger(commands.Cog):
(message.type == MessageType.thread_created) (message.type == MessageType.thread_created)
and (message.channel is not None) and (message.channel is not None)
and ( and (
await col_users.count_documents({"custom_channel": message.channel.id}) await col_users.count_documents({"customchannel": message.channel.id})
> 0 > 0
) )
): ):
try: await message.delete()
logger.info(
"Deleting the thread creation message in a custom channel %s",
message.channel.id,
)
await message.delete()
except Exception as exc:
logger.warning(
"Could not delete the thread creation message in a custom channel %s due to %s",
message.channel.id,
exc,
)
@commands.Cog.listener() @commands.Cog.listener()
async def on_member_join(self, member: Member) -> None: async def on_member_join(self, member: Member) -> None:

View File

@@ -28,12 +28,12 @@
}, },
"defaults": { "defaults": {
"user": { "user": {
"custom_role": null, "customrole": null,
"custom_channel": null "customchannel": null
} }
}, },
"categories": { "categories": {
"custom_channels": 0 "customchannels": 0
}, },
"channels": { "channels": {
"text": { "text": {

28
main.py
View File

@@ -1,7 +1,5 @@
import contextlib
import logging import logging
import sys import sys
from argparse import ArgumentParser
from logging import Logger from logging import Logger
from pathlib import Path from pathlib import Path
@@ -9,7 +7,6 @@ from discord import LoginFailure, Intents
from libbot.utils import config_get from libbot.utils import config_get
from classes.holo_bot import HoloBot from classes.holo_bot import HoloBot
from modules.migrator import migrate_database
from modules.scheduler import scheduler from modules.scheduler import scheduler
logging.basicConfig( logging.basicConfig(
@@ -20,24 +17,12 @@ logging.basicConfig(
logger: Logger = logging.getLogger(__name__) logger: Logger = logging.getLogger(__name__)
# Declare the parser that retrieves the command line arguments try:
parser = ArgumentParser( import uvloop # type: ignore
prog="HoloUA Discord",
description="Discord bot for the HoloUA community.",
)
# Add a switch argument --migrate to be parsed...
parser.add_argument("--migrate", action="store_true")
# ...and parse the arguments we added
args = parser.parse_args()
# Try to import the module that improves performance
# and ignore errors when module is not installed
with contextlib.suppress(ImportError):
import uvloop
uvloop.install() uvloop.install()
except ImportError:
pass
def main() -> None: def main() -> None:
@@ -47,11 +32,6 @@ def main() -> None:
) )
sys.exit() sys.exit()
# Perform migration if command line argument was provided
if args.migrate:
logger.info("Performing migrations...")
migrate_database()
intents: Intents = Intents().all() intents: Intents = Intents().all()
client: HoloBot = HoloBot(intents=intents, scheduler=scheduler) client: HoloBot = HoloBot(intents=intents, scheduler=scheduler)

View File

@@ -1,79 +0,0 @@
import logging
from logging import Logger
from libbot.utils import config_get, config_set, config_delete
from mongodb_migrations.base import BaseMigration
logger: Logger = logging.getLogger(__name__)
class Migration(BaseMigration):
def upgrade(self):
try:
# Categories
config_set(
"custom_channels",
config_get("customchannels", "categories"),
"categories",
)
config_delete("customchannels", "categories")
# User defaults
config_delete(
"user",
"defaults",
)
except Exception as exc:
logger.error(
"Could not upgrade the config during migration '%s' due to: %s",
__name__,
exc,
)
self.db.users.update_many(
{"customchannel": {"$exists": True}},
{"$rename": {"customchannel": "custom_channel"}},
)
self.db.users.update_many(
{"customrole": {"$exists": True}},
{"$rename": {"customrole": "custom_role"}},
)
def downgrade(self):
try:
# Categories
config_set(
"customchannels",
config_get("custom_channels", "categories"),
"categories",
)
config_delete("custom_channels", "categories")
# User defaults
config_set(
"customrole",
None,
"defaults",
"user",
)
config_set(
"customchannel",
None,
"defaults",
"user",
)
except Exception as exc:
logger.error(
"Could not downgrade the config during migration '%s' due to: %s",
__name__,
exc,
)
self.db.users.update_many(
{"custom_channel": {"$exists": True}},
{"$rename": {"custom_channel": "customchannel"}},
)
self.db.users.update_many(
{"custom_role": {"$exists": True}},
{"$rename": {"custom_role": "customrole"}},
)

View File

@@ -1,22 +0,0 @@
from typing import Any, Mapping
from libbot.utils import config_get
from mongodb_migrations.cli import MigrationManager
from mongodb_migrations.config import Configuration
def migrate_database() -> None:
"""Apply migrations from folder `migrations/` to the database"""
database_config: Mapping[str, Any] = config_get("database")
manager_config = Configuration(
{
"mongo_host": database_config["host"],
"mongo_port": database_config["port"],
"mongo_database": database_config["name"],
"mongo_username": database_config["user"],
"mongo_password": database_config["password"],
}
)
manager = MigrationManager(manager_config)
manager.run()

View File

@@ -5,7 +5,7 @@ requests>=2.32.2
aiofiles~=24.1.0 aiofiles~=24.1.0
apscheduler>=3.10.0 apscheduler>=3.10.0
async_pymongo==0.1.11 async_pymongo==0.1.11
libbot[speed,pycord]==4.0.2 libbot[speed,pycord]==4.0.0
mongodb-migrations==1.3.1 typing-extensions~=4.12.2
ujson~=5.10.0 ujson~=5.10.0
WaifuPicsPython==0.2.0 WaifuPicsPython==0.2.0

View File

@@ -2,26 +2,21 @@
"$jsonSchema": { "$jsonSchema": {
"required": [ "required": [
"user", "user",
"custom_role", "customrole",
"custom_channel" "customchannel"
], ],
"properties": { "properties": {
"user": { "user": {
"bsonType": "long", "bsonType": "long",
"description": "Discord ID of user" "description": "Discord ID of user"
}, },
"custom_role": { "customrole": {
"bsonType": [ "bsonType": ["null", "long"],
"null",
"long"
],
"description": "Discord ID of custom role or 'null' if not set" "description": "Discord ID of custom role or 'null' if not set"
}, },
"custom_channel": { "customchannel": {
"bsonType": [ "bsonType": ["null", "long"],
"null",
"long"
],
"description": "Discord ID of custom channel or 'null' if not set" "description": "Discord ID of custom channel or 'null' if not set"
} }
} }

View File

@@ -2,14 +2,14 @@
"$jsonSchema": { "$jsonSchema": {
"required": [ "required": [
"user", "user",
"warnings" "warns"
], ],
"properties": { "properties": {
"user": { "user": {
"bsonType": "long", "bsonType": "long",
"description": "Discord ID of user" "description": "Discord ID of user"
}, },
"warnings": { "warns": {
"bsonType": "int", "bsonType": "int",
"description": "Number of warnings on count" "description": "Number of warnings on count"
} }