8 Commits

16 changed files with 244 additions and 116 deletions

40
.dockerignore Normal file
View File

@@ -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

27
Dockerfile Normal file
View File

@@ -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"]

View File

@@ -7,23 +7,41 @@
<a href="https://git.end-play.xyz/HoloUA/Discord"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a> <a href="https://git.end-play.xyz/HoloUA/Discord"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
</p> </p>
## Installation ## 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. `git clone https://git.end-play.xyz/HoloUA/Discord.git` 2. Install Python 3.9+ (3.11+ is recommended)
3. `cd Discord` 3. Download the [latest release](https://git.end-play.xyz/HoloUA/Discord/releases/latest)'s archive
4. Install Python 3.9+ (at least 3.11 is recommended) for your OS 4. Extract the archive
5. `python3 -m pip install -r requirements.txt` 5. Navigate to the extracted folder and subfolder `Discord` in it
6. Run it with `python3 main.py` after configuring 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 ## Configuration
There's a file `config_example.json` which contains default 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: Mandatory keys to modify:
@@ -35,4 +53,23 @@ Mandatory keys to modify:
- channels.* - channels.*
- roles.* - roles.*
After all of that you're good to go! Happy using :) 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`

6
classes/holo_bot.py Normal file
View File

@@ -0,0 +1,6 @@
from libbot.pycord.classes import PycordBot
class HoloBot(PycordBot):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -3,7 +3,7 @@ from typing import Any, Union, Dict
from bson import ObjectId from bson import ObjectId
from discord import User, Member from discord import User, Member
from libbot import config_get from libbot.utils import config_get
from errors import UserNotFoundError from errors import UserNotFoundError
from modules.database import col_warnings, sync_col_users, sync_col_warnings, col_users from modules.database import col_warnings, sync_col_users, sync_col_warnings, col_users

View File

@@ -13,10 +13,9 @@ from discord import (
) )
from discord import utils as ds_utils from discord import utils as ds_utils
from discord.ext import commands from discord.ext import commands
from libbot import config_get from libbot.utils 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 enums import Color
from modules.scheduler import scheduler from modules.scheduler import scheduler
from modules.utils_sync import guild_name from modules.utils_sync import guild_name
@@ -28,8 +27,8 @@ logger = logging.getLogger(__name__)
class Admin(commands.Cog): class Admin(commands.Cog):
"""Cog with utility commands for admins.""" """Cog with utility commands for admins."""
def __init__(self, client: PycordBot): def __init__(self, client: HoloBot):
self.client: PycordBot = client self.client: HoloBot = client
# Disabled because warning functionality is temporarily not needed # Disabled because warning functionality is temporarily not needed
# @slash_command( # @slash_command(
@@ -117,7 +116,7 @@ class Admin(commands.Cog):
@slash_command( @slash_command(
name="clear", name="clear",
description="Видалити деяку кількість повідомлень в каналі", description="Видалити деяку кількість повідомлень в каналі",
guild_ids=[sync_config_get("guild")], guild_ids=[config_get("guild")],
) )
@option("amount", description="Кількість") @option("amount", description="Кількість")
@option("user", description="Користувач", default=None) @option("user", description="Користувач", default=None)
@@ -182,7 +181,7 @@ class Admin(commands.Cog):
@slash_command( @slash_command(
name="reboot", name="reboot",
description="Перезапустити бота", description="Перезапустити бота",
guild_ids=[sync_config_get("guild")], guild_ids=[config_get("guild")],
) )
async def reboot_cmd(self, ctx: ApplicationContext) -> None: async def reboot_cmd(self, ctx: ApplicationContext) -> None:
await ctx.defer(ephemeral=True) await ctx.defer(ephemeral=True)
@@ -236,5 +235,5 @@ class Admin(commands.Cog):
) )
def setup(client: PycordBot) -> None: def setup(client: HoloBot) -> None:
client.add_cog(Admin(client)) client.add_cog(Admin(client))

View File

@@ -3,16 +3,16 @@ from typing import Dict, List, Any
from discord import Cog, Message from discord import Cog, Message
from discord.ext import commands from discord.ext import commands
from libbot.pycord.classes import PycordBot
from classes.holo_bot import HoloBot
from modules.database import col_analytics from modules.database import col_analytics
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Analytics(commands.Cog): class Analytics(commands.Cog):
def __init__(self, client: PycordBot): def __init__(self, client: HoloBot):
self.client: PycordBot = client self.client: HoloBot = client
@Cog.listener() @Cog.listener()
async def on_message(self, message: Message) -> None: 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)) client.add_cog(Analytics(client))

View File

@@ -6,10 +6,9 @@ from discord import utils as ds_utils
from discord.abc import GuildChannel from discord.abc import GuildChannel
from discord.commands import SlashCommandGroup from discord.commands import SlashCommandGroup
from discord.ext import commands from discord.ext import commands
from libbot import config_get from libbot.utils 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 classes.holo_user import HoloUser
from enums import Color from enums import Color
from modules.database import col_users from modules.database import col_users
@@ -19,8 +18,8 @@ logger = logging.getLogger(__name__)
class CustomChannels(commands.Cog): class CustomChannels(commands.Cog):
def __init__(self, client: PycordBot): def __init__(self, client: HoloBot):
self.client: PycordBot = client self.client: HoloBot = client
@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:
@@ -35,7 +34,7 @@ class CustomChannels(commands.Cog):
@custom_channel_group.command( @custom_channel_group.command(
name="get", name="get",
description="Отримати персональний текстовий канал", description="Отримати персональний текстовий канал",
guild_ids=[sync_config_get("guild")], guild_ids=[config_get("guild")],
) )
@option("name", description="Назва каналу") @option("name", description="Назва каналу")
@option("reactions", description="Дозволити реакції") @option("reactions", description="Дозволити реакції")
@@ -122,7 +121,7 @@ class CustomChannels(commands.Cog):
@custom_channel_group.command( @custom_channel_group.command(
name="edit", name="edit",
description="Змінити параметри особистого каналу", description="Змінити параметри особистого каналу",
guild_ids=[sync_config_get("guild")], guild_ids=[config_get("guild")],
) )
@option("name", description="Назва каналу") @option("name", description="Назва каналу")
@option("reactions", description="Дозволити реакції") @option("reactions", description="Дозволити реакції")
@@ -167,7 +166,7 @@ class CustomChannels(commands.Cog):
@custom_channel_group.command( @custom_channel_group.command(
name="remove", name="remove",
description="Відібрати канал, знищуючи його, та частково повернути кошти", description="Відібрати канал, знищуючи його, та частково повернути кошти",
guild_ids=[sync_config_get("guild")], guild_ids=[config_get("guild")],
) )
@option("confirm", description="Підтвердження операції") @option("confirm", description="Підтвердження операції")
async def custom_channel_remove_cmd( async def custom_channel_remove_cmd(
@@ -234,5 +233,5 @@ class CustomChannels(commands.Cog):
) )
def setup(client: PycordBot) -> None: def setup(client: HoloBot) -> None:
client.add_cog(CustomChannels(client)) client.add_cog(CustomChannels(client))

View File

@@ -1,4 +1,5 @@
import logging import logging
from logging import Logger
from os import makedirs from os import makedirs
from pathlib import Path from pathlib import Path
from typing import Union, List, Dict, Any from typing import Union, List, Dict, Any
@@ -8,29 +9,27 @@ from discord import ApplicationContext, Embed, File, option, Role, TextChannel
from discord import utils as ds_utils from discord import utils as ds_utils
from discord.commands import SlashCommandGroup from discord.commands import SlashCommandGroup
from discord.ext import commands from discord.ext import commands
from libbot import config_get from libbot.utils import config_get, json_write
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 classes.holo_user import HoloUser
from enums import Color 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 = logging.getLogger(__name__) logger: Logger = logging.getLogger(__name__)
class Data(commands.Cog): class Data(commands.Cog):
def __init__(self, client: PycordBot): def __init__(self, client: HoloBot):
self.client: PycordBot = client self.client: HoloBot = client
data: SlashCommandGroup = SlashCommandGroup("data", "Керування даними користувачів") data: SlashCommandGroup = SlashCommandGroup("data", "Керування даними користувачів")
@data.command( @data.command(
name="export", name="export",
description="Експортувати дані", description="Експортувати дані",
guild_ids=[sync_config_get("guild")], guild_ids=[config_get("guild")],
) )
@option( @option(
"kind", description="Тип даних, які треба експортувати", choices=["Користувачі"] "kind", description="Тип даних, які треба експортувати", choices=["Користувачі"]
@@ -93,14 +92,16 @@ class Data(commands.Cog):
} }
) )
sync_json_write(users, Path(f"tmp/{uuid}")) # 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")) await ctx.respond(file=File(Path(f"tmp/{uuid}"), filename="users.json"))
@data.command( @data.command(
name="migrate", name="migrate",
description="Мігрувати всіх користувачів до бази", description="Мігрувати всіх користувачів до бази",
guild_ids=[sync_config_get("guild")], guild_ids=[config_get("guild")],
) )
@option( @option(
"kind", description="Тип даних, які треба експортувати", choices=["Користувачі"] "kind", description="Тип даних, які треба експортувати", choices=["Користувачі"]
@@ -179,5 +180,5 @@ class Data(commands.Cog):
) )
def setup(client: PycordBot) -> None: def setup(client: HoloBot) -> None:
client.add_cog(Data(client)) client.add_cog(Data(client))

View File

@@ -2,10 +2,9 @@ import logging
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
from libbot import config_get from libbot.utils 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.utils_sync import guild_name
from modules.waifu_pics import waifu_pics from modules.waifu_pics import waifu_pics
@@ -13,18 +12,18 @@ logger = logging.getLogger(__name__)
class Fun(commands.Cog): class Fun(commands.Cog):
def __init__(self, client: PycordBot): def __init__(self, client: HoloBot):
self.client: PycordBot = client self.client: HoloBot = client
@slash_command( @slash_command(
name="action", name="action",
description="Провести над користувачем РП дію", description="Провести над користувачем РП дію",
guild_ids=[sync_config_get("guild")], guild_ids=[config_get("guild")],
) )
@option( @option(
"type", "type",
description="Тип дії, яку хочете провести з користувачем", description="Тип дії, яку хочете провести з користувачем",
choices=sync_config_get("actions").keys(), choices=config_get("actions").keys(),
) )
@option("user", description="Користувач") @option("user", description="Користувач")
async def action_cmd(self, ctx: ApplicationContext, type: str, user: User) -> None: async def action_cmd(self, ctx: ApplicationContext, type: str, user: User) -> None:
@@ -54,5 +53,5 @@ class Fun(commands.Cog):
await ctx.respond(embed=embed) await ctx.respond(embed=embed)
def setup(client: PycordBot) -> None: def setup(client: HoloBot) -> None:
client.add_cog(Fun(client)) client.add_cog(Fun(client))

View File

@@ -3,15 +3,15 @@ from typing import Dict, Any, Union
from discord import Member, Message, TextChannel from discord import Member, Message, TextChannel
from discord import utils as ds_utils from discord import utils as ds_utils
from discord.ext import commands from discord.ext import commands
from libbot import config_get from libbot.utils import config_get
from libbot.pycord.classes import PycordBot
from classes.holo_bot import HoloBot
from modules.database import col_users from modules.database import col_users
class Logger(commands.Cog): class Logger(commands.Cog):
def __init__(self, client: PycordBot): def __init__(self, client: HoloBot):
self.client: PycordBot = client self.client: HoloBot = client
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message: Message): async def on_message(self, message: Message):
@@ -65,5 +65,5 @@ class Logger(commands.Cog):
await col_users.insert_one(document=user) await col_users.insert_one(document=user)
def setup(client: PycordBot) -> None: def setup(client: HoloBot) -> None:
client.add_cog(Logger(client)) client.add_cog(Logger(client))

57
cogs/utility.py Normal file
View File

@@ -0,0 +1,57 @@
import logging
from logging import Logger
from discord import Activity, ActivityType
from discord.ext import commands
from libbot.utils 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))

64
main.py
View File

@@ -1,12 +1,12 @@
import logging import logging
import sys import sys
from logging import Logger from logging import Logger
from pathlib import Path
from discord import Activity, ActivityType from discord import LoginFailure, Intents
from libbot import config_get from libbot.utils import config_get
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 from modules.scheduler import scheduler
logging.basicConfig( logging.basicConfig(
@@ -25,53 +25,25 @@ except ImportError:
pass 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: 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") client.load_extension("cogs")
try: try:
scheduler.start() client.run(config_get("bot_token", "bot"))
client.run(sync_config_get("bot_token", "bot")) except LoginFailure as exc:
logger.error("Provided bot token is invalid: %s", exc)
except KeyboardInterrupt: except KeyboardInterrupt:
scheduler.shutdown() logger.info("KeyboardInterrupt received: Shutting down gracefully.")
finally:
sys.exit() sys.exit()

View File

@@ -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)

View File

@@ -1,12 +1,12 @@
from typing import Dict, Any from typing import Dict, Any
from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase 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 import MongoClient
from pymongo.synchronous.collection import Collection from pymongo.synchronous.collection import Collection
from pymongo.synchronous.database import Database 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 = ( con_string: str = (
"mongodb://{0}:{1}/{2}".format( "mongodb://{0}:{1}/{2}".format(

View File

@@ -5,6 +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]==3.2.3 libbot[speed,pycord]==4.0.0
typing-extensions~=4.12.2
ujson~=5.10.0 ujson~=5.10.0
WaifuPicsPython==0.2.0 WaifuPicsPython==0.2.0