From 194b5aaff1a794dbd934615119ae232832a7327f Mon Sep 17 00:00:00 2001 From: Llloooggg Date: Sat, 19 Nov 2022 01:31:35 +0300 Subject: [PATCH] =?UTF-8?q?backend=20+=20telegram:=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D1=81=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=B6=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/migrations/versions/02142c549a8c_.py | 44 ++++++++ backend/migrations/versions/07950b032eb7_.py | 36 ++++++ backend/textsouls/common/views.py | 22 ++-- backend/textsouls/main.py | 6 +- backend/textsouls/models.py | 18 ++- telegram/requirements.txt | 8 +- telegram/textsouls/common.py | 7 ++ .../textsouls/handlers/character_creation.py | 105 ++++++++++++++++++ telegram/textsouls/handlers/control.py | 38 +++++++ telegram/textsouls/keyboards/common.py | 18 +++ telegram/textsouls/main.py | 40 +++---- 11 files changed, 291 insertions(+), 51 deletions(-) create mode 100644 backend/migrations/versions/02142c549a8c_.py create mode 100644 backend/migrations/versions/07950b032eb7_.py create mode 100644 telegram/textsouls/handlers/character_creation.py create mode 100644 telegram/textsouls/handlers/control.py create mode 100644 telegram/textsouls/keyboards/common.py mode change 100755 => 100644 telegram/textsouls/main.py diff --git a/backend/migrations/versions/02142c549a8c_.py b/backend/migrations/versions/02142c549a8c_.py new file mode 100644 index 0000000..11c1187 --- /dev/null +++ b/backend/migrations/versions/02142c549a8c_.py @@ -0,0 +1,44 @@ +"""empty message + +Revision ID: 02142c549a8c +Revises: 07950b032eb7 +Create Date: 2022-11-19 00:29:12.011840 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '02142c549a8c' +down_revision = '07950b032eb7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('character_classes', schema=None) as batch_op: + batch_op.create_unique_constraint(None, ['name']) + + with op.batch_alter_table('character_races', schema=None) as batch_op: + batch_op.create_unique_constraint(None, ['name']) + + with op.batch_alter_table('characters', schema=None) as batch_op: + batch_op.create_unique_constraint(None, ['name']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('characters', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + + with op.batch_alter_table('character_races', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + + with op.batch_alter_table('character_classes', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + + # ### end Alembic commands ### diff --git a/backend/migrations/versions/07950b032eb7_.py b/backend/migrations/versions/07950b032eb7_.py new file mode 100644 index 0000000..8ab6a86 --- /dev/null +++ b/backend/migrations/versions/07950b032eb7_.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: 07950b032eb7 +Revises: 5aef83a8a42f +Create Date: 2022-11-18 23:40:33.130706 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '07950b032eb7' +down_revision = '5aef83a8a42f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('characters', schema=None) as batch_op: + batch_op.alter_column('owner', + existing_type=mysql.INTEGER(), + nullable=True) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('characters', schema=None) as batch_op: + batch_op.alter_column('owner', + existing_type=mysql.INTEGER(), + nullable=False) + + # ### end Alembic commands ### diff --git a/backend/textsouls/common/views.py b/backend/textsouls/common/views.py index 45bf457..47d664e 100644 --- a/backend/textsouls/common/views.py +++ b/backend/textsouls/common/views.py @@ -18,17 +18,6 @@ class ItemAPI(MethodView): item = self._get_item(id) return item.to_dict() - def patch(self, id): - item = self._get_item(id) - errors = self.validator.validate(item, request.json) - - if errors: - return jsonify(errors), 400 - - item.update_from_json(request.json) - db.session.commit() - return item.to_dict() - def delete(self, id): item = self._get_item(id) db.session.delete(item) @@ -51,12 +40,15 @@ class ListAPI(MethodView): def post(self): - item = self._get_item(request.json["id"]) + data = request.json - if item: - return "Already exists!", 400 + if data.get("id"): + item = self._get_item(data["id"]) - db.session.add(self.model(**request.json)) + if item: + return "Already exists!", 400 + + db.session.add(self.model(**data)) db.session.commit() return "", 200 diff --git a/backend/textsouls/main.py b/backend/textsouls/main.py index 9848e23..e706b0f 100644 --- a/backend/textsouls/main.py +++ b/backend/textsouls/main.py @@ -11,6 +11,6 @@ main = Blueprint("main", __name__) register_api(main, User, "users") -register_api(main, CharacterRace, "charachter-races") -register_api(main, CharacterClass, "charachter-classes") -register_api(main, Character, "character") +register_api(main, CharacterRace, "character_races") +register_api(main, CharacterClass, "character_classes") +register_api(main, Character, "characters") diff --git a/backend/textsouls/models.py b/backend/textsouls/models.py index 24e326b..ead9701 100644 --- a/backend/textsouls/models.py +++ b/backend/textsouls/models.py @@ -11,6 +11,8 @@ class User(db.Model, SerializerMixin): __tablename__ = "users" + serialize_rules = ("-characters",) + id = db.Column(db.Integer, primary_key=True, autoincrement=False) first_name = db.Column(db.String(255), nullable=True) last_name = db.Column(db.String(255), nullable=True) @@ -19,7 +21,7 @@ class User(db.Model, SerializerMixin): db.DateTime, nullable=False, default=datetime.datetime.now() ) is_admin = db.Column(db.Boolean, nullable=False, default=False) - character = db.relationship("Character", backref="user", lazy="dynamic") + characters = db.relationship("Character", backref="user", lazy="dynamic") def __str__(self): return f"{self.id}: {self.username}" @@ -29,9 +31,11 @@ class CharacterRace(db.Model, SerializerMixin): __tablename__ = "character_races" + serialize_rules = ("-characters",) + id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(255), nullable=True) - character = db.relationship("Character", backref="race", lazy="dynamic") + name = db.Column(db.String(255), nullable=True, unique=True) + characters = db.relationship("Character", backref="race", lazy="dynamic") def __str__(self): return self.name @@ -41,9 +45,11 @@ class CharacterClass(db.Model, SerializerMixin): __tablename__ = "character_classes" + serialize_rules = ("-characters",) + id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(255), nullable=True) - character = db.relationship("Character", backref="class", lazy="dynamic") + name = db.Column(db.String(255), nullable=True, unique=True) + characters = db.relationship("Character", backref="class", lazy="dynamic") def __str__(self): return self.name @@ -59,7 +65,7 @@ class Character(db.Model, SerializerMixin): db.ForeignKey("users.id"), nullable=True, ) - name = db.Column(db.String(255), nullable=False) + name = db.Column(db.String(255), nullable=False, unique=True) character_race = db.Column( db.Integer, db.ForeignKey("character_races.id"), diff --git a/telegram/requirements.txt b/telegram/requirements.txt index da6b889..7adb59c 100644 --- a/telegram/requirements.txt +++ b/telegram/requirements.txt @@ -1,16 +1,22 @@ -aiogram==2.23.1 +aiofiles==22.1.0 +aiogram==3.0.0b6 aiohttp==3.8.3 aiosignal==1.3.1 async-timeout==4.0.2 attrs==22.1.0 Babel==2.9.1 +cachetools==4.2.4 certifi==2022.9.24 charset-normalizer==2.1.1 frozenlist==1.3.3 idna==3.4 +Jinja2==3.1.2 magic-filter==1.0.9 +MarkupSafe==2.1.1 multidict==6.0.2 +pydantic==1.10.2 pytz==2022.6 requests==2.28.1 +typing-extensions==4.4.0 urllib3==1.26.12 yarl==1.8.1 diff --git a/telegram/textsouls/common.py b/telegram/textsouls/common.py index 719ef6a..3fa5c89 100644 --- a/telegram/textsouls/common.py +++ b/telegram/textsouls/common.py @@ -8,6 +8,13 @@ with open("textsouls/config.json") as config_file: class Backend: base_url = config_data["BACKEND_SETTINGS"]["BASE_URL"] + def get(self, relative_url): + try: + response = requests.get(f"{self.base_url}{relative_url}") + return {"error": None, "response": response} + except Exception as err: + return {"error": err} + def post(self, relative_url, data): try: response = requests.post( diff --git a/telegram/textsouls/handlers/character_creation.py b/telegram/textsouls/handlers/character_creation.py new file mode 100644 index 0000000..ccf2ba2 --- /dev/null +++ b/telegram/textsouls/handlers/character_creation.py @@ -0,0 +1,105 @@ +from aiogram import Router +from aiogram.filters import Command +from aiogram.fsm.state import StatesGroup, State + +from common import backend +from keyboards.common import row_kb + + +router = Router() + +available_races = {} +available_classes = {} + + +class CharachterCreation(StatesGroup): + choosing_race = State() + choosing_class = State() + choosing_name = State() + creation_ending = State() + + +@router.message(Command(commands=["createchar"])) +async def character_creation(message, state): + + global available_races + result = backend.get("/character_races") + if not result["error"] and result["response"].status_code == 200: + for race in result["response"].json(): + available_races[race["name"]] = race["id"] + + await message.answer( + text="Выбери расу:", + reply_markup=row_kb(available_races.keys()), + ) + await state.set_state(CharachterCreation.choosing_class) + + +@router.message(CharachterCreation.choosing_class) +async def race_chosen(message, state): + + if not available_races.get(message.text): + await message.answer( + text="Хм, такую не знаю.\nПопробуй из вариантов ниже:", + reply_markup=row_kb(available_races), + ) + else: + await state.update_data(chosen_race=message.text) + + global available_classes + result = backend.get("/character_classes") + if not result["error"] and result["response"].status_code == 200: + for char_class in result["response"].json(): + available_classes[char_class["name"]] = char_class["id"] + await message.answer( + text="А теперь выбери класс:", + reply_markup=row_kb(available_classes.keys()), + ) + await state.set_state(CharachterCreation.choosing_name) + + +@router.message(CharachterCreation.choosing_name) +async def class_chosen(message, state): + + if not available_classes.get(message.text): + await message.answer( + text="Хм, такой не знаю.\nПопробуй из вариантов ниже:", + reply_markup=row_kb(available_classes), + ) + else: + await state.update_data(chosen_class=message.text) + + await message.answer( + text=f"Ну, неплохо.\nА зовут-то тебя как, {message.text.lower()}?", + reply_markup="", + ) + await state.set_state(CharachterCreation.creation_ending) + + +@router.message(CharachterCreation.creation_ending) +async def name_chosen(message, state): + + name = message.text + + user_elections = await state.get_data() + + new_character = { + "owner": message.from_user.id, + "name": name, + "character_race": available_races[user_elections["chosen_race"]], + "character_class": available_classes[user_elections["chosen_class"]], + } + + result = backend.post("/characters", new_character) + if not result["error"] and result["response"].status_code == 200: + await message.answer( + text="Так и запишем.\nТеперь постарайся не умереть", + reply_markup="", + ) + await state.clear() + else: + await message.answer( + text="Что-то пошло не так!\nДавай-ка по-новой", + ) + await state.set_state(CharachterCreation.choosing_race) + await character_creation(message, state) diff --git a/telegram/textsouls/handlers/control.py b/telegram/textsouls/handlers/control.py new file mode 100644 index 0000000..1ea41f4 --- /dev/null +++ b/telegram/textsouls/handlers/control.py @@ -0,0 +1,38 @@ +from aiogram import Router +from aiogram.filters import Command + +from common import backend + +from handlers.character_creation import CharachterCreation +from handlers.character_creation import character_creation + + +router = Router() + + +@router.message(Command(commands=["start"])) +async def start(message, state): + + tg_user = message.from_user + ts_user = { + "id": tg_user.id, + "first_name": tg_user.first_name, + "last_name": tg_user.last_name, + "username": tg_user.username, + } + + result = backend.post("/users", ts_user) + if not result["error"]: + response_code = result["response"].status_code + if response_code == 200: + await message.reply("Привет, скиталец!", reply_markup="") + await state.set_state(CharachterCreation.choosing_race) + await character_creation(message, state) + elif response_code == 400: + await message.reply("Добро пожаловать! Снова") + else: + await message.reply("Что-то случилось") + else: + await message.reply("Упс! Что-то пошло не так") + + await state.set_state(CharachterCreation.choosing_class) diff --git a/telegram/textsouls/keyboards/common.py b/telegram/textsouls/keyboards/common.py new file mode 100644 index 0000000..0089f74 --- /dev/null +++ b/telegram/textsouls/keyboards/common.py @@ -0,0 +1,18 @@ +from aiogram.utils.keyboard import ( + ReplyKeyboardBuilder, + ReplyKeyboardMarkup, + KeyboardButton, +) + + +def yes_no_kb(): + kb = ReplyKeyboardBuilder() + kb.button(text="Да") + kb.button(text="Нет") + kb.adjust(2) + return kb.as_markup(resize_keyboard=True) + + +def row_kb(items): + row = [KeyboardButton(text=item) for item in items] + return ReplyKeyboardMarkup(keyboard=[row], resize_keyboard=True) diff --git a/telegram/textsouls/main.py b/telegram/textsouls/main.py old mode 100755 new mode 100644 index f2e21c9..167af16 --- a/telegram/textsouls/main.py +++ b/telegram/textsouls/main.py @@ -1,39 +1,27 @@ import json -from aiogram import Bot, Dispatcher, executor +import asyncio -from common import backend +from aiogram import Bot, Dispatcher +from aiogram.fsm.storage.memory import MemoryStorage + +from handlers import control +from handlers import character_creation with open("textsouls/config.json") as config_file: config_data = json.load(config_file) -API_TOKEN = config_data["MAIN_SETTINGS"]["BOT_TOKEN"] -bot = Bot(token=API_TOKEN) -dp = Dispatcher(bot) +async def main(): + bot = Bot(token=config_data["MAIN_SETTINGS"]["BOT_TOKEN"]) + dp = Dispatcher(storage=MemoryStorage()) + dp.include_router(control.router) + dp.include_router(character_creation.router) -@dp.message_handler(commands=["start"]) -async def start(message): - tg_user = message.from_user - ts_user = { - "id": tg_user.id, - "first_name": tg_user.first_name, - "last_name": tg_user.last_name, - "username": tg_user.username, - } - result = backend.post("/users", ts_user) - if not result["error"]: - response_code = result["response"].status_code - if response_code == 200: - await message.reply("Добро пожаловать!") - elif response_code == 400: - await message.reply("Добро пожаловать! Снова") - else: - await message.reply("Что-то другое") - else: - await message.reply("Упс! Что-то пошло не так") + await bot.delete_webhook(drop_pending_updates=True) + await dp.start_polling(bot) if __name__ == "__main__": - executor.start_polling(dp, skip_updates=True) + asyncio.run(main())