Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
DB_PORT=5432

# ==========
# Minio
# ==========
MINIO_ROOT_USER=minio-admin
MINIO_ROOT_PASSWORD=minio-admin

# ==========
# Redis
# ==========
Expand Down
24 changes: 24 additions & 0 deletions db/minio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import logging

from exceptions.minio import BucketsUncreatedError
from services.settings import settings
from utils.minio import ensure_bucket

logger = logging.getLogger(__name__)


async def init_minio() -> None:
for bucket_name in settings.MINIO_BUCKETS:
ensure_bucket(bucket_name)
logger.info("Minio инициализирована")


async def test_minio() -> None:
"""Проверка подключения к Minio"""

created_buckets = {b.name for b in settings.minio.list_buckets()}
logger.debug("Minio created buckets %s", created_buckets)
for bucket in settings.MINIO_BUCKETS:
if bucket not in created_buckets:
raise BucketsUncreatedError(bucket)
logger.info("Соединение с Minio успешно")
2 changes: 1 addition & 1 deletion db/models/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
class Campaign(TimestampedModel, UuidModel):
title = fields.CharField(max_length=255)
description = fields.CharField(max_length=1023, default="")
icon = fields.CharField(max_length=1023, default="")
icon = fields.UUIDField(null=True)
verified = fields.BooleanField(default=0)
10 changes: 9 additions & 1 deletion db/main.py → db/postgres.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from tortoise import Tortoise
from tortoise import Tortoise, connections

from services.settings import settings

Expand All @@ -22,3 +22,11 @@ async def close_db() -> None:

await Tortoise.close_connections()
logger.info("Tortoise ORM соединения закрыты")


async def test_db() -> None:
"""Проверка подключения к DB"""

conn = connections.get("default")
await conn.execute_query("SELECT 1")
logger.info("Соединение с DB успешно")
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
ports:
- "8080:8080"

minio:
image: minio/minio
command: server /data --console-address ":9001"
env_file:
- .env
volumes:
- minio_data:/data
ports:
- "9000:9000"
- "9001:9001"

bot:
build:
context: .
Expand All @@ -38,3 +49,4 @@
volumes:
postgres_data:
redis_data:
minio_data:
6 changes: 6 additions & 0 deletions exceptions/minio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class BucketsUncreatedError(Exception):
def __init__(self, bucket_name: str) -> None:
self.bucket_name = bucket_name

def __str__(self) -> str:
return f"Bucket {self.bucket_name} wasn't created."
6 changes: 3 additions & 3 deletions handlers/admin/campaign_manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from aiogram.enums import ContentType
from aiogram.types import CallbackQuery
from aiogram_dialog import Data, Dialog, DialogManager, Window
from aiogram_dialog.api.entities import MediaAttachment, MediaId
from aiogram_dialog.api.entities import MediaAttachment
from aiogram_dialog.widgets.kbd import Button, Cancel, Group
from aiogram_dialog.widgets.media import DynamicMedia
from aiogram_dialog.widgets.text import Const, Format
Expand All @@ -32,8 +32,8 @@ async def get_campaign_manage_data(dialog_manager: DialogManager, **kwargs):
participation: Participation = await Participation.get(id=participation_id)

icon = None
if file_id := campaign.icon:
icon = MediaAttachment(type=ContentType.PHOTO, file_id=MediaId(file_id))
if object_name := campaign.icon:
icon = MediaAttachment(type=ContentType.PHOTO, path=f"minio://campaign-icons:{object_name}")

return {
"campaign_title": campaign.title,
Expand Down
24 changes: 18 additions & 6 deletions handlers/admin/create_campaign.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
from typing import TYPE_CHECKING
import uuid
from typing import TYPE_CHECKING, BinaryIO

from aiogram import Router
from aiogram.enums import ContentType
from aiogram.types import CallbackQuery, Message
from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.api.entities import MediaAttachment, MediaId
from aiogram_dialog.api.entities import MediaAttachment
from aiogram_dialog.widgets.input import ManagedTextInput, MessageInput, TextInput
from aiogram_dialog.widgets.kbd import Back, Button, Cancel, Next, Row
from aiogram_dialog.widgets.media import DynamicMedia
Expand All @@ -28,8 +29,8 @@
# === Гетеры ===
async def get_confirm_data(dialog_manager: DialogManager, **kwargs):
icon = None
if file_id := dialog_manager.dialog_data.get("icon"):
icon = MediaAttachment(type=ContentType.PHOTO, file_id=MediaId(file_id))
if object_name := dialog_manager.dialog_data.get("icon"):
icon = MediaAttachment(type=ContentType.PHOTO, path=f"minio://campaign-icons:{object_name}")

return {
"title": dialog_manager.dialog_data.get("title", ""),
Expand Down Expand Up @@ -59,7 +60,7 @@ async def on_description_entered(
text: str,
):
if len(text) > settings.MAX_DESCRIPTION_LEN:
mes.answer("Максимум 1023 символа, можно пропустить")
await mes.answer("Максимум 1023 символа, можно пропустить")
return
dialog_manager.dialog_data["description"] = text
await dialog_manager.next()
Expand All @@ -68,7 +69,18 @@ async def on_description_entered(
async def on_icon_entered(mes: Message, wid: MessageInput, dialog_manager: DialogManager):
if mes.photo:
photo = mes.photo[-1]
dialog_manager.dialog_data["icon"] = photo.file_id
file = await mes.bot.get_file(photo.file_id)
bin_stream: BinaryIO = await mes.bot.download_file(file.file_path)

object_id = uuid.uuid4()
settings.minio.put_object(
"campaign-icons",
str(object_id),
bin_stream,
file.file_size,
)

dialog_manager.dialog_data["icon"] = object_id

await dialog_manager.next()
else:
Expand Down
8 changes: 4 additions & 4 deletions handlers/admin/edit_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from aiogram.enums import ContentType
from aiogram.types import CallbackQuery, Message
from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.api.entities import MediaAttachment, MediaId
from aiogram_dialog.api.entities import MediaAttachment
from aiogram_dialog.widgets.input import ManagedTextInput, MessageInput, TextInput
from aiogram_dialog.widgets.kbd import Button, Cancel, Column, SwitchTo
from aiogram_dialog.widgets.media import DynamicMedia
Expand Down Expand Up @@ -36,8 +36,8 @@ async def get_campaign_edit_data(dialog_manager: DialogManager, **kwargs):
dialog_manager.dialog_data["new_data"] = {}

icon = None
if file_id := dialog_manager.dialog_data["new_data"].get("icon", campaign.icon):
icon = MediaAttachment(type=ContentType.PHOTO, file_id=MediaId(file_id))
if object_name := dialog_manager.dialog_data["new_data"].get("icon", campaign.icon):
icon = MediaAttachment(type=ContentType.PHOTO, path=f"minio://campaign-icons:{object_name}")

return {
"campaign_title": dialog_manager.dialog_data["new_data"].get("title", campaign.title),
Expand Down Expand Up @@ -93,7 +93,7 @@ async def on_icon_entered(mes: Message, wid: MessageInput, dialog_manager: Dialo
if mes.photo:
photo = mes.photo[-1]

dialog_manager.dialog_data["new_data"]["icon"] = photo.file_id
dialog_manager.dialog_data["new_data"]["icon"] = photo.file_unique_id

await dialog_manager.switch_to(states.EditCampaignInfo.confirm)
else:
Expand Down
23 changes: 19 additions & 4 deletions handlers/player/academy_campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
from uuid import UUID

from aiogram import Router
from aiogram.enums import ContentType
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.api.entities import MediaAttachment
from aiogram_dialog.widgets.kbd import Button, Cancel, ScrollingGroup, Select
from aiogram_dialog.widgets.media import DynamicMedia
from aiogram_dialog.widgets.text import Const, Format

from db.models import Campaign, Participation
from states.academy_campaigns import AcademyCampaignPreview, AcademyCampaigns
from utils.redirect import redirect
from utils.role import Role

logger = logging.getLogger(__name__)
router = Router()
Expand All @@ -31,10 +35,21 @@ async def on_campaign(c: CallbackQuery, b: Button, m: DialogManager, participati


async def campaign_getter(dialog_manager: DialogManager, **kwargs):
campaign = await Campaign.get(id=dialog_manager.start_data["campaign_id"])
campaign_id = dialog_manager.start_data.get("campaign_id")
participation_id = dialog_manager.start_data.get("participation_id")

campaign = await Campaign.get(id=campaign_id)
participation: Participation = await Participation.get(id=participation_id)

icon = None
if object_name := campaign.icon:
icon = MediaAttachment(type=ContentType.PHOTO, path=f"minio://campaign-icons:{object_name}")

return {
"title": campaign.title,
"description": campaign.description,
"description": campaign.description or "Описание отсутствует",
"icon": icon,
"is_owner": participation.role == Role.OWNER,
}


Expand Down Expand Up @@ -66,8 +81,8 @@ async def campaign_getter(dialog_manager: DialogManager, **kwargs):
router.include_router(
Dialog(
Window(
Const("Инфа о кампании"),
Format("{title}\n{description}"),
Format("Информация о кампании: {title}\n\nОписание: {description}\n\nВыберите действие:"),
DynamicMedia("icon"),
Cancel(Const("Назад")),
getter=campaign_getter,
state=AcademyCampaignPreview.preview,
Expand Down
12 changes: 10 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
from aiogram.fsm.storage.redis import RedisStorage
from aiogram_dialog import setup_dialogs

from db.main import close_db, init_db
from db.minio import init_minio, test_minio
from db.postgres import close_db, init_db, test_db
from services.settings import settings
from utils import json
from utils.minio import MinioMessageManager

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -132,7 +134,7 @@ async def run_bot(
register_all_middlewares(dp, path=MIDDLEWARE_PATH / suffix, package=MIDDLEWARE_PACKAGE + "." + suffix)
register_all_middlewares(dp, path=MIDDLEWARE_PATH / "shared", package=MIDDLEWARE_PACKAGE + "." + "shared")
register_all_handlers(dp, path=HANDLERS_PATH / suffix, package=HANDLERS_PACKAGE + "." + suffix)
setup_dialogs(dp)
setup_dialogs(dp, message_manager=MinioMessageManager())

dp.startup.register(on_startup)
dp.shutdown.register(on_shutdown)
Expand All @@ -153,6 +155,11 @@ async def main() -> None:
logger.info("Запущен проект: %s", settings.PROJECT_NAME)

await init_db()
await init_minio()

await test_db()
await test_minio()

# стартуем ботов как background задачи
task_player = asyncio.create_task(
run_bot_safe(
Expand Down Expand Up @@ -200,4 +207,5 @@ def cancel_tasks(*tasks):

if __name__ == "__main__":
logging.basicConfig(level=settings.LOG_LEVEL)
logger.info("Using LOG_LEVEL: %s", settings.LOG_LEVEL)
asyncio.run(main())
63 changes: 63 additions & 0 deletions migrations/models/13_20251213211155_icon-uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from tortoise import BaseDBAsyncClient

RUN_IN_TRANSACTION = True


async def upgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "campaign" ALTER COLUMN "icon" DROP DEFAULT;
ALTER TABLE "campaign" ALTER COLUMN "icon" DROP NOT NULL;
ALTER TABLE "campaign" ALTER COLUMN "icon" TYPE UUID USING "icon"::UUID;"""


async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "campaign" ALTER COLUMN "icon" TYPE VARCHAR(1023) USING "icon"::VARCHAR(1023);
ALTER TABLE "campaign" ALTER COLUMN "icon" SET NOT NULL;
ALTER TABLE "campaign" ALTER COLUMN "icon" SET DEFAULT '';"""


MODELS_STATE = (
"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"
)
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "dnd"
version = "0.1.0"
requires-python = "==3.12,<4.0"
requires-python = "==3.12.0"
dependencies = [
"aiogram==3.22.0",
"requests==2.32.5",
Expand All @@ -21,6 +21,7 @@ dependencies = [
"aerich==0.9.2",
"qrcode==7.4.2",
"cryptography>=46.0.3",
"minio==7.2.20",
]

[project.optional-dependencies]
Expand Down
Loading