diff --git a/.flake8 b/.flake8 index 4e2b181..fb9ec7c 100644 --- a/.flake8 +++ b/.flake8 @@ -4,3 +4,5 @@ per-file-ignores = max-line-length = 88 count = true extend-ignore = E203,E701 + +extend-select = TC010,TC200 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f672c6..dccbb06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,8 @@ repos: rev: 7.1.0 # sync:flake8:poetry.lock hooks: - id: flake8 + additional_dependencies: + - flake8-type-checking - repo: https://github.com/PyCQA/bandit rev: 1.7.9 # sync:bandit:poetry.lock hooks: diff --git a/karkas.code-workspace b/karkas.code-workspace index 3aad916..7aaf5d8 100644 --- a/karkas.code-workspace +++ b/karkas.code-workspace @@ -12,10 +12,6 @@ "name": "Karkas Core", "path": "src/karkas_core" }, - { - "name": "Gnomik", - "path": "src/gnomik" - }, { "name": "ALT Linux", "path": "src/altlinux" diff --git a/src/altlinux/README.md b/src/altlinux/README.md index 8bee95a..3a3fc50 100644 --- a/src/altlinux/README.md +++ b/src/altlinux/README.md @@ -2,14 +2,14 @@ ## Описание -Подготовленная версия Karkas Lite для интеграции в чат [Альт Линукс](https://t.me/alt_linux) +Подготовленная версия Karkas для интеграции в чат [Альт Линукс](https://t.me/alt_linux) ## Функционал Список OCAB-модулей используемых в боте: * report - Вызов администрации чата одной командой * welcome - Автоматическая вариативная проверка пользователей на признаки бота или другой автоматической рекламной системы -* help - Получение информации об Karkas Lite +* help - Получение справки о боте ## Запуск @@ -17,29 +17,31 @@ 1. Соберите Docker-образ: ```bash - docker build -t gnomik . + docker build -t altlinux -f Dockerfile ../.. ``` 2. Запустите контейнер: ```bash - docker run -p 9000:9000 -v ./config.yaml:/app/config.yaml -v ./database:/app/database gnomik + docker run -v ./config.yaml:/app/config.yaml altlinux ``` - Замените `./config.yaml` и `./database` на пути к вашим локальным файлам конфигурации и паки для базы данных. + Замените `./config.yaml` на путь к вашему локальному файлу конфигурации. ### Вручную -1. Активируйте виртуальное окружение Gnomика: +1. Активируйте виртуальное окружение: ```bash poetry shell ``` 2. Запустите бота: ```bash - python -m gnomik + python -m altlinux ``` ## Конфигурация -Конфигурация бота находится в файле `config.yaml`. +Конфигурация хранится в файле `config.yaml`. + +Пример конфигурации бота находится в файле `config-example.yaml`. ## Модули diff --git a/src/altlinux/config-example.yaml b/src/altlinux/config-example.yaml index a294ec7..dde109b 100644 --- a/src/altlinux/config-example.yaml +++ b/src/altlinux/config-example.yaml @@ -2,6 +2,45 @@ core: mode: LONG_POLLING token: xxx -filters: - approved_chat_id: - - -111111 +# +# filters: +# approved_chat_id: +# - -100000000000 +# + +# +# welcome: +# show_success_message: True +# max_attempts: 5 +# timeout: 60 +# tasks: +# math_buttons: +# enabled: False +# math_poll: +# enabled: True +# question_buttons: +# enabled: False +# question_poll: +# enabled: False +# + +# +# report: +# errors: +# no_reply_message: "Пожалуйста, используйте команду в ответ на сообщение" +# mention: +# text: "⚠️ Внимание, жалоба на спам! $mention" +# limit: 5 +# list: +# - "@test1" +# - "@test2" +# - "@test3" +# - "@test4" +# - "@test5" +# - "@test6" +# + +# +# help: +# message: "$commands" +# diff --git a/src/altlinux/docker-compose.yml b/src/altlinux/docker-compose.yml index 47c8c01..a6923a2 100644 --- a/src/altlinux/docker-compose.yml +++ b/src/altlinux/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - services: app: build: diff --git a/src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py b/src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py index 799be60..420bd8f 100644 --- a/src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py +++ b/src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py @@ -1,8 +1,6 @@ import asyncio from typing import TYPE_CHECKING -from .config_manager import ConfigManager - try: import dash_bootstrap_components as dbc import flask @@ -17,8 +15,10 @@ from karkas_core.modules_system.public_api import get_module if TYPE_CHECKING: from karkas_blocks.standard.roles import Roles as IRoles + from .config_manager import ConfigManager -def create_control(key: str, config: ConfigManager): + +def create_control(key: str, config: "ConfigManager"): value = config.get(key) meta = config.get_meta(key) @@ -85,7 +85,7 @@ def create_control(key: str, config: ConfigManager): return dbc.Row(row, className="mb-3 mx-1") -def build_settings_tree(config: ConfigManager): +def build_settings_tree(config: "ConfigManager"): tree = {} for key, value in config._metadata.items(): @@ -134,7 +134,7 @@ def create_settings_components(tree, level=0): return components -def get_miniapp_blueprint(config: ConfigManager, prefix: str): +def get_miniapp_blueprint(config: "ConfigManager", prefix: str): Roles: "type[IRoles]" = get_module("standard.roles", "Roles") roles = Roles() diff --git a/src/karkas_blocks/karkas_blocks/standard/filters/filters.py b/src/karkas_blocks/karkas_blocks/standard/filters/filters.py index dfe98bd..f5cb247 100644 --- a/src/karkas_blocks/karkas_blocks/standard/filters/filters.py +++ b/src/karkas_blocks/karkas_blocks/standard/filters/filters.py @@ -1,13 +1,14 @@ from typing import TYPE_CHECKING -from aiogram import Bot from aiogram.filters import BaseFilter -from aiogram.types import Message from typing_extensions import deprecated from karkas_core.modules_system.public_api import get_module if TYPE_CHECKING: + from aiogram import Bot + from aiogram.types import Message + from karkas_blocks.standard.config import IConfig from karkas_blocks.standard.roles import Roles as IRoles @@ -34,7 +35,7 @@ def get_approved_chat_id() -> list: @deprecated("Use ChatIDFilter or own implementation") -def chat_not_in_approve(message: Message) -> bool: +def chat_not_in_approve(message: "Message") -> bool: chat_id = message.chat.id if chat_id in get_approved_chat_id(): # log(f"Chat in approve list: {chat_id}") @@ -50,7 +51,7 @@ class ChatIDFilter(BaseFilter): self.approved_chats = approved_chats super().__init__() - async def __call__(self, message: Message, bot: Bot) -> bool: + async def __call__(self, message: "Message", bot: "Bot") -> bool: chat_id = message.chat.id approved_chats = self.approved_chats or get_approved_chat_id() @@ -70,7 +71,7 @@ class ChatNotInApproveFilter(ChatIDFilter): class ChatModerOrAdminFilter(BaseFilter): - async def __call__(self, message: Message, bot: Bot) -> bool: + async def __call__(self, message: "Message", bot: "Bot") -> bool: if not ROLES_MODULE_LOADED: raise Exception("Roles module not loaded") diff --git a/src/karkas_blocks/karkas_blocks/standard/help/main.py b/src/karkas_blocks/karkas_blocks/standard/help/main.py index a5e1de0..a254934 100644 --- a/src/karkas_blocks/karkas_blocks/standard/help/main.py +++ b/src/karkas_blocks/karkas_blocks/standard/help/main.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING from aiogram import Router from aiogram.filters import Command -from aiogram.types import Message from karkas_core.modules_system.public_api import ( get_metainfo, @@ -12,6 +11,8 @@ from karkas_core.modules_system.public_api import ( ) if TYPE_CHECKING: + from aiogram.types import Message + from karkas_blocks.standard.config import IConfig config: "IConfig" = get_module("standard.config", "config") @@ -46,7 +47,7 @@ def format_commands(commands_dict): return "\n".join(formatted_commands) -async def help(message: Message): +async def help(message: "Message"): commands = "" version = "" diff --git a/src/karkas_blocks/karkas_blocks/standard/report/main.py b/src/karkas_blocks/karkas_blocks/standard/report/main.py index 6da63ab..0ba587c 100644 --- a/src/karkas_blocks/karkas_blocks/standard/report/main.py +++ b/src/karkas_blocks/karkas_blocks/standard/report/main.py @@ -4,11 +4,13 @@ from typing import TYPE_CHECKING from aiogram import Router from aiogram.filters import Command -from aiogram.types import ChatMemberAdministrator, ChatMemberOwner, Message +from aiogram.types import ChatMemberOwner from karkas_core.modules_system.public_api import get_module, log, register_router if TYPE_CHECKING: + from aiogram.types import ChatMemberAdministrator, Message + from karkas_blocks.standard.config import IConfig from karkas_blocks.standard.filters import ChatIDFilter as IChatIDFilter @@ -29,7 +31,7 @@ except Exception: pass -def can_moderate(admin: ChatMemberOwner | ChatMemberAdministrator) -> bool: +def can_moderate(admin: "ChatMemberOwner | ChatMemberAdministrator") -> bool: if isinstance(admin, ChatMemberOwner): return True @@ -38,7 +40,7 @@ def can_moderate(admin: ChatMemberOwner | ChatMemberAdministrator) -> bool: ) -async def report(message: Message): +async def report(message: "Message"): try: if message.reply_to_message is None: await message.reply(config.get("report::errors::no_reply_message")) diff --git a/src/karkas_blocks/karkas_blocks/standard/welcome/main.py b/src/karkas_blocks/karkas_blocks/standard/welcome/main.py index bac7446..2f4acba 100644 --- a/src/karkas_blocks/karkas_blocks/standard/welcome/main.py +++ b/src/karkas_blocks/karkas_blocks/standard/welcome/main.py @@ -7,7 +7,6 @@ from aiogram import Bot, Router, types from aiogram.enums import ChatMemberStatus, ParseMode from aiogram.exceptions import TelegramBadRequest from aiogram.filters import JOIN_TRANSITION, LEAVE_TRANSITION, ChatMemberUpdatedFilter -from aiogram.types import ChatMemberUpdated, PollAnswer from karkas_core.modules_system.public_api import get_module, log, register_router @@ -22,6 +21,8 @@ from .verifications_methods.simple import ( from .verifications_methods.utils import user_mention if TYPE_CHECKING: + from aiogram.types import ChatMemberUpdated, PollAnswer + from karkas_blocks.standard.config import IConfig from karkas_blocks.standard.filters import ChatIDFilter as IChatIDFilter @@ -78,7 +79,7 @@ verification_tasks = MultiKeyDict() last_success = {} -async def new_member_handler(event: ChatMemberUpdated, bot: Bot): +async def new_member_handler(event: "ChatMemberUpdated", bot: Bot): # НЕ СРАБОТАЕТ, ЕСЛИ ЧЕЛОВЕК УЖЕ ОГРАНИЧЕН В ПРАВАХ (RESTRICTED) if event.new_chat_member.status == ChatMemberStatus.MEMBER: task = task_manager.build_random_task(event, bot) @@ -87,7 +88,7 @@ async def new_member_handler(event: ChatMemberUpdated, bot: Bot): verification_tasks.add(task, keys) -async def left_member_handler(event: ChatMemberUpdated, bot: Bot): +async def left_member_handler(event: "ChatMemberUpdated", bot: Bot): user_id = event.from_user.id chat_id = event.chat.id @@ -144,7 +145,7 @@ async def success_end(task: BaseTask): last_success[task.from_chat_id] = message.message_id -async def handle_poll_verification(answer: PollAnswer, bot: Bot): +async def handle_poll_verification(answer: "PollAnswer", bot: Bot): key = key_from_poll(answer.poll_id) if not verification_tasks.exists(key): return diff --git a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/base.py b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/base.py index afc3853..f9be39a 100644 --- a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/base.py +++ b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/base.py @@ -1,21 +1,24 @@ import asyncio from functools import wraps +from typing import TYPE_CHECKING -from aiogram import Bot from aiogram.exceptions import TelegramBadRequest from aiogram.filters.callback_data import CallbackData -from aiogram.types import ChatMemberUpdated from karkas_core.modules_system.public_api import log from .utils import mute_user, unmute_user +if TYPE_CHECKING: + from aiogram import Bot + from aiogram.types import ChatMemberUpdated + class BaseTask: def __init__( self, - event: ChatMemberUpdated, - bot: Bot, + event: "ChatMemberUpdated", + bot: "Bot", timeout_func=None, attempt_number=1, max_attempts=1, diff --git a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/simple.py b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/simple.py index f396822..d392622 100644 --- a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/simple.py +++ b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/simple.py @@ -1,13 +1,17 @@ from string import Template +from typing import TYPE_CHECKING -from aiogram import Bot from aiogram.enums import ParseMode, PollType -from aiogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from ..utils import get_plural_form, key_from_poll, key_from_user_chat from .base import BaseTask, VerificationCallback, mute_while_task from .utils import user_mention +if TYPE_CHECKING: + from aiogram import Bot + from aiogram.types import ChatMemberUpdated + class SimpleBaseTask(BaseTask): pass @@ -119,8 +123,8 @@ class SimpleInlineButtonsTask(SimpleVariantsBaseTask): class SimplePollTask(SimpleVariantsBaseTask): def __init__( self, - event: ChatMemberUpdated, - bot: Bot, + event: "ChatMemberUpdated", + bot: "Bot", timeout_func=None, attempt_number=1, max_attempts=1, diff --git a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/utils.py b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/utils.py index f544735..0365f6f 100644 --- a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/utils.py +++ b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/utils.py @@ -1,9 +1,12 @@ import time +from typing import TYPE_CHECKING -from aiogram import Bot from aiogram.enums import ParseMode from aiogram.types import ChatPermissions, User +if TYPE_CHECKING: + from aiogram import Bot + def user_mention(user: User, mode=ParseMode.HTML): if mode == ParseMode.HTML: @@ -14,7 +17,7 @@ def user_mention(user: User, mode=ParseMode.HTML): raise ValueError(f"Unknown parse mode {mode}") -async def mute_user(chat_id, user_id, until, bot: Bot): +async def mute_user(chat_id, user_id, until, bot: "Bot"): end_time = until + int(time.time()) await bot.restrict_chat_member( chat_id, @@ -40,7 +43,7 @@ async def mute_user(chat_id, user_id, until, bot: Bot): ) -async def unmute_user(chat_id, user_id, bot: Bot): +async def unmute_user(chat_id, user_id, bot: "Bot"): await bot.restrict_chat_member( chat_id, user_id, diff --git a/src/karkas_core/karkas_core/lib.py b/src/karkas_core/karkas_core/lib.py index 7cc7acb..6608524 100644 --- a/src/karkas_core/karkas_core/lib.py +++ b/src/karkas_core/karkas_core/lib.py @@ -1,10 +1,14 @@ import importlib import os import traceback +from typing import TYPE_CHECKING -from aiogram import Bot, Dispatcher from aiogram.types import Update +if TYPE_CHECKING: + from aiogram import Bot, Dispatcher + from fastapi import FastAPI, Request + def get_module_directory(module_name): spec = importlib.util.find_spec(module_name) @@ -16,23 +20,15 @@ def get_module_directory(module_name): return os.path.dirname(module_path) -try: - from fastapi import FastAPI, Request +async def register_bot_webhook(app: "FastAPI", bot: "Bot", dp: "Dispatcher"): + async def handle_webhook(request: "Request"): + try: + update = Update.model_validate(await request.json(), context={"bot": bot}) + await dp.feed_update(bot, update) + except Exception: + traceback.print_exc() + return {"ok": False} - async def register_bot_webhook(app: FastAPI, bot: Bot, dp: Dispatcher): - async def handle_webhook(request: Request): - try: - update = Update.model_validate( - await request.json(), context={"bot": bot} - ) - await dp.feed_update(bot, update) - except Exception: - traceback.print_exc() - return {"ok": False} + return {"ok": True} - return {"ok": True} - - app.post("/webhook")(handle_webhook) - -except ImportError: - pass + app.post("/webhook")(handle_webhook) diff --git a/src/karkas_core/karkas_core/main.py b/src/karkas_core/karkas_core/main.py index 95a8764..05eb1bb 100644 --- a/src/karkas_core/karkas_core/main.py +++ b/src/karkas_core/karkas_core/main.py @@ -84,7 +84,7 @@ class Karkas: await register_bot_webhook(app, singleton.bot, singleton.dp) await singleton.bot.set_webhook(config.get("core::webhook::public_url")) hyperConfig = HyperConfig() - hyperConfig.bind = [f"0.0.0.0:{config.get("core::webhook::port")}"] + hyperConfig.bind = [f"0.0.0.0:{config.get('core::webhook::port')}"] hyperConfig.logger_class = CustomLogger await serve(app, hyperConfig) diff --git a/src/karkas_core/karkas_core/modules_system/loaders/base.py b/src/karkas_core/karkas_core/modules_system/loaders/base.py index ebcb89e..9696931 100644 --- a/src/karkas_core/karkas_core/modules_system/loaders/base.py +++ b/src/karkas_core/karkas_core/modules_system/loaders/base.py @@ -1,9 +1,11 @@ -import types from dataclasses import dataclass -from typing import Dict, List, Optional, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Union from dataclasses_json import dataclass_json +if TYPE_CHECKING: + import types + @dataclass_json @dataclass @@ -39,5 +41,5 @@ class AbstractLoader: def info(self) -> ModuleInfo: raise NotImplementedError - def load(self) -> types.ModuleType: + def load(self) -> "types.ModuleType": raise NotImplementedError diff --git a/src/karkas_core/karkas_core/modules_system/public_api/public_api.py b/src/karkas_core/karkas_core/modules_system/public_api/public_api.py index 53e6185..6b0869a 100644 --- a/src/karkas_core/karkas_core/modules_system/public_api/public_api.py +++ b/src/karkas_core/karkas_core/modules_system/public_api/public_api.py @@ -1,8 +1,7 @@ import inspect import types -from typing import Any, Tuple, Union +from typing import TYPE_CHECKING, Any, Tuple, Union -from aiogram import BaseMiddleware, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.storage.base import StorageKey @@ -10,18 +9,21 @@ from aiogram.fsm.storage.base import StorageKey from karkas_core.modules_system.loaders.base import DependencyInfo from karkas_core.singleton import Singleton +if TYPE_CHECKING: + from aiogram import BaseMiddleware, Router + async def set_chat_menu_button(menu_button): app = Singleton() await app.bot.set_chat_menu_button(menu_button=menu_button) -def register_router(router: Router): +def register_router(router: "Router"): app = Singleton() app.storage["_routers"].append(router) -def register_outer_message_middleware(middleware: BaseMiddleware): +def register_outer_message_middleware(middleware: "BaseMiddleware"): app = Singleton() app.storage["_outer_message_middlewares"].append(middleware) diff --git a/src/karkas_core/karkas_core/modules_system/safe/policy.py b/src/karkas_core/karkas_core/modules_system/safe/policy.py index 8dfc14b..58c11ac 100644 --- a/src/karkas_core/karkas_core/modules_system/safe/policy.py +++ b/src/karkas_core/karkas_core/modules_system/safe/policy.py @@ -1,6 +1,5 @@ import types -from _ast import AnnAssign -from typing import Any +from typing import TYPE_CHECKING, Any from aiogram import Bot from RestrictedPython import ( @@ -20,6 +19,9 @@ from RestrictedPython.Guards import ( # guarded_setattr,; full_write_guard, from karkas_core.logger import log from karkas_core.modules_system.safe.zope_guards import extra_safe_builtins +if TYPE_CHECKING: + from _ast import AnnAssign + class RestrictedPythonPolicy(RestrictingNodeTransformer): def visit_AsyncFunctionDef(self, node): @@ -50,7 +52,7 @@ class RestrictedPythonPolicy(RestrictingNodeTransformer): return self.node_contents_visit(node) """ - def visit_AnnAssign(self, node: AnnAssign) -> Any: + def visit_AnnAssign(self, node: "AnnAssign") -> Any: # missing in RestrictingNodeTransformer # this doesn't need the logic that is in visit_Assign # because it doesn't have a "targets" attribute, diff --git a/src/karkas_core/karkas_core/singleton.py b/src/karkas_core/karkas_core/singleton.py index d0db542..cc0a2f0 100644 --- a/src/karkas_core/karkas_core/singleton.py +++ b/src/karkas_core/karkas_core/singleton.py @@ -1,7 +1,11 @@ -from aiogram import Bot, Dispatcher +from typing import TYPE_CHECKING + from aiogram.fsm.storage.memory import MemoryStorage -from karkas_core.modules_system import ModulesManager +if TYPE_CHECKING: + from aiogram import Bot, Dispatcher + + from karkas_core.modules_system import ModulesManager class SingletonMeta(type): @@ -15,9 +19,9 @@ class SingletonMeta(type): class Singleton(metaclass=SingletonMeta): - bot: Bot - dp: Dispatcher = None - modules_manager: ModulesManager = None + bot: "Bot" + dp: "Dispatcher" = None + modules_manager: "ModulesManager" = None storage = { "_fsm_storage": MemoryStorage(), "_routers": [],