Merged with feat/add-report-module

This commit is contained in:
Maxim Slipenko 2024-08-11 11:10:29 +03:00
commit e3443f835a
23 changed files with 2462 additions and 91 deletions

View File

@ -15,6 +15,10 @@
{
"name": "Gnomik",
"path": "src/gnomik"
},
{
"name": "ALT Linux",
"path": "src/altlinux"
}
],
"extensions": {

23
src/altlinux/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM python:3.12-slim as builder
RUN pip install poetry
RUN mkdir -p /app
COPY . /app
# Фикс
RUN sed -i '/ocab-core = {/{s/, develop = true//}' /app/src/altlinux/pyproject.toml && \
sed -i '/ocab-modules = {/{s/, develop = true//}' /app/src/altlinux/pyproject.toml && \
sed -i '/ocab-core = {/{s/, develop = true//}' /app/src/ocab_modules/pyproject.toml
WORKDIR /app/src/altlinux
RUN poetry lock && poetry install
FROM python:3.12-slim as base
COPY --from=builder /app/src/altlinux /app
WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH"
CMD ["python", "-m", "altlinux"]

View File

@ -0,0 +1,14 @@
**/Dockerfile
**/*.dockerignore
**/docker-compose.yml
**/.git
**/.gitignore
**/.venv
**/.mypy_cache
**/__pycache__/
src/gnomik/config.yaml
src/gnomik/database/*

47
src/altlinux/README.md Normal file
View File

@ -0,0 +1,47 @@
# ALT Linux
## Описание
<!--
TODO: добавить описание
-->
## Функционал
<!--
TODO: описать функционал
-->
## Запуск
### Docker
1. Соберите Docker-образ:
```bash
docker build -t gnomik .
```
2. Запустите контейнер:
```bash
docker run -p 9000:9000 -v ./config.yaml:/app/config.yaml -v ./database:/app/database gnomik
```
Замените `./config.yaml` и `./database` на пути к вашим локальным файлам конфигурации и паки для базы данных.
### Вручную
1. Активируйте виртуальное окружение Gnomика:
```bash
poetry shell
```
2. Запустите бота:
```bash
python -m gnomik
```
## Конфигурация
Конфигурация бота находится в файле `config.yaml`.
## Модули
Список загружаемых модулей указан в файле `__main__.py`.

View File

View File

@ -0,0 +1,21 @@
import asyncio
from ocab_core import OCAB
from ocab_modules import module_loader
async def main():
ocab = OCAB()
await ocab.init_app(
[
module_loader("standard", "config", safe=False),
module_loader("standard", "command_helper"),
# safe=False из-за super().__init__()
module_loader("standard", "filters", safe=False),
module_loader("standard", "report"),
]
)
await ocab.start()
asyncio.run(main())

View File

@ -0,0 +1,7 @@
core:
mode: LONG_POLLING
token: xxx
filters:
approved_chat_id:
- -111111

View File

@ -0,0 +1,9 @@
version: '3'
services:
app:
build:
context: ../..
dockerfile: src/altlinux/Dockerfile
volumes:
- ./config.yaml:/app/config.yaml

2162
src/altlinux/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

2
src/altlinux/poetry.toml Normal file
View File

@ -0,0 +1,2 @@
[virtualenvs]
in-project = true

View File

@ -0,0 +1,17 @@
[tool.poetry]
name = "altlinux"
version = "0.1.0"
description = ""
authors = [
"Максим Слипенко <maxim@slipenko.com>"
]
readme = "README.md"
[tool.poetry.dependencies]
python = "~3.12"
ocab-core = { extras=["webhook"], path = "../ocab_core", develop = true }
ocab-modules = { path = "../ocab_modules", develop = true }
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -7,7 +7,8 @@
"privileged": false,
"dependencies": {
"required": {
"standard.filters": "^1.0.0"
"standard.filters": "^1.0.0",
"standard.roles": "^1.0.0"
}
}
}

View File

@ -1 +1 @@
from .main import module_init, register_command
from .main import module_late_init, register_command

View File

@ -5,10 +5,5 @@
"author": "OCAB Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {
"required": {
"standard.roles": "^1.0.0",
"standard.database": "^1.0.0"
}
}
"dependencies": {}
}

View File

@ -1,28 +1,11 @@
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import BotCommand, TelegramObject
from aiogram.types import BotCommand
from ocab_core.modules_system.public_api import (
get_module,
register_outer_message_middleware,
set_my_commands,
set_my_commands, log
)
if TYPE_CHECKING:
from ocab_modules.standard.database import db_api as IDbApi
from ocab_modules.standard.roles import Roles as IRoles
commands = dict()
db_api: "IDbApi" = get_module(
"standard.database",
"db_api",
)
Roles: "IRoles" = get_module("standard.roles", "Roles")
def register_command(command, description, role="USER"):
if role not in commands:
commands[role] = dict()
@ -31,53 +14,6 @@ def register_command(command, description, role="USER"):
}
class OuterMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
):
# if not isinstance(event, Message):
# return await handler(event, data)
#
# user = db_api.get_user(event.from_user.id)
#
# if user is None:
# return
#
# roles = Roles()
# role_name = await roles.get_role_name(role_id=user.user_role)
#
# if role_name not in commands:
# return await handler(event, data)
# bot_commands = []
# for role_command in commands[role_name]:
# bot_commands.append(
# BotCommand(
# command=role_command,
# description=commands[role_name][role_command]["description"],
# )
# )
# await event.bot.set_my_commands(
# bot_commands,
# BotCommandScopeChatMember(
# chat_id=event.chat.id,
# user_id=event.from_user.id,
# ),
# )
return await handler(event, data)
async def module_init():
register_outer_message_middleware(OuterMiddleware())
async def set_user_commands():
bot_commands = []
if "USER" in commands:
@ -90,6 +26,8 @@ async def set_user_commands():
)
)
log(bot_commands)
await set_my_commands(
bot_commands,
)

View File

@ -84,7 +84,8 @@ class ConfigManager:
key: str,
value_type: str,
options: List[Any] = None,
default_value=None,
multiple: bool = False,
default_value = None,
editable: bool = True,
shared: bool = False,
required: bool = False,
@ -101,6 +102,7 @@ class ConfigManager:
self._metadata[key] = {
"type": value_type,
"multiple": multiple,
"options": options,
"default_value": default_value,
"visible": visible,

View File

@ -1,6 +1,7 @@
from .filters import (
ChatModerOrAdminFilter,
ChatNotInApproveFilter,
ChatIDFilter,
chat_not_in_approve,
module_init,
)

View File

@ -1,4 +1,5 @@
from typing import TYPE_CHECKING
from typing_extensions import deprecated
from aiogram import Bot
from aiogram.filters import BaseFilter
@ -8,35 +9,63 @@ from ocab_core.modules_system.public_api import get_module, log
if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig
from ocab_modules.standard.roles import Roles as IRoles
config: "IConfig" = get_module("standard.config", "config")
Roles = get_module("standard.roles", "Roles")
try:
Roles: "type[IRoles]" = get_module("standard.roles", "Roles")
ROLES_MODULE_LOADED = True
except Exception:
ROLES_MODULE_LOADED = False
pass
def module_init():
config.register("filters::approved_chat_id", "string", shared=True)
config.register("filters::approved_chat_id", "int", multiple=True, shared=True, default_value=[])
config.register("filters::default_chat_tag", "string", shared=True)
def get_approved_chat_id() -> list:
# Возваращем сплитованный список id чатов в формате int
return [
int(chat_id) for chat_id in config.get("filters::approved_chat_id").split(" | ")
]
return config.get("filters::approved_chat_id")
@deprecated("Use ChatIDFilter or own implementation")
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}")
# log(f"Chat in approve list: {chat_id}")
return False
else:
log(f"Chat not in approve list: {chat_id}")
# log(f"Chat not in approve list: {chat_id}")
return True
class ChatIDFilter(BaseFilter):
def __init__(self, blacklist = False, approved_chats = None) -> None:
self.blacklist = blacklist
self.approved_chats = approved_chats
super().__init__()
async def __call__(self, message: Message, bot: Bot) -> bool:
chat_id = message.chat.id
approved_chats = self.approved_chats or get_approved_chat_id()
print(approved_chats)
res = chat_id in approved_chats
return res ^ (self.blacklist)
class ChatNotInApproveFilter(ChatIDFilter):
def __init__(self) -> None:
super().__init__(allow = False)
class ChatModerOrAdminFilter(BaseFilter):
async def __call__(self, message: Message, bot: Bot) -> bool:
if not ROLES_MODULE_LOADED:
raise Exception("Roles module not loaded")
user_id = message.from_user.id
roles = Roles()
admins = await bot.get_chat_administrators(message.chat.id)
@ -44,9 +73,4 @@ class ChatModerOrAdminFilter(BaseFilter):
await roles.check_admin_permission(user_id)
or await roles.check_moderator_permission(user_id)
or any(user_id == admin.user.id for admin in admins)
)
class ChatNotInApproveFilter(BaseFilter):
async def __call__(self, message: Message, bot: Bot) -> bool:
return chat_not_in_approve(message)
)

View File

@ -4,11 +4,13 @@
"description": "Модуль с фильтрами",
"author": "OCAB Team",
"version": "1.0.0",
"privileged": false,
"privileged": true,
"dependencies": {
"required": {
"standard.roles": "^1.0.0",
"standard.config": "^1.0.0"
},
"optional": {
"standard.roles": "^1.0.0"
}
}
}

View File

@ -0,0 +1,18 @@
# Модуль Report
Модуль `report` позволяет пользователям сообщать о спам-сообщениях в чате.
## Команды
- `/report` - пожаловаться на сообщение как на спам.
## Использование
Чтобы сообщить о сообщении как о спаме, отправьте команду `/report`, ответив на сообщение, которое вы хотите отметить. Модуль уведомит администраторов, которые имеют права модерации.
### Пример использования
1. Найдите сообщение, которое вы хотите отметить как спам.
2. Ответьте на это сообщение командой `/report`.
Примечание: Команда `/report` должна быть отправлена в ответ на сообщение, которое вы хотите отметить.

View File

@ -0,0 +1 @@
from .main import module_init

View File

@ -0,0 +1,14 @@
{
"id": "standard.report",
"name": "Report",
"description": "Модуль для быстрой жалобы на спам",
"author": "OCAB Team",
"version": "1.0.0",
"privileged": false,
"dependencies": {
"optional": {
"standard.command_helper": "^1.0.0",
"standard.filters": "^1.0.0"
}
}
}

View File

@ -0,0 +1,69 @@
from typing import TYPE_CHECKING
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message, ChatMemberOwner, ChatMemberAdministrator
from ocab_core.modules_system.public_api import get_module, register_router, log
if TYPE_CHECKING:
from ocab_modules.standard.filters import ChatIDFilter as IChatIDFilter
try:
ChatIDFilter: "type[IChatIDFilter]" = get_module("standard.filters", "ChatIDFilter")
FILTERS_MODULE_LOADED = True
except Exception as e:
FILTERS_MODULE_LOADED = False
pass
try:
register_command = get_module("standard.command_helper", "register_command")
COMMAND_HELPER_MODULE_LOADED = True
except Exception as e:
COMMAND_HELPER_MODULE_LOADED = False
pass
def can_moderate(admin: ChatMemberOwner | ChatMemberAdministrator) -> bool:
if isinstance(admin, ChatMemberOwner):
return True
return (
admin.user.is_bot == False and
(
admin.can_delete_messages and
admin.can_restrict_members
)
)
async def report(message: Message):
try:
if message.reply_to_message is None:
await message.reply("Пожалуйста, используйте команду /report в ответ на сообщение, которое вы хотите отметить как спам.")
return
admins = await message.chat.get_administrators()
admin_usernames = [
admin.user.mention_html()
for admin in admins
if can_moderate(admin)
]
if admin_usernames:
ping_message = "⚠️ Внимание, жалоба на спам! " + ", ".join(admin_usernames)
await message.reply_to_message.reply(ping_message, parse_mode="HTML")
except Exception as e:
log(e)
async def module_init():
router = Router()
if FILTERS_MODULE_LOADED:
router.message.register(report, ChatIDFilter(), Command("report"))
else:
router.message.register(report, Command("report"))
register_router(router)
if COMMAND_HELPER_MODULE_LOADED:
register_command = get_module("standard.command_helper", "register_command")
register_command("report", "Пожаловаться на спам")