diff --git a/alembic.ini b/alembic.ini index 510a45e..28606b4 100644 --- a/alembic.ini +++ b/alembic.ini @@ -53,7 +53,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = postgresql+asyncpg://kts_user:kts_pass@db:5432/kts +sqlalchemy.url = postgresql+asyncpg://kts_user:kts_pass@postgr:5432/kts [post_write_hooks] diff --git a/alembic/env.py b/alembic/env.py index 1e737bd..fed0517 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -5,7 +5,7 @@ from sqlalchemy import pool from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import AsyncEngine -from app.store.models.model import ParticipantsModel, GameModel +from app.store.models.model import ParticipantsModel, GameModel, GameSession from alembic import context diff --git a/alembic/versions/e0fb8c912758_add_gamemodel.py b/alembic/versions/02d03f3e7912_.py similarity index 88% rename from alembic/versions/e0fb8c912758_add_gamemodel.py rename to alembic/versions/02d03f3e7912_.py index 6838a7e..7a6a65e 100644 --- a/alembic/versions/e0fb8c912758_add_gamemodel.py +++ b/alembic/versions/02d03f3e7912_.py @@ -1,8 +1,8 @@ -"""Add GameModel +"""empty message -Revision ID: e0fb8c912758 +Revision ID: 02d03f3e7912 Revises: -Create Date: 2023-03-04 19:24:14.714777 +Create Date: 2023-03-13 18:46:54.457627 """ from alembic import op @@ -10,7 +10,7 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = 'e0fb8c912758' +revision = '02d03f3e7912' down_revision = None branch_labels = None depends_on = None @@ -25,7 +25,7 @@ def upgrade() -> None: sa.UniqueConstraint('chat_id') ) op.create_table('game', - sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), sa.Column('chat_id', sa.BigInteger(), nullable=True), sa.Column('users', postgresql.JSONB(astext_type=sa.Text()), nullable=True), sa.Column('state_photo', sa.Boolean(), nullable=True), @@ -38,12 +38,14 @@ def upgrade() -> None: sa.Column('voters', postgresql.JSONB(astext_type=sa.Text()), nullable=True), sa.Column('amount_users', sa.BigInteger(), nullable=True), sa.Column('last_winner', sa.Text(), nullable=True), + sa.Column('kicked_users', postgresql.JSONB(astext_type=sa.Text()), nullable=True), sa.ForeignKeyConstraint(['chat_id'], ['game_session.chat_id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_table('participants', sa.Column('id', sa.BigInteger(), nullable=False), sa.Column('name', sa.Text(), nullable=False), + sa.Column('user_id', sa.BigInteger(), nullable=True), sa.Column('wins', sa.BigInteger(), nullable=True), sa.Column('chat_id', sa.BigInteger(), nullable=True), sa.Column('owner_id', sa.BigInteger(), nullable=True), diff --git a/alembic/versions/4a29dffe74c1_.py b/alembic/versions/4a29dffe74c1_.py new file mode 100644 index 0000000..5ab7e42 --- /dev/null +++ b/alembic/versions/4a29dffe74c1_.py @@ -0,0 +1,64 @@ +"""empty message + +Revision ID: 4a29dffe74c1 +Revises: af9f3a08a2fe +Create Date: 2023-03-13 19:58:38.369716 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '4a29dffe74c1' +down_revision = 'af9f3a08a2fe' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('game_session', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('chat_id', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('chat_id') + ) + op.create_table('game', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('chat_id', sa.BigInteger(), nullable=True), + sa.Column('users', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('state_photo', sa.Boolean(), nullable=True), + sa.Column('state_in_game', sa.Boolean(), nullable=True), + sa.Column('state_wait_votes', sa.Boolean(), nullable=True), + sa.Column('new_pair', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('first_votes', sa.BigInteger(), nullable=True), + sa.Column('second_votes', sa.BigInteger(), nullable=True), + sa.Column('state_send_photo', sa.Boolean(), nullable=True), + sa.Column('voters', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('amount_users', sa.BigInteger(), nullable=True), + sa.Column('last_winner', sa.Text(), nullable=True), + sa.Column('kicked_users', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.ForeignKeyConstraint(['chat_id'], ['game_session.chat_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('participants', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('user_id', sa.BigInteger(), nullable=True), + sa.Column('wins', sa.BigInteger(), nullable=True), + sa.Column('chat_id', sa.BigInteger(), nullable=True), + sa.Column('owner_id', sa.BigInteger(), nullable=True), + sa.Column('photo_id', sa.BigInteger(), nullable=True), + sa.Column('access_key', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['chat_id'], ['game_session.chat_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('game', sa.Column('state_photo', sa.BOOLEAN(), autoincrement=False, nullable=True)) + # ### end Alembic commands ### diff --git a/alembic/versions/eb47d650c4bf_add_gamemodel.py b/alembic/versions/af9f3a08a2fe_.py similarity index 52% rename from alembic/versions/eb47d650c4bf_add_gamemodel.py rename to alembic/versions/af9f3a08a2fe_.py index 02468e9..eedafce 100644 --- a/alembic/versions/eb47d650c4bf_add_gamemodel.py +++ b/alembic/versions/af9f3a08a2fe_.py @@ -1,28 +1,29 @@ -"""Add GameModel +"""empty message -Revision ID: eb47d650c4bf -Revises: e0fb8c912758 -Create Date: 2023-03-07 21:26:04.620744 +Revision ID: af9f3a08a2fe +Revises: 02d03f3e7912 +Create Date: 2023-03-13 19:55:17.806925 """ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql + + # revision identifiers, used by Alembic. -revision = 'eb47d650c4bf' -down_revision = 'e0fb8c912758' +revision = 'af9f3a08a2fe' +down_revision = '02d03f3e7912' branch_labels = None depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.add_column('game', sa.Column('kicked_users', postgresql.JSONB(astext_type=sa.Text()), nullable=True)) + pass # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('game', 'kicked_users') + pass # ### end Alembic commands ### diff --git a/app/store/bot/bot_runner.py b/app/store/bot/bot_runner.py index fea8477..5dadac8 100644 --- a/app/store/bot/bot_runner.py +++ b/app/store/bot/bot_runner.py @@ -7,7 +7,7 @@ def run(): loop = asyncio.get_event_loop() - bot = Bot(3) + bot = Bot(100) try: print("Bot has been started") loop.create_task(bot.start()) diff --git a/app/store/bot/keyboards.py b/app/store/bot/keyboards.py index 355f724..1f060bf 100644 --- a/app/store/bot/keyboards.py +++ b/app/store/bot/keyboards.py @@ -17,7 +17,6 @@ def get_but(text, color): "buttons": [ [ get_but("Регистрация!", "primary"), - get_but("Загрузить фотографии!", "primary"), ], [ get_but("Начать игру!", "primary"), diff --git a/app/store/bot/manager.py b/app/store/bot/manager.py index fdc9492..3f6af44 100644 --- a/app/store/bot/manager.py +++ b/app/store/bot/manager.py @@ -71,33 +71,6 @@ async def handle_updates(self, update): self.reader = list(map(int, self.reader)) if update.object.body in self.commands: await self.commands[update.object.body](update, game) - if ( - update.object.body - == f"{lexicon_for_messages['ID_GROUP']} Загрузить фотографии!" - or game["state_photo"] - ): - if len(users) == 0: - self.out_queue.put_nowait( - ( - "message", - update.object.chat_id, - lexicon_for_messages["NO_REG"], - ) - ) - else: - if not game["state_in_game"]: - if not game["state_photo"]: - await self.set_state_photo(update, True) - else: - await self.command_download_photo(update) - else: - self.out_queue.put_nowait( - ( - "message", - update.object.chat_id, - lexicon_for_messages["DUR_GAME"], - ) - ) if ( len(update.object.body.split()) == 2 and "Исключить" in update.object.body.split() @@ -157,24 +130,25 @@ async def command_registery(self, update, game): result = await app.store.vk_api.make_userlist( update.object.chat_id, self.app ) - for k, v in result: - users_exists_select = select( - ParticipantsModel.__table__.c.chat_id, - ParticipantsModel.__table__.c.name, - ).where( - ParticipantsModel.__table__.columns.chat_id - == update.object.chat_id, - ParticipantsModel.__table__.c.name == k, - ) - result = await session.execute(users_exists_select) - if not ((update.object.chat_id, k) in result.fetchall()): + users_exists_select = select( + ParticipantsModel.__table__.c.chat_id, + ParticipantsModel.__table__.c.name, + ).where( + ParticipantsModel.__table__.columns.chat_id + == update.object.chat_id, + ) + response_db = await session.execute(users_exists_select) + response_db = response_db.fetchall() + for i in result: + if not ((update.object.chat_id, i[0][0]) in response_db): new_user = ParticipantsModel( - name=k, + name=i[0][0], wins=0, + user_id=i[0][1], chat_id=update.object.chat_id, - owner_id=v, - photo_id=None, - access_key=None, + owner_id=i[1][2], + photo_id=i[1][0], + access_key=i[1][1], ) session.add(new_user) await session.commit() @@ -197,6 +171,8 @@ async def command_registery(self, update, game): async def command_next_round(self, update, game): if game["state_in_game"]: game["state_wait_votes"] = False + if game["voters"] is None: + game["voters"] = {} game["voters"]["already_voted"] = [] await self.set_state_wait_votes(update, game["state_wait_votes"]) await self.set_voters(update, [], game) @@ -332,17 +308,16 @@ async def get_game(self, update): result = { "chat_id": temp[0][1], "users": temp[0][2], - "state_photo": temp[0][3], - "state_in_game": temp[0][4], - "state_wait_votes": temp[0][5], - "new_pair": temp[0][6], - "first_votes": temp[0][7], - "second_votes": temp[0][8], - "state_send_photo": temp[0][9], - "voters": temp[0][10], - "amount_users": temp[0][11], - "last_winner": temp[0][12], - "kicked_users": temp[0][13], + "state_in_game": temp[0][3], + "state_wait_votes": temp[0][4], + "new_pair": temp[0][5], + "first_votes": temp[0][6], + "second_votes": temp[0][7], + "state_send_photo": temp[0][8], + "voters": temp[0][9], + "amount_users": temp[0][10], + "last_winner": temp[0][11], + "kicked_users": temp[0][12], } return result @@ -368,18 +343,6 @@ async def set_last_winner(self, update, value): await session.execute(query_state_photo) await session.commit() - async def set_state_photo(self, update, value): - await self.app.database.connect() - async with self.app.database.session.begin() as session: - query_state_photo = ( - refresh(GameModel.__table__) - .where( - GameModel.__table__.c.chat_id == update.object.chat_id, - ) - .values(state_photo=value) - ) - await session.execute(query_state_photo) - await session.commit() async def set_state_send_photo(self, update, value): await self.app.database.connect() @@ -549,7 +512,7 @@ async def new_win(self, update, game): .values( wins=ParticipantsModel.__table__.c.wins + 1, ) - ) + ) # New win await session.execute(user_new_win) await session.commit() @@ -562,7 +525,7 @@ async def get_statistics(self, update): ).where( ParticipantsModel.__table__.columns.chat_id == update.object.chat_id, - ParticipantsModel.__table__.c.owner_id == update.object.id, + ParticipantsModel.__table__.c.user_id == update.object.id, ) result = await session.execute(user_check_wins) return result.fetchall() @@ -586,7 +549,7 @@ async def command_kick(self, update): user_check_wins = delete(ParticipantsModel.__table__).where( ParticipantsModel.__table__.columns.chat_id == update.object.chat_id, - ParticipantsModel.__table__.c.owner_id == update.object.id, + ParticipantsModel.__table__.c.user_id == update.object.id, ) await session.execute(user_check_wins) await session.commit() @@ -616,34 +579,6 @@ def check_users(self, game): return 1 return 0 - async def command_download_photo(self, update): - if hasattr(update.object, "type") and update.object.type == "photo": - await self.app.database.connect() - async with self.app.database.session.begin() as session: - users_add_photos = ( - refresh(ParticipantsModel.__table__) - .where( - ParticipantsModel.__table__.c.owner_id - == update.object.owner_id, - ParticipantsModel.__table__.c.chat_id - == update.object.chat_id, - ) - .values( - photo_id=update.object.photo_id, - access_key=update.object.access_key, - ) - ) - await self.set_state_photo(update, False) - await session.execute(users_add_photos) - await session.commit() - - self.out_queue.put_nowait( - ( - "message", - update.object.chat_id, - lexicon_for_messages["SUCC_PHOTO"], - ) - ) async def proccess_start_game(self, chat_id): await self.app.database.connect() diff --git a/app/store/bot/sender.py b/app/store/bot/sender.py index 9a3d23d..524a0c7 100644 --- a/app/store/bot/sender.py +++ b/app/store/bot/sender.py @@ -42,7 +42,7 @@ async def _worker(self): self.out_queue.task_done() async def start(self): - asyncio.create_task(self._worker()) + self._tasks.append(asyncio.create_task(self._worker())) async def stop(self): await self.out_queue.join() diff --git a/app/store/database/database.py b/app/store/database/database.py index 3d84b2a..bee485b 100644 --- a/app/store/database/database.py +++ b/app/store/database/database.py @@ -22,7 +22,7 @@ def __init__(self, app: "Application"): async def connect(self, *_: list, **__: dict) -> None: self._db = db self._engine = create_async_engine( - "postgresql+asyncpg://kts_user:kts_pass@db:5432/kts", + "postgresql+asyncpg://kts_user:kts_pass@postgr:5432/kts", #db_1 echo=True, ) self.session = sessionmaker( diff --git a/app/store/models/model.py b/app/store/models/model.py index 42c09bb..5e5ac45 100644 --- a/app/store/models/model.py +++ b/app/store/models/model.py @@ -2,7 +2,7 @@ from app.store.database.sqlalchemy_base import db -from sqlalchemy import Column, Text, BigInteger, ForeignKey, Boolean +from sqlalchemy import Column, Text, BigInteger, ForeignKey, Boolean, Integer from sqlalchemy.dialects.postgresql import JSONB @@ -13,11 +13,10 @@ class GameModel(db): BigInteger, ForeignKey("game_session.chat_id", ondelete="CASCADE") ) users = Column(JSONB) - state_photo = Column(Boolean) state_in_game = Column(Boolean) state_wait_votes = Column(Boolean) new_pair = Column(JSONB) - first_votes = Column(BigInteger, default=0) + first_votes = Column(Integer, default=0) second_votes = Column(BigInteger, default=0) state_send_photo = Column(Boolean) voters = Column(JSONB) @@ -36,6 +35,7 @@ class ParticipantsModel(db): __tablename__ = "participants" id = Column(BigInteger, primary_key=True) name = Column(Text, nullable=False) + user_id = Column(BigInteger) wins = Column(BigInteger) chat_id = Column( BigInteger, ForeignKey("game_session.chat_id", ondelete="CASCADE") diff --git a/app/store/vk_api/accessor.py b/app/store/vk_api/accessor.py index f21f367..5c1ef87 100644 --- a/app/store/vk_api/accessor.py +++ b/app/store/vk_api/accessor.py @@ -1,5 +1,9 @@ +import asyncio import random import typing +from io import BytesIO + +import requests from typing import Optional from aiohttp import TCPConnector from aiohttp.client import ClientSession @@ -21,6 +25,51 @@ API_PATH = "https://api.vk.com/method/" +class FilesOpener(object): + def __init__(self, paths, key_format='file{}'): + if not isinstance(paths, list): + paths = [paths] + + self.paths = paths + self.key_format = key_format + self.opened_files = [] + + def __enter__(self): + return self.open_files() + + def __exit__(self, type, value, traceback): + self.close_files() + + def open_files(self): + self.close_files() + + files = [] + + for x, file in enumerate(self.paths): + if hasattr(file, 'read'): + f = file + + if hasattr(file, 'name'): + filename = file.name + else: + filename = '.jpg' + else: + filename = file + f = open(filename, 'rb') + self.opened_files.append(f) + + ext = filename.split('.')[-1] + files.append( + (self.key_format.format(x), ('file{}.{}'.format(x, ext), f)) + ) + + return files + + def close_files(self): + for f in self.opened_files: + f.close() + + self.opened_files = [] class VkApiAccessor(BaseAccessor): def __init__(self, app: "Application", *args, **kwargs): super().__init__(app, *args, **kwargs) @@ -52,6 +101,7 @@ def _build_query(host: str, method: str, params: dict) -> str: url += "&".join([f"{k}={v}" for k, v in params.items()]) return url + async def _get_long_poll_service(self): async with self.session.get( self._build_query( @@ -63,6 +113,7 @@ async def _get_long_poll_service(self): }, ) ) as resp: + a = await resp.json() data = (await resp.json())["response"] self.logger.info(data) @@ -81,7 +132,7 @@ async def poll(self, app): "act": "a_check", "key": self.key, "ts": self.ts, - "wait": 10, + "wait": 30, }, ) ) as resp: @@ -150,6 +201,83 @@ async def poll(self, app): await self.disconnect(app) return updates + + async def get_messages_server(self, app, chat_id): + async with self.session.get( + self._build_query( + API_PATH, + "photos.getMessagesUploadServer", + params={ + "peer_id": chat_id, + "fields": "photo_400_orig", + "access_token": self.app.config.bot.token, + }, + ) + ) as resp: + data = (await resp.json())["response"] + return data["upload_url"] + + + async def get_photo(self, app, user_ids, chat_id): + user_ids = ",".join(user_ids) + async with self.session.get( + self._build_query( + API_PATH, + "users.get", + params={ + "user_ids": user_ids, + "fields": "photo_400_orig", + "access_token": self.app.config.bot.token, + }, + ) + ) as resp: + data = (await resp.json())["response"] + return [i["photo_400_orig"] for i in data] + + + + async def save_photo(self, app, photo_links, chat_id): + url = await self.get_messages_server(app, chat_id) + massiv = [] + tasks = [] + for i in photo_links: + img = requests.get(i).content + f = BytesIO(img) + with FilesOpener(f) as photo_files: + response = requests.post(url, files=photo_files) + answer = response.json() + massiv.append(answer) + return massiv + + async def process_of_get_fields(self, app, object, result): + async with self.session.get( + self._build_query( + API_PATH, + "photos.saveMessagesPhoto", + params={ + "photo": object["photo"], + "server": object["server"], + "hash": object["hash"], + "access_token": self.app.config.bot.token + }, + ) + ) as resp: + photo_attr = (await resp.json())["response"][-1] + result.append((photo_attr["id"], photo_attr["access_key"], photo_attr["owner_id"])) + + async def download_photo(self, app, user_ids, chat_id): + photo_links = await self.get_photo(app, user_ids, chat_id) + data = await self.save_photo(app, photo_links, chat_id) + result = [] + tasks = [] + for i in data: + tasks.append(asyncio.create_task(self.process_of_get_fields(app, i, result))) + await asyncio.gather(*tasks) + return result + + + + async def send_message( self, message: Message | MessageKeyboard, app ) -> None: @@ -174,7 +302,7 @@ async def send_message( ) as resp: data = await resp.json() self.logger.info(data) - await self.disconnect(app) + @staticmethod def _build_attachment(attach_mass: list[str]): @@ -202,7 +330,7 @@ async def send_photo(self, attachment: Attachment, app) -> None: ) as resp: data = await resp.json() self.logger.info(data) - await self.disconnect(app) + async def make_userlist(self, chat_id, app): await self.connect(app) @@ -222,5 +350,6 @@ async def make_userlist(self, chat_id, app): full_name = f'@{data["response"]["profiles"][i]["screen_name"]}' id_profile = data["response"]["profiles"][i]["id"] participants.append((full_name, id_profile)) - await self.disconnect(app) + photo_fields = await self.app.store.vk_api.download_photo(app, [str(i[1]) for i in participants], chat_id) + participants = list(zip(participants, photo_fields)) return participants diff --git a/docker-compose.yaml b/docker-compose.yaml index f15a8d7..5d23f12 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ version: '3.5' services: - db: + postgr: container_name: db restart: on-failure ports: @@ -14,11 +14,11 @@ services: volumes: - pgdata:/var/lib/postgresql/data bot: - image: olred/vk_bot_project:my-image + image: olred/vk_bot_project:latest command: sh -c "make migrate && python run_bot.py" restart: always depends_on: - - db + - postgr volumes: pgdata: diff --git a/requirements.txt b/requirements.txt index ae7c9ec..ab198d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ async-timeout==4.0.2 asyncpg==0.27.0 attrs==22.2.0 black==22.6.0 +certifi==2022.12.7 cffi==1.15.1 charset-normalizer==2.1.1 click==8.1.3 @@ -44,10 +45,12 @@ pytest-mock==3.10.0 python-dateutil==2.8.2 python-dotenv==0.21.1 PyYAML==6.0 +requests==2.28.2 simplejson==3.18.3 six==1.16.0 SQLAlchemy==2.0.3 tomli==2.0.1 typing_extensions==4.4.0 +urllib3==1.26.15 webargs==5.5.3 yarl==1.8.2