diff --git a/handlers/admin/create_campaign.py b/handlers/admin/create_campaign.py
index b184a3f..20dc3e1 100644
--- a/handlers/admin/create_campaign.py
+++ b/handlers/admin/create_campaign.py
@@ -67,10 +67,10 @@ async def on_description_entered(
async def on_icon_entered(mes: Message, wid: MessageInput, dialog_manager: DialogManager):
- if mes.photo:
+ if mes.photo and mes.bot:
photo = mes.photo[-1]
file = await mes.bot.get_file(photo.file_id)
- bin_stream: BinaryIO = await mes.bot.download_file(file.file_path)
+ bin_stream: BinaryIO | None = await mes.bot.download_file(file.file_path or "")
object_id = uuid.uuid4()
settings.minio.put_object(
@@ -99,7 +99,7 @@ async def on_confirm(mes: CallbackQuery, button: Button, dialog_manager: DialogM
new_campaign: Campaign = await Campaign.create(
title=campaign_data.get("title", ""),
description=campaign_data.get("description", ""),
- icon=campaign_data.get("icon", ""),
+ icon=campaign_data.get("icon"),
verified=verified,
)
diff --git a/handlers/admin/inventory_management.py b/handlers/admin/inventory_management.py
index cacf9e8..bf91f80 100644
--- a/handlers/admin/inventory_management.py
+++ b/handlers/admin/inventory_management.py
@@ -256,7 +256,7 @@ async def on_delete_inventory_item(callback: CallbackQuery, button: Button, dial
)
add_item_description_window = Window(
- Format("Название {new_item_name}"),
+ Format("📦 Название: {new_item_name}"),
Const("📝 Введите описание предмета (или '-' чтобы пропустить):"),
TextInput(
id="item_description_input",
@@ -268,8 +268,8 @@ async def on_delete_inventory_item(callback: CallbackQuery, button: Button, dial
)
add_item_quantity_window = Window(
- Format("Название: {new_item_name}"),
- Format("Описание {new_item_description}"),
+ Format("📦 Название: {new_item_name}"),
+ Format("📄 Описание {new_item_description}"),
Const("🔢 Введите количество предмета:"),
TextInput(
id="item_quantity_input",
@@ -283,32 +283,31 @@ async def on_delete_inventory_item(callback: CallbackQuery, button: Button, dial
edit_inventory_item_window = Window(
Multi(
Const("✏️ Редактирование предмета"),
- Format("📦 {item.title}"),
+ Format("📦 Название: {item.title}"),
Format("📝 Описание: {item.description}", when="has_description"),
- Const("📝 Описание отсутствует", when=lambda data, *_: not data.get("item", {}).description),
+ Const("📭 Описание отсутствует", when=lambda data, *_: not data.get("item", {}).description),
Format("🔢 Количество: {item.quantity}"),
- Const(""),
- Const("Выберите что изменить:"),
+ Const("\nВыберите что изменить:"),
sep="\n",
),
Group(
SwitchTo(
- Const("✏️ Название"),
+ Const("✏️ Изменить название"),
id="edit_name",
state=states.ManageInventory.edit_inventory_item_name,
),
SwitchTo(
- Const("📝 Описание"),
+ Const("📝 Изменить описание"),
id="edit_description",
state=states.ManageInventory.edit_inventory_item_description,
),
SwitchTo(
- Const("🔢 Количество"),
+ Const("🔢 Изменить количество"),
id="edit_quantity",
state=states.ManageInventory.edit_inventory_item_quantity,
),
SwitchTo(
- Const("🗑️ Удалить"),
+ Const("🗑️ Удалить предмет"),
id="delete_item",
state=states.ManageInventory.accept_delete,
),
@@ -354,7 +353,7 @@ async def on_delete_inventory_item(callback: CallbackQuery, button: Button, dial
)
accept_delete_item_window = Window(
- Const("🎯 Точно удалить?"),
+ Const("⚠️ Вы точно хотите удалить этот предмет?"),
Button(
Const("🚫 Удалить"),
id="accept_delete",
diff --git a/handlers/admin/invitation.py b/handlers/admin/invitation.py
index 9ead916..38806e5 100644
--- a/handlers/admin/invitation.py
+++ b/handlers/admin/invitation.py
@@ -6,7 +6,7 @@
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
from aiogram_dialog.api.entities import MediaAttachment
from aiogram_dialog.widgets.input import ManagedTextInput, TextInput
-from aiogram_dialog.widgets.kbd import Back, Button, Cancel, Next
+from aiogram_dialog.widgets.kbd import Back, Button, Cancel, Next, Row
from aiogram_dialog.widgets.link_preview import LinkPreview
from aiogram_dialog.widgets.media import DynamicMedia
from aiogram_dialog.widgets.text import Const, Format, Multi
@@ -28,7 +28,7 @@
async def get_link(dialog_manager: DialogManager, **_):
created_by: User = dialog_manager.middleware_data["user"]
- link = dialog_manager.dialog_data.get("link")
+ link = dialog_manager.dialog_data.get("link", "")
if "link" not in dialog_manager.dialog_data and isinstance(dialog_manager.start_data, dict):
campaign_id = dialog_manager.start_data.get("campaign_id", 0)
role = dialog_manager.start_data.get("role", Role.PLAYER)
@@ -177,16 +177,24 @@ async def on_accept(msg: CallbackQuery, _: Button, dialog_manager: DialogManager
getter=get_qr,
)
-
invite_window = Window(
- Format("🎉 Вас пригласили в кампанию!\n\n{campaign_title}\nРоль: {role}"),
- Button(Const("✅ Присоединиться"), id="accept_admin", on_click=on_accept),
- Cancel(Const("❌ Отказаться")),
+ Multi(
+ Const("🎉 Вам пришло приглашение!"),
+ Const(""),
+ Format("🏰 Кампания: {campaign_title}"),
+ Format("👑 Роль: {role}"),
+ Const(""),
+ Const("Присоединиться к кампании?"),
+ sep="\n",
+ ),
+ Row(
+ Button(Const("✅ Присоединиться"), id="accept_admin", on_click=on_accept),
+ Cancel(Const("❌ Отказаться")),
+ ),
getter=invitation_getter,
state=states.InviteMenu.invite,
)
-
# === Создание диалога и роутера ===
dialog = Dialog(invite_menu_window, qr_window, invite_window)
router = Router()
diff --git a/handlers/player/academy.py b/handlers/player/academy.py
index 06ff79b..bfba651 100644
--- a/handlers/player/academy.py
+++ b/handlers/player/academy.py
@@ -4,9 +4,9 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.kbd import Button, Cancel, Column
+from aiogram_dialog.widgets.kbd import Button, Cancel, Column, Row
from aiogram_dialog.widgets.media import DynamicMedia
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.text import Const, Format, Multi
from services.character_data import character_preview_getter
from states.academy import Academy
@@ -41,26 +41,46 @@ async def on_campaigns(c: CallbackQuery, b: Button, m: DialogManager):
async def character_data_getter(dialog_manager: DialogManager, **kwargs) -> dict:
user = dialog_manager.middleware_data["user"]
- data = json.loads(user.data["data"])
-
- return character_preview_getter(user, data)
-
-
-router.include_router(
- Dialog(
- Window(
- DynamicMedia("avatar", when="avatar"),
- Format("{character_data_preview}", when="character_data_preview"),
- Column(
- Button(Const("Посмотреть инвентарь"), id="inventory", on_click=on_inventory),
- Button(Const("Загрузить обновленный .json"), id="update_json", on_click=on_update),
- Button(Const("Рейтинг"), id="rating", on_click=on_rating),
- Button(Const("Кампании внутри академии"), id="campaigns", on_click=on_campaigns),
- Cancel(Const("Назад")),
+
+ if user.data is None:
+ return {"has_character_data": False, "avatar": False}
+
+ data = json.loads(user.data.get("data", "{}"))
+
+ character_preview = character_preview_getter(user, data)
+
+ return {
+ **character_preview,
+ "has_character_data": True,
+ }
+
+
+academy_dialog = Dialog(
+ Window(
+ Multi(
+ Const("🎓 Ваш профиль в академии"),
+ Const(""),
+ ),
+ DynamicMedia("avatar", when="avatar"),
+ Format("{character_data_preview}", when="has_character_data"),
+ Const(
+ "📭 У вас пока нет загруженного персонажа", when=lambda data, *_: not data.get("has_character_data", False)
+ ),
+ Column(
+ Row(
+ Button(Const("🏰 Кампании"), id="campaigns", on_click=on_campaigns),
+ Button(Const("🎒 Инвентарь"), id="inventory", on_click=on_inventory),
+ ),
+ Row(
+ Button(Const("📈 Рейтинг"), id="rating", on_click=on_rating),
+ Button(Const("📤 Обновить .json"), id="update_json", on_click=on_update),
),
- getter=character_data_getter,
- state=Academy.main,
+ Cancel(Const("⬅️ Назад")),
),
- on_start=redirect,
- )
+ getter=character_data_getter,
+ state=Academy.main,
+ ),
+ on_start=redirect,
)
+
+router.include_router(academy_dialog)
diff --git a/handlers/player/academy_campaigns.py b/handlers/player/academy_campaigns.py
index 3b8efe7..eeca7d2 100644
--- a/handlers/player/academy_campaigns.py
+++ b/handlers/player/academy_campaigns.py
@@ -4,9 +4,9 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.kbd import Button, Cancel, ScrollingGroup, Select
+from aiogram_dialog.widgets.kbd import Cancel, ScrollingGroup, Select
from aiogram_dialog.widgets.media import DynamicMedia
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.text import Const, Format, Multi
from db.models import Participation
from services.campaigns import campaign_getter
@@ -19,12 +19,15 @@
async def campaigns_getter(dialog_manager: DialogManager, **kwargs):
user = dialog_manager.middleware_data["user"]
+ participations = await Participation.filter(user=user, campaign__verified=True).prefetch_related("campaign").all()
+
return {
- "campaigns": await Participation.filter(user=user, campaign__verified=True).prefetch_related("campaign").all()
+ "campaigns": participations,
+ "has_campaigns": len(participations) > 0,
}
-async def on_campaign(c: CallbackQuery, b: Button, m: DialogManager, participation_id: UUID):
+async def on_campaign(c: CallbackQuery, b: Select, m: DialogManager, participation_id: UUID):
participation = await Participation.get(id=participation_id).prefetch_related("campaign")
await m.start(
AcademyCampaignPreview.preview,
@@ -32,40 +35,56 @@ async def on_campaign(c: CallbackQuery, b: Button, m: DialogManager, participati
)
-router.include_router(
- Dialog(
- Window(
- Const("Кампании внутри академии, в которых вы участвуете"),
- ScrollingGroup(
- Select(
- Format("{item.campaign.title}"),
- id="campaign",
- items="campaigns",
- item_id_getter=lambda x: x.id,
- on_click=on_campaign,
- ),
- hide_on_single_page=True,
- height=5,
- id="campaigns",
+# Диалог списка кампаний академии
+campaigns_dialog = Dialog(
+ Window(
+ Multi(
+ Const("🏰 Кампании академии"),
+ Const(""),
+ Const("Здесь собраны официальные кампании, в которых вы участвуете."),
+ Const(""),
+ Const("📭 У вас пока нет кампаний в академии", when=lambda data, *_: not data.get("has_campaigns", False)),
+ sep="\n",
+ ),
+ ScrollingGroup(
+ Select(
+ Format("🎮 {item.campaign.title}"),
+ id="campaign",
+ items="campaigns",
+ item_id_getter=lambda x: x.id,
+ on_click=on_campaign,
+ type_factory=UUID,
),
- Cancel(Const("Назад")),
- getter=campaigns_getter,
- state=AcademyCampaigns.campaigns,
+ hide_on_single_page=True,
+ height=5,
+ id="campaigns",
+ when="has_campaigns",
),
- on_start=redirect,
- )
+ Cancel(Const("⬅️ Назад")),
+ getter=campaigns_getter,
+ state=AcademyCampaigns.campaigns,
+ ),
+ on_start=redirect,
)
-router.include_router(
- Dialog(
- Window(
- Format("Информация о кампании: {title}\n\nОписание: {description}\n\nВыберите действие:"),
- DynamicMedia("icon"),
- Cancel(Const("Назад")),
- getter=campaign_getter,
- state=AcademyCampaignPreview.preview,
+# Диалог предпросмотра кампании
+preview_dialog = Dialog(
+ Window(
+ DynamicMedia("icon"),
+ Multi(
+ Format("🎓 Информация о кампании: {title}"),
+ Const(""),
+ Format("📝 Описание: {description}"),
+ Const(""),
+ Const("Здесь вы можете управлять своей кампанией."),
+ sep="\n",
),
- on_start=redirect,
- )
+ Cancel(Const("⬅️ Назад")),
+ getter=campaign_getter,
+ state=AcademyCampaignPreview.preview,
+ ),
+ on_start=redirect,
)
+
+router.include_routers(campaigns_dialog, preview_dialog)
diff --git a/handlers/player/inventory.py b/handlers/player/inventory.py
index 83b23fc..440ba09 100644
--- a/handlers/player/inventory.py
+++ b/handlers/player/inventory.py
@@ -4,8 +4,8 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.kbd import Back, Button, Cancel, ScrollingGroup, Select
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.kbd import Back, Cancel, ScrollingGroup, Select
+from aiogram_dialog.widgets.text import Const, Format, Multi
from pydantic import BaseModel, field_validator
from db.models import Campaign, Item
@@ -59,6 +59,7 @@ async def inventory_data_getter(dialog_manager: DialogManager, **kwargs) -> dict
return {
"inventory": items,
"has_items": len(items) > 0,
+ "items_count": len(items),
}
@@ -69,45 +70,59 @@ async def get_inventory_item_data(dialog_manager: DialogManager, **kwargs):
return {"item": None}
item = await Item.get(id=item_id)
- return {"item": item, "has_description": item.description != ""}
+ return {
+ "item": item,
+ "has_description": item.description != "",
+ "is_empty": item.description == "",
+ }
-async def on_inventory_item_selected(c: CallbackQuery, b: Button, m: DialogManager, item_id: UUID):
+async def on_inventory_item_selected(c: CallbackQuery, b: Select, m: DialogManager, item_id: UUID):
m.dialog_data["selected_item_id"] = item_id
await m.switch_to(InventoryView.preview)
-router.include_router(
- Dialog(
- Window(
- Format("Инвентарь персонажа"),
- ScrollingGroup(
- Select(
- Format("{item.title} ×{item.quantity}"),
- id="inventory_select",
- item_id_getter=lambda item: item.id,
- items="inventory",
- on_click=on_inventory_item_selected,
- type_factory=UUID,
- ),
- id="inventory_scroll",
- width=1,
- height=10,
- hide_on_single_page=True,
- when="has_items",
+inventory_dialog = Dialog(
+ Window(
+ Multi(
+ Const("🎒 Инвентарь персонажа"),
+ Const(""),
+ Format("📦 Всего предметов: {items_count}", when="has_items"),
+ Const("📭 Инвентарь пуст", when=lambda data, *_: not data.get("has_items", False)),
+ Const("Выберите предмет для просмотра:"),
+ sep="\n",
+ ),
+ ScrollingGroup(
+ Select(
+ Format("📦 {item.title} ×{item.quantity}"),
+ id="inventory_select",
+ item_id_getter=lambda item: item.id,
+ items="inventory",
+ on_click=on_inventory_item_selected,
+ type_factory=UUID,
),
- Cancel(Const("Назад")),
- getter=inventory_data_getter,
- state=InventoryView.view,
+ id="inventory_scroll",
+ width=1,
+ height=8,
+ hide_on_single_page=True,
+ when="has_items",
),
- Window(
- Format("📦 {item.title}"),
+ Cancel(Const("⬅️ Назад")),
+ getter=inventory_data_getter,
+ state=InventoryView.view,
+ ),
+ Window(
+ Multi(
+ Format("📦 Предмет: {item.title}"),
+ Const(""),
Format("📝 Описание: {item.description}", when="has_description"),
- Const("📝 Описание отсутствует", when=~F["has_description"]),
+ Const("📭 Описание отсутствует", when=~F["has_description"]),
Format("🔢 Количество: {item.quantity}"),
- Back(Const("Назад")),
- getter=get_inventory_item_data,
- state=InventoryView.preview,
),
- )
+ Back(Const("⬅️ Назад к инвентарю")),
+ getter=get_inventory_item_data,
+ state=InventoryView.preview,
+ ),
)
+
+router.include_router(inventory_dialog)
diff --git a/handlers/player/invitation.py b/handlers/player/invitation.py
index 0fa6a83..cfce6f9 100644
--- a/handlers/player/invitation.py
+++ b/handlers/player/invitation.py
@@ -3,8 +3,8 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
-from aiogram_dialog.widgets.kbd import Button, Cancel
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.kbd import Button, Cancel, Row
+from aiogram_dialog.widgets.text import Const, Format, Multi
from db.models import Invitation
from services.invitation import handle_accept_invitation, invitation_getter
@@ -53,13 +53,24 @@ async def on_accept(msg: CallbackQuery, _: Button, dialog_manager: DialogManager
pass
-invite_window = Window(
- Format("🎉 Вас пригласили в кампанию!\n\n{campaign_title}\nРоль: {role}"),
- Button(Const("✅ Присоединиться"), id="accept_admin", on_click=on_accept),
- Cancel(Const("❌ Отказаться")),
- getter=invitation_getter,
- state=InvitationAccept.invitation,
+invite_dialog = Dialog(
+ Window(
+ Multi(
+ Const("🎉 Вам пришло приглашение!"),
+ Const(""),
+ Format("🏰 Кампания: {campaign_title}"),
+ Format("👑 Роль: {role}"),
+ Const(""),
+ Const("Присоединиться к кампании?"),
+ sep="\n",
+ ),
+ Row(
+ Button(Const("✅ Присоединиться"), id="accept_admin", on_click=on_accept),
+ Cancel(Const("❌ Отказаться")),
+ ),
+ getter=invitation_getter,
+ state=InvitationAccept.invitation,
+ )
)
-
-router.include_router(Dialog(invite_window))
+router.include_router(invite_dialog)
diff --git a/handlers/player/other_games.py b/handlers/player/other_games.py
index e4fcb87..1efc88d 100644
--- a/handlers/player/other_games.py
+++ b/handlers/player/other_games.py
@@ -4,8 +4,8 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.kbd import Back, Button, Cancel, ScrollingGroup, Select
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.kbd import Back, Button, Cancel, Row, ScrollingGroup, Select
+from aiogram_dialog.widgets.text import Const, Format, Multi
from db.models import Campaign, Character, Participation, User
from states.other_games import OtherGames
@@ -42,10 +42,13 @@ async def available_campaigns_getter(dialog_manager: DialogManager, **kwargs) ->
for p in (await Participation.filter(user=user, campaign__verified=False).prefetch_related("campaign").all())
]
- return {"participations": participations, "participations_exist": len(participations) > 0}
+ return {
+ "participations": participations,
+ "has_participations": len(participations) > 0,
+ }
-async def on_character_selected(c: CallbackQuery, b: Button, m: DialogManager, character_id: UUID):
+async def on_character_selected(c: CallbackQuery, b: Select, m: DialogManager, character_id: UUID):
await m.start(
OtherGamesCharacter.preview,
data={
@@ -58,7 +61,7 @@ async def on_available_games(c: CallbackQuery, b: Button, m: DialogManager):
await m.switch_to(OtherGames.available)
-async def on_campaign_selected(c: CallbackQuery, b: Button, m: DialogManager, participation_id: UUID):
+async def on_campaign_selected(c: CallbackQuery, b: Select, m: DialogManager, participation_id: UUID):
user: User = m.middleware_data["user"]
participation = await Participation.get(id=participation_id).prefetch_related("campaign")
campaign: Campaign = participation.campaign
@@ -76,50 +79,71 @@ async def on_campaign_selected(c: CallbackQuery, b: Button, m: DialogManager, pa
)
-router.include_router(
- Dialog(
- Window(
- Const("Вы находитесь в меню неофициальных игр"),
- ScrollingGroup(
- Select(
- Format("{item[1].name} - {item[2].title}"),
- id="character_select",
- items="characters_data",
- item_id_getter=lambda c: c[0].id,
- on_click=on_character_selected,
- type_factory=UUID,
- ),
- id="characters_scroll",
- width=1,
- height=8,
- hide_on_single_page=True,
- when="has_characters",
+other_games_dialog = Dialog(
+ Window(
+ Multi(
+ Const("🎮 Другие игры"),
+ Const(""),
+ Const("Здесь вы можете управлять своими персонажами в неофициальных кампаниях."),
+ Const(""),
+ Const(
+ "📭 У вас пока нет персонажей в других играх",
+ when=lambda data, *_: not data.get("has_characters", False),
),
- Button(Const("Посмотреть доступные кампании"), id="available_games", on_click=on_available_games),
- Cancel(Const("Назад")),
- getter=main_getter,
- state=OtherGames.main,
+ Const("Выберите персонажа для управления:"),
+ sep="\n",
),
- Window(
- Const("Вот кампании к которым у вас есть доступ"),
- ScrollingGroup(
- Select(
- Format("{item[0].title} - {item[1].role.name}"),
- id="campaign_select",
- items="participations",
- item_id_getter=lambda c: c[1].id,
- on_click=on_campaign_selected,
- type_factory=UUID,
- ),
- id="participations_scroll",
- width=1,
- height=8,
- hide_on_single_page=True,
- when="participations_exist",
+ ScrollingGroup(
+ Select(
+ Format("👤 {item[1].name} - {item[2].title}"),
+ id="character_select",
+ items="characters_data",
+ item_id_getter=lambda c: c[0].id,
+ on_click=on_character_selected,
+ type_factory=UUID,
),
- Back(Const("Назад")),
- getter=available_campaigns_getter,
- state=OtherGames.available,
+ id="characters_scroll",
+ width=1,
+ height=6,
+ hide_on_single_page=True,
+ when="has_characters",
),
- )
+ Row(
+ Button(Const("🏰 Доступные кампании"), id="available_games", on_click=on_available_games),
+ Cancel(Const("⬅️ Назад")),
+ ),
+ getter=main_getter,
+ state=OtherGames.main,
+ ),
+ Window(
+ Multi(
+ Const("🏰 Доступные кампании"),
+ Const(""),
+ Const("Кампании, к которым у вас есть доступ:"),
+ Const(""),
+ Const("📭 У вас нет доступных кампаний", when=lambda data, *_: not data.get("has_participations", False)),
+ Const("Выберите кампанию для присоединения:"),
+ sep="\n",
+ ),
+ ScrollingGroup(
+ Select(
+ Format("🎮 {item[0].title} (Роль: {item[1].role.name})"),
+ id="campaign_select",
+ items="participations",
+ item_id_getter=lambda c: c[1].id,
+ on_click=on_campaign_selected,
+ type_factory=UUID,
+ ),
+ id="participations_scroll",
+ width=1,
+ height=6,
+ hide_on_single_page=True,
+ when="has_participations",
+ ),
+ Back(Const("⬅️ Назад")),
+ getter=available_campaigns_getter,
+ state=OtherGames.available,
+ ),
)
+
+router.include_router(other_games_dialog)
diff --git a/handlers/player/other_games_campaigns.py b/handlers/player/other_games_campaigns.py
index c60d287..b182138 100644
--- a/handlers/player/other_games_campaigns.py
+++ b/handlers/player/other_games_campaigns.py
@@ -3,7 +3,7 @@
from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Cancel
from aiogram_dialog.widgets.media import DynamicMedia
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.text import Const, Format, Multi
from db.models import Character
from services.campaigns import campaign_getter
@@ -15,13 +15,16 @@
async def campaign_preview_getter(dialog_manager: DialogManager, **kwargs):
- campaign_id = dialog_manager.start_data.get("campaign_id")
+ if "campaign_id" not in dialog_manager.dialog_data and isinstance(dialog_manager.start_data, dict):
+ dialog_manager.dialog_data["campaign_id"] = dialog_manager.start_data.get("campaign_id", 0)
+ campaign_id = dialog_manager.dialog_data["campaign_id"]
user = dialog_manager.middleware_data["user"]
character: Character | None = await Character.get_or_none(campaign_id=campaign_id, user=user)
return {
**await campaign_getter(dialog_manager, **kwargs),
"should_join": character is None,
+ "has_character": character is not None,
}
@@ -31,20 +34,28 @@ async def on_join_campaign(c: CallbackQuery, b: Button, m: DialogManager):
data={
"target_type": TargetType.CHARACTER,
"target_id": None,
- "campaign_id": m.start_data.get("campaign_id"),
+ "campaign_id": m.dialog_data["campaign_id"],
},
)
-router.include_router(
- Dialog(
- Window(
- Format("Информация о кампании: {title}\n\nОписание: {description}\n\nВыберите действие:"),
- DynamicMedia("icon"),
- Button(Const("Присоединиться"), id="join", on_click=on_join_campaign, when="should_join"),
- Cancel(Const("Назад")),
- getter=campaign_preview_getter,
- state=OtherGamesCampaign.preview,
- )
+campaign_preview_dialog = Dialog(
+ Window(
+ DynamicMedia("icon"),
+ Multi(
+ Format("🎮 Кампания: {title}"),
+ Const(""),
+ Format("📝 Описание: {description}"),
+ Const(""),
+ Const("🌟 Вы ещё не создали персонажа для этой кампании", when="should_join"),
+ Const("✅ У вас уже есть персонаж в этой кампании", when="has_character"),
+ sep="\n",
+ ),
+ Button(Const("➕ Присоединиться"), id="join", on_click=on_join_campaign, when="should_join"),
+ Cancel(Const("⬅️ Назад")),
+ getter=campaign_preview_getter,
+ state=OtherGamesCampaign.preview,
)
)
+
+router.include_router(campaign_preview_dialog)
diff --git a/handlers/player/other_games_character.py b/handlers/player/other_games_character.py
index c461e81..4d24dac 100644
--- a/handlers/player/other_games_character.py
+++ b/handlers/player/other_games_character.py
@@ -3,9 +3,9 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.kbd import Button, Cancel
+from aiogram_dialog.widgets.kbd import Button, Cancel, Column, Row
from aiogram_dialog.widgets.media import DynamicMedia
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.text import Const, Format, Multi
from db.models import Campaign, Character, Participation, User
from services.character_data import character_preview_getter
@@ -21,7 +21,12 @@ async def character_data_getter(dialog_manager: DialogManager, **kwargs) -> dict
character = await Character.get(id=dialog_manager.start_data["character_id"])
data = json.loads(character.data["data"])
- return character_preview_getter(character, data)
+ character_preview = character_preview_getter(character, data)
+
+ return {
+ **character_preview,
+ "has_character_data": character_preview.get("character_data_preview") not in [None, ""],
+ }
async def on_campaign_info(c: CallbackQuery, b: Button, m: DialogManager):
@@ -63,17 +68,26 @@ async def on_upload_json(c: CallbackQuery, b: Button, m: DialogManager):
)
-router.include_router(
- Dialog(
- Window(
- DynamicMedia("avatar", when="avatar"),
- Format("{character_data_preview}", when="character_data_preview"),
- Button(Const("Посмотреть инвентарь"), id="inventory", on_click=on_inventory),
- Button(Const("Информация о кампании"), id="campaign_info", on_click=on_campaign_info),
- Button(Const("Загрузить обновленный .json"), id="update_json", on_click=on_upload_json),
- Cancel(Const("Назад")),
- getter=character_data_getter,
- state=OtherGamesCharacter.preview,
- )
+character_dialog = Dialog(
+ Window(
+ Multi(
+ Const("👤 Персонаж"),
+ Const(""),
+ ),
+ DynamicMedia("avatar", when="avatar"),
+ Format("{character_data_preview}", when="character_data_preview"),
+ Const("📭 У персонажа нет данных", when=lambda data, *_: not data.get("character_data_preview", "")),
+ Column(
+ Row(
+ Button(Const("🎒 Инвентарь"), id="inventory", on_click=on_inventory),
+ Button(Const("🏰 Кампания"), id="campaign_info", on_click=on_campaign_info),
+ ),
+ Button(Const("📤 Обновить .json"), id="update_json", on_click=on_upload_json),
+ Cancel(Const("⬅️ Назад")),
+ ),
+ getter=character_data_getter,
+ state=OtherGamesCharacter.preview,
)
)
+
+router.include_router(character_dialog)
diff --git a/handlers/player/player_preview.py b/handlers/player/player_preview.py
index 754c5fe..302f85d 100644
--- a/handlers/player/player_preview.py
+++ b/handlers/player/player_preview.py
@@ -3,9 +3,9 @@
from aiogram import Router
from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.kbd import Cancel, Url
+from aiogram_dialog.widgets.kbd import Cancel, Row, Url
from aiogram_dialog.widgets.media import DynamicMedia
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.text import Const, Format, Multi
from db.models import User
from services.character_data import character_preview_getter
@@ -16,24 +16,42 @@
async def preview_getter(dialog_manager: DialogManager, **kwargs):
- user = await User.get(id=dialog_manager.start_data["user_id"])
+ if "light" not in dialog_manager.dialog_data and isinstance(dialog_manager.start_data, dict):
+ dialog_manager.dialog_data["light"] = dialog_manager.start_data.get("light", True)
+ dialog_manager.dialog_data["user_id"] = dialog_manager.start_data.get("user_id", 0)
+
+ user = await User.get(id=dialog_manager.dialog_data["user_id"])
data = json.loads(user.data["data"])
+ light = dialog_manager.dialog_data["light"]
+
+ character_preview = character_preview_getter(user, data, light=light)
return {
"profile_link": f"tg://user?id={user.id}",
- **character_preview_getter(user, data),
+ "username": user.username or "Пользователь",
+ **character_preview,
+ "has_character_data": character_preview.get("character_data_preview") not in [None, ""],
}
-router.include_router(
- Dialog(
- Window(
- DynamicMedia("avatar", when="avatar"),
- Format("{character_data_preview}", when="character_data_preview"),
- Url(Const("Перейти в профиль"), Format("{profile_link}")),
- Cancel(Const("Назад")),
- getter=preview_getter,
- state=PlayerPreview.preview,
- )
+preview_dialog = Dialog(
+ Window(
+ Multi(
+ Format("👤 Профиль: @{username}"),
+ Const(""),
+ ),
+ DynamicMedia("avatar", when="avatar"),
+ Format("{character_data_preview}", when="character_data_preview"),
+ Const(
+ "📭 У игрока нет загруженного персонажа", when=lambda data, *_: not data.get("character_data_preview", "")
+ ),
+ Row(
+ Url(Const("📨 Написать"), Format("{profile_link}")),
+ Cancel(Const("⬅️ Назад")),
+ ),
+ getter=preview_getter,
+ state=PlayerPreview.preview,
)
)
+
+router.include_router(preview_dialog)
diff --git a/handlers/player/rating.py b/handlers/player/rating.py
index a12fcc6..dd1591a 100644
--- a/handlers/player/rating.py
+++ b/handlers/player/rating.py
@@ -3,8 +3,8 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.kbd import Button, Cancel, ScrollingGroup, Select
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.kbd import Cancel, ScrollingGroup, Select
+from aiogram_dialog.widgets.text import Const, Format, Multi
from db.models import User
from states.player_preview import PlayerPreview
@@ -15,33 +15,50 @@
async def rating_getter(dialog_manager: DialogManager, **kwargs):
- return {"top": await User.all().order_by("-rating")}
-
-
-async def on_preview(c: CallbackQuery, b: Button, m: DialogManager, user_id: int):
- await m.start(PlayerPreview.preview, data={"user_id": user_id})
-
-
-router.include_router(
- Dialog(
- Window(
- Const("Вот топ по рейтингу"),
- ScrollingGroup(
- Select(
- Format("@{item.username} - {item.rating}"),
- id="preview",
- items="top",
- item_id_getter=lambda x: x.id,
- on_click=on_preview,
- ),
- hide_on_single_page=True,
- width=1,
- height=5,
- id="top",
+ top_users = await User.all().order_by("-rating")
+
+ # Добавляем позиции в рейтинге
+ top_with_positions = [(i + 1, user) for i, user in enumerate(top_users)]
+
+ return {
+ "top_with_positions": top_with_positions,
+ "has_users": len(top_with_positions) > 0,
+ }
+
+
+async def on_preview(c: CallbackQuery, b: Select, m: DialogManager, user_id: int):
+ await m.start(PlayerPreview.preview, data={"user_id": user_id, "light": True})
+
+
+rating_dialog = Dialog(
+ Window(
+ Multi(
+ Const("🏆 Рейтинг игроков"),
+ Const(""),
+ Const("Топ игроков по рейтингу в академии:"),
+ Const(""),
+ Const("📭 В рейтинге пока нет игроков", when=lambda data, *_: not data.get("has_users", False)),
+ sep="\n",
+ ),
+ ScrollingGroup(
+ Select(
+ Format("{item[0]}. @{item[1].username} - {item[1].rating} ⭐"),
+ id="preview",
+ items="top_with_positions",
+ item_id_getter=lambda x: x[1].id,
+ on_click=on_preview,
+ type_factory=int,
),
- Cancel(Const("Назад")),
- getter=rating_getter,
- state=AcademyRating.rating,
+ hide_on_single_page=True,
+ width=1,
+ height=6,
+ id="top",
+ when="has_users",
),
- )
+ Cancel(Const("⬅️ Назад")),
+ getter=rating_getter,
+ state=AcademyRating.rating,
+ ),
)
+
+router.include_router(rating_dialog)
diff --git a/handlers/player/start.py b/handlers/player/start.py
index 6488ed2..b5df548 100644
--- a/handlers/player/start.py
+++ b/handlers/player/start.py
@@ -5,7 +5,7 @@
from aiogram.types import CallbackQuery, Message
from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Column
-from aiogram_dialog.widgets.text import Const
+from aiogram_dialog.widgets.text import Const, Multi
from db.models import Invitation, User
from db.models.participation import Participation
@@ -84,9 +84,8 @@ async def start_args(message: Message, command: CommandObject, dialog_manager: D
command.args,
invite.user.id,
)
- await message.reply(
- f"🗳️ Вы уже участвуете в этой кампании в качестве {'игрока' if (i := participation.role == 0) else str(i)}"
- )
+ role_name = "игрока" if participation.role.value == 0 else participation.role.name
+ await message.reply(f"🗳️ Вы уже участвуете в этой кампании в качестве {role_name}")
return
logger.debug(
@@ -122,18 +121,25 @@ async def on_other(c: CallbackQuery, b: Button, m: DialogManager):
await m.start(OtherGames.main)
-router.include_router(
- Dialog(
- Window(
- Const("Обычный /start"),
- Column(
- Button(Const("Академия"), id="academy", on_click=on_academy),
- Button(Const("Другие игры"), id="other_games", on_click=on_other),
- # TODO (@pxc1984): Добавить ближайшие встречи
- # https://github.com/cu-tabletop/dnd/issues/11
- ),
- state=StartSimple.simple,
+start_dialog = Dialog(
+ Window(
+ Multi(
+ Const("🎲 Добро пожаловать!"),
+ Const(""),
+ Const("Я - бот для настольных ролевых игр."),
+ Const(""),
+ Const("Выберите раздел для продолжения:"),
+ sep="\n",
),
- on_start=redirect,
- )
+ Column(
+ Button(Const("🎓 Академия"), id="academy", on_click=on_academy),
+ Button(Const("🎮 Другие игры"), id="other_games", on_click=on_other),
+ # TODO (@pxc1984): Добавить ближайшие встречи
+ # https://github.com/cu-tabletop/dnd/issues/11
+ ),
+ state=StartSimple.simple,
+ ),
+ on_start=redirect,
)
+
+router.include_router(start_dialog)
diff --git a/handlers/player/upload.py b/handlers/player/upload.py
index 58f629c..9d1f4a8 100644
--- a/handlers/player/upload.py
+++ b/handlers/player/upload.py
@@ -9,13 +9,15 @@
from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Cancel
-from aiogram_dialog.widgets.text import Const
-from pydantic import BaseModel, field_validator
+from aiogram_dialog.widgets.link_preview import LinkPreview
+from aiogram_dialog.widgets.text import Const, Multi
+from pydantic import BaseModel, ValidationError, field_validator
from db.models import Character
from services.character_data import update_char_data
from states.inventory_view import TargetType
from states.upload_character import UploadCharacter
+from utils.character import parse_character_data
if TYPE_CHECKING:
from db.models.base import CharacterData
@@ -77,7 +79,7 @@ def validate_campaign_id(cls, v: int | None, values: dict) -> int:
async def upload_document(msg: Message, _: MessageInput, manager: DialogManager):
if not msg.document or not msg.document.file_name.endswith(".json"):
- await msg.answer("Отправь .json!")
+ await msg.answer("❌ Пожалуйста, отправьте файл в формате .json!")
logger.warning("User %d didn't send us a valid json", msg.from_user.id)
return
@@ -104,30 +106,46 @@ async def upload_document(msg: Message, _: MessageInput, manager: DialogManager)
return
try:
- await update_char_data(source, json.loads(content.decode("utf-8")))
+ data = json.loads(content.decode("utf-8"))
+ parse_character_data(data)
+ await update_char_data(source, data)
except UnicodeDecodeError:
logger.warning("Failed to unicode decode payload from user %d", msg.from_user.id)
- await msg.answer("Это не json, проверь еще раз")
+ await msg.answer("❌ Не удалось прочитать файл. Убедитесь, что это корректный JSON-файл.")
return
- except json.JSONDecodeError:
+ except (json.JSONDecodeError, ValidationError):
logger.warning("User %d sent incorrect json", msg.from_user.id)
- await msg.answer("Это не json, проверь еще раз")
+ await msg.answer("❌ Это невалидный JSON-файл. Проверьте его содержимое.")
return
- await msg.answer("Успешно загружено")
+ await msg.answer("✅ Данные персонажа успешно загружены!")
await manager.done()
+async def has_data(dialog_manager: DialogManager, **kwargs):
+ return {"has_data": dialog_manager.middleware_data["user"].data}
+
+
"""
Этот диалог обязательно должен включать в start_data параметр request: UploadCharacterRequest
"""
-router.include_router(
- Dialog(
- Window(
- Const("Отправь нам .json из LSH"),
- Cancel(Const("Отмена")),
- MessageInput(content_types=ContentType.DOCUMENT, func=upload_document),
- state=UploadCharacter.upload,
+upload_dialog = Dialog(
+ Window(
+ Multi(
+ Const("📤 Загрузка персонажа"),
+ Const(""),
+ Const("Отправьте файл персонажа в формате .json."),
+ Const('Файл должен быть экспортирован из LSH.'),
+ Const(""),
+ Const("⚠️ Внимание: существующие данные будут перезаписаны.", when=""),
+ sep="\n",
),
- )
+ LinkPreview(is_disabled=True),
+ Cancel(Const("❌ Отмена")),
+ MessageInput(content_types=ContentType.DOCUMENT, func=upload_document),
+ getter=has_data,
+ state=UploadCharacter.upload,
+ ),
)
+
+router.include_router(upload_dialog)
diff --git a/services/character.py b/services/character.py
new file mode 100644
index 0000000..cbfdf9a
--- /dev/null
+++ b/services/character.py
@@ -0,0 +1,245 @@
+import logging
+from typing import Any
+
+from pydantic import BaseModel, Field
+
+logger = logging.getLogger(__name__)
+
+
+class CharacterStat(BaseModel):
+ score: int = 0
+ modifier: int = 0
+
+
+class CharacterHP(BaseModel):
+ current: int = 0
+ max: int = 0
+ temp: int = 0
+ ac: int = 0
+ speed: int = 0
+
+
+class CharacterWeapon(BaseModel):
+ name: str = "Неизвестно"
+ mod: str = ""
+ damage: str = ""
+ notes: str = ""
+
+
+class CharacterCoins(BaseModel):
+ pp: int = 0
+ gp: int = 0
+ ep: int = 0
+ sp: int = 0
+ cp: int = 0
+
+
+class CharacterData(BaseModel):
+ name: str = "Неизвестно"
+ klass: str = Field(default="Неизвестно", alias="class")
+ subclass: str = ""
+ level: int = 0
+ race: str = "Неизвестно"
+ background: str = "Неизвестно"
+ alignment: str = "Неизвестно"
+ avatar_link: str | None
+
+ age: str = ""
+ height: str = ""
+ weight: str = ""
+ eyes: str = ""
+ skin: str = ""
+ hair: str = ""
+
+ proficiency: int = 0
+ stats: dict[str, CharacterStat] = Field(default_factory=dict)
+
+ skills: dict[str, dict] = Field(default_factory=dict)
+ prof_skills: list[str] = Field(default_factory=list)
+
+ hp: CharacterHP = Field(default_factory=CharacterHP)
+
+ weapons: list[CharacterWeapon] = Field(default_factory=list)
+
+ traits: str = ""
+ equipment: str = ""
+ background_story: str = ""
+ personality: str = ""
+ appearance: str = ""
+ allies: str = ""
+ proficiencies: str = ""
+
+ coins: CharacterCoins = Field(default_factory=CharacterCoins)
+
+ class Config:
+ validate_by_name = True
+
+ def preview(self) -> str:
+ return (
+ f"Имя: {self.name}\n"
+ f"Класс: {self.klass} {f'({self.subclass})' if self.subclass else ''}\n"
+ f"Уровень: {self.level}\n"
+ f"Хиты: {self.hp.current}/{self.hp.max} {f'(+{self.hp.temp} временное)' if self.hp.temp else ''}\n"
+ f"Класс брони: {self.hp.ac}\n"
+ f"Скорость: {self.hp.speed} фт.\n"
+ f"Раса: {self.race}\n"
+ f"Предыстория: {self.background}\n"
+ f"Мировоззрение: {self.alignment}"
+ )
+
+ def preview_stats(self) -> str:
+ return "\n".join(
+ [f"{STATS_CONVERSION[key]}: {value.score}({value.modifier})" for key, value in self.stats.items()]
+ )
+
+
+STATS_CONVERSION = {
+ "str": "Сила",
+ "dex": "Ловкость",
+ "con": "Телосложение",
+ "int": "Интеллект",
+ "wis": "Мудрость",
+ "cha": "Харизма",
+}
+
+
+def parse_character_data(data: dict) -> CharacterData:
+ """
+ Превращает json персонажа Long Story Short в адекватный Pydantic объект
+ """
+ basic_info = data.get("info", {})
+ sub_info = data.get("subInfo", {})
+ vitality = data.get("vitality", {})
+ coins_data = data.get("coins", {})
+ prof_skills = []
+ for skill_name, skill_data in data.get("skills", {}).items():
+ if skill_data.get("isProf"):
+ prof_skills.append(skill_name)
+ stats = {}
+ for stat_name, stat_data in data.get("stats", {}).items():
+ stats[stat_name] = CharacterStat(
+ score=stat_data.get("score", 0),
+ modifier=stat_data.get("modifier", 0),
+ )
+ weapons = [
+ CharacterWeapon(
+ name=weapon.get("name", {}).get("value", "Неизвестно"),
+ mod=weapon.get("mod", {}).get("value", ""),
+ damage=weapon.get("dmg", {}).get("value", ""),
+ notes=weapon.get("notes", {}).get("value", ""),
+ )
+ for weapon in data.get("weaponsList", [])
+ ]
+
+ # noinspection PyArgumentList
+ return CharacterData(
+ # Basic info
+ name=data.get("name", {}).get("value", "Неизвестно"),
+ klass=basic_info.get("charClass", {}).get("value", "Неизвестно"),
+ subclass=basic_info.get("charSubclass", {}).get("value", ""),
+ level=basic_info.get("level", {}).get("value", 0),
+ race=basic_info.get("race", {}).get("value", "Неизвестно"),
+ background=basic_info.get("background", {}).get("value", "Неизвестно"),
+ alignment=basic_info.get("alignment", {}).get("value", "Неизвестно"),
+ avatar_link=basic_info.get("avatar", {}).get("webp") or basic_info.get("avatar", {}).get("jpeg"),
+ # Physical characteristics
+ age=sub_info.get("age", {}).get("value", ""),
+ height=sub_info.get("height", {}).get("value", ""),
+ weight=sub_info.get("weight", {}).get("value", ""),
+ eyes=sub_info.get("eyes", {}).get("value", ""),
+ skin=sub_info.get("skin", {}).get("value", ""),
+ hair=sub_info.get("hair", {}).get("value", ""),
+ # Stats
+ proficiency=data.get("proficiency", 0),
+ stats=stats,
+ # Skills
+ skills=data.get("skills", {}),
+ prof_skills=prof_skills,
+ # Vitality
+ hp=CharacterHP(
+ current=vitality.get("hp-current", {}).get("value", 0),
+ max=vitality.get("hp-max", {}).get("value", 0),
+ temp=vitality.get("hp-temp", {}).get("value", 0),
+ ac=vitality.get("ac", {}).get("value", 0),
+ speed=vitality.get("speed", {}).get("value", 0),
+ ),
+ # Weapons
+ weapons=weapons,
+ # Text content
+ traits=extract_telegram_text(data.get("text", {}).get("traits", {}).get("value", {})),
+ equipment=extract_telegram_text(data.get("equipment", {}).get("value", {})),
+ background_story=extract_telegram_text(data.get("quests", {}).get("value", {})),
+ personality=extract_telegram_text(data.get("background", {}).get("value", {})),
+ appearance=extract_telegram_text(data.get("appearance", {}).get("value", {})),
+ allies=extract_telegram_text(data.get("allies", {}).get("value", {})),
+ proficiencies=extract_telegram_text(data.get("prof", {}).get("value", {})),
+ # Currency
+ coins=CharacterCoins(
+ pp=coins_data.get("pp", {}).get("value", 0),
+ gp=coins_data.get("gp", {}).get("value", 0),
+ ep=coins_data.get("ep", {}).get("value", 0),
+ sp=coins_data.get("sp", {}).get("value", 0),
+ cp=coins_data.get("cp", {}).get("value", 0),
+ ),
+ )
+
+
+def extract_telegram_text(text_data: dict[str, Any]) -> str:
+ """
+ Превращает json документооборот в HTML
+ """
+ if not text_data:
+ return ""
+
+ content = text_data.get("data", {}).get("content", [])
+ paragraphs = []
+
+ for block in content:
+ if block.get("type") == "paragraph":
+ paragraph_text = process_paragraph(block.get("content", []))
+ if paragraph_text:
+ paragraphs.append(paragraph_text)
+
+ return "\n".join(paragraphs)
+
+
+def process_paragraph(items: list[dict[str, Any]]) -> str:
+ """Process all items in paragraph into HTML."""
+ result = []
+
+ for item in items:
+ item_type = item.get("type")
+
+ if item_type == "text":
+ html_text = process_text_item(item)
+ if html_text:
+ result.append(html_text)
+
+ elif item_type == "roller":
+ result.append(process_roller_item(item))
+
+ return " ".join(result).strip()
+
+
+def process_text_item(item: dict[str, Any]) -> str:
+ """Apply HTML formatting to text item."""
+ text = item.get("text", "").strip()
+ if not text:
+ return ""
+
+ for mark in item.get("marks", []):
+ mark_type = mark.get("type")
+ if mark_type == "bold":
+ text = f"{text}"
+ elif mark_type == "italic":
+ text = f"{text}"
+ elif mark_type == "underline":
+ text = f"{text}"
+
+ return text
+
+
+def process_roller_item(item: dict[str, Any]) -> str:
+ """Convert roller item to placeholder HTML-like form."""
+ roller_text = item.get("content", [{}])[0].get("text", "")
+ return f"[{roller_text}]"
diff --git a/services/character_data.py b/services/character_data.py
index 30ae6f2..cfd809a 100644
--- a/services/character_data.py
+++ b/services/character_data.py
@@ -4,6 +4,8 @@
from aiogram_dialog.api.entities import MediaAttachment
from db.models.base import CharacterData as BaseCharacterData
+from db.models.character import Character
+from db.models.user import User
from utils.character import CharacterData, parse_character_data
logger = logging.getLogger(__name__)
@@ -14,10 +16,10 @@ async def update_char_data(holder: BaseCharacterData, data: dict):
await holder.save()
-def character_preview_getter(user: BaseCharacterData, data: dict):
+def character_preview_getter(user: User | Character, data: dict, *, light: bool = False):
ret = {}
info: CharacterData = parse_character_data(data)
- ret["character_data_preview"] = info.preview()
+ ret["character_data_preview"] = info.light_preview() if light else info.preview()
avatar_url = data.get("avatar", {}).get("webp")
if avatar_url:
ret["avatar"] = MediaAttachment(
diff --git a/services/settings.py b/services/settings.py
index e44b9ff..7c9e467 100644
--- a/services/settings.py
+++ b/services/settings.py
@@ -40,7 +40,7 @@ class Settings(BaseSettings):
DB_PASSWORD: str = Field(default="admin", alias="POSTGRES_PASSWORD")
# ^ Minio
- MINIO_HOST: str = "localhost"
+ MINIO_HOST: str = "minio"
MINIO_PORT: str = "9000"
MINIO_ROOT_USER: str = Field(default="admin", alias="MINIO_ROOT_USER")
MINIO_ROOT_PASSWORD: str = Field(default="admin", alias="MINIO_ROOT_PASSWORD")
diff --git a/utils/character.py b/utils/character.py
index cbfdf9a..90dff19 100644
--- a/utils/character.py
+++ b/utils/character.py
@@ -87,6 +87,9 @@ def preview(self) -> str:
f"Мировоззрение: {self.alignment}"
)
+ def light_preview(self) -> str:
+ return f"Имя: {self.name}\n"
+
def preview_stats(self) -> str:
return "\n".join(
[f"{STATS_CONVERSION[key]}: {value.score}({value.modifier})" for key, value in self.stats.items()]