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
1 change: 1 addition & 0 deletions handlers/admin/campaign_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async def on_campaign_selected(
type_factory=UUID,
),
hide_on_single_page=True,
width=1,
height=5,
id="campaigns",
),
Expand Down
5 changes: 4 additions & 1 deletion handlers/player/academy.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ async def on_inventory(c: CallbackQuery, b: Button, m: DialogManager):


async def on_update(c: CallbackQuery, b: Button, m: DialogManager):
await m.start(UploadCharacter.upload, data={"source": "user"})
await m.start(
UploadCharacter.upload,
data={"target_type": TargetType.USER, "target_id": m.middleware_data["user"].id},
)


async def on_rating(c: CallbackQuery, b: Button, m: DialogManager):
Expand Down
25 changes: 2 additions & 23 deletions handlers/player/academy_campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@
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 db.models import Participation
from services.campaigns import campaign_getter
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 @@ -34,25 +32,6 @@ async def on_campaign(c: CallbackQuery, b: Button, m: DialogManager, participati
)


async def campaign_getter(dialog_manager: DialogManager, **kwargs):
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 or "Описание отсутствует",
"icon": icon,
"is_owner": participation.role == Role.OWNER,
}


router.include_router(
Dialog(
Window(
Expand Down
114 changes: 114 additions & 0 deletions handlers/player/other_games.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import json
from uuid import UUID

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 db.models import Campaign, Character, Participation, User
from states.other_games import OtherGames
from states.other_games_campaign import OtherGamesCampaign
from utils.character import CharacterData, parse_character_data

router = Router()


async def main_getter(dialog_manager: DialogManager, **kwargs) -> dict:
user: User = dialog_manager.middleware_data["user"]

characters: list[Character] = (
await Character.filter(user=user, campaign__verified=False).prefetch_related("campaign").all()
)

characters_data: list[tuple[Character, CharacterData, Campaign]] = [
(character, parse_character_data(json.loads(character.data["data"])), character.campaign)
for character in characters
]

return {
"characters_data": characters_data,
"has_characters": len(characters_data) > 0,
}


async def available_campaigns_getter(dialog_manager: DialogManager, **kwargs) -> dict:
user: User = dialog_manager.middleware_data["user"]

participations: list[tuple[Campaign, Participation]] = [
(p.campaign, p)
for p in (await Participation.filter(user=user, campaign__verified=False).prefetch_related("campaign").all())
]

return {"participations": participations, "participations_exist": len(participations) > 0}


async def on_character_selected(c: CallbackQuery, b: Button, m: DialogManager, character_id: UUID):
pass


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):
user: User = m.middleware_data["user"]
participation = await Participation.get(id=participation_id).prefetch_related("campaign")
campaign: Campaign = participation.campaign
character = await Character.get_or_none(user=user, campaign=campaign)
if character is None:
await m.start(
OtherGamesCampaign.preview, data={"campaign_id": campaign.id, "participation_id": participation.id}
)
else:
...


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",
),
Button(Const("Посмотреть доступные кампании"), id="available_games", on_click=on_available_games),
Cancel(Const("Назад")),
getter=main_getter,
state=OtherGames.main,
),
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",
),
Back(Const("Назад")),
getter=available_campaigns_getter,
state=OtherGames.available,
),
)
)
50 changes: 50 additions & 0 deletions handlers/player/other_games_campaigns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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.media import DynamicMedia
from aiogram_dialog.widgets.text import Const, Format

from db.models import Character
from services.campaigns import campaign_getter
from states.inventory_view import TargetType
from states.other_games_campaign import OtherGamesCampaign
from states.upload_character import UploadCharacter

router = Router()


async def campaign_preview_getter(dialog_manager: DialogManager, **kwargs):
campaign_id = dialog_manager.start_data.get("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,
}


async def on_join_campaign(c: CallbackQuery, b: Button, m: DialogManager):
await m.start(
UploadCharacter.upload,
data={
"target_type": TargetType.CHARACTER,
"target_id": None,
"campaign_id": m.start_data.get("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,
)
)
)
10 changes: 8 additions & 2 deletions handlers/player/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from db.models import Invitation, User
from db.models.participation import Participation
from states.academy import Academy
from states.inventory_view import TargetType
from states.invitation import InvitationAccept
from states.other_games import OtherGames
from states.start_simple import StartSimple
from states.upload_character import UploadCharacter
from utils.redirect import redirect
Expand Down Expand Up @@ -108,12 +110,16 @@ async def start_simple(message: Message, dialog_manager: DialogManager, user: Us
async def on_academy(c: CallbackQuery, b: Button, m: DialogManager):
user: User = m.middleware_data["user"]
if user.data is None:
await m.start(UploadCharacter.upload, data={"source": "user"})
await m.start(
UploadCharacter.upload,
data={"target_type": TargetType.USER, "target_id": user.id},
)
return
await m.start(Academy.main)


async def on_other(c: CallbackQuery, b: Button, m: DialogManager): ...
async def on_other(c: CallbackQuery, b: Button, m: DialogManager):
await m.start(OtherGames.main)


router.include_router(
Expand Down
77 changes: 72 additions & 5 deletions handlers/player/upload.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
import logging
from typing import TYPE_CHECKING
from uuid import UUID

from aiogram import Router
from aiogram.enums import ContentType
Expand All @@ -8,15 +10,71 @@
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 db.models import Character
from services.character_data import update_char_data
from states.inventory_view import TargetType
from states.upload_character import UploadCharacter

if TYPE_CHECKING:
from db.models.base import CharacterData

logger = logging.getLogger(__name__)
router = Router()


class UploadCharacterRequest(BaseModel):
target_type: TargetType
target_id: int | UUID | None
campaign_id: UUID | None = None

@classmethod
@field_validator("target_type", mode="before")
def validate_target_type(cls, v: TargetType | int | str) -> TargetType | None:
if isinstance(v, TargetType):
return v
try:
if isinstance(v, int):
return TargetType(v)
if isinstance(v, str):
try:
return TargetType(int(v))
except ValueError:
return TargetType[v.upper()]
except (ValueError, KeyError) as e:
msg = f"Invalid target_type: {v}"
raise ValueError(msg) from e

@classmethod
@field_validator("target_id", mode="wrap")
def validate_target_id(cls, v: int | UUID | None, values: dict) -> UUID | None | int:
if "target_type" not in values:
msg = "target_type is required to be passed"
raise ValueError(msg)
target_type: TargetType = values["target_type"]
if target_type == TargetType.CHARACTER:
if isinstance(v, UUID) or v is None:
return v
msg = "you should provide UUID or None as target_id for CHARACTER target"
raise ValueError(msg)
if target_type == TargetType.USER:
if isinstance(v, int):
return v
msg = "you should provide int as target_id for USER target"
raise ValueError(msg)
msg = "you provided unrecognized target_type"
raise ValueError(msg)

@classmethod
@field_validator("campaign_id", mode="wrap")
def validate_campaign_id(cls, v: int | None, values: dict) -> int:
if "target_type" in values and values["target_type"] == TargetType.CHARACTER and v is None:
msg = "campaign_id is required for CHARACTER target type"
raise ValueError(msg)
return v


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!")
Expand All @@ -26,15 +84,25 @@ async def upload_document(msg: Message, _: MessageInput, manager: DialogManager)
f = await msg.bot.download(msg.document.file_id)
content = f.read()

source = manager.start_data.get("source")
user = manager.middleware_data["user"]
if source == "user":
request = UploadCharacterRequest(**manager.start_data)

source: CharacterData
if request.target_type == TargetType.USER:
source = user
elif request.target_type == TargetType.CHARACTER:
if request.target_id is None:
source = await Character.create(user=user, campaign_id=request.campaign_id)
else:
source = await Character.get(id=request.target_id)
else:
source = await Character.get_or_none(id=source)
logger.error("Failed to find source for user %d", user)
return

if not source:
logger.error("Failed to find source for user %d", user)
return

try:
await update_char_data(source, json.loads(content.decode("utf-8")))
except UnicodeDecodeError:
Expand All @@ -51,8 +119,7 @@ async def upload_document(msg: Message, _: MessageInput, manager: DialogManager)


"""
Этот диалог обязательно должен включать в start_data параметр source,
который должен содержать либо "user", что означает, что нам надо сохранять в юзере, либо же UUID персонажа
Этот диалог обязательно должен включать в start_data параметр request: UploadCharacterRequest
"""
router.include_router(
Dialog(
Expand Down
Loading