diff --git a/alembic/README b/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py deleted file mode 100644 index 1fe72d8..0000000 --- a/alembic/env.py +++ /dev/null @@ -1,84 +0,0 @@ -import os -from logging.config import fileConfig -from os import getenv -from os.path import dirname, join - -from dotenv import load_dotenv -from sqlalchemy import create_engine - -from alembic import context -from db.data import Base - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - -load_dotenv(join(dirname(__file__), '..', '.env')) -DATABASE_PASSWORD=os.getenv('DATABASE_PASSWORD') -DATABASE_NAME=os.getenv('DATABASE_NAME') -DATABASE_USER=os.getenv('DATABASE_USER') -DATABASE_PORT=os.getenv('DATABASE_PORT') -DATABASE_HOST=os.getenv('DATABASE_HOST') -URL=f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = URL - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = create_engine(URL) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako deleted file mode 100644 index fbc4b07..0000000 --- a/alembic/script.py.mako +++ /dev/null @@ -1,26 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/1d040fbb74ce_added_file_id_to_image_table.py b/alembic/versions/1d040fbb74ce_added_file_id_to_image_table.py deleted file mode 100644 index ffa0de9..0000000 --- a/alembic/versions/1d040fbb74ce_added_file_id_to_image_table.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Added file_id to image table - -Revision ID: 1d040fbb74ce -Revises: 8c2a92134271 -Create Date: 2023-11-04 03:35:28.011123 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '1d040fbb74ce' -down_revision: Union[str, None] = '8c2a92134271' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('photo', sa.Column('file_id', sa.String(), nullable=False)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('photo', 'file_id') - # ### end Alembic commands ### diff --git a/alembic/versions/278f7650482f_moved_media_group_from_image_to_post.py b/alembic/versions/278f7650482f_moved_media_group_from_image_to_post.py deleted file mode 100644 index 2acfb2a..0000000 --- a/alembic/versions/278f7650482f_moved_media_group_from_image_to_post.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Moved media group from image to post - -Revision ID: 278f7650482f -Revises: ca01506184b5 -Create Date: 2023-11-04 02:32:22.398760 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '278f7650482f' -down_revision: Union[str, None] = 'ca01506184b5' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('photo', 'media_group_id') - op.add_column('post', sa.Column('media_group_id', sa.Integer(), nullable=False)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('post', 'media_group_id') - op.add_column('photo', sa.Column('media_group_id', sa.INTEGER(), autoincrement=False, nullable=False)) - # ### end Alembic commands ### diff --git a/alembic/versions/2af6a0df717b_added_timestamp_and_posted.py b/alembic/versions/2af6a0df717b_added_timestamp_and_posted.py deleted file mode 100644 index a6b60d1..0000000 --- a/alembic/versions/2af6a0df717b_added_timestamp_and_posted.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Added timestamp and posted - -Revision ID: 2af6a0df717b -Revises: 1d040fbb74ce -Create Date: 2023-11-04 04:09:12.689628 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '2af6a0df717b' -down_revision: Union[str, None] = '1d040fbb74ce' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('post', sa.Column('timestamp', sa.DateTime(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('post', 'timestamp') - # ### end Alembic commands ### diff --git a/alembic/versions/8c2a92134271_change_to_str_mediag_group_id.py b/alembic/versions/8c2a92134271_change_to_str_mediag_group_id.py deleted file mode 100644 index bc13f49..0000000 --- a/alembic/versions/8c2a92134271_change_to_str_mediag_group_id.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Change to str mediag_group_id - -Revision ID: 8c2a92134271 -Revises: 278f7650482f -Create Date: 2023-11-04 03:06:58.339894 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '8c2a92134271' -down_revision: Union[str, None] = '278f7650482f' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('post', 'media_group_id', - existing_type=sa.INTEGER(), - type_=sa.String(), - existing_nullable=False) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('post', 'media_group_id', - existing_type=sa.String(), - type_=sa.INTEGER(), - existing_nullable=False) - # ### end Alembic commands ### diff --git a/alembic/versions/9de2db27ca6e_initial_migration.py b/alembic/versions/9de2db27ca6e_initial_migration.py deleted file mode 100644 index f7e5871..0000000 --- a/alembic/versions/9de2db27ca6e_initial_migration.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Initial migration - -Revision ID: 9de2db27ca6e -Revises: -Create Date: 2023-10-29 01:23:30.890347 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '9de2db27ca6e' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/alembic/versions/c9872bd4d4b5_added_spoiler_to_photo.py b/alembic/versions/c9872bd4d4b5_added_spoiler_to_photo.py deleted file mode 100644 index 7860dc8..0000000 --- a/alembic/versions/c9872bd4d4b5_added_spoiler_to_photo.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Added spoiler to photo - -Revision ID: c9872bd4d4b5 -Revises: f0ed48a3ded3 -Create Date: 2023-11-15 23:38:03.886970 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'c9872bd4d4b5' -down_revision: Union[str, None] = 'f0ed48a3ded3' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('photo', sa.Column('has_spoiler', sa.Boolean(), nullable=False)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('photo', 'has_spoiler') - # ### end Alembic commands ### diff --git a/alembic/versions/ca01506184b5_added_image_table.py b/alembic/versions/ca01506184b5_added_image_table.py deleted file mode 100644 index d85ac1c..0000000 --- a/alembic/versions/ca01506184b5_added_image_table.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Added image table - -Revision ID: ca01506184b5 -Revises: 9de2db27ca6e -Create Date: 2023-11-04 02:21:28.077631 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'ca01506184b5' -down_revision: Union[str, None] = '9de2db27ca6e' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('photo', - sa.Column('message_id', sa.Integer(), nullable=False), - sa.Column('media_group_id', sa.Integer(), nullable=False), - sa.Column('post_id', sa.Uuid(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['post.uuid'], ), - sa.PrimaryKeyConstraint('message_id') - ) - op.drop_column('post', 'images') - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('post', sa.Column('images', sa.VARCHAR(), autoincrement=False, nullable=False)) - op.drop_table('photo') - # ### end Alembic commands ### diff --git a/alembic/versions/f0ed48a3ded3_added_posted.py b/alembic/versions/f0ed48a3ded3_added_posted.py deleted file mode 100644 index a11cb7c..0000000 --- a/alembic/versions/f0ed48a3ded3_added_posted.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Added posted - -Revision ID: f0ed48a3ded3 -Revises: 2af6a0df717b -Create Date: 2023-11-04 04:12:46.479474 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'f0ed48a3ded3' -down_revision: Union[str, None] = '2af6a0df717b' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('post', sa.Column('posted', sa.Boolean(), nullable=False)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('post', 'posted') - # ### end Alembic commands ### diff --git a/db/data.py b/db/data.py deleted file mode 100644 index ddbf6b1..0000000 --- a/db/data.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -from datetime import datetime -from os.path import dirname, join -from typing import List, Optional -from uuid import UUID - -from dotenv import load_dotenv -from sqlalchemy import Column, DateTime, ForeignKey, create_engine -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship - -load_dotenv(join(dirname(__file__), '..', '.env')) -DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD') -DATABASE_NAME = os.getenv('DATABASE_NAME') -DATABASE_USER = os.getenv('DATABASE_USER') -DATABASE_PORT = os.getenv('DATABASE_PORT') -DATABASE_HOST = os.getenv('DATABASE_HOST') -engine = create_engine( - f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}", echo=True) - - -class Base(DeclarativeBase): - pass - - -class User(Base): - __tablename__ = 'user' - - id: Mapped[int] = mapped_column(primary_key=True) - user_name: Mapped[Optional[str]] - - admin: Mapped['Admin'] = relationship( - back_populates='user', cascade='all, delete-orphan') - - def __repr__(self) -> str: - return f'User(id={self.id!r}, user_name={self.user_name!r})' - - def __str__(self) -> str: - return f'User(id={self.id!r}, user_name={self.user_name!r})' - - -class Admin(Base): - __tablename__ = 'admin' - - user_id: Mapped[int] = mapped_column( - ForeignKey('user.id'), primary_key=True) - user: Mapped['User'] = relationship(back_populates='admin') - posts: Mapped[List['Post']] = relationship( - back_populates='user', cascade='all, delete') - - -class Post(Base): - __tablename__ = 'post' - - uuid: Mapped[UUID] = mapped_column(primary_key=True) - posted: Mapped[bool] = mapped_column(default=False) - from_user_id: Mapped[int] = mapped_column(ForeignKey('admin.user_id')) - user: Mapped['Admin'] = relationship(back_populates='posts') - text: Mapped[str] - media_group_id: Mapped[str] - images: Mapped[List['Image']] = relationship( - back_populates='post', cascade='all, delete') - timestamp = Column(DateTime, default=datetime.utcnow) - - def __repr__(self) -> str: - return f"UUID: {self.uuid}. From_user_id: {self.from_user_id}. media group id: {self.media_group_id}. Text: {self.text}" - - -class Image(Base): - __tablename__ = 'photo' - message_id: Mapped[int] = mapped_column(primary_key=True) - post: Mapped['Post'] = relationship(back_populates='images') - post_id: Mapped[int] = mapped_column(ForeignKey('post.uuid')) - file_id: Mapped[str] - has_spoiler: Mapped[bool] - - -if __name__ == '__main__': - Base.metadata.create_all(engine) diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index cfbf242..c3b1894 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -1,24 +1,22 @@ -from datetime import datetime from typing import Any -from uuid import uuid4 from aiogram import Bot, F, Router, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.utils.media_group import MediaGroupBuilder -from sqlalchemy.orm import Session -from db.data import Admin, Image, Post, User, engine from handlers.filters.new_post import (ChangePosts, NewPostFilter, NewSoloPostFilter) +from handlers.filters.reply_to_user import ReplyToUser from handlers.middlewares.user import AdminMiddleware from handlers.states.change_post import ChangePost +from neuroapi import neuroapi -def get_post_info(post: Post, post_id: int) -> str: - text = post.text - time = post.timestamp - from_user = post.from_user_id +def get_post_info(post: dict, post_id: int) -> str: + text = post["text"] + time = post["timestamp"] + from_user = post["from_user_id"] s = f"""Индекс: {post_id}\nТекст: {text}\nВремя отправки: {time}\nОт: [id{from_user}](tg://user?id={from_user})""".replace('#', '\#').replace( "_", "\_").replace('.', '\.').replace(',', '\,').replace('!', '\!').replace('-', '\-').replace(':', '\:') return s @@ -33,67 +31,50 @@ class Admin_commands: self.router = Router() self.router.message.middleware(AdminMiddleware()) - @self.router.message(Command('test')) - async def test_command(message: types.Message): - with Session(engine) as session: - user = session.get(User, message.from_user.id) - await message.answer(str(user)) - @self.router.message(NewPostFilter()) async def new_post(message: types.Message): - with Session(engine) as session: - post = session.query(Post).filter( - Post.media_group_id == message.media_group_id).first() - if post: - photo = Image(message_id=message.message_id, - post=post, file_id=message.photo[-1].file_id, has_spoiler=bool(message.has_media_spoiler)) - session.add(photo) - session.commit() - else: - print('No posts anymore ;-(') + post = await neuroapi.post.get_by_media_group_id(message.media_group_id) + await neuroapi.image.add(post['uuid'], message.photo[-1].file_id, message.has_media_spoiler, message.message_id) @self.router.message(Command('info')) async def info_command(message: types.Message): - with Session(engine) as session: - posts = session.query(Post).filter(Post.posted == False).all() - admins = session.query(Admin).all() - post_c = {} - for admin in admins: - post_c[str(admin.user_id)] = 0 - for post in posts: - post_c[str(post.from_user_id)] += 1 + posts = await neuroapi.post.get_will_post() + post_c = {} + for post in posts: + if post['from_user_id'] not in post_c: + post_c[post['from_user_id']] = 1 + else: + post_c[post['from_user_id']] += 1 await message.answer(str(post_c)) @self.router.message(ChangePosts()) async def change_post(message: types.Message, state: FSMContext): - with Session(engine) as session: - posts = session.query(Post).filter( - Post.posted == False).order_by(Post.timestamp.asc()).all() - if len(posts): - await state.update_data(posts=posts, id=0) - select_btns = [] - if len(posts) > 1: - select_btns.append(types.InlineKeyboardButton( - text='->', callback_data='next_post')) - kb = [ - select_btns, - [types.InlineKeyboardButton( - callback_data='change_post_text', text='Текст')], - [types.InlineKeyboardButton( - text='Отмена', callback_data='cancel')] - ] - keyboard = types.InlineKeyboardMarkup(inline_keyboard=kb) - images = MediaGroupBuilder( - caption=get_post_info(posts[0], 1)) - for image in posts[0].images: - images.add_photo( - image.file_id, parse_mode='markdownv2') - mes = await message.answer_media_group(media=images.build()) - await state.update_data(edit_msg=mes[0].message_id) - await message.answer('Действия', reply_markup=keyboard) - # await message.answer(get_post_info(posts[0]), reply_markup=keyboard, parse_mode='markdownv2') - else: - await message.answer('Нет постов') + posts = await neuroapi.post.get_will_post() + if (posts): + await state.update_data(posts=posts, id=0) + select_btns = [] + if len(posts) > 1: + select_btns.append(types.InlineKeyboardButton( + text='->', callback_data='next_post')) + kb = [ + select_btns, + [types.InlineKeyboardButton( + callback_data='change_post_text', text='Текст')], + [types.InlineKeyboardButton( + text='Отмена', callback_data='cancel')] + ] + keyboard = types.InlineKeyboardMarkup(inline_keyboard=kb) + post = await neuroapi.post.get(posts[0]['uuid']) + images = MediaGroupBuilder( + caption=get_post_info(post, 1)) + for image in sorted(post['images'], key=lambda x: x['message_id']): + images.add_photo(image['file_id'], + has_spoiler=image['has_spoiler'], parse_mode='markdownv2') + mes = await message.answer_media_group(images.build()) + await state.update_data(edit_msg=mes[0].message_id) + await message.answer('Действия', reply_markup=keyboard) + else: + await message.answer('Нет постов') @self.router.callback_query(F.data == 'next_post') async def next_post_changing(callback: types.CallbackQuery, state: FSMContext): @@ -119,7 +100,8 @@ class Admin_commands: ] keyboard = types.InlineKeyboardMarkup(inline_keyboard=kb) await state.update_data(id=post_id) - await bot.edit_message_caption(caption=get_post_info(posts[post_id], post_id+1), chat_id=callback.message.chat.id, message_id=data['edit_msg'], parse_mode='markdownv2') + post = await neuroapi.post.get(posts[post_id]['uuid']) + await bot.edit_message_caption(caption=get_post_info(post, post_id+1), chat_id=callback.message.chat.id, message_id=data['edit_msg'], parse_mode='markdownv2') await callback.message.edit_reply_markup(reply_markup=keyboard) await callback.answer() @@ -149,13 +131,13 @@ class Admin_commands: return posts = data['posts'] post_id = data['id'] - post: Post = posts[post_id] - with Session(engine) as session: - p = session.get(Post, post.uuid) - p.text = message.text - session.commit() + post_uuid = posts[post_id]['uuid'] + try: + await neuroapi.post.edit_text(post_uuid, message.text) + await message.answer(f'Текст поста изменен на: {message.text}') + except: + await message.answer('Ошибка') await state.clear() - await message.answer(f'Текст поста изменен на: {message.text}') @self.router.callback_query(F.data == 'prev_post') async def prev_post_changing(callback: types.CallbackQuery, state: FSMContext): @@ -181,7 +163,8 @@ class Admin_commands: ] keyboard = types.InlineKeyboardMarkup(inline_keyboard=kb) await state.update_data(id=post_id) - await bot.edit_message_caption(caption=get_post_info(posts[post_id], post_id), chat_id=callback.message.chat.id, message_id=data['edit_msg'], parse_mode='markdownv2') + post = await neuroapi.post.get(posts[post_id]['uuid']) + await bot.edit_message_caption(caption=get_post_info(post, post_id), chat_id=callback.message.chat.id, message_id=data['edit_msg'], parse_mode='markdownv2') await callback.message.edit_reply_markup(reply_markup=keyboard) await callback.answer() @@ -196,33 +179,33 @@ class Admin_commands: @self.router.message(Command('post')) async def post(message: types.Message): - with Session(engine) as session: - post = session.query(Post).filter( - Post.posted == False).order_by(Post.timestamp.asc()).first() - if post: - images = MediaGroupBuilder(caption=post.text) - for image in post.images[::-1]: - images.add_photo( - image.file_id, has_spoiler=image.has_spoiler) - await message.answer_media_group(images.build()) - post.posted = True - session.commit() - else: - await message.answer('Постов немаэ') + posts = await neuroapi.post.get_will_post() + if (posts): + post = await neuroapi.post.get(posts[0]['uuid']) + images = MediaGroupBuilder(caption=post['text']) + for image in sorted(post['images'], key=lambda x: x['message_id']): + images.add_photo(image['file_id'], + has_spoiler=image['has_spoiler']) + await message.answer_media_group(images.build()) + else: + await message.answer('Нет постов') @self.router.message(NewSoloPostFilter()) async def post_solo(message: types.Message): - with Session(engine) as session: - post = Post(uuid=uuid4(), text=message.caption.replace( - '/newpost ', ''), media_group_id='') - post_user = session.get(Admin, message.from_user.id) - post.user = post_user - photo = Image(message_id=message.message_id, - post=post, file_id=message.photo[-1].file_id, has_spoiler=bool(message.has_media_spoiler)) - session.add(photo) - session.add(post) - session.commit() - await message.answer('Пост успешно добавлен!') + post = await neuroapi.post.new(message.caption.replace('/newpost ', ''), message.from_user.id) + await neuroapi.image.add(post['uuid'], message.photo[-1].file_id, message.has_media_spoiler, message.message_id) + await message.answer('Пост успешно добавлен!') + + @self.router.message(ReplyToUser()) + async def reply_user(message: types.Message): + if message.reply_to_message.forward_from is None: + await message.reply('Пользователь стесняшка и не разрешает отвечать на его сообщения...') + else: + try: + await bot.send_message(message.reply_to_message.forward_from.id, f'Вам ответил админ:\n{message.text}') + await message.reply('Ваше сообщение было отправлено!') + except Exception as e: + print(e) def __call__(self, *args: Any, **kwds: Any) -> Router: return self.router diff --git a/handlers/filters/new_post.py b/handlers/filters/new_post.py index 2430481..aade686 100644 --- a/handlers/filters/new_post.py +++ b/handlers/filters/new_post.py @@ -1,40 +1,29 @@ -from asyncio import create_task -from time import sleep -from typing import Any -from uuid import uuid4 - from aiogram import types from aiogram.filters import Filter -from sqlalchemy.orm import Session -from db.data import Admin, Image, Post, User, engine +from neuroapi import neuroapi class NewPostFilter(Filter): async def __call__(self, message: types.Message) -> bool: if message.media_group_id is None or message.content_type != 'photo': return False - with Session(engine) as session: - post = session.query(Post).filter( - Post.media_group_id == message.media_group_id).first() - if post is None: - if not (message.caption.startswith('/newpost ') if message.caption else False): - return False - new_post = Post(uuid=uuid4(), text=message.caption.replace( - '/newpost ', ''), media_group_id=message.media_group_id) - post_user = session.get(Admin, message.from_user.id) - new_post.user = post_user - session.add(new_post) - session.commit() - - await message.answer('Пост успешно добавлен!') + try: + await neuroapi.post.get_by_media_group_id(message.media_group_id) + except: + if not (message.caption.startswith('/newpost ') if message.caption else False): + return False + await neuroapi.post.new(message.caption.replace( + '/newpost ', ''), str(message.from_user.id), str(message.media_group_id)) + await message.answer('Пост успешно добавлен!') return True class NewSoloPostFilter(Filter): async def __call__(self, message: types.Message) -> bool: - return message.media_group_id is None and message.content_type == 'photo' and message.caption.startswith('/newpost ') - + return message.media_group_id is None and message.content_type == 'photo' and message.caption and message.caption.startswith('/newpost ') + + class ChangePosts(Filter): async def __call__(self, message: types.Message) -> bool: - return message.text and message.text.startswith("/change") and message.chat.type == 'private' \ No newline at end of file + return message.text and message.text.startswith("/change") and message.chat.type == 'private' diff --git a/handlers/filters/reply_to_user.py b/handlers/filters/reply_to_user.py new file mode 100644 index 0000000..9c88187 --- /dev/null +++ b/handlers/filters/reply_to_user.py @@ -0,0 +1,9 @@ +from aiogram import types +from aiogram.filters import Filter + + +class ReplyToUser(Filter): + async def __call__(self, message: types.Message) -> bool: + if message.reply_to_message is None or message.chat.type != 'private': + return False + return True diff --git a/handlers/middlewares/user.py b/handlers/middlewares/user.py index 0029779..f155055 100644 --- a/handlers/middlewares/user.py +++ b/handlers/middlewares/user.py @@ -2,9 +2,8 @@ from typing import Any, Awaitable, Callable, Dict from aiogram import BaseMiddleware from aiogram.types import Message -from sqlalchemy.orm import Session -from db.data import Admin, User, engine +from neuroapi import neuroapi class AdminMiddleware(BaseMiddleware): @@ -12,12 +11,8 @@ class AdminMiddleware(BaseMiddleware): pass async def __call__(self, handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], event: Message, data: Dict[str, Any]) -> Any: - with Session(engine) as session: - if not session.get(User, event.from_user.id): - user = User(id=event.from_user.id, user_name=event.from_user.username) - session.add(user) - session.commit() - isAdmin = session.get(Admin, event.from_user.id) + await neuroapi.user.get(str(event.from_user.id), event.from_user.username) + isAdmin = await neuroapi.admin.is_admin(str(event.from_user.id)) if not isAdmin: await event.answer('Команда только для админов!') return None diff --git a/handlers/user_commands.py b/handlers/user_commands.py new file mode 100644 index 0000000..28863ff --- /dev/null +++ b/handlers/user_commands.py @@ -0,0 +1,33 @@ +from typing import Any + +from aiogram import Bot, F, Router, types + +from neuroapi import neuroapi + + +class User_commands: + bot: Bot + router: Router + + def __init__(self, bot: Bot) -> None: + self.bot = bot + self.router = Router() + + @self.router.message(F.chat.type == 'private') + async def forward_post(message: types.Message): + admins = await neuroapi.admin.get() + canReply = True + for a in admins: + await bot.send_message(a['user_id'], f'Вам новое сообщение от пользователя {message.from_user.full_name}. ' + + (f'\nНик: @{message.from_user.username}' if message.from_user.username else f'ID: {message.from_user.id}')) + forwarded_message = await bot.forward_message(a['user_id'], message.chat.id, message.message_id) + if forwarded_message.forward_from is None: + canReply = False + await message.reply('Ваше сообщение было отправлено администраторам'+('' if canReply else '\nНо они не смогут вам ответить из-за ваших настроек конфиденциальности.')) + + def __call__(self, *args: Any, **kwds: Any) -> Router: + return self.router + + +def setup(bot: Bot) -> Router: + return User_commands(bot)() diff --git a/neuroapi/__init__.py b/neuroapi/__init__.py new file mode 100644 index 0000000..9cf92ac --- /dev/null +++ b/neuroapi/__init__.py @@ -0,0 +1,17 @@ +from .post import Post +from .admin import Admin +from .user import User +from .image import Image +from dotenv import load_dotenv +import os +from os.path import join, dirname + + +load_dotenv(join(dirname(__file__), "..", '.env')) + + +class neuroapi: + post = Post(os.environ.get('API_URL')) + admin = Admin(os.environ.get('API_URL')) + user = User(os.environ.get('API_URL')) + image = Image(os.environ.get('API_URL')) diff --git a/neuroapi/admin.py b/neuroapi/admin.py new file mode 100644 index 0000000..05d1eff --- /dev/null +++ b/neuroapi/admin.py @@ -0,0 +1,18 @@ +from aiohttp import ClientSession + +from .api_method import ApiMethod + + +class Admin(ApiMethod): + + async def get(self): + async with ClientSession() as session: + response = await session.get(self.api_url+'/admin/get') + return await response.json() + + async def is_admin(self, id: str): + async with ClientSession() as session: + response = await session.get(self.api_url+f'/admin/is-admin/{id}') + if await response.text() == 'false': + return False + return True diff --git a/neuroapi/api_method.py b/neuroapi/api_method.py new file mode 100644 index 0000000..48bad77 --- /dev/null +++ b/neuroapi/api_method.py @@ -0,0 +1,5 @@ +class ApiMethod: + api_url: str + + def __init__(self, api_url: str) -> None: + self.api_url = api_url diff --git a/neuroapi/enums/__init__.py b/neuroapi/enums/__init__.py new file mode 100644 index 0000000..4ab1fb4 --- /dev/null +++ b/neuroapi/enums/__init__.py @@ -0,0 +1 @@ +from .get_all import EGetAll diff --git a/neuroapi/enums/get_all.py b/neuroapi/enums/get_all.py new file mode 100644 index 0000000..9100757 --- /dev/null +++ b/neuroapi/enums/get_all.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class EGetAll(Enum): + all = 'all' + will_post = 'will-post' + posted = 'posted' diff --git a/neuroapi/image.py b/neuroapi/image.py new file mode 100644 index 0000000..e1dc946 --- /dev/null +++ b/neuroapi/image.py @@ -0,0 +1,20 @@ +import json + +from aiohttp import ClientSession + +from .api_method import ApiMethod + + +class Image(ApiMethod): + async def add(self, post_id: str, file_id: str, has_spoiler: bool | None, message_id: int): + payload = {'post_id': post_id, 'file_id': file_id, + 'has_spoiler': has_spoiler, 'message_id': message_id} + if has_spoiler is None: + payload.pop('has_spoiler') + payload = json.dumps(payload) + async with ClientSession() as session: + response = await session.post( + self.api_url+'/image/add', data=payload, headers={'Content-Type': 'application/json'}) + data = await response.json() + if 'statusCode' in data: + raise Exception(data['message']) diff --git a/neuroapi/post.py b/neuroapi/post.py new file mode 100644 index 0000000..1ad0b1b --- /dev/null +++ b/neuroapi/post.py @@ -0,0 +1,57 @@ +import requests +from aiohttp import ClientSession + +from .api_method import ApiMethod +from .enums import EGetAll + + +class Post(ApiMethod): + + async def new(self, text: str, from_user_id: str, media_group_id: str = "None"): + payload = {'text': text, 'from_user_id': from_user_id} + if media_group_id != 'None': + payload['media_group_id'] = media_group_id + response = requests.post(self.api_url+'/post/new', data=payload) + data = response.json() + if 'statusCode' in data: + raise Exception(data['message']) + return data + + async def __get_all(self, status: EGetAll): + async with ClientSession() as session: + response = await session.get(self.api_url+f'/post/get-all/{status.value}') + return response + + async def get_all(self): + result = await self.__get_all(EGetAll.all) + return await result.json() + + async def get_will_post(self): + result = await self.__get_all(EGetAll.will_post) + return await result.json() + + async def get_posted(self): + result = await self.__get_all(EGetAll.posted) + return await result.json() + + async def get(self, post_id: str): + async with ClientSession() as session: + response = await session.get(self.api_url+f'/post/get/{post_id}') + data = await response.json() + if 'statusCode' in data: + raise Exception(data['message']) + return data + + async def get_by_media_group_id(self, media_group_id: str): + response = requests.get(self.api_url+f'/post/get-by-media-group-id/{media_group_id}') + data = response.json() + if 'statusCode' in data: + raise Exception(data['message']) + return data + + async def edit_text(self, post_id: str, text: str): + response = requests.post(self.api_url+f"/post/edit/{post_id}", data={"text": text}) + data = response.json() + if 'statusCode' in data: + raise Exception(data['message']) + return data \ No newline at end of file diff --git a/neuroapi/user.py b/neuroapi/user.py new file mode 100644 index 0000000..faf4246 --- /dev/null +++ b/neuroapi/user.py @@ -0,0 +1,13 @@ +from aiohttp import ClientSession +from .api_method import ApiMethod + + +class User(ApiMethod): + async def get(self, id: str, username: str): + payload = {'id': id, 'username': username} + async with ClientSession() as session: + response = await session.post( + self.api_url+'/user/get', data=payload) + data = await response.json() + if 'statusCode' in data: + raise Exception(data['message'])