From 6389c4f29e45c1b1b8438252f7f9c9a697c33402 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Wed, 22 Nov 2023 21:04:39 +0300 Subject: [PATCH 01/26] Deleted DB trash --- .env.example | 6 +-- alembic.ini | 119 --------------------------------------------- docker-compose.yml | 17 ------- requirements.txt | 4 +- 4 files changed, 2 insertions(+), 144 deletions(-) delete mode 100644 alembic.ini delete mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example index 22e3807..317fe12 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,3 @@ TOKEN= -DATABASE_PASSWORD=postgres -DATABASE_NAME=bot_db -DATABASE_USER=postgres -DATABASE_HOST=localhost -DATABASE_PORT=15432 +API_URL="http://localhost:3000" \ No newline at end of file diff --git a/alembic.ini b/alembic.ini deleted file mode 100644 index 0fbfb68..0000000 --- a/alembic.ini +++ /dev/null @@ -1,119 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -# sqlalchemy.url = driver://user:pass@localhost/dbname - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# lint with attempts to fix using "ruff" - use the exec runner, execute a binary -# hooks = ruff -# ruff.type = exec -# ruff.executable = %(here)s/.venv/bin/ruff -# ruff.options = --fix REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S - -[alembic.ext] -sourceless=false diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 944b822..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: '3.9' - -services: - db: - container_name: bot_db - image: postgres:alpine - environment: - - POSTGRES_USER=${DATABASE_USER} - - POSTGRES_PASSWORD=${DATABASE_PASSWORD} - - POSTGRES_DB=${DATABASE_NAME} - - PGDATA=/var/lib/postgresql/data/pgdata - ports: - - "${DATABASE_PORT}:5432" - env_file: - - .env - volumes: - - ./data:/var/lib/postgresql/data \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0865abd..a2298b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ aiogram==3.1.1 aioschedule @ https://github.com/AleksHeller/python-aioschedule/archive/refs/heads/master.zip -python-dotenv==1.0.0 -SQLAlchemy==2.0.22 -alembic==1.12.1 \ No newline at end of file +python-dotenv==1.0.0 \ No newline at end of file From 8c2d706401670443ce7813140c72feaa1d65c04c Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Thu, 23 Nov 2023 22:13:10 +0300 Subject: [PATCH 02/26] Added singleton config and changed module structure --- main.py | 8 +------- neuroapi/__init__.py | 18 +----------------- neuroapi/{ => _methods}/admin.py | 0 neuroapi/_methods/api_method.py | 8 ++++++++ neuroapi/{ => _methods}/enums/__init__.py | 0 neuroapi/{ => _methods}/enums/get_all.py | 0 neuroapi/{ => _methods}/image.py | 0 neuroapi/{ => _methods}/post.py | 0 neuroapi/{ => _methods}/user.py | 1 + neuroapi/_neuroapi.py | 11 +++++++++++ neuroapi/api_method.py | 5 ----- neuroapi/config.py | 19 +++++++++++++++++++ 12 files changed, 41 insertions(+), 29 deletions(-) rename neuroapi/{ => _methods}/admin.py (100%) create mode 100644 neuroapi/_methods/api_method.py rename neuroapi/{ => _methods}/enums/__init__.py (100%) rename neuroapi/{ => _methods}/enums/get_all.py (100%) rename neuroapi/{ => _methods}/image.py (100%) rename neuroapi/{ => _methods}/post.py (100%) rename neuroapi/{ => _methods}/user.py (99%) create mode 100644 neuroapi/_neuroapi.py delete mode 100644 neuroapi/api_method.py create mode 100644 neuroapi/config.py diff --git a/main.py b/main.py index 4696ea8..c8cd075 100644 --- a/main.py +++ b/main.py @@ -7,12 +7,7 @@ from os.path import dirname, join import aioschedule as schedule import dotenv from aiogram import Bot, Dispatcher, F, types -from aiogram.filters import Command, CommandStart -from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import State, StatesGroup -from aiogram.utils.keyboard import InlineKeyboardBuilder - -from handlers.admin_commands import Admin_commands +from aiogram.filters import CommandStart dotenv.load_dotenv() @@ -35,7 +30,6 @@ for filename in os.listdir(handlers_dir): async def main() -> None: - # dp.include_router(Admin_commands(bot)()) await dp.start_polling(bot, skip_updates=True) if __name__ == '__main__': diff --git a/neuroapi/__init__.py b/neuroapi/__init__.py index 9cf92ac..df4285b 100644 --- a/neuroapi/__init__.py +++ b/neuroapi/__init__.py @@ -1,17 +1 @@ -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')) +from ._neuroapi import neuroapi diff --git a/neuroapi/admin.py b/neuroapi/_methods/admin.py similarity index 100% rename from neuroapi/admin.py rename to neuroapi/_methods/admin.py diff --git a/neuroapi/_methods/api_method.py b/neuroapi/_methods/api_method.py new file mode 100644 index 0000000..af239e4 --- /dev/null +++ b/neuroapi/_methods/api_method.py @@ -0,0 +1,8 @@ +from ..config import Config + + +class ApiMethod: + api_url: str + + def __init__(self) -> None: + self.api_url = Config().api_url diff --git a/neuroapi/enums/__init__.py b/neuroapi/_methods/enums/__init__.py similarity index 100% rename from neuroapi/enums/__init__.py rename to neuroapi/_methods/enums/__init__.py diff --git a/neuroapi/enums/get_all.py b/neuroapi/_methods/enums/get_all.py similarity index 100% rename from neuroapi/enums/get_all.py rename to neuroapi/_methods/enums/get_all.py diff --git a/neuroapi/image.py b/neuroapi/_methods/image.py similarity index 100% rename from neuroapi/image.py rename to neuroapi/_methods/image.py diff --git a/neuroapi/post.py b/neuroapi/_methods/post.py similarity index 100% rename from neuroapi/post.py rename to neuroapi/_methods/post.py diff --git a/neuroapi/user.py b/neuroapi/_methods/user.py similarity index 99% rename from neuroapi/user.py rename to neuroapi/_methods/user.py index faf4246..8cb55e9 100644 --- a/neuroapi/user.py +++ b/neuroapi/_methods/user.py @@ -1,4 +1,5 @@ from aiohttp import ClientSession + from .api_method import ApiMethod diff --git a/neuroapi/_neuroapi.py b/neuroapi/_neuroapi.py new file mode 100644 index 0000000..6949754 --- /dev/null +++ b/neuroapi/_neuroapi.py @@ -0,0 +1,11 @@ +from ._methods.admin import Admin +from ._methods.image import Image +from ._methods.post import Post +from ._methods.user import User + + +class neuroapi: + post = Post() + admin = Admin() + user = User() + image = Image() diff --git a/neuroapi/api_method.py b/neuroapi/api_method.py deleted file mode 100644 index 48bad77..0000000 --- a/neuroapi/api_method.py +++ /dev/null @@ -1,5 +0,0 @@ -class ApiMethod: - api_url: str - - def __init__(self, api_url: str) -> None: - self.api_url = api_url diff --git a/neuroapi/config.py b/neuroapi/config.py new file mode 100644 index 0000000..6beabeb --- /dev/null +++ b/neuroapi/config.py @@ -0,0 +1,19 @@ +import os +from typing import Self + +from dotenv import load_dotenv + + +class _Singleton: + _instances = {} + + def __new__(cls) -> Self: + if cls not in cls._instances: + cls._instances[cls] = super(_Singleton, cls).__new__(cls) + return cls._instances[cls] + +class Config(_Singleton): + api_url: str + def __init__(self): + load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env')) + self.api_url = os.environ.get('API_URL') \ No newline at end of file From dbf88381839f691ffe053326d72d807244398828 Mon Sep 17 00:00:00 2001 From: burdukov Date: Sun, 26 Nov 2023 02:23:49 +0500 Subject: [PATCH 03/26] added: types for api --- neuroapi/types/__init__.py | 4 ++++ neuroapi/types/_admin.py | 22 ++++++++++++++++++ neuroapi/types/_helpers.py | 47 ++++++++++++++++++++++++++++++++++++++ neuroapi/types/_image.py | 28 +++++++++++++++++++++++ neuroapi/types/_post.py | 44 +++++++++++++++++++++++++++++++++++ neuroapi/types/_user.py | 22 ++++++++++++++++++ 6 files changed, 167 insertions(+) create mode 100644 neuroapi/types/__init__.py create mode 100644 neuroapi/types/_admin.py create mode 100644 neuroapi/types/_helpers.py create mode 100644 neuroapi/types/_image.py create mode 100644 neuroapi/types/_post.py create mode 100644 neuroapi/types/_user.py diff --git a/neuroapi/types/__init__.py b/neuroapi/types/__init__.py new file mode 100644 index 0000000..cb41a50 --- /dev/null +++ b/neuroapi/types/__init__.py @@ -0,0 +1,4 @@ +from ._post import Post +from ._image import Image +from ._admin import Admin +from ._user import User diff --git a/neuroapi/types/_admin.py b/neuroapi/types/_admin.py new file mode 100644 index 0000000..6ce6e5e --- /dev/null +++ b/neuroapi/types/_admin.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from typing import Any +from ._helpers import * + + +@dataclass +class Admin: + id: int + user_id: int + + @staticmethod + def from_dict(obj: Any) -> 'Admin': + assert isinstance(obj, dict) + id = from_int(obj.get("id")) + user_id = int(from_str(obj.get("user_id"))) + return Admin(id, user_id) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_int(self.id) + result["user_id"] = from_str(str(self.user_id)) + return result diff --git a/neuroapi/types/_helpers.py b/neuroapi/types/_helpers.py new file mode 100644 index 0000000..9324d7d --- /dev/null +++ b/neuroapi/types/_helpers.py @@ -0,0 +1,47 @@ +from datetime import datetime +from typing import Any, Callable, List, TypeVar, Type, cast +import dateutil.parser +T = TypeVar("T") + + +def from_bool(x: Any) -> bool: + assert isinstance(x, bool) + return x + + +def from_str(x: Any) -> str: + assert isinstance(x, str) + return x + + +def from_union(fs, x): + for f in fs: + try: + return f(x) + except: + pass + assert False + + +def from_none(x: Any) -> Any: + assert x is None + return x + + +def from_list(f: Callable[[Any], T], x: Any) -> List[T]: + assert isinstance(x, list) + return [f(y) for y in x] + + +def from_datetime(x: Any) -> datetime: + return dateutil.parser.parse(x) + + +def to_class(c: Type[T], x: Any) -> dict: + assert isinstance(x, c) + return cast(Any, x).to_dict() + + +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x diff --git a/neuroapi/types/_image.py b/neuroapi/types/_image.py new file mode 100644 index 0000000..bce6fb7 --- /dev/null +++ b/neuroapi/types/_image.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from uuid import UUID +from ._helpers import * + + +@dataclass +class Image: + message_id: int + file_id: str + has_spoiler: bool + post_uuid: UUID + + @staticmethod + def from_dict(obj: Any) -> 'Image': + assert isinstance(obj, dict) + message_id = from_int(obj.get("message_id")) + file_id = from_str(obj.get("file_id")) + has_spoiler = from_bool(obj.get("has_spoiler")) + post_uuid = UUID(obj.get("post_uuid")) + return Image(message_id, file_id, has_spoiler, post_uuid) + + def to_dict(self) -> dict: + result: dict = {} + result["message_id"] = from_int(self.message_id) + result["file_id"] = from_str(self.file_id) + result["has_spoiler"] = from_bool(self.has_spoiler) + result["post_uuid"] = str(self.post_uuid) + return result diff --git a/neuroapi/types/_post.py b/neuroapi/types/_post.py new file mode 100644 index 0000000..cb1badb --- /dev/null +++ b/neuroapi/types/_post.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass +from uuid import UUID +from datetime import datetime +from typing import Any, List, Optional +from ._image import Image +from ._helpers import * + + +@dataclass +class Post: + uuid: UUID + posted: bool + text: str + media_group_id: int | str + timestamp: datetime + from_user_id: int + images: Optional[List[Image]] = None + + @staticmethod + def from_dict(obj: Any) -> 'Post': + assert isinstance(obj, dict) + uuid = UUID(obj.get("uuid")) + posted = from_bool(obj.get("posted")) + text = from_str(obj.get("text")) + media_group_id = from_str(obj.get("media_group_id")) if obj.get( + "media_group_id") is not None else 'None' + timestamp = from_datetime(obj.get("timestamp")) + from_user_id = int(from_str(obj.get("from_user_id"))) + images = from_union([lambda x: from_list( + Image.from_dict, x), from_none], obj.get("images")) + return Post(uuid, posted, text, media_group_id, timestamp, from_user_id, images) + + def to_dict(self) -> dict: + result: dict = {} + result["uuid"] = str(self.uuid) + result["posted"] = from_bool(self.posted) + result["text"] = from_str(self.text) + result["media_group_id"] = from_str(str(self.media_group_id)) + result["timestamp"] = self.timestamp.isoformat() + result["from_user_id"] = from_str(str(self.from_user_id)) + if self.images is not None: + result["images"] = from_union([lambda x: from_list( + lambda x: to_class(Image, x), x), from_none], self.images) + return result diff --git a/neuroapi/types/_user.py b/neuroapi/types/_user.py new file mode 100644 index 0000000..71300a0 --- /dev/null +++ b/neuroapi/types/_user.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from typing import Any +from ._helpers import * + + +@dataclass +class User: + id: int + username: str + + @staticmethod + def from_dict(obj: Any) -> 'User': + assert isinstance(obj, dict) + id = int(from_str(obj.get("id"))) + username = from_str(obj.get("username")) + return User(id, username) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(str(self.id)) + result["username"] = from_str(self.username) + return result From 137d3d6e790f81333ca319c527001b0cf4d4fa12 Mon Sep 17 00:00:00 2001 From: burdukov Date: Sun, 26 Nov 2023 02:24:35 +0500 Subject: [PATCH 04/26] edited: methods for new api types --- handlers/admin_commands.py | 61 ++++++++++++++++++++------------------ handlers/user_commands.py | 14 +++++---- neuroapi/_methods/admin.py | 4 ++- neuroapi/_methods/post.py | 21 +++++++------ 4 files changed, 55 insertions(+), 45 deletions(-) diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index c3b1894..fae250d 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, List from aiogram import Bot, F, Router, types from aiogram.filters import Command @@ -11,14 +11,15 @@ 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 +import neuroapi.types as nat -def get_post_info(post: dict, post_id: int) -> str: - text = post["text"] - time = post["timestamp"] - from_user = post["from_user_id"] +def get_post_info(post: nat.Post, 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(':', '\:') + "_", "\_").replace('.', '\.').replace(',', '\,').replace('!', '\!').replace('-', '\-').replace(':', '\:').replace('+', '\+') return s @@ -33,18 +34,18 @@ class Admin_commands: @self.router.message(NewPostFilter()) async def new_post(message: types.Message): - 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) + post: nat.Post = await neuroapi.post.get_by_media_group_id(message.media_group_id) + await neuroapi.image.add(str(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): - posts = await neuroapi.post.get_will_post() + posts: List[nat.Post] = 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 + if post.from_user_id not in post_c: + post_c[post.from_user_id] = 1 else: - post_c[post['from_user_id']] += 1 + post_c[post.from_user_id] += 1 await message.answer(str(post_c)) @self.router.message(ChangePosts()) @@ -64,12 +65,13 @@ class Admin_commands: text='Отмена', callback_data='cancel')] ] keyboard = types.InlineKeyboardMarkup(inline_keyboard=kb) - post = await neuroapi.post.get(posts[0]['uuid']) + post = await neuroapi.post.get(str(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') + image: nat.Image + 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) @@ -84,7 +86,7 @@ class Admin_commands: await callback.answer() await callback.message.delete() return - posts = data['posts'] + posts: List[nat.Post] = data['posts'] post_id = data['id']+1 select_btns = [types.InlineKeyboardButton( text='<-', callback_data='prev_post')] @@ -100,7 +102,7 @@ class Admin_commands: ] keyboard = types.InlineKeyboardMarkup(inline_keyboard=kb) await state.update_data(id=post_id) - post = await neuroapi.post.get(posts[post_id]['uuid']) + post = await neuroapi.post.get(str(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() @@ -129,9 +131,9 @@ class Admin_commands: if 'posts' not in data: await state.clear() return - posts = data['posts'] + posts: List[nat.Post] = data['posts'] post_id = data['id'] - post_uuid = posts[post_id]['uuid'] + post_uuid = str(posts[post_id].uuid) try: await neuroapi.post.edit_text(post_uuid, message.text) await message.answer(f'Текст поста изменен на: {message.text}') @@ -147,7 +149,7 @@ class Admin_commands: await callback.answer() await callback.message.delete() return - posts = data['posts'] + posts: List[nat.Post] = data['posts'] post_id = data['id']-1 select_btns = [types.InlineKeyboardButton( text='->', callback_data='next_post')] @@ -163,7 +165,7 @@ class Admin_commands: ] keyboard = types.InlineKeyboardMarkup(inline_keyboard=kb) await state.update_data(id=post_id) - post = await neuroapi.post.get(posts[post_id]['uuid']) + post = await neuroapi.post.get(str(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() @@ -181,19 +183,20 @@ class Admin_commands: async def post(message: types.Message): 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']) + post = await neuroapi.post.get(str(posts[0].uuid)) + images = MediaGroupBuilder(caption=post.text) + image: nat.Image + 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): - 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) + post: nat.Post = await neuroapi.post.new(message.caption.replace('/newpost ', ''), message.from_user.id) + await neuroapi.image.add(str(post.uuid), message.photo[-1].file_id, message.has_media_spoiler, message.message_id) await message.answer('Пост успешно добавлен!') @self.router.message(ReplyToUser()) diff --git a/handlers/user_commands.py b/handlers/user_commands.py index 28863ff..2cf48cc 100644 --- a/handlers/user_commands.py +++ b/handlers/user_commands.py @@ -1,9 +1,11 @@ -from typing import Any +from typing import Any, List from aiogram import Bot, F, Router, types from neuroapi import neuroapi +from neuroapi.types import Admin as AdminType + class User_commands: bot: Bot @@ -15,12 +17,12 @@ class User_commands: @self.router.message(F.chat.type == 'private') async def forward_post(message: types.Message): - admins = await neuroapi.admin.get() + admins: List[AdminType] = 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) + for admin in admins: + await bot.send_message(admin.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(admin.user_id, message.chat.id, message.message_id) if forwarded_message.forward_from is None: canReply = False await message.reply('Ваше сообщение было отправлено администраторам'+('' if canReply else '\nНо они не смогут вам ответить из-за ваших настроек конфиденциальности.')) diff --git a/neuroapi/_methods/admin.py b/neuroapi/_methods/admin.py index 05d1eff..826b675 100644 --- a/neuroapi/_methods/admin.py +++ b/neuroapi/_methods/admin.py @@ -2,13 +2,15 @@ from aiohttp import ClientSession from .api_method import ApiMethod +from neuroapi.types import Admin as AdminType + 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() + return [AdminType.from_dict(admin) for admin in await response.json()] async def is_admin(self, id: str): async with ClientSession() as session: diff --git a/neuroapi/_methods/post.py b/neuroapi/_methods/post.py index 1ad0b1b..6a3c241 100644 --- a/neuroapi/_methods/post.py +++ b/neuroapi/_methods/post.py @@ -3,6 +3,7 @@ from aiohttp import ClientSession from .api_method import ApiMethod from .enums import EGetAll +import neuroapi.types as nat class Post(ApiMethod): @@ -15,7 +16,7 @@ class Post(ApiMethod): data = response.json() if 'statusCode' in data: raise Exception(data['message']) - return data + return nat.Post.from_dict(data) async def __get_all(self, status: EGetAll): async with ClientSession() as session: @@ -24,15 +25,15 @@ class Post(ApiMethod): async def get_all(self): result = await self.__get_all(EGetAll.all) - return await result.json() + return [nat.Post.from_dict(post) for post in await result.json()] async def get_will_post(self): result = await self.__get_all(EGetAll.will_post) - return await result.json() + return [nat.Post.from_dict(post) for post in await result.json()] async def get_posted(self): result = await self.__get_all(EGetAll.posted) - return await result.json() + return [nat.Post.from_dict(post) for post in await result.json()] async def get(self, post_id: str): async with ClientSession() as session: @@ -40,18 +41,20 @@ class Post(ApiMethod): data = await response.json() if 'statusCode' in data: raise Exception(data['message']) - return data + return nat.Post.from_dict(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}') + 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 + return nat.Post.from_dict(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}) + 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 + return nat.Post.from_dict(data) From ae5f3c7a304115619dd2b58ff1a3f9e138a9d47f Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Sun, 26 Nov 2023 13:56:12 +0300 Subject: [PATCH 05/26] edit: nat to neuroTypes --- handlers/admin_commands.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index fae250d..81a462b 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -5,16 +5,16 @@ from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.utils.media_group import MediaGroupBuilder +import neuroapi.types as neuroTypes 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 -import neuroapi.types as nat -def get_post_info(post: nat.Post, post_id: int) -> str: +def get_post_info(post: neuroTypes.Post, post_id: int) -> str: text = post.text time = post.timestamp from_user = post.from_user_id @@ -34,12 +34,12 @@ class Admin_commands: @self.router.message(NewPostFilter()) async def new_post(message: types.Message): - post: nat.Post = await neuroapi.post.get_by_media_group_id(message.media_group_id) + post: neuroTypes.Post = await neuroapi.post.get_by_media_group_id(message.media_group_id) await neuroapi.image.add(str(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): - posts: List[nat.Post] = await neuroapi.post.get_will_post() + posts: List[neuroTypes.Post] = await neuroapi.post.get_will_post() post_c = {} for post in posts: if post.from_user_id not in post_c: @@ -68,7 +68,7 @@ class Admin_commands: post = await neuroapi.post.get(str(posts[0].uuid)) images = MediaGroupBuilder( caption=get_post_info(post, 1)) - image: nat.Image + image: neuroTypes.Image 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') @@ -86,7 +86,7 @@ class Admin_commands: await callback.answer() await callback.message.delete() return - posts: List[nat.Post] = data['posts'] + posts: List[neuroTypes.Post] = data['posts'] post_id = data['id']+1 select_btns = [types.InlineKeyboardButton( text='<-', callback_data='prev_post')] @@ -131,7 +131,7 @@ class Admin_commands: if 'posts' not in data: await state.clear() return - posts: List[nat.Post] = data['posts'] + posts: List[neuroTypes.Post] = data['posts'] post_id = data['id'] post_uuid = str(posts[post_id].uuid) try: @@ -149,7 +149,7 @@ class Admin_commands: await callback.answer() await callback.message.delete() return - posts: List[nat.Post] = data['posts'] + posts: List[neuroTypes.Post] = data['posts'] post_id = data['id']-1 select_btns = [types.InlineKeyboardButton( text='->', callback_data='next_post')] @@ -185,7 +185,7 @@ class Admin_commands: if (posts): post = await neuroapi.post.get(str(posts[0].uuid)) images = MediaGroupBuilder(caption=post.text) - image: nat.Image + image: neuroTypes.Image for image in sorted(post.images, key=lambda x: x.message_id): images.add_photo(image.file_id, has_spoiler=image.has_spoiler) @@ -195,7 +195,7 @@ class Admin_commands: @self.router.message(NewSoloPostFilter()) async def post_solo(message: types.Message): - post: nat.Post = await neuroapi.post.new(message.caption.replace('/newpost ', ''), message.from_user.id) + post: neuroTypes.Post = await neuroapi.post.new(message.caption.replace('/newpost ', ''), message.from_user.id) await neuroapi.image.add(str(post.uuid), message.photo[-1].file_id, message.has_media_spoiler, message.message_id) await message.answer('Пост успешно добавлен!') From f2640da66a173603363d46d1f61b75b621dca947 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Sun, 26 Nov 2023 13:56:28 +0300 Subject: [PATCH 06/26] edit: removed unused imports --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index c8cd075..e4f8657 100644 --- a/main.py +++ b/main.py @@ -4,9 +4,9 @@ import os import sys from os.path import dirname, join -import aioschedule as schedule +# import aioschedule as schedule import dotenv -from aiogram import Bot, Dispatcher, F, types +from aiogram import Bot, Dispatcher, types from aiogram.filters import CommandStart dotenv.load_dotenv() From 9ea191f0cbe476fd84e9f1e8940f77f12a7589fe Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Sun, 26 Nov 2023 14:00:41 +0300 Subject: [PATCH 07/26] edit: added some libs to requirements.txt --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a2298b9..23c1acb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ aiogram==3.1.1 aioschedule @ https://github.com/AleksHeller/python-aioschedule/archive/refs/heads/master.zip -python-dotenv==1.0.0 \ No newline at end of file +python-dotenv==1.0.0 +requests==2.31.0 +python-dateutil==2.8.2 \ No newline at end of file From 42e9360fb7088fe29c874a715ed77eaad7509ecf Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 27 Nov 2023 00:39:26 +0300 Subject: [PATCH 08/26] edited nat to neuroTypes --- neuroapi/_methods/post.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/neuroapi/_methods/post.py b/neuroapi/_methods/post.py index 6a3c241..f60fa3f 100644 --- a/neuroapi/_methods/post.py +++ b/neuroapi/_methods/post.py @@ -1,9 +1,10 @@ import requests from aiohttp import ClientSession +import neuroapi.types as neuroTypes + from .api_method import ApiMethod from .enums import EGetAll -import neuroapi.types as nat class Post(ApiMethod): @@ -16,7 +17,7 @@ class Post(ApiMethod): data = response.json() if 'statusCode' in data: raise Exception(data['message']) - return nat.Post.from_dict(data) + return neuroTypes.Post.from_dict(data) async def __get_all(self, status: EGetAll): async with ClientSession() as session: @@ -25,15 +26,15 @@ class Post(ApiMethod): async def get_all(self): result = await self.__get_all(EGetAll.all) - return [nat.Post.from_dict(post) for post in await result.json()] + return [neuroTypes.Post.from_dict(post) for post in await result.json()] async def get_will_post(self): result = await self.__get_all(EGetAll.will_post) - return [nat.Post.from_dict(post) for post in await result.json()] + return [neuroTypes.Post.from_dict(post) for post in await result.json()] async def get_posted(self): result = await self.__get_all(EGetAll.posted) - return [nat.Post.from_dict(post) for post in await result.json()] + return [neuroTypes.Post.from_dict(post) for post in await result.json()] async def get(self, post_id: str): async with ClientSession() as session: @@ -41,7 +42,7 @@ class Post(ApiMethod): data = await response.json() if 'statusCode' in data: raise Exception(data['message']) - return nat.Post.from_dict(data) + return neuroTypes.Post.from_dict(data) async def get_by_media_group_id(self, media_group_id: str): response = requests.get( @@ -49,7 +50,7 @@ class Post(ApiMethod): data = response.json() if 'statusCode' in data: raise Exception(data['message']) - return nat.Post.from_dict(data) + return neuroTypes.Post.from_dict(data) async def edit_text(self, post_id: str, text: str): response = requests.post( @@ -57,4 +58,4 @@ class Post(ApiMethod): data = response.json() if 'statusCode' in data: raise Exception(data['message']) - return nat.Post.from_dict(data) + return neuroTypes.Post.from_dict(data) From 388ebd58c3fb075313c361e57c69d137dbb53677 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 27 Nov 2023 00:39:51 +0300 Subject: [PATCH 09/26] edit gitignore .env --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4ec906d..962e0d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ venv **/__pycache__ +.env.* .env +!.env.example data **/*_old \ No newline at end of file From b14eeb8342fe2889439c7ac6dda46ab984e02dd6 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 27 Nov 2023 18:54:40 +0300 Subject: [PATCH 10/26] Added: settings file with time to post --- main.py | 2 +- neuroapi/config.py | 26 +++++++++++++++++++++++++- settings.toml | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 settings.toml diff --git a/main.py b/main.py index e4f8657..2cff85e 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,7 @@ dp = Dispatcher() @dp.message(CommandStart()) async def start_message(message: types.Message): - await message.answer('Абоба') + await message.answer('Добро пожаловать в бота ') handlers_dir = join(dirname(__file__), 'handlers') diff --git a/neuroapi/config.py b/neuroapi/config.py index 6beabeb..9dd571d 100644 --- a/neuroapi/config.py +++ b/neuroapi/config.py @@ -1,8 +1,12 @@ import os -from typing import Self +import tomllib +from typing import List, Self, TypeVar +from attr import dataclass from dotenv import load_dotenv +from .types._helpers import * + class _Singleton: _instances = {} @@ -11,9 +15,29 @@ class _Singleton: if cls not in cls._instances: cls._instances[cls] = super(_Singleton, cls).__new__(cls) return cls._instances[cls] + +@dataclass +class Settings: + time: List[str] + + @staticmethod + def from_dict(obj: Any) -> 'Settings': + assert isinstance(obj, dict) + time = from_list(from_str, obj.get("time", [])) + return Settings(time) + + def to_dict(self) -> dict: + result: dict = {} + result['time'] = from_list(from_str, self.time) + return result class Config(_Singleton): api_url: str + settings: Settings def __init__(self): load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env')) + if not os.path.exists(os.path.join(os.path.dirname(__file__), '..', 'settings.toml')): raise Exception('Settings.toml must be in root folder') + with open(os.path.join(os.path.dirname(__file__), '..', 'settings.toml'), 'rb') as f: + settings = tomllib.load(f) + self.settings = Settings.from_dict(settings) self.api_url = os.environ.get('API_URL') \ No newline at end of file diff --git a/settings.toml b/settings.toml new file mode 100644 index 0000000..5e0b040 --- /dev/null +++ b/settings.toml @@ -0,0 +1 @@ +time = ["12:00", "14:00", "16:00", "18:00"] \ No newline at end of file From a8487334226ca69591e3b1d295fd811fdcfb12ab Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 27 Nov 2023 22:04:50 +0300 Subject: [PATCH 11/26] Some refactoring and started second bot --- .env.example | 2 + handlers/admin_commands.py | 18 +++------ handlers/handler.py | 16 ++++++++ handlers/user_commands.py | 21 +++------- main.py | 75 +++++++++++++++++++++++++----------- neuroapi/config.py | 24 ++++++------ neuroapi/types/__init__.py | 5 ++- neuroapi/types/_singleton.py | 10 +++++ 8 files changed, 108 insertions(+), 63 deletions(-) create mode 100644 handlers/handler.py create mode 100644 neuroapi/types/_singleton.py diff --git a/.env.example b/.env.example index 317fe12..ae0343f 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ TOKEN= +PROXY_TOKEN= + API_URL="http://localhost:3000" \ No newline at end of file diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index 81a462b..2641225 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -1,6 +1,6 @@ -from typing import Any, List +from typing import List -from aiogram import Bot, F, Router, types +from aiogram import Bot, F, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.utils.media_group import MediaGroupBuilder @@ -9,6 +9,7 @@ import neuroapi.types as neuroTypes from handlers.filters.new_post import (ChangePosts, NewPostFilter, NewSoloPostFilter) from handlers.filters.reply_to_user import ReplyToUser +from handlers.handler import Handler from handlers.middlewares.user import AdminMiddleware from handlers.states.change_post import ChangePost from neuroapi import neuroapi @@ -23,13 +24,9 @@ def get_post_info(post: neuroTypes.Post, post_id: int) -> str: return s -class Admin_commands: - bot: Bot - router: Router - +class AdminCommands(Handler): def __init__(self, bot: Bot) -> None: - self.bot = bot - self.router = Router() + super().__init__(bot) self.router.message.middleware(AdminMiddleware()) @self.router.message(NewPostFilter()) @@ -210,9 +207,4 @@ class Admin_commands: except Exception as e: print(e) - def __call__(self, *args: Any, **kwds: Any) -> Router: - return self.router - -def setup(bot: Bot) -> Router: - return Admin_commands(bot)() diff --git a/handlers/handler.py b/handlers/handler.py new file mode 100644 index 0000000..ff4548c --- /dev/null +++ b/handlers/handler.py @@ -0,0 +1,16 @@ +from typing import Any + +from aiogram import Bot, Router + + +class Handler: + bot: Bot + router: Router + + def __init__(self, bot: Bot) -> None: + assert isinstance(bot, Bot) + self.bot = bot + self.router = Router() + + def __call__(self) -> Router: + return self.router \ No newline at end of file diff --git a/handlers/user_commands.py b/handlers/user_commands.py index 2cf48cc..4c94102 100644 --- a/handlers/user_commands.py +++ b/handlers/user_commands.py @@ -1,20 +1,17 @@ -from typing import Any, List +from typing import List -from aiogram import Bot, F, Router, types +from aiogram import Bot, F, types +from handlers.handler import Handler from neuroapi import neuroapi - from neuroapi.types import Admin as AdminType -class User_commands: - bot: Bot - router: Router +class UserCommands(Handler): def __init__(self, bot: Bot) -> None: - self.bot = bot - self.router = Router() - + super().__init__(bot) + @self.router.message(F.chat.type == 'private') async def forward_post(message: types.Message): admins: List[AdminType] = await neuroapi.admin.get() @@ -27,9 +24,3 @@ class User_commands: 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/main.py b/main.py index 2cff85e..5795646 100644 --- a/main.py +++ b/main.py @@ -1,37 +1,68 @@ import asyncio import logging -import os +import signal import sys -from os.path import dirname, join # import aioschedule as schedule -import dotenv -from aiogram import Bot, Dispatcher, types -from aiogram.filters import CommandStart +from aiogram import Bot, Dispatcher -dotenv.load_dotenv() +from handlers.admin_commands import AdminCommands +from handlers.handler import Handler +from handlers.user_commands import UserCommands +from neuroapi.config import Config -token = os.getenv('TOKEN') -bot = Bot(token) -dp = Dispatcher() +class NeuroApiBot: + bot: Bot + dp: Dispatcher + + _instances = {} + + def __init__(self, token: str) -> None: + self.bot = Bot(token) + self.dp = Dispatcher() + self._instances + + def __new__(cls, token: str) -> 'NeuroApiBot': + assert isinstance(token, str) + if token not in cls._instances: + cls._instances[token] = super(NeuroApiBot, cls).__new__(cls) + return cls._instances[token] + + def include_router(self, *routerClasses: Handler) -> None: + for routerClass in routerClasses: + assert issubclass(routerClass, Handler) + self.dp.include_routers(routerClass(self.bot)()) + + async def start(self, skip_updates=True): + await self.dp.start_polling(self.bot, skip_updates=skip_updates) -@dp.message(CommandStart()) -async def start_message(message: types.Message): - await message.answer('Добро пожаловать в бота ') - -handlers_dir = join(dirname(__file__), 'handlers') - -for filename in os.listdir(handlers_dir): - if filename.endswith('.py'): - module_name = filename[:-3] - setup = __import__(f"handlers.{module_name}", locals(), globals(), ['setup']).setup - dp.include_router(setup(bot)) +async def delay_bot()->None: + if Config().token is None: + print('Delay bot needs token in environment') + return + bot = NeuroApiBot(Config().token) + bot.include_router(AdminCommands, UserCommands) + await bot.start() +async def proxy_bot()->None: + if Config().proxy_token is None: + print('Proxy bot needs token in environment') + return + bot = NeuroApiBot(Config().proxy_token) + bot.include_router() + await bot.start() async def main() -> None: - await dp.start_polling(bot, skip_updates=True) + tasks = [asyncio.create_task(delay_bot()), asyncio.create_task(proxy_bot())] + await asyncio.gather(*tasks) if __name__ == '__main__': logging.basicConfig(level=logging.INFO, stream=sys.stdout) - asyncio.run(main()) \ No newline at end of file + loop = asyncio.get_event_loop() + for signame in ('SIGINT', 'SIGTERM'): + loop.add_signal_handler(getattr(signal, signame), loop.stop) + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass \ No newline at end of file diff --git a/neuroapi/config.py b/neuroapi/config.py index 9dd571d..3d82414 100644 --- a/neuroapi/config.py +++ b/neuroapi/config.py @@ -1,21 +1,15 @@ import os import tomllib -from typing import List, Self, TypeVar +from typing import List, Optional from attr import dataclass from dotenv import load_dotenv +from neuroapi.types import Singleton + from .types._helpers import * -class _Singleton: - _instances = {} - - def __new__(cls) -> Self: - if cls not in cls._instances: - cls._instances[cls] = super(_Singleton, cls).__new__(cls) - return cls._instances[cls] - @dataclass class Settings: time: List[str] @@ -31,13 +25,21 @@ class Settings: result['time'] = from_list(from_str, self.time) return result -class Config(_Singleton): +class Config(Singleton): api_url: str settings: Settings + token: Optional[str] + proxy_token: Optional[str] def __init__(self): load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env')) if not os.path.exists(os.path.join(os.path.dirname(__file__), '..', 'settings.toml')): raise Exception('Settings.toml must be in root folder') with open(os.path.join(os.path.dirname(__file__), '..', 'settings.toml'), 'rb') as f: settings = tomllib.load(f) self.settings = Settings.from_dict(settings) - self.api_url = os.environ.get('API_URL') \ No newline at end of file + self.api_url = os.environ.get('API_URL') + self.token = os.environ.get('TOKEN') + if self.token == '': + self.token = None + self.proxy_token = os.environ.get('PROXY_TOKEN') + if self.proxy_token == '': + self.proxy_token = None \ No newline at end of file diff --git a/neuroapi/types/__init__.py b/neuroapi/types/__init__.py index cb41a50..bfa4e2f 100644 --- a/neuroapi/types/__init__.py +++ b/neuroapi/types/__init__.py @@ -1,4 +1,5 @@ -from ._post import Post -from ._image import Image from ._admin import Admin +from ._image import Image +from ._post import Post +from ._singleton import Singleton from ._user import User diff --git a/neuroapi/types/_singleton.py b/neuroapi/types/_singleton.py new file mode 100644 index 0000000..46d6a87 --- /dev/null +++ b/neuroapi/types/_singleton.py @@ -0,0 +1,10 @@ +from typing import Self + + +class Singleton: + _instances = {} + + def __new__(cls, *args, **kwargs) -> Self: + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__new__(cls) + return cls._instances[cls] \ No newline at end of file From 0e79ce74e12816fde9b2b83c929234d5730155f2 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 27 Nov 2023 22:21:03 +0300 Subject: [PATCH 12/26] Moved bot class to neuroapi module --- main.py | 30 ++---------------------------- neuroapi/types/__init__.py | 1 + neuroapi/types/_bot.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 neuroapi/types/_bot.py diff --git a/main.py b/main.py index 5795646..98c1979 100644 --- a/main.py +++ b/main.py @@ -3,39 +3,13 @@ import logging import signal import sys -# import aioschedule as schedule -from aiogram import Bot, Dispatcher - from handlers.admin_commands import AdminCommands -from handlers.handler import Handler from handlers.user_commands import UserCommands from neuroapi.config import Config +from neuroapi.types import NeuroApiBot +# import aioschedule as schedule -class NeuroApiBot: - bot: Bot - dp: Dispatcher - - _instances = {} - - def __init__(self, token: str) -> None: - self.bot = Bot(token) - self.dp = Dispatcher() - self._instances - - def __new__(cls, token: str) -> 'NeuroApiBot': - assert isinstance(token, str) - if token not in cls._instances: - cls._instances[token] = super(NeuroApiBot, cls).__new__(cls) - return cls._instances[token] - - def include_router(self, *routerClasses: Handler) -> None: - for routerClass in routerClasses: - assert issubclass(routerClass, Handler) - self.dp.include_routers(routerClass(self.bot)()) - - async def start(self, skip_updates=True): - await self.dp.start_polling(self.bot, skip_updates=skip_updates) async def delay_bot()->None: if Config().token is None: diff --git a/neuroapi/types/__init__.py b/neuroapi/types/__init__.py index bfa4e2f..f3e3efe 100644 --- a/neuroapi/types/__init__.py +++ b/neuroapi/types/__init__.py @@ -1,4 +1,5 @@ from ._admin import Admin +from ._bot import NeuroApiBot from ._image import Image from ._post import Post from ._singleton import Singleton diff --git a/neuroapi/types/_bot.py b/neuroapi/types/_bot.py new file mode 100644 index 0000000..6b9febf --- /dev/null +++ b/neuroapi/types/_bot.py @@ -0,0 +1,29 @@ +from aiogram import Bot, Dispatcher + +from handlers.handler import Handler + + +class NeuroApiBot: + bot: Bot + dp: Dispatcher + + _instances = {} + + def __init__(self, token: str) -> None: + self.bot = Bot(token) + self.dp = Dispatcher() + self._instances + + def __new__(cls, token: str) -> 'NeuroApiBot': + assert isinstance(token, str) + if token not in cls._instances: + cls._instances[token] = super(NeuroApiBot, cls).__new__(cls) + return cls._instances[token] + + def include_router(self, *routerClasses: Handler) -> None: + for routerClass in routerClasses: + assert issubclass(routerClass, Handler) + self.dp.include_routers(routerClass(self.bot)()) + + async def start(self, skip_updates=True): + await self.dp.start_polling(self.bot, skip_updates=skip_updates) \ No newline at end of file From a46a6c659f3832cca4e2aa61c4876ac6b08edcf4 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Tue, 28 Nov 2023 21:40:57 +0300 Subject: [PATCH 13/26] Removed trash --- neuroapi/types/_bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neuroapi/types/_bot.py b/neuroapi/types/_bot.py index 6b9febf..58306bd 100644 --- a/neuroapi/types/_bot.py +++ b/neuroapi/types/_bot.py @@ -12,7 +12,6 @@ class NeuroApiBot: def __init__(self, token: str) -> None: self.bot = Bot(token) self.dp = Dispatcher() - self._instances def __new__(cls, token: str) -> 'NeuroApiBot': assert isinstance(token, str) From 8081fbd77c1ba3807df4b1a7d38b358d2a4b512f Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Tue, 28 Nov 2023 22:28:15 +0300 Subject: [PATCH 14/26] Method for post/post --- handlers/admin_commands.py | 24 +++++++++++++----------- neuroapi/_methods/post.py | 10 ++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index 2641225..7ebbbcb 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -178,17 +178,19 @@ class AdminCommands(Handler): @self.router.message(Command('post')) async def post(message: types.Message): - posts = await neuroapi.post.get_will_post() - if (posts): - post = await neuroapi.post.get(str(posts[0].uuid)) - images = MediaGroupBuilder(caption=post.text) - image: neuroTypes.Image - 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('Нет постов') + try: + post = await neuroapi.post.get_post_to_post() + if (post): + images = MediaGroupBuilder(caption=post.text) + image: neuroTypes.Image + 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('Нет постов') + except Exception as e: + await message.answer(f'Ошибка {e}') @self.router.message(NewSoloPostFilter()) async def post_solo(message: types.Message): diff --git a/neuroapi/_methods/post.py b/neuroapi/_methods/post.py index f60fa3f..2bf4489 100644 --- a/neuroapi/_methods/post.py +++ b/neuroapi/_methods/post.py @@ -59,3 +59,13 @@ class Post(ApiMethod): if 'statusCode' in data: raise Exception(data['message']) return neuroTypes.Post.from_dict(data) + + async def get_post_to_post(self): + response = requests.get(self.api_url+f"/post/post") + data = response.json() + if 'statusCode' in data: + if response.status_code==404: + return None + else: + raise Exception(data['message']) + return neuroTypes.Post.from_dict(data) \ No newline at end of file From 16e37df15d52e8e454766be40969d5b18b367277 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Tue, 28 Nov 2023 22:28:31 +0300 Subject: [PATCH 15/26] Checking backend availability --- main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/main.py b/main.py index 98c1979..33fbd16 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,8 @@ import logging import signal import sys +import aiohttp + from handlers.admin_commands import AdminCommands from handlers.user_commands import UserCommands from neuroapi.config import Config @@ -28,6 +30,20 @@ async def proxy_bot()->None: await bot.start() async def main() -> None: + for i in range(5): + print(f'Checking connectivity to backend ({i+1}/5)...') + try: + async with aiohttp.ClientSession() as session: + response = await session.get(Config().api_url+'/ping') + data = str(await response.content.read(), encoding='utf-8') + if data == 'pong': + print('Successfully connected to backend') + break + else: + raise TimeoutError() + except: + print('Error! Waiting 3 secs and retrying...') + await asyncio.sleep(3) tasks = [asyncio.create_task(delay_bot()), asyncio.create_task(proxy_bot())] await asyncio.gather(*tasks) From c8229ffbed2e0aada037302f6d00791f8e828e2a Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Wed, 29 Nov 2023 18:51:05 +0300 Subject: [PATCH 16/26] Added aiohttp to requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 23c1acb..2333377 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ aiogram==3.1.1 aioschedule @ https://github.com/AleksHeller/python-aioschedule/archive/refs/heads/master.zip python-dotenv==1.0.0 requests==2.31.0 -python-dateutil==2.8.2 \ No newline at end of file +python-dateutil==2.8.2 +aiohttp==3.8.6 \ No newline at end of file From f63954e8ff50c6d727ec8193bfe2fc90e80e5da3 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Wed, 29 Nov 2023 18:51:16 +0300 Subject: [PATCH 17/26] Added docker config --- .dockerignore | 3 +++ Dockerfile | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..067ec1d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +venv +.env.example +Dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ec3a254 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-alpine + +WORKDIR /app + +COPY . . + +RUN pip install -r requirements.txt + +CMD [ "python", "main.py" ] \ No newline at end of file From 142d18bac35f49e7e26047fdf696eb7cb1c7f7b1 Mon Sep 17 00:00:00 2001 From: Errormacr Date: Thu, 30 Nov 2023 18:25:09 +0300 Subject: [PATCH 18/26] create Dockerfile.dev --- Dockerfile.dev | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Dockerfile.dev diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..792b572 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,11 @@ +FROM python:3.11-alpine + +WORKDIR /app + +COPY . . + +RUN pip install -r requirements.txt + +RUN pip install jurigged + +CMD [ "python", "-m", "jurigged", "-v", "main.py" ] \ No newline at end of file From 1893ab4ef9375c2e3731d2bae66f28ca6ac3c748 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 25 Dec 2023 14:38:03 +0300 Subject: [PATCH 19/26] Added getting bot settings from server --- handlers/admin_commands.py | 23 ++++++++++++++++++++++- neuroapi/_methods/bot_settings.py | 13 +++++++++++++ neuroapi/_neuroapi.py | 2 ++ neuroapi/types/__init__.py | 1 + neuroapi/types/_bot_settings.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 neuroapi/_methods/bot_settings.py create mode 100644 neuroapi/types/_bot_settings.py diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index 7ebbbcb..f5f0728 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -1,5 +1,7 @@ +import asyncio from typing import List +import aioschedule as schedule from aiogram import Bot, F, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext @@ -13,6 +15,7 @@ from handlers.handler import Handler from handlers.middlewares.user import AdminMiddleware from handlers.states.change_post import ChangePost from neuroapi import neuroapi +from neuroapi.types import BotSettings as BotSettingsType def get_post_info(post: neuroTypes.Post, post_id: int) -> str: @@ -25,10 +28,11 @@ def get_post_info(post: neuroTypes.Post, post_id: int) -> str: class AdminCommands(Handler): + settings: BotSettingsType def __init__(self, bot: Bot) -> None: super().__init__(bot) self.router.message.middleware(AdminMiddleware()) - + @self.router.message(NewPostFilter()) async def new_post(message: types.Message): post: neuroTypes.Post = await neuroapi.post.get_by_media_group_id(message.media_group_id) @@ -208,5 +212,22 @@ class AdminCommands(Handler): await message.reply('Ваше сообщение было отправлено!') except Exception as e: print(e) + + @self.router.message(Command('update_settings')) + async def update_settings(mes: types.Message| None = None): + self.settings = await neuroapi.bot_settings.get() + if mes: await mes.answer('Настройки обновлены!') + + + async def schedule_checker(): + await update_settings() + schedule.every().minute.do(update_settings, None) + while 1: + await schedule.run_pending() + await asyncio.sleep(1) + + asyncio.create_task(schedule_checker()) + + diff --git a/neuroapi/_methods/bot_settings.py b/neuroapi/_methods/bot_settings.py new file mode 100644 index 0000000..12cc228 --- /dev/null +++ b/neuroapi/_methods/bot_settings.py @@ -0,0 +1,13 @@ +from aiohttp import ClientSession + +from neuroapi.types import BotSettings as BotSettingsType + +from .api_method import ApiMethod + + +class BotSettings(ApiMethod): + async def get(self)-> BotSettingsType: + async with ClientSession() as session: + response = await session.get(self.api_url+'/settings') + settings = BotSettingsType.from_dict(await response.json()) + return settings \ No newline at end of file diff --git a/neuroapi/_neuroapi.py b/neuroapi/_neuroapi.py index 6949754..f22f373 100644 --- a/neuroapi/_neuroapi.py +++ b/neuroapi/_neuroapi.py @@ -1,4 +1,5 @@ from ._methods.admin import Admin +from ._methods.bot_settings import BotSettings from ._methods.image import Image from ._methods.post import Post from ._methods.user import User @@ -9,3 +10,4 @@ class neuroapi: admin = Admin() user = User() image = Image() + bot_settings = BotSettings() diff --git a/neuroapi/types/__init__.py b/neuroapi/types/__init__.py index f3e3efe..7e8f67d 100644 --- a/neuroapi/types/__init__.py +++ b/neuroapi/types/__init__.py @@ -1,5 +1,6 @@ from ._admin import Admin from ._bot import NeuroApiBot +from ._bot_settings import BotSettings from ._image import Image from ._post import Post from ._singleton import Singleton diff --git a/neuroapi/types/_bot_settings.py b/neuroapi/types/_bot_settings.py new file mode 100644 index 0000000..f774261 --- /dev/null +++ b/neuroapi/types/_bot_settings.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from uuid import UUID + +from ._helpers import * + + +@dataclass +class BotSettings: + uuid: UUID + message_times: List[str] + channel: str + is_active: bool + + @staticmethod + def from_dict(obj: Any) -> 'BotSettings': + assert isinstance(obj, dict) + uuid = UUID(obj.get("uuid")) + message_times = from_list(from_str, obj.get("messageTimes")) + channel = from_str(obj.get("channel")) + is_active = from_bool(obj.get("isActive")) + return BotSettings(uuid, message_times, channel, is_active) + + def to_dict(self) -> dict: + result: dict = {} + result["uuid"] = str(self.uuid) + result["messageTimes"] = from_list(from_str, self.message_times) + result["channel"] = from_str(self.channel) + result["isActive"] = from_bool(self.is_active) + return result From 5ad8a4212e67fcf4a44b26ad556a61a0fa8e01a2 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 25 Dec 2023 14:54:44 +0300 Subject: [PATCH 20/26] Added posting to channel from settings by the time --- handlers/admin_commands.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index f5f0728..81d306b 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -181,7 +181,7 @@ class AdminCommands(Handler): await bot.delete_message(message_id=data['edit_msg'], chat_id=callback.message.chat.id) @self.router.message(Command('post')) - async def post(message: types.Message): + async def post(message: types.Message | None = None): try: post = await neuroapi.post.get_post_to_post() if (post): @@ -190,11 +190,13 @@ class AdminCommands(Handler): 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 self.bot.send_media_group(self.settings.channel, images.build()) + if message: + await message.answer('Посту успешно опубликован!') + elif message: await message.answer('Нет постов') except Exception as e: - await message.answer(f'Ошибка {e}') + if message: await message.answer(f'Ошибка {e}') @self.router.message(NewSoloPostFilter()) async def post_solo(message: types.Message): @@ -216,17 +218,26 @@ class AdminCommands(Handler): @self.router.message(Command('update_settings')) async def update_settings(mes: types.Message| None = None): self.settings = await neuroapi.bot_settings.get() + schedule.clear() + schedule.every().minute.do(update_settings, None) + for i in self.settings.message_times: + schedule.every().monday.at(i).do(post, None) + schedule.every().tuesday.at(i).do(post, None) + schedule.every().wednesday.at(i).do(post, None) + schedule.every().thursday.at(i).do(post, None) + schedule.every().friday.at(i).do(post, None) + if i not in ['10:00', '20:00']: + schedule.every().sunday.at(i).do(post, None) if mes: await mes.answer('Настройки обновлены!') - async def schedule_checker(): + async def settings_and_schedule_checker(): await update_settings() - schedule.every().minute.do(update_settings, None) while 1: await schedule.run_pending() await asyncio.sleep(1) - asyncio.create_task(schedule_checker()) + asyncio.create_task(settings_and_schedule_checker()) From e0c0bfa59b0a18f0a9ce32a87f922b2d2dc9f5ec Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Mon, 25 Dec 2023 14:56:30 +0300 Subject: [PATCH 21/26] Added todo --- handlers/admin_commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index 81d306b..e7075c8 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -220,6 +220,8 @@ class AdminCommands(Handler): self.settings = await neuroapi.bot_settings.get() schedule.clear() schedule.every().minute.do(update_settings, None) + + #TODO: Сделать в бэке и в боте, чтоб дни тоже можно было в настройках хранить for i in self.settings.message_times: schedule.every().monday.at(i).do(post, None) schedule.every().tuesday.at(i).do(post, None) From 23e7d8e5952751bc6ad5662838cdb4ac24d42f63 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Wed, 27 Dec 2023 15:13:25 +0300 Subject: [PATCH 22/26] Platform-related running --- main.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index 33fbd16..3bf04f7 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import asyncio import logging +import platform import signal import sys @@ -50,9 +51,17 @@ async def main() -> None: if __name__ == '__main__': logging.basicConfig(level=logging.INFO, stream=sys.stdout) loop = asyncio.get_event_loop() - for signame in ('SIGINT', 'SIGTERM'): - loop.add_signal_handler(getattr(signal, signame), loop.stop) - try: - asyncio.run(main()) - except KeyboardInterrupt: - pass \ No newline at end of file + if platform.system() == 'Windows': + try: + loop.run_until_complete(main()) + except KeyboardInterrupt: + print("KeyboardInterrupt occurred") + finally: + loop.close() + else: + for signame in ('SIGINT', 'SIGTERM'): + loop.add_signal_handler(getattr(signal, signame), loop.stop) + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass \ No newline at end of file From 80918ad07bdb74ce7b27200f7c9a3073745a3ccd Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Wed, 27 Dec 2023 15:13:51 +0300 Subject: [PATCH 23/26] Removed unnecessary import --- main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/main.py b/main.py index 3bf04f7..36a4866 100644 --- a/main.py +++ b/main.py @@ -11,8 +11,6 @@ from handlers.user_commands import UserCommands from neuroapi.config import Config from neuroapi.types import NeuroApiBot -# import aioschedule as schedule - async def delay_bot()->None: if Config().token is None: From 915e36890b9c574412d2f5b5abbaac031dd6f71f Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Wed, 27 Dec 2023 15:25:46 +0300 Subject: [PATCH 24/26] Reply bot text in post --- handlers/admin_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index e7075c8..b5f81a5 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -185,14 +185,14 @@ class AdminCommands(Handler): try: post = await neuroapi.post.get_post_to_post() if (post): - images = MediaGroupBuilder(caption=post.text) + images = MediaGroupBuilder(caption=post.text + '\n\nПредложка: @neur0w0men_reply_bot') image: neuroTypes.Image for image in sorted(post.images, key=lambda x: x.message_id): images.add_photo(image.file_id, has_spoiler=image.has_spoiler) await self.bot.send_media_group(self.settings.channel, images.build()) if message: - await message.answer('Посту успешно опубликован!') + await message.answer('Пост успешно опубликован!') elif message: await message.answer('Нет постов') except Exception as e: From 43b1c5ecf4bc1def0af2bc3750db4644715e7031 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Wed, 27 Dec 2023 16:01:42 +0300 Subject: [PATCH 25/26] Info command --- handlers/admin_commands.py | 53 ++++++++++++++++++++++++++------------ neuroapi/types/_admin.py | 9 ++++--- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/handlers/admin_commands.py b/handlers/admin_commands.py index b5f81a5..1507264 100644 --- a/handlers/admin_commands.py +++ b/handlers/admin_commands.py @@ -29,10 +29,11 @@ def get_post_info(post: neuroTypes.Post, post_id: int) -> str: class AdminCommands(Handler): settings: BotSettingsType + def __init__(self, bot: Bot) -> None: super().__init__(bot) self.router.message.middleware(AdminMiddleware()) - + @self.router.message(NewPostFilter()) async def new_post(message: types.Message): post: neuroTypes.Post = await neuroapi.post.get_by_media_group_id(message.media_group_id) @@ -41,14 +42,33 @@ class AdminCommands(Handler): @self.router.message(Command('info')) async def info_command(message: types.Message): posts: List[neuroTypes.Post] = await neuroapi.post.get_will_post() + admins: List[neuroTypes.Admin] = await neuroapi.admin.get() 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)) + res = "Количество постов от админов:\n" + res2 = "\nПосты:\n" + for admin in admins: + if admin.user_id in post_c: + res += f'[{admin.user_name}](tg://user?id={admin.user_id}): {post_c[admin.user_id]}\n' + else: + res += f'[{admin.user_name}](tg://user?id={admin.user_id}): 0\n' + admin_posts = list( + filter(lambda x: x.from_user_id == admin.user_id, posts)) + res2 += f'Посты от {admin.user_name}:\n' + if len(admin_posts): + for i, post in enumerate(admin_posts): + #TODO: Если возможно, сделать чтоб было ссылкой на сообщений с /newpost + res2 += f'{i+1}. {post.text}\n' + else: + res2 += 'Их нет\)\n' + await message.answer((res+res2).replace('#', '\#').replace("_", "\_").replace('.', '\.').replace(',', '\,').replace('!', '\!'), parse_mode='markdownv2') + """ + TODO: Изменение постов сделать нормально, не через редактирование сообщений @self.router.message(ChangePosts()) async def change_post(message: types.Message, state: FSMContext): posts = await neuroapi.post.get_will_post() @@ -179,24 +199,27 @@ class AdminCommands(Handler): data = await state.get_data() if 'edit_msg' in data: await bot.delete_message(message_id=data['edit_msg'], chat_id=callback.message.chat.id) + """ @self.router.message(Command('post')) async def post(message: types.Message | None = None): try: post = await neuroapi.post.get_post_to_post() if (post): - images = MediaGroupBuilder(caption=post.text + '\n\nПредложка: @neur0w0men_reply_bot') + images = MediaGroupBuilder( + caption=post.text + '\n\nПредложка: @neur0w0men_reply_bot') image: neuroTypes.Image for image in sorted(post.images, key=lambda x: x.message_id): images.add_photo(image.file_id, - has_spoiler=image.has_spoiler) + has_spoiler=image.has_spoiler) await self.bot.send_media_group(self.settings.channel, images.build()) if message: await message.answer('Пост успешно опубликован!') elif message: await message.answer('Нет постов') except Exception as e: - if message: await message.answer(f'Ошибка {e}') + if message: + await message.answer(f'Ошибка {e}') @self.router.message(NewSoloPostFilter()) async def post_solo(message: types.Message): @@ -214,14 +237,14 @@ class AdminCommands(Handler): await message.reply('Ваше сообщение было отправлено!') except Exception as e: print(e) - + @self.router.message(Command('update_settings')) - async def update_settings(mes: types.Message| None = None): + async def update_settings(mes: types.Message | None = None): self.settings = await neuroapi.bot_settings.get() schedule.clear() schedule.every().minute.do(update_settings, None) - - #TODO: Сделать в бэке и в боте, чтоб дни тоже можно было в настройках хранить + + # TODO: Сделать в бэке и в боте, чтоб дни тоже можно было в настройках хранить for i in self.settings.message_times: schedule.every().monday.at(i).do(post, None) schedule.every().tuesday.at(i).do(post, None) @@ -230,17 +253,13 @@ class AdminCommands(Handler): schedule.every().friday.at(i).do(post, None) if i not in ['10:00', '20:00']: schedule.every().sunday.at(i).do(post, None) - if mes: await mes.answer('Настройки обновлены!') - - + if mes: + await mes.answer('Настройки обновлены!') + async def settings_and_schedule_checker(): await update_settings() while 1: await schedule.run_pending() await asyncio.sleep(1) - - asyncio.create_task(settings_and_schedule_checker()) - - - + asyncio.create_task(settings_and_schedule_checker()) diff --git a/neuroapi/types/_admin.py b/neuroapi/types/_admin.py index 6ce6e5e..19692c7 100644 --- a/neuroapi/types/_admin.py +++ b/neuroapi/types/_admin.py @@ -1,22 +1,23 @@ from dataclasses import dataclass from typing import Any + from ._helpers import * @dataclass class Admin: - id: int user_id: int + user_name: str @staticmethod def from_dict(obj: Any) -> 'Admin': assert isinstance(obj, dict) - id = from_int(obj.get("id")) user_id = int(from_str(obj.get("user_id"))) - return Admin(id, user_id) + user_name = from_str(obj.get("user_name")) + return Admin(user_id, user_name) def to_dict(self) -> dict: result: dict = {} - result["id"] = from_int(self.id) result["user_id"] = from_str(str(self.user_id)) + result["user_name"] = from_str(self.user_name) return result From e17aaadeb913a607494b20124431591562a5fa41 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Thu, 28 Dec 2023 14:02:42 +0300 Subject: [PATCH 26/26] Admin commands work only in private or group --- handlers/middlewares/user.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/handlers/middlewares/user.py b/handlers/middlewares/user.py index f155055..9f10cee 100644 --- a/handlers/middlewares/user.py +++ b/handlers/middlewares/user.py @@ -11,6 +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: + if event.chat.type not in ['private', 'group']: + return None 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: