Merged with chore/small-fixes-KarkasLite

This commit is contained in:
Maxim Slipenko 2024-08-22 17:21:55 +03:00
commit da2a5c21c1
20 changed files with 142 additions and 82 deletions

View File

@ -4,3 +4,5 @@ per-file-ignores =
max-line-length = 88 max-line-length = 88
count = true count = true
extend-ignore = E203,E701 extend-ignore = E203,E701
extend-select = TC010,TC200

View File

@ -29,6 +29,8 @@ repos:
rev: 7.1.0 # sync:flake8:poetry.lock rev: 7.1.0 # sync:flake8:poetry.lock
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies:
- flake8-type-checking
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.9 # sync:bandit:poetry.lock rev: 1.7.9 # sync:bandit:poetry.lock
hooks: hooks:

View File

@ -12,10 +12,6 @@
"name": "Karkas Core", "name": "Karkas Core",
"path": "src/karkas_core" "path": "src/karkas_core"
}, },
{
"name": "Gnomik",
"path": "src/gnomik"
},
{ {
"name": "ALT Linux", "name": "ALT Linux",
"path": "src/altlinux" "path": "src/altlinux"

View File

@ -2,14 +2,14 @@
## Описание ## Описание
Подготовленная версия Karkas Lite для интеграции в чат [Альт Линукс](https://t.me/alt_linux) Подготовленная версия Karkas для интеграции в чат [Альт Линукс](https://t.me/alt_linux)
## Функционал ## Функционал
Список OCAB-модулей используемых в боте: Список OCAB-модулей используемых в боте:
* report - Вызов администрации чата одной командой * report - Вызов администрации чата одной командой
* welcome - Автоматическая вариативная проверка пользователей на признаки бота или другой автоматической рекламной системы * welcome - Автоматическая вариативная проверка пользователей на признаки бота или другой автоматической рекламной системы
* help - Получение информации об Karkas Lite * help - Получение справки о боте
## Запуск ## Запуск
@ -17,29 +17,31 @@
1. Соберите Docker-образ: 1. Соберите Docker-образ:
```bash ```bash
docker build -t gnomik . docker build -t altlinux -f Dockerfile ../..
``` ```
2. Запустите контейнер: 2. Запустите контейнер:
```bash ```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 ```bash
poetry shell poetry shell
``` ```
2. Запустите бота: 2. Запустите бота:
```bash ```bash
python -m gnomik python -m altlinux
``` ```
## Конфигурация ## Конфигурация
Конфигурация бота находится в файле `config.yaml`. Конфигурация хранится в файле `config.yaml`.
Пример конфигурации бота находится в файле `config-example.yaml`.
## Модули ## Модули

View File

@ -2,6 +2,45 @@ core:
mode: LONG_POLLING mode: LONG_POLLING
token: xxx token: xxx
filters: #
approved_chat_id: # filters:
- -111111 # 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"
#

View File

@ -1,5 +1,3 @@
version: '3'
services: services:
app: app:
build: build:

View File

@ -1,8 +1,6 @@
import asyncio import asyncio
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .config_manager import ConfigManager
try: try:
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
import flask import flask
@ -17,8 +15,10 @@ from karkas_core.modules_system.public_api import get_module
if TYPE_CHECKING: if TYPE_CHECKING:
from karkas_blocks.standard.roles import Roles as IRoles 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) value = config.get(key)
meta = config.get_meta(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") return dbc.Row(row, className="mb-3 mx-1")
def build_settings_tree(config: ConfigManager): def build_settings_tree(config: "ConfigManager"):
tree = {} tree = {}
for key, value in config._metadata.items(): for key, value in config._metadata.items():
@ -134,7 +134,7 @@ def create_settings_components(tree, level=0):
return components 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: "type[IRoles]" = get_module("standard.roles", "Roles")
roles = Roles() roles = Roles()

View File

@ -1,13 +1,14 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from aiogram import Bot
from aiogram.filters import BaseFilter from aiogram.filters import BaseFilter
from aiogram.types import Message
from typing_extensions import deprecated from typing_extensions import deprecated
from karkas_core.modules_system.public_api import get_module from karkas_core.modules_system.public_api import get_module
if TYPE_CHECKING: if TYPE_CHECKING:
from aiogram import Bot
from aiogram.types import Message
from karkas_blocks.standard.config import IConfig from karkas_blocks.standard.config import IConfig
from karkas_blocks.standard.roles import Roles as IRoles 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") @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 chat_id = message.chat.id
if chat_id in get_approved_chat_id(): if chat_id in get_approved_chat_id():
# log(f"Chat in approve list: {chat_id}") # log(f"Chat in approve list: {chat_id}")
@ -50,7 +51,7 @@ class ChatIDFilter(BaseFilter):
self.approved_chats = approved_chats self.approved_chats = approved_chats
super().__init__() 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 chat_id = message.chat.id
approved_chats = self.approved_chats or get_approved_chat_id() approved_chats = self.approved_chats or get_approved_chat_id()
@ -70,7 +71,7 @@ class ChatNotInApproveFilter(ChatIDFilter):
class ChatModerOrAdminFilter(BaseFilter): 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: if not ROLES_MODULE_LOADED:
raise Exception("Roles module not loaded") raise Exception("Roles module not loaded")

View File

@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
from aiogram import Router from aiogram import Router
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.types import Message
from karkas_core.modules_system.public_api import ( from karkas_core.modules_system.public_api import (
get_metainfo, get_metainfo,
@ -12,6 +11,8 @@ from karkas_core.modules_system.public_api import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from aiogram.types import Message
from karkas_blocks.standard.config import IConfig from karkas_blocks.standard.config import IConfig
config: "IConfig" = get_module("standard.config", "config") config: "IConfig" = get_module("standard.config", "config")
@ -46,7 +47,7 @@ def format_commands(commands_dict):
return "\n".join(formatted_commands) return "\n".join(formatted_commands)
async def help(message: Message): async def help(message: "Message"):
commands = "" commands = ""
version = "" version = ""

View File

@ -4,11 +4,13 @@ from typing import TYPE_CHECKING
from aiogram import Router from aiogram import Router
from aiogram.filters import Command 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 from karkas_core.modules_system.public_api import get_module, log, register_router
if TYPE_CHECKING: if TYPE_CHECKING:
from aiogram.types import ChatMemberAdministrator, Message
from karkas_blocks.standard.config import IConfig from karkas_blocks.standard.config import IConfig
from karkas_blocks.standard.filters import ChatIDFilter as IChatIDFilter from karkas_blocks.standard.filters import ChatIDFilter as IChatIDFilter
@ -29,7 +31,7 @@ except Exception:
pass pass
def can_moderate(admin: ChatMemberOwner | ChatMemberAdministrator) -> bool: def can_moderate(admin: "ChatMemberOwner | ChatMemberAdministrator") -> bool:
if isinstance(admin, ChatMemberOwner): if isinstance(admin, ChatMemberOwner):
return True return True
@ -38,7 +40,7 @@ def can_moderate(admin: ChatMemberOwner | ChatMemberAdministrator) -> bool:
) )
async def report(message: Message): async def report(message: "Message"):
try: try:
if message.reply_to_message is None: if message.reply_to_message is None:
await message.reply(config.get("report::errors::no_reply_message")) await message.reply(config.get("report::errors::no_reply_message"))

View File

@ -7,7 +7,6 @@ from aiogram import Bot, Router, types
from aiogram.enums import ChatMemberStatus, ParseMode from aiogram.enums import ChatMemberStatus, ParseMode
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import JOIN_TRANSITION, LEAVE_TRANSITION, ChatMemberUpdatedFilter 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 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 from .verifications_methods.utils import user_mention
if TYPE_CHECKING: if TYPE_CHECKING:
from aiogram.types import ChatMemberUpdated, PollAnswer
from karkas_blocks.standard.config import IConfig from karkas_blocks.standard.config import IConfig
from karkas_blocks.standard.filters import ChatIDFilter as IChatIDFilter from karkas_blocks.standard.filters import ChatIDFilter as IChatIDFilter
@ -78,7 +79,7 @@ verification_tasks = MultiKeyDict()
last_success = {} last_success = {}
async def new_member_handler(event: ChatMemberUpdated, bot: Bot): async def new_member_handler(event: "ChatMemberUpdated", bot: Bot):
# НЕ СРАБОТАЕТ, ЕСЛИ ЧЕЛОВЕК УЖЕ ОГРАНИЧЕН В ПРАВАХ (RESTRICTED) # НЕ СРАБОТАЕТ, ЕСЛИ ЧЕЛОВЕК УЖЕ ОГРАНИЧЕН В ПРАВАХ (RESTRICTED)
if event.new_chat_member.status == ChatMemberStatus.MEMBER: if event.new_chat_member.status == ChatMemberStatus.MEMBER:
task = task_manager.build_random_task(event, bot) 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) 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 user_id = event.from_user.id
chat_id = event.chat.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 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) key = key_from_poll(answer.poll_id)
if not verification_tasks.exists(key): if not verification_tasks.exists(key):
return return

View File

@ -1,21 +1,24 @@
import asyncio import asyncio
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
from aiogram.filters.callback_data import CallbackData from aiogram.filters.callback_data import CallbackData
from aiogram.types import ChatMemberUpdated
from karkas_core.modules_system.public_api import log from karkas_core.modules_system.public_api import log
from .utils import mute_user, unmute_user from .utils import mute_user, unmute_user
if TYPE_CHECKING:
from aiogram import Bot
from aiogram.types import ChatMemberUpdated
class BaseTask: class BaseTask:
def __init__( def __init__(
self, self,
event: ChatMemberUpdated, event: "ChatMemberUpdated",
bot: Bot, bot: "Bot",
timeout_func=None, timeout_func=None,
attempt_number=1, attempt_number=1,
max_attempts=1, max_attempts=1,

View File

@ -1,13 +1,17 @@
from string import Template from string import Template
from typing import TYPE_CHECKING
from aiogram import Bot
from aiogram.enums import ParseMode, PollType 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 ..utils import get_plural_form, key_from_poll, key_from_user_chat
from .base import BaseTask, VerificationCallback, mute_while_task from .base import BaseTask, VerificationCallback, mute_while_task
from .utils import user_mention from .utils import user_mention
if TYPE_CHECKING:
from aiogram import Bot
from aiogram.types import ChatMemberUpdated
class SimpleBaseTask(BaseTask): class SimpleBaseTask(BaseTask):
pass pass
@ -119,8 +123,8 @@ class SimpleInlineButtonsTask(SimpleVariantsBaseTask):
class SimplePollTask(SimpleVariantsBaseTask): class SimplePollTask(SimpleVariantsBaseTask):
def __init__( def __init__(
self, self,
event: ChatMemberUpdated, event: "ChatMemberUpdated",
bot: Bot, bot: "Bot",
timeout_func=None, timeout_func=None,
attempt_number=1, attempt_number=1,
max_attempts=1, max_attempts=1,

View File

@ -1,9 +1,12 @@
import time import time
from typing import TYPE_CHECKING
from aiogram import Bot
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.types import ChatPermissions, User from aiogram.types import ChatPermissions, User
if TYPE_CHECKING:
from aiogram import Bot
def user_mention(user: User, mode=ParseMode.HTML): def user_mention(user: User, mode=ParseMode.HTML):
if 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}") 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()) end_time = until + int(time.time())
await bot.restrict_chat_member( await bot.restrict_chat_member(
chat_id, 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( await bot.restrict_chat_member(
chat_id, chat_id,
user_id, user_id,

View File

@ -1,10 +1,14 @@
import importlib import importlib
import os import os
import traceback import traceback
from typing import TYPE_CHECKING
from aiogram import Bot, Dispatcher
from aiogram.types import Update from aiogram.types import Update
if TYPE_CHECKING:
from aiogram import Bot, Dispatcher
from fastapi import FastAPI, Request
def get_module_directory(module_name): def get_module_directory(module_name):
spec = importlib.util.find_spec(module_name) spec = importlib.util.find_spec(module_name)
@ -16,15 +20,10 @@ def get_module_directory(module_name):
return os.path.dirname(module_path) return os.path.dirname(module_path)
async def register_bot_webhook(app: "FastAPI", bot: "Bot", dp: "Dispatcher"):
async def handle_webhook(request: "Request"):
try: try:
from fastapi import FastAPI, Request update = Update.model_validate(await request.json(), context={"bot": bot})
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) await dp.feed_update(bot, update)
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
@ -33,6 +32,3 @@ try:
return {"ok": True} return {"ok": True}
app.post("/webhook")(handle_webhook) app.post("/webhook")(handle_webhook)
except ImportError:
pass

View File

@ -84,7 +84,7 @@ class Karkas:
await register_bot_webhook(app, singleton.bot, singleton.dp) await register_bot_webhook(app, singleton.bot, singleton.dp)
await singleton.bot.set_webhook(config.get("core::webhook::public_url")) await singleton.bot.set_webhook(config.get("core::webhook::public_url"))
hyperConfig = HyperConfig() 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 hyperConfig.logger_class = CustomLogger
await serve(app, hyperConfig) await serve(app, hyperConfig)

View File

@ -1,9 +1,11 @@
import types
from dataclasses import dataclass 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 from dataclasses_json import dataclass_json
if TYPE_CHECKING:
import types
@dataclass_json @dataclass_json
@dataclass @dataclass
@ -39,5 +41,5 @@ class AbstractLoader:
def info(self) -> ModuleInfo: def info(self) -> ModuleInfo:
raise NotImplementedError raise NotImplementedError
def load(self) -> types.ModuleType: def load(self) -> "types.ModuleType":
raise NotImplementedError raise NotImplementedError

View File

@ -1,8 +1,7 @@
import inspect import inspect
import types 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.context import FSMContext
from aiogram.fsm.storage.base import StorageKey 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.modules_system.loaders.base import DependencyInfo
from karkas_core.singleton import Singleton from karkas_core.singleton import Singleton
if TYPE_CHECKING:
from aiogram import BaseMiddleware, Router
async def set_chat_menu_button(menu_button): async def set_chat_menu_button(menu_button):
app = Singleton() app = Singleton()
await app.bot.set_chat_menu_button(menu_button=menu_button) await app.bot.set_chat_menu_button(menu_button=menu_button)
def register_router(router: Router): def register_router(router: "Router"):
app = Singleton() app = Singleton()
app.storage["_routers"].append(router) app.storage["_routers"].append(router)
def register_outer_message_middleware(middleware: BaseMiddleware): def register_outer_message_middleware(middleware: "BaseMiddleware"):
app = Singleton() app = Singleton()
app.storage["_outer_message_middlewares"].append(middleware) app.storage["_outer_message_middlewares"].append(middleware)

View File

@ -1,6 +1,5 @@
import types import types
from _ast import AnnAssign from typing import TYPE_CHECKING, Any
from typing import Any
from aiogram import Bot from aiogram import Bot
from RestrictedPython import ( 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.logger import log
from karkas_core.modules_system.safe.zope_guards import extra_safe_builtins from karkas_core.modules_system.safe.zope_guards import extra_safe_builtins
if TYPE_CHECKING:
from _ast import AnnAssign
class RestrictedPythonPolicy(RestrictingNodeTransformer): class RestrictedPythonPolicy(RestrictingNodeTransformer):
def visit_AsyncFunctionDef(self, node): def visit_AsyncFunctionDef(self, node):
@ -50,7 +52,7 @@ class RestrictedPythonPolicy(RestrictingNodeTransformer):
return self.node_contents_visit(node) return self.node_contents_visit(node)
""" """
def visit_AnnAssign(self, node: AnnAssign) -> Any: def visit_AnnAssign(self, node: "AnnAssign") -> Any:
# missing in RestrictingNodeTransformer # missing in RestrictingNodeTransformer
# this doesn't need the logic that is in visit_Assign # this doesn't need the logic that is in visit_Assign
# because it doesn't have a "targets" attribute, # because it doesn't have a "targets" attribute,

View File

@ -1,6 +1,10 @@
from aiogram import Bot, Dispatcher from typing import TYPE_CHECKING
from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.storage.memory import MemoryStorage
if TYPE_CHECKING:
from aiogram import Bot, Dispatcher
from karkas_core.modules_system import ModulesManager from karkas_core.modules_system import ModulesManager
@ -15,9 +19,9 @@ class SingletonMeta(type):
class Singleton(metaclass=SingletonMeta): class Singleton(metaclass=SingletonMeta):
bot: Bot bot: "Bot"
dp: Dispatcher = None dp: "Dispatcher" = None
modules_manager: ModulesManager = None modules_manager: "ModulesManager" = None
storage = { storage = {
"_fsm_storage": MemoryStorage(), "_fsm_storage": MemoryStorage(),
"_routers": [], "_routers": [],