From 0283f659f290b23364fb643fdb4102c955e2ca4f Mon Sep 17 00:00:00 2001 From: Mihail Butvin Date: Sat, 21 Sep 2024 17:29:08 +0300 Subject: [PATCH 1/6] Restructure --- BotMicro/logo.png | Bin 60388 -> 0 bytes BotMicro/main.py | 24 ------------- BotMicro/pyproject.toml | 19 ---------- BotMicro/requirements.txt | 3 -- {BotMicro => antispambot}/.gitignore | 0 .../analysis/__init__.py | 0 .../analysis/checking.py | 0 .../analysis/normilize.py | 0 .../bot/callbacks/event_message.py | 0 {BotMicro => antispambot}/bot/factory.py | 0 .../bot/handlers/__init__.py | 0 .../bot/handlers/error.py | 0 .../bot/handlers/groups/__init__.py | 0 .../bot/handlers/groups/group_message.py | 0 .../bot/handlers/groups/new_group.py | 0 .../bot/handlers/groups/new_member.py | 0 .../bot/handlers/private/__init__.py | 0 .../bot/handlers/private/edit_words.py | 0 .../bot/handlers/private/event_message.py | 0 .../bot/handlers/private/groups.py | 0 .../bot/handlers/private/ignored_users.py | 0 .../bot/handlers/private/list_words.py | 0 .../bot/handlers/private/profanity_filter.py | 0 .../bot/handlers/private/start.py | 0 .../bot/handlers/private/strike_mode.py | 0 {BotMicro => antispambot}/bot/messages.py | 0 .../bot/middlewares/active_group.py | 0 .../bot/middlewares/callback_message.py | 0 .../bot/middlewares/logging.py | 0 .../bot/states/private.py | 0 .../bot/utils/chat_queries.py | 0 {BotMicro => antispambot}/bot/utils/events.py | 0 .../bot/utils/group_utils.py | 0 .../bot/utils/message.py | 0 {BotMicro => antispambot}/bot/utils/spread.py | 0 antispambot/main.py | 34 ++++++++++++++++++ {BotMicro => antispambot}/models/__init__.py | 0 {BotMicro => antispambot}/models/chat.py | 0 .../models/dictionaries.py | 0 {BotMicro => antispambot}/models/events.py | 0 {BotMicro => antispambot}/models/group.py | 0 {BotMicro => antispambot}/models/history.py | 0 {BotMicro => antispambot}/models/member.py | 0 {BotMicro => antispambot}/utils/logging.py | 0 {BotMicro => antispambot}/vartrie.py | 0 {BotMicro => antispambot}/web/factory.py | 0 .../web/routers/__init__.py | 0 .../web/routers/develop.py | 0 .../web/routers/webhook.py | 0 {BotMicro => antispambot}/web/stubs.py | 0 pyproject.toml | 18 ++++++++++ 51 files changed, 52 insertions(+), 46 deletions(-) delete mode 100644 BotMicro/logo.png delete mode 100644 BotMicro/main.py delete mode 100644 BotMicro/pyproject.toml delete mode 100644 BotMicro/requirements.txt rename {BotMicro => antispambot}/.gitignore (100%) rename {BotMicro => antispambot}/analysis/__init__.py (100%) rename {BotMicro => antispambot}/analysis/checking.py (100%) rename {BotMicro => antispambot}/analysis/normilize.py (100%) rename {BotMicro => antispambot}/bot/callbacks/event_message.py (100%) rename {BotMicro => antispambot}/bot/factory.py (100%) rename {BotMicro => antispambot}/bot/handlers/__init__.py (100%) rename {BotMicro => antispambot}/bot/handlers/error.py (100%) rename {BotMicro => antispambot}/bot/handlers/groups/__init__.py (100%) rename {BotMicro => antispambot}/bot/handlers/groups/group_message.py (100%) rename {BotMicro => antispambot}/bot/handlers/groups/new_group.py (100%) rename {BotMicro => antispambot}/bot/handlers/groups/new_member.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/__init__.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/edit_words.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/event_message.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/groups.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/ignored_users.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/list_words.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/profanity_filter.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/start.py (100%) rename {BotMicro => antispambot}/bot/handlers/private/strike_mode.py (100%) rename {BotMicro => antispambot}/bot/messages.py (100%) rename {BotMicro => antispambot}/bot/middlewares/active_group.py (100%) rename {BotMicro => antispambot}/bot/middlewares/callback_message.py (100%) rename {BotMicro => antispambot}/bot/middlewares/logging.py (100%) rename {BotMicro => antispambot}/bot/states/private.py (100%) rename {BotMicro => antispambot}/bot/utils/chat_queries.py (100%) rename {BotMicro => antispambot}/bot/utils/events.py (100%) rename {BotMicro => antispambot}/bot/utils/group_utils.py (100%) rename {BotMicro => antispambot}/bot/utils/message.py (100%) rename {BotMicro => antispambot}/bot/utils/spread.py (100%) create mode 100644 antispambot/main.py rename {BotMicro => antispambot}/models/__init__.py (100%) rename {BotMicro => antispambot}/models/chat.py (100%) rename {BotMicro => antispambot}/models/dictionaries.py (100%) rename {BotMicro => antispambot}/models/events.py (100%) rename {BotMicro => antispambot}/models/group.py (100%) rename {BotMicro => antispambot}/models/history.py (100%) rename {BotMicro => antispambot}/models/member.py (100%) rename {BotMicro => antispambot}/utils/logging.py (100%) rename {BotMicro => antispambot}/vartrie.py (100%) rename {BotMicro => antispambot}/web/factory.py (100%) rename {BotMicro => antispambot}/web/routers/__init__.py (100%) rename {BotMicro => antispambot}/web/routers/develop.py (100%) rename {BotMicro => antispambot}/web/routers/webhook.py (100%) rename {BotMicro => antispambot}/web/stubs.py (100%) create mode 100644 pyproject.toml diff --git a/BotMicro/logo.png b/BotMicro/logo.png deleted file mode 100644 index 082a70773fdd9e095ba51913b22d53b06595a39e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60388 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"] -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.9" -aiogram-deta = { git = "https://github.com/mamsdeveloper/deta.git", branch = "main" } -aiogram = "^3.0.0b1" -odetam = "^1.4.0" - -[tool.poetry.group.dev.dependencies] -autopep8 = "^2.0.2" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/BotMicro/requirements.txt b/BotMicro/requirements.txt deleted file mode 100644 index 69da75d..0000000 --- a/BotMicro/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -aiogram>=3.0.0b1 -aiogram-deta @ git+https://github.com/mamsdeveloper/deta.git@main#egg=aiogram-deta -odetam diff --git a/BotMicro/.gitignore b/antispambot/.gitignore similarity index 100% rename from BotMicro/.gitignore rename to antispambot/.gitignore diff --git a/BotMicro/analysis/__init__.py b/antispambot/analysis/__init__.py similarity index 100% rename from BotMicro/analysis/__init__.py rename to antispambot/analysis/__init__.py diff --git a/BotMicro/analysis/checking.py b/antispambot/analysis/checking.py similarity index 100% rename from BotMicro/analysis/checking.py rename to antispambot/analysis/checking.py diff --git a/BotMicro/analysis/normilize.py b/antispambot/analysis/normilize.py similarity index 100% rename from BotMicro/analysis/normilize.py rename to antispambot/analysis/normilize.py diff --git a/BotMicro/bot/callbacks/event_message.py b/antispambot/bot/callbacks/event_message.py similarity index 100% rename from BotMicro/bot/callbacks/event_message.py rename to antispambot/bot/callbacks/event_message.py diff --git a/BotMicro/bot/factory.py b/antispambot/bot/factory.py similarity index 100% rename from BotMicro/bot/factory.py rename to antispambot/bot/factory.py diff --git a/BotMicro/bot/handlers/__init__.py b/antispambot/bot/handlers/__init__.py similarity index 100% rename from BotMicro/bot/handlers/__init__.py rename to antispambot/bot/handlers/__init__.py diff --git a/BotMicro/bot/handlers/error.py b/antispambot/bot/handlers/error.py similarity index 100% rename from BotMicro/bot/handlers/error.py rename to antispambot/bot/handlers/error.py diff --git a/BotMicro/bot/handlers/groups/__init__.py b/antispambot/bot/handlers/groups/__init__.py similarity index 100% rename from BotMicro/bot/handlers/groups/__init__.py rename to antispambot/bot/handlers/groups/__init__.py diff --git a/BotMicro/bot/handlers/groups/group_message.py b/antispambot/bot/handlers/groups/group_message.py similarity index 100% rename from BotMicro/bot/handlers/groups/group_message.py rename to antispambot/bot/handlers/groups/group_message.py diff --git a/BotMicro/bot/handlers/groups/new_group.py b/antispambot/bot/handlers/groups/new_group.py similarity index 100% rename from BotMicro/bot/handlers/groups/new_group.py rename to antispambot/bot/handlers/groups/new_group.py diff --git a/BotMicro/bot/handlers/groups/new_member.py b/antispambot/bot/handlers/groups/new_member.py similarity index 100% rename from BotMicro/bot/handlers/groups/new_member.py rename to antispambot/bot/handlers/groups/new_member.py diff --git a/BotMicro/bot/handlers/private/__init__.py b/antispambot/bot/handlers/private/__init__.py similarity index 100% rename from BotMicro/bot/handlers/private/__init__.py rename to antispambot/bot/handlers/private/__init__.py diff --git a/BotMicro/bot/handlers/private/edit_words.py b/antispambot/bot/handlers/private/edit_words.py similarity index 100% rename from BotMicro/bot/handlers/private/edit_words.py rename to antispambot/bot/handlers/private/edit_words.py diff --git a/BotMicro/bot/handlers/private/event_message.py b/antispambot/bot/handlers/private/event_message.py similarity index 100% rename from BotMicro/bot/handlers/private/event_message.py rename to antispambot/bot/handlers/private/event_message.py diff --git a/BotMicro/bot/handlers/private/groups.py b/antispambot/bot/handlers/private/groups.py similarity index 100% rename from BotMicro/bot/handlers/private/groups.py rename to antispambot/bot/handlers/private/groups.py diff --git a/BotMicro/bot/handlers/private/ignored_users.py b/antispambot/bot/handlers/private/ignored_users.py similarity index 100% rename from BotMicro/bot/handlers/private/ignored_users.py rename to antispambot/bot/handlers/private/ignored_users.py diff --git a/BotMicro/bot/handlers/private/list_words.py b/antispambot/bot/handlers/private/list_words.py similarity index 100% rename from BotMicro/bot/handlers/private/list_words.py rename to antispambot/bot/handlers/private/list_words.py diff --git a/BotMicro/bot/handlers/private/profanity_filter.py b/antispambot/bot/handlers/private/profanity_filter.py similarity index 100% rename from BotMicro/bot/handlers/private/profanity_filter.py rename to antispambot/bot/handlers/private/profanity_filter.py diff --git a/BotMicro/bot/handlers/private/start.py b/antispambot/bot/handlers/private/start.py similarity index 100% rename from BotMicro/bot/handlers/private/start.py rename to antispambot/bot/handlers/private/start.py diff --git a/BotMicro/bot/handlers/private/strike_mode.py b/antispambot/bot/handlers/private/strike_mode.py similarity index 100% rename from BotMicro/bot/handlers/private/strike_mode.py rename to antispambot/bot/handlers/private/strike_mode.py diff --git a/BotMicro/bot/messages.py b/antispambot/bot/messages.py similarity index 100% rename from BotMicro/bot/messages.py rename to antispambot/bot/messages.py diff --git a/BotMicro/bot/middlewares/active_group.py b/antispambot/bot/middlewares/active_group.py similarity index 100% rename from BotMicro/bot/middlewares/active_group.py rename to antispambot/bot/middlewares/active_group.py diff --git a/BotMicro/bot/middlewares/callback_message.py b/antispambot/bot/middlewares/callback_message.py similarity index 100% rename from BotMicro/bot/middlewares/callback_message.py rename to antispambot/bot/middlewares/callback_message.py diff --git a/BotMicro/bot/middlewares/logging.py b/antispambot/bot/middlewares/logging.py similarity index 100% rename from BotMicro/bot/middlewares/logging.py rename to antispambot/bot/middlewares/logging.py diff --git a/BotMicro/bot/states/private.py b/antispambot/bot/states/private.py similarity index 100% rename from BotMicro/bot/states/private.py rename to antispambot/bot/states/private.py diff --git a/BotMicro/bot/utils/chat_queries.py b/antispambot/bot/utils/chat_queries.py similarity index 100% rename from BotMicro/bot/utils/chat_queries.py rename to antispambot/bot/utils/chat_queries.py diff --git a/BotMicro/bot/utils/events.py b/antispambot/bot/utils/events.py similarity index 100% rename from BotMicro/bot/utils/events.py rename to antispambot/bot/utils/events.py diff --git a/BotMicro/bot/utils/group_utils.py b/antispambot/bot/utils/group_utils.py similarity index 100% rename from BotMicro/bot/utils/group_utils.py rename to antispambot/bot/utils/group_utils.py diff --git a/BotMicro/bot/utils/message.py b/antispambot/bot/utils/message.py similarity index 100% rename from BotMicro/bot/utils/message.py rename to antispambot/bot/utils/message.py diff --git a/BotMicro/bot/utils/spread.py b/antispambot/bot/utils/spread.py similarity index 100% rename from BotMicro/bot/utils/spread.py rename to antispambot/bot/utils/spread.py diff --git a/antispambot/main.py b/antispambot/main.py new file mode 100644 index 0000000..81def7b --- /dev/null +++ b/antispambot/main.py @@ -0,0 +1,34 @@ +# from os import getenv + +# from deta import Deta + +# from bot.factory import create_bot, create_dispatcher +# from web.factory import create_app + + +# BOT_TOKEN = getenv('BOT_TOKEN') +# assert BOT_TOKEN + + +# deta = Deta() + +# bot, webhook_secret = create_bot(BOT_TOKEN) +# dispatcher = create_dispatcher(deta) + + +# app = create_app( +# deta, +# bot, +# dispatcher, +# webhook_secret +# ) + + +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def read_root(): + return "HelloWorld" + diff --git a/BotMicro/models/__init__.py b/antispambot/models/__init__.py similarity index 100% rename from BotMicro/models/__init__.py rename to antispambot/models/__init__.py diff --git a/BotMicro/models/chat.py b/antispambot/models/chat.py similarity index 100% rename from BotMicro/models/chat.py rename to antispambot/models/chat.py diff --git a/BotMicro/models/dictionaries.py b/antispambot/models/dictionaries.py similarity index 100% rename from BotMicro/models/dictionaries.py rename to antispambot/models/dictionaries.py diff --git a/BotMicro/models/events.py b/antispambot/models/events.py similarity index 100% rename from BotMicro/models/events.py rename to antispambot/models/events.py diff --git a/BotMicro/models/group.py b/antispambot/models/group.py similarity index 100% rename from BotMicro/models/group.py rename to antispambot/models/group.py diff --git a/BotMicro/models/history.py b/antispambot/models/history.py similarity index 100% rename from BotMicro/models/history.py rename to antispambot/models/history.py diff --git a/BotMicro/models/member.py b/antispambot/models/member.py similarity index 100% rename from BotMicro/models/member.py rename to antispambot/models/member.py diff --git a/BotMicro/utils/logging.py b/antispambot/utils/logging.py similarity index 100% rename from BotMicro/utils/logging.py rename to antispambot/utils/logging.py diff --git a/BotMicro/vartrie.py b/antispambot/vartrie.py similarity index 100% rename from BotMicro/vartrie.py rename to antispambot/vartrie.py diff --git a/BotMicro/web/factory.py b/antispambot/web/factory.py similarity index 100% rename from BotMicro/web/factory.py rename to antispambot/web/factory.py diff --git a/BotMicro/web/routers/__init__.py b/antispambot/web/routers/__init__.py similarity index 100% rename from BotMicro/web/routers/__init__.py rename to antispambot/web/routers/__init__.py diff --git a/BotMicro/web/routers/develop.py b/antispambot/web/routers/develop.py similarity index 100% rename from BotMicro/web/routers/develop.py rename to antispambot/web/routers/develop.py diff --git a/BotMicro/web/routers/webhook.py b/antispambot/web/routers/webhook.py similarity index 100% rename from BotMicro/web/routers/webhook.py rename to antispambot/web/routers/webhook.py diff --git a/BotMicro/web/stubs.py b/antispambot/web/stubs.py similarity index 100% rename from BotMicro/web/stubs.py rename to antispambot/web/stubs.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e7fcd64 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "antispambot" +version = "0.1.0" +authors = ["butvinm "] +description = "Telegram bot for spam protection" +package-mode = false + +[tool.poetry.dependencies] +python = "~3.12" +aiogram = "3.13.1" +fastapi = { version = "0.115.0", extras = ["standard"] } + +[tool.poetry.group.dev.dependencies] +autopep8 = "^2.0.2" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From eb0c3ae7c5bf63e1ae82c929ebb8fa29d510bf24 Mon Sep 17 00:00:00 2001 From: Mihail Butvin Date: Fri, 18 Oct 2024 18:46:41 +0300 Subject: [PATCH 2/6] Use actual code version exported from Deta --- antispambot/analysis/checking.py | 14 ++++++ .../bot/handlers/groups/group_message.py | 13 +++++- antispambot/main.py | 40 +++++++---------- pyproject.toml | 18 -------- requirements.txt | 45 +++++++++++++++++++ 5 files changed, 86 insertions(+), 44 deletions(-) delete mode 100644 pyproject.toml create mode 100644 requirements.txt diff --git a/antispambot/analysis/checking.py b/antispambot/analysis/checking.py index 4b643c4..796207b 100644 --- a/antispambot/analysis/checking.py +++ b/antispambot/analysis/checking.py @@ -1,5 +1,6 @@ import pickle import re +import string from typing import Optional from deta import Drive @@ -86,3 +87,16 @@ def check_substitution(text: str) -> Optional[str]: return match[0][0] return None + + +def check_emoji(text: str) -> bool: + """Check if text is spam consists of custom emojies.""" + if len(text) < 15: + # it is probable normal text + return False + + non_emoji = re.findall(r'[\w\d{re.escape(string.punctuation)}]', text) + if len(non_emoji) < 3: + return True + + return False diff --git a/antispambot/bot/handlers/groups/group_message.py b/antispambot/bot/handlers/groups/group_message.py index 1f1968e..25e4642 100644 --- a/antispambot/bot/handlers/groups/group_message.py +++ b/antispambot/bot/handlers/groups/group_message.py @@ -4,7 +4,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message from odetam.exceptions import ItemNotFound -from analysis.checking import (check_full_words, check_partial_words, +from analysis.checking import (check_emoji, check_full_words, check_partial_words, check_profanity, check_regexps, check_substitution) from analysis.normilize import get_normalized_text, remove_stop_words @@ -56,6 +56,17 @@ async def group_message_handler(message: Message, bot: Bot, group: Group) -> Non ) return {'result': substitution_result} + emoji_result = check_emoji(text) + if emoji_result: + await message_delete_event( + group, + member, + message, + 'сообщение содержит только эмодзи', + bot, + ) + return {'result': emoji_result} + full_check_result = check_full_words(text, dictionary.full_words) if full_check_result: await message_delete_event( diff --git a/antispambot/main.py b/antispambot/main.py index 81def7b..729f44e 100644 --- a/antispambot/main.py +++ b/antispambot/main.py @@ -1,34 +1,24 @@ -# from os import getenv +from os import getenv -# from deta import Deta +from deta import Deta -# from bot.factory import create_bot, create_dispatcher -# from web.factory import create_app +from bot.factory import create_bot, create_dispatcher +from web.factory import create_app -# BOT_TOKEN = getenv('BOT_TOKEN') -# assert BOT_TOKEN +BOT_TOKEN = getenv('BOT_TOKEN') +assert BOT_TOKEN -# deta = Deta() +deta = Deta() -# bot, webhook_secret = create_bot(BOT_TOKEN) -# dispatcher = create_dispatcher(deta) +bot, webhook_secret = create_bot(BOT_TOKEN) +dispatcher = create_dispatcher(deta) -# app = create_app( -# deta, -# bot, -# dispatcher, -# webhook_secret -# ) - - -from fastapi import FastAPI - -app = FastAPI() - -@app.get("/") -def read_root(): - return "HelloWorld" - +app = create_app( + deta, + bot, + dispatcher, + webhook_secret +) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index e7fcd64..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,18 +0,0 @@ -[tool.poetry] -name = "antispambot" -version = "0.1.0" -authors = ["butvinm "] -description = "Telegram bot for spam protection" -package-mode = false - -[tool.poetry.dependencies] -python = "~3.12" -aiogram = "3.13.1" -fastapi = { version = "0.115.0", extras = ["standard"] } - -[tool.poetry.group.dev.dependencies] -autopep8 = "^2.0.2" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c97b8f4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,45 @@ +aiofiles==24.1.0 +aiogram==3.13.1 +aiohappyeyeballs==2.4.0 +aiohttp==3.10.5 +aiosignal==1.3.1 +annotated-types==0.7.0 +anyio==4.6.0 +attrs==24.2.0 +autopep8==2.3.1 +certifi==2024.8.30 +click==8.1.7 +dnspython==2.6.1 +email_validator==2.2.0 +fastapi==0.115.0 +fastapi-cli==0.0.5 +frozenlist==1.4.1 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.2 +idna==3.10 +Jinja2==3.1.4 +magic-filter==1.0.12 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +multidict==6.1.0 +pycodestyle==2.12.1 +pydantic==2.9.2 +pydantic_core==2.23.4 +Pygments==2.18.0 +python-dotenv==1.0.1 +python-multipart==0.0.10 +PyYAML==6.0.2 +rich==13.8.1 +shellingham==1.5.4 +sniffio==1.3.1 +starlette==0.38.5 +typer==0.12.5 +typing_extensions==4.12.2 +uvicorn==0.30.6 +uvloop==0.20.0 +watchfiles==0.24.0 +websockets==13.0.1 +yarl==1.11.1 From 22ab3db79fd9608e31a6ebcefc2d7be193a5ad44 Mon Sep 17 00:00:00 2001 From: Mihail Butvin Date: Sat, 19 Oct 2024 10:53:25 +0300 Subject: [PATCH 3/6] Refactor to run outside Deta ecosystem --- .gitignore | 227 +--------------- LICENSE | 2 +- Spacefile | 27 -- antispambot/__init__.py | 0 antispambot/analysis/__init__.py | 3 - antispambot/analysis/checking.py | 21 +- antispambot/analysis/normilize.py | 1 - antispambot/bot/factory.py | 54 +--- antispambot/bot/handlers/__init__.py | 6 +- antispambot/bot/handlers/error.py | 16 +- antispambot/bot/handlers/groups/__init__.py | 34 ++- .../bot/handlers/groups/group_message.py | 95 +++---- antispambot/bot/handlers/groups/new_group.py | 22 +- antispambot/bot/handlers/groups/new_member.py | 58 ++-- antispambot/bot/handlers/private/__init__.py | 41 ++- .../bot/handlers/private/edit_words.py | 27 +- .../bot/handlers/private/event_message.py | 27 +- antispambot/bot/handlers/private/groups.py | 10 +- .../bot/handlers/private/ignored_users.py | 24 +- .../bot/handlers/private/list_words.py | 17 +- .../bot/handlers/private/profanity_filter.py | 14 +- antispambot/bot/handlers/private/start.py | 18 +- .../bot/handlers/private/strike_mode.py | 32 +-- antispambot/bot/messages.py | 3 +- antispambot/bot/middlewares/active_group.py | 36 ++- antispambot/bot/middlewares/logging.py | 19 +- antispambot/bot/utils/chat_queries.py | 53 ++-- antispambot/bot/utils/events.py | 94 +++++-- antispambot/bot/utils/group_utils.py | 5 +- antispambot/bot/utils/spread.py | 8 +- antispambot/main.py | 24 -- antispambot/models/__init__.py | 9 - antispambot/models/chat.py | 6 +- .../models/{dictionaries.py => dictionary.py} | 9 +- antispambot/models/{events.py => event.py} | 12 +- antispambot/models/group.py | 4 +- antispambot/models/history.py | 7 - antispambot/models/member.py | 5 +- antispambot/utils/logging.py | 16 -- antispambot/vartrie.py | 252 ------------------ antispambot/web/factory.py | 18 -- antispambot/web/routers/__init__.py | 11 - antispambot/web/routers/develop.py | 15 -- antispambot/web/routers/webhook.py | 27 -- antispambot/web/stubs.py | 10 - main.py | 24 ++ setup.cfg | 7 + 47 files changed, 415 insertions(+), 1035 deletions(-) delete mode 100644 Spacefile create mode 100644 antispambot/__init__.py delete mode 100644 antispambot/main.py rename antispambot/models/{dictionaries.py => dictionary.py} (57%) rename antispambot/models/{events.py => event.py} (56%) delete mode 100644 antispambot/models/history.py delete mode 100644 antispambot/utils/logging.py delete mode 100644 antispambot/vartrie.py delete mode 100644 antispambot/web/factory.py delete mode 100644 antispambot/web/routers/__init__.py delete mode 100644 antispambot/web/routers/develop.py delete mode 100644 antispambot/web/routers/webhook.py delete mode 100644 antispambot/web/stubs.py create mode 100644 main.py create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index cc3fcee..202094f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,226 +1,3 @@ -# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig -# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python -# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,python - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +.venv/ +storage/ .env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - -### VisualStudioCode ### -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python - -# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) - -.space diff --git a/LICENSE b/LICENSE index b195233..c18368c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 mamsdeveloper +Copyright (c) 2024 Mikhail Butvin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Spacefile b/Spacefile deleted file mode 100644 index 0eae9dc..0000000 --- a/Spacefile +++ /dev/null @@ -1,27 +0,0 @@ -# Spacefile Docs: https://go.deta.dev/docs/spacefile/v0 -v: 0 -micros: - - name: BotMicro - src: BotMicro - engine: python3.9 - primary: true - public_routes: - - "/webhook" - - "/webhook/*" - - presets: - env: - - name: BOT_TOKEN - description: Secret token of telegram bot - - name: ENABLE_ERRORS_LOGS - description: Enable logging of errors. Logs are stored in the "logs" Deta Base. - default: "True" - - name: ENABLE_EVENTS_LOGS - description: Enable logging of each telegram event. Logs are stored in the "logs" Deta Base. - default: "True" - - name: LOGS_EXPIRE_IN - description: Time in seconds after which logs will be deleted. - default: "604800" - - name: MESSAGES_THRESHOLD - description: Number of messages to be sent after member will not be banned. - default: "5" diff --git a/antispambot/__init__.py b/antispambot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/antispambot/analysis/__init__.py b/antispambot/analysis/__init__.py index 9a4d344..e69de29 100644 --- a/antispambot/analysis/__init__.py +++ b/antispambot/analysis/__init__.py @@ -1,3 +0,0 @@ -from .checking import check_text - -__all__ = ['check_text'] \ No newline at end of file diff --git a/antispambot/analysis/checking.py b/antispambot/analysis/checking.py index 796207b..9ee56df 100644 --- a/antispambot/analysis/checking.py +++ b/antispambot/analysis/checking.py @@ -1,12 +1,7 @@ -import pickle import re -import string from typing import Optional -from deta import Drive - -from analysis.normilize import get_normalized_text, get_obfuscated_words -from vartrie import VarTrie +from antispambot.analysis.normilize import get_normalized_text def check_regex_inject(text: str) -> bool: @@ -49,20 +44,6 @@ def check_regexps(text: str, patterns: list[str]) -> Optional[tuple[str, str]]: return None -def check_profanity(text: str) -> Optional[str]: - drive = Drive('profanity') - profanity_trie_pkl = drive.get('trie.pkl') - profanity_trie: VarTrie = pickle.loads(profanity_trie_pkl.read()) - - text = get_normalized_text(text) - words = get_obfuscated_words(text) - for word in words: - if profanity_trie.search(word): - return word - - return None - - def check_text(text: str, full_words: list[str], partial_words: list[str]) -> Optional[str]: text = get_normalized_text(text) diff --git a/antispambot/analysis/normilize.py b/antispambot/analysis/normilize.py index b8d38ab..b9d24cc 100644 --- a/antispambot/analysis/normilize.py +++ b/antispambot/analysis/normilize.py @@ -1,6 +1,5 @@ import re - analyzer = None diff --git a/antispambot/bot/factory.py b/antispambot/bot/factory.py index 98e23c9..a10671c 100644 --- a/antispambot/bot/factory.py +++ b/antispambot/bot/factory.py @@ -1,52 +1,26 @@ -import asyncio -from os import getenv - from aiogram import Bot, Dispatcher -from aiogram.enums.update_type import UpdateType +from aiogram.client.default import DefaultBotProperties +from aiogram.enums.parse_mode import ParseMode +from aiogram.fsm.storage.memory import MemoryStorage from aiogram.utils.callback_answer import CallbackAnswerMiddleware -from aiogram_deta.storage import DefaultKeyBuilder, DetaStorage -from bot.handlers import root_router as root_router -from bot.middlewares.callback_message import CallbackMessageMiddleware -from bot.middlewares.logging import LoggingMiddleware -from deta import Deta - - -def get_webhook_secret() -> str: - return getenv('DETA_SPACE_APP_MICRO_NAME', '') + getenv('DETA_PROJECT_KEY', '')[:4] - - -def create_bot(token: str) -> tuple[Bot, str]: - bot = Bot(token, parse_mode='HTML') - webhook_url = getenv('DETA_SPACE_APP_HOSTNAME', '') + '/webhook' - webhook_secret = get_webhook_secret() +from antispambot.bot.handlers import root_router as root_router +from antispambot.bot.middlewares.callback_message import ( + CallbackMessageMiddleware, +) +from antispambot.bot.middlewares.logging import LoggingMiddleware - loop = asyncio.get_event_loop() - loop.run_until_complete(bot.set_webhook( - url=webhook_url, - secret_token=webhook_secret, - allowed_updates=[item.value for item in UpdateType] - )) - return bot, webhook_secret +def create_bot(token: str) -> Bot: + bot = Bot(token, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) + return bot -def create_dispatcher(deta: Deta) -> Dispatcher: - base = deta.AsyncBase('fsm') # type: ignore - storage = DetaStorage(base, DefaultKeyBuilder( - with_destiny=True)) # type: ignore - +def create_dispatcher() -> Dispatcher: + storage = MemoryStorage() dispatcher = Dispatcher(storage=storage) - dispatcher.include_router(root_router) dispatcher.callback_query.middleware(CallbackMessageMiddleware()) dispatcher.callback_query.middleware(CallbackAnswerMiddleware()) - - # if getenv('ENABLE_EVENTS_LOGS') == 'True': - # if getenv('LOGS_EXPIRE_IN') is not None: - # expire_in = int(getenv('LOGS_EXPIRE_IN')) - # dispatcher.update.middleware(LoggingMiddleware(expire_in)) - # else: - # dispatcher.update.middleware(LoggingMiddleware()) - + dispatcher.update.middleware(LoggingMiddleware()) return dispatcher diff --git a/antispambot/bot/handlers/__init__.py b/antispambot/bot/handlers/__init__.py index 3afeeda..3b53a29 100644 --- a/antispambot/bot/handlers/__init__.py +++ b/antispambot/bot/handlers/__init__.py @@ -1,8 +1,8 @@ from aiogram import Router -from .error import router as error_router -from .groups import router as groups_router -from .private import router as private_router +from antispambot.bot.handlers.error import router as error_router +from antispambot.bot.handlers.groups import router as groups_router +from antispambot.bot.handlers.private import router as private_router root_router = Router() root_router.include_router(private_router) diff --git a/antispambot/bot/handlers/error.py b/antispambot/bot/handlers/error.py index 1708db8..acb8536 100644 --- a/antispambot/bot/handlers/error.py +++ b/antispambot/bot/handlers/error.py @@ -1,19 +1,15 @@ -from datetime import datetime +import logging +import traceback from aiogram import Router from aiogram.types.error_event import ErrorEvent -from deta import Base + +logger = logging.getLogger(__name__) router = Router() @router.errors() async def errors_handler(event: ErrorEvent): - time = datetime.now() - - logging_base = Base('logs') - logging_base.put( - key=str(2 * 10**9 - time.timestamp()), - data={'time': time.isoformat(), 'update': event.update.json(), 'exception': repr(event.exception)}, - expire_in=60 * 60 * 2 # expire in two hours - ) + logger.error(f'Error processing update [{event.update}]: {event.exception}') + logger.error(''.join(traceback.format_exception(event.exception))) diff --git a/antispambot/bot/handlers/groups/__init__.py b/antispambot/bot/handlers/groups/__init__.py index b73c2bc..e6c40e8 100644 --- a/antispambot/bot/handlers/groups/__init__.py +++ b/antispambot/bot/handlers/groups/__init__.py @@ -1,13 +1,18 @@ from typing import Any, Awaitable, Callable, Dict from aiogram import Router -from aiogram.types import Message - -from bot.middlewares.active_group import ActiveGroupMiddleware - -from .group_message import router as group_message_router -from .new_group import router as new_group_router -from .new_member import router as new_member_router +from aiogram.types import Message, TelegramObject + +from antispambot.bot.handlers.groups.group_message import ( + router as group_message_router, +) +from antispambot.bot.handlers.groups.new_group import ( + router as new_group_router, +) +from antispambot.bot.handlers.groups.new_member import ( + router as new_member_router, +) +from antispambot.bot.middlewares.active_group import ActiveGroupMiddleware router = Router() router.include_router(group_message_router) @@ -19,14 +24,15 @@ router.chat_member.middleware(ActiveGroupMiddleware()) -@router.message.middleware() +@router.message.middleware async def group_chat_middleware( - handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], - event: Message, + handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + event: TelegramObject, data: Dict[str, Any] ) -> Any: - if event.from_user and event.from_user.full_name == 'Telegram': - return + if isinstance(event, Message): + if event.from_user and event.from_user.full_name == 'Telegram': + return - if event.chat.type in ('group', 'supergroup'): - return await handler(event, data) + if event.chat.type in ('group', 'supergroup'): + return await handler(event, data) diff --git a/antispambot/bot/handlers/groups/group_message.py b/antispambot/bot/handlers/groups/group_message.py index 25e4642..7e02ac9 100644 --- a/antispambot/bot/handlers/groups/group_message.py +++ b/antispambot/bot/handlers/groups/group_message.py @@ -1,21 +1,25 @@ from os import getenv +from venv import logger from aiogram import Bot, F, Router -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message -from odetam.exceptions import ItemNotFound - -from analysis.checking import (check_emoji, check_full_words, check_partial_words, - check_profanity, check_regexps, check_substitution) -from analysis.normilize import get_normalized_text, remove_stop_words - -from bot.messages import PROFANITY_EVENT -from bot.utils.events import message_delete_event, profanity_filter_event -from bot.utils.spread import SendMessage, forward_messages, spread_messages -from bot.utils.message import get_full_text -from models import Dictionary, Group -from models.chat import Chat -from models.member import Member -from utils.logging import log +from aiogram.types import Message + +from antispambot.analysis.checking import ( + check_emoji, + check_full_words, + check_partial_words, + check_regexps, + check_substitution, +) +from antispambot.analysis.normilize import ( + get_normalized_text, + remove_stop_words, +) +from antispambot.bot.utils.events import message_delete_event +from antispambot.bot.utils.message import get_full_text +from antispambot.models.group import Group +from antispambot.models.member import Member +from antispambot.storage.storages import dictionary_storage, member_storage router = Router() @@ -23,8 +27,8 @@ @router.edited_message(F.from_user.is_bot == False) @router.message(F.from_user.is_bot == False) async def group_message_handler(message: Message, bot: Bot, group: Group) -> None: - member = await Member.get_or_none(str(message.from_user.id)) - if not member: + member = member_storage.get(str(message.from_user.id)) + if member is None: member = Member( key=str(message.from_user.id), strikes_count={group.key: 0} @@ -32,14 +36,17 @@ async def group_message_handler(message: Message, bot: Bot, group: Group) -> Non member.messages_count.setdefault(group.key, 0) member.messages_count[group.key] += 1 - await member.save() # type: ignore + member_storage.save(member) messages_threshold = int(getenv('MESSAGES_THRESHOLD', 5)) if member.messages_count.get(group.key, 0) >= messages_threshold: - return {'messages_count': member.messages_count.get(group.key, 0)} + logger.debug(f'User {member.key} has reached the messages threshold in group {group.key}') + return - - dictionary: Dictionary = await Dictionary.get(group.key) + dictionary = dictionary_storage.get(group.key) + if dictionary is None: + logger.warning(f'Dictionary for group {group.key} is not found') + return text = get_full_text(message) text = get_normalized_text(text) @@ -47,31 +54,18 @@ async def group_message_handler(message: Message, bot: Bot, group: Group) -> Non substitution_result = check_substitution(text) if substitution_result: - await message_delete_event( - group, - member, - message, - f'замена букв в слове "{substitution_result}"', - bot, - ) - return {'result': substitution_result} + await message_delete_event(group, member, message, f'замена букв в слове "{substitution_result}"', bot) + return emoji_result = check_emoji(text) if emoji_result: - await message_delete_event( - group, - member, - message, - 'сообщение содержит только эмодзи', - bot, - ) - return {'result': emoji_result} + await message_delete_event(group, member, message, 'сообщение содержит только эмодзи', bot) + return full_check_result = check_full_words(text, dictionary.full_words) if full_check_result: - await message_delete_event( - group, member, message, f'слово "{full_check_result}"', bot) - return {'result': full_check_result} + await message_delete_event(group, member, message, f'слово "{full_check_result}"', bot) + return partial_search_result = check_partial_words(text, dictionary.partial_words) if partial_search_result: @@ -81,24 +75,13 @@ async def group_message_handler(message: Message, bot: Bot, group: Group) -> Non else: reason = f'слово "{part}"' - await message_delete_event( - group, member, message, reason, bot) - - return {'result': partial_search_result} + await message_delete_event(group, member, message, reason, bot) + return regex_search_result = check_regexps(text, dictionary.regex_patterns) if regex_search_result: word, pattern = regex_search_result - await message_delete_event( - group, member, message, f'шаблон "{pattern}" в слове "{word}"', bot) - - return {'result': regex_search_result} - - profanity_check_result = check_profanity(text) - if profanity_check_result: - await profanity_filter_event( - group, member, message, profanity_check_result, bot) - - return {'result': profanity_check_result} + await message_delete_event(group, member, message, f'шаблон "{pattern}" в слове "{word}"', bot) + return - return {'result': 'nothing found'} + return diff --git a/antispambot/bot/handlers/groups/new_group.py b/antispambot/bot/handlers/groups/new_group.py index 0f339a2..9976cd3 100644 --- a/antispambot/bot/handlers/groups/new_group.py +++ b/antispambot/bot/handlers/groups/new_group.py @@ -1,10 +1,10 @@ -from aiogram import Router, F -from aiogram.filters import ChatMemberUpdatedFilter, IS_ADMIN +from aiogram import Router +from aiogram.filters import IS_ADMIN, ChatMemberUpdatedFilter from aiogram.types import ChatMemberUpdated -from bot.handlers.private import ignored_users - -from models import Group, Dictionary +from antispambot.models.dictionary import Dictionary +from antispambot.models.group import Group +from antispambot.storage.storages import dictionary_storage, group_storage router = Router() @@ -14,16 +14,16 @@ async def new_group_handler(event: ChatMemberUpdated): group = Group( key=str(event.chat.id), title=event.chat.title or '', - active=True , + active=True, strike_mode=True, strike_limit=3, ignored_users=[] ) - await group.save() + group_storage.save(group) - # history = History(key=group.key, events=[]) - # await history.save() + dictionary = dictionary_storage.get('default') + if dictionary is None: + dictionary = Dictionary(key='default', full_words=[], partial_words=[]) - dictionary: Dictionary = await Dictionary.get('default') dictionary.key = group.key - await dictionary.save() \ No newline at end of file + dictionary_storage.save(dictionary) diff --git a/antispambot/bot/handlers/groups/new_member.py b/antispambot/bot/handlers/groups/new_member.py index 75a6c7a..b1b636b 100644 --- a/antispambot/bot/handlers/groups/new_member.py +++ b/antispambot/bot/handlers/groups/new_member.py @@ -1,29 +1,43 @@ +import logging import re from aiogram import Bot, Router -from aiogram.filters.chat_member_updated import (IS_MEMBER, IS_NOT_MEMBER, - ChatMemberUpdatedFilter) -from aiogram.types import (ChatMemberUpdated, InlineKeyboardButton, - InlineKeyboardMarkup) +from aiogram.filters.chat_member_updated import ( + IS_MEMBER, + IS_NOT_MEMBER, + ChatMemberUpdatedFilter, +) +from aiogram.types import ( + ChatMemberUpdated, + InlineKeyboardButton, + InlineKeyboardMarkup, +) -from bot import messages -from bot.callbacks.event_message import AllowNicknameCallback -from models.chat import Chat -from models.group import Group -from models.member import Member -from utils.logging import log +from antispambot.bot import messages +from antispambot.bot.callbacks.event_message import AllowNicknameCallback +from antispambot.models.chat import Chat +from antispambot.storage.storages import ( + chat_storage, + group_storage, + member_storage, +) + +logger = logging.getLogger(__name__) router = Router() @router.chat_member(ChatMemberUpdatedFilter(IS_NOT_MEMBER >> IS_MEMBER)) async def new_member_handler(event: ChatMemberUpdated, bot: Bot): - member = await Member.get_or_none(str(event.new_chat_member.user.id)) - if member and member.nickname_pass.get(str(event.chat.id)): - return + member = member_storage.get(str(event.new_chat_member.user.id)) + if member is not None: + if member.nickname_pass.get(str(event.chat.id)) is not None: + logger.info(f'User {event.new_chat_member.user.id} has nickname pass') + return - group = await Group.get_or_none(str(event.chat.id)) - if not group: + group = group_storage.get(str(event.chat.id)) + if group is None: + logger.warning(f'Group {event.chat.id} not found in storage') return fullname = event.new_chat_member.user.full_name @@ -34,14 +48,12 @@ async def new_member_handler(event: ChatMemberUpdated, bot: Bot): or len(re.sub(r'[^a-zа-я]', '', fullname)) <= 2 or re.search(r'[\u0600-\u06FF\u0530-\u058F\u4E00-\u9FFF]+', fullname) ): - result = await bot.ban_chat_member(event.chat.id, event.new_chat_member.user.id) - # log(data={ - # 'chat_id': event.chat.id, - # 'user_id': event.new_chat_member.user.id, - # 'ban_chat_member_result': result - # }) - - admins: list[Chat] = await Chat.query(Chat.groups.contains(group.key)) + await bot.ban_chat_member(event.chat.id, event.new_chat_member.user.id) + admins = [ + chat + for chat in chat_storage.get_all() + if group.key in chat.groups + ] for admin in admins: try: await send_ban_member_alert(bot, event, admin) diff --git a/antispambot/bot/handlers/private/__init__.py b/antispambot/bot/handlers/private/__init__.py index 5ecf927..b705643 100644 --- a/antispambot/bot/handlers/private/__init__.py +++ b/antispambot/bot/handlers/private/__init__.py @@ -1,16 +1,28 @@ from typing import Any, Awaitable, Callable, Dict from aiogram import Router -from aiogram.types import Message +from aiogram.types import Message, TelegramObject -from .edit_words import router as edit_words_router -from .event_message import router as event_message_router -from .groups import router as groups_router -from .ignored_users import router as ignored_users_router -from .list_words import router as list_words_router -from .start import router as start_router -from .strike_mode import router as strike_mode_router -from .profanity_filter import router as profanity_filter_router +from antispambot.bot.handlers.private.edit_words import ( + router as edit_words_router, +) +from antispambot.bot.handlers.private.event_message import ( + router as event_message_router, +) +from antispambot.bot.handlers.private.groups import router as groups_router +from antispambot.bot.handlers.private.ignored_users import ( + router as ignored_users_router, +) +from antispambot.bot.handlers.private.list_words import ( + router as list_words_router, +) +from antispambot.bot.handlers.private.profanity_filter import ( + router as profanity_filter_router, +) +from antispambot.bot.handlers.private.start import router as start_router +from antispambot.bot.handlers.private.strike_mode import ( + router as strike_mode_router, +) router = Router() router.include_router(start_router) @@ -23,11 +35,12 @@ router.include_router(profanity_filter_router) -@router.message.middleware() +@router.message.middleware async def private_chat_middleware( - handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], - event: Message, + handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + event: TelegramObject, data: Dict[str, Any] ) -> Any: - if event.chat.type == 'private': - return await handler(event, data) + if isinstance(event, Message): + if event.chat.type == 'private': + return await handler(event, data) diff --git a/antispambot/bot/handlers/private/edit_words.py b/antispambot/bot/handlers/private/edit_words.py index ba08b6e..8e7e0cd 100644 --- a/antispambot/bot/handlers/private/edit_words.py +++ b/antispambot/bot/handlers/private/edit_words.py @@ -1,12 +1,11 @@ -from aiogram import Router, F +from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import Message -from bot import messages -from bot.states.private import EditWords -from bot.utils.chat_queries import get_chat_groups_dictionaries -from models import Dictionary - +from antispambot.bot import messages +from antispambot.bot.states.private import EditWords +from antispambot.bot.utils.chat_queries import get_chat_groups_dictionaries +from antispambot.storage.storages import dictionary_storage router = Router() @@ -15,7 +14,7 @@ async def drop_words_handler(message: Message, state: FSMContext): await state.clear() - chat_dicts = await get_chat_groups_dictionaries(message.chat.id) + chat_dicts = get_chat_groups_dictionaries(message.chat.id) for dictionary in chat_dicts: if message.text == 'Убрать все полные слова': dictionary.full_words = [] @@ -24,7 +23,7 @@ async def drop_words_handler(message: Message, state: FSMContext): elif message.text == 'Убрать все шаблоны': dictionary.regex_patterns = [] - await dictionary.save() + dictionary_storage.save(dictionary) await message.answer(messages.SUCCESSFUL_DROP_WORDS) @@ -33,16 +32,18 @@ async def drop_words_handler(message: Message, state: FSMContext): async def repair_words_handler(message: Message, state: FSMContext): await state.clear() - default_dict: Dictionary = await Dictionary.get('default') + default_dict = dictionary_storage.get('default') + if default_dict is None: + return - chat_dicts = await get_chat_groups_dictionaries(message.chat.id) + chat_dicts = get_chat_groups_dictionaries(message.chat.id) for dictionary in chat_dicts: if message.text == 'Восстановить словарь полных слов': dictionary.full_words = default_dict.full_words elif message.text == 'Восстановить словарь частичных слов': dictionary.partial_words = default_dict.partial_words - await dictionary.save() + dictionary_storage.save(dictionary) await message.answer(messages.SUCCESSFUL_REPAIR_WORDS) @@ -76,7 +77,7 @@ async def words_handler(message: Message, state: FSMContext): action = data.get('command') await state.clear() - chat_dicts = await get_chat_groups_dictionaries(message.chat.id) + chat_dicts = get_chat_groups_dictionaries(message.chat.id) for dictionary in chat_dicts: if action == 'Добавить полные слова': dictionary.full_words.extend(words) @@ -95,6 +96,6 @@ async def words_handler(message: Message, state: FSMContext): elif action == 'Убрать пропуск слов': dictionary.stop_words = [word for word in dictionary.stop_words if word not in words] - await dictionary.save() + dictionary_storage.save(dictionary) await message.answer(messages.SUCCESSFUL_UPDATE_WORDS) \ No newline at end of file diff --git a/antispambot/bot/handlers/private/event_message.py b/antispambot/bot/handlers/private/event_message.py index 649ab97..a464cce 100644 --- a/antispambot/bot/handlers/private/event_message.py +++ b/antispambot/bot/handlers/private/event_message.py @@ -1,9 +1,14 @@ from aiogram import Bot, Router from aiogram.types import CallbackQuery, Message -from odetam.exceptions import ItemNotFound -from bot.callbacks.event_message import AllowNicknameCallback, BanMemberCallback, DeleteMessageCallback, UnbanMemberCallback -from models.member import Member +from antispambot.bot.callbacks.event_message import ( + AllowNicknameCallback, + BanMemberCallback, + DeleteMessageCallback, + UnbanMemberCallback, +) +from antispambot.models.member import Member +from antispambot.storage.storages import member_storage router = Router() @@ -19,14 +24,12 @@ async def unban_member_handler(query: CallbackQuery, message: Message, callback_ await bot.unban_chat_member(callback_data.chat_id, callback_data.user_id, only_if_banned=True) group_key = str(callback_data.chat_id) - try: - member: Member = await Member.get(str(callback_data.user_id)) - except ItemNotFound: + member = member_storage.get(str(callback_data.user_id)) + if member is None: member = Member(key=str(callback_data.user_id), strikes_count={}) member.strikes_count[group_key] = 0 - await member.save() - + member_storage.save(member) await message.edit_reply_markup(reply_markup=None) @@ -34,15 +37,13 @@ async def unban_member_handler(query: CallbackQuery, message: Message, callback_ async def allow_nickname_handler(query: CallbackQuery, message: Message, callback_data: AllowNicknameCallback, bot: Bot) -> None: await bot.unban_chat_member(callback_data.chat_id, callback_data.user_id, only_if_banned=True) - try: - member: Member = await Member.get(str(callback_data.user_id)) - except ItemNotFound: + member = member_storage.get(str(callback_data.user_id)) + if member is None: member = Member(key=str(callback_data.user_id), strikes_count={}) member.strikes_count[str(callback_data.chat_id)] = 0 member.nickname_pass[str(callback_data.chat_id)] = True - await member.save() - + member_storage.save(member) await message.edit_reply_markup(reply_markup=None) diff --git a/antispambot/bot/handlers/private/groups.py b/antispambot/bot/handlers/private/groups.py index 08bb596..81a64fc 100644 --- a/antispambot/bot/handlers/private/groups.py +++ b/antispambot/bot/handlers/private/groups.py @@ -1,10 +1,9 @@ -from aiogram import Router, F +from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import Message -from bot import messages -from bot.utils.chat_queries import get_chat_groups_and_dictionaries - +from antispambot.bot import messages +from antispambot.bot.utils.chat_queries import get_chat_groups_and_dictionaries router = Router() @@ -13,9 +12,8 @@ async def start_handler(message: Message, state: FSMContext) -> None: await state.clear() - groups_and_dicts = await get_chat_groups_and_dictionaries(message.chat.id) + groups_and_dicts = get_chat_groups_and_dictionaries(message.chat.id) if not groups_and_dicts: await message.answer(messages.NO_AVAILABLE_GROUPS) else: await message.answer(messages.build_groups_list(groups_and_dicts)) - \ No newline at end of file diff --git a/antispambot/bot/handlers/private/ignored_users.py b/antispambot/bot/handlers/private/ignored_users.py index 14cac3a..5449772 100644 --- a/antispambot/bot/handlers/private/ignored_users.py +++ b/antispambot/bot/handlers/private/ignored_users.py @@ -1,10 +1,11 @@ -from aiogram import Router, F +from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import Message -from bot.states.private import IgnoredUserState -from bot.utils.chat_queries import get_chat_groups -from bot import messages +from antispambot.bot import messages +from antispambot.bot.states.private import IgnoredUserState +from antispambot.bot.utils.chat_queries import get_chat_groups +from antispambot.storage.storages import group_storage router = Router() @@ -20,29 +21,28 @@ async def update_ignored_users_handler(message: Message, state: FSMContext): async def full_name_handler(message: Message, state: FSMContext): if not message.text: return - + data = await state.get_data() command = data['command'] await state.clear() - groups = await get_chat_groups(message.chat.id) + groups = get_chat_groups(message.chat.id) for group in groups: if command == 'Добавить исключение': group.ignored_users.append(message.text) elif command == 'Убрать исключение': if message.text in group.ignored_users: group.ignored_users.remove(message.text) - - await group.save() - + + group_storage.save(group) + await message.answer(messages.IGNORED_USERS_UPDATED) @router.message(F.text == 'Пользователи-исключения') async def list_ignored_users_handler(message: Message, state: FSMContext): await state.clear() - - groups = await get_chat_groups(message.chat.id) + + groups = get_chat_groups(message.chat.id) for group in groups: await message.answer(messages.build_ignored_users_list(group)) - diff --git a/antispambot/bot/handlers/private/list_words.py b/antispambot/bot/handlers/private/list_words.py index 399af76..a5bec79 100644 --- a/antispambot/bot/handlers/private/list_words.py +++ b/antispambot/bot/handlers/private/list_words.py @@ -1,12 +1,10 @@ -from aiogram import Router, F +from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import Message -from odetam.exceptions import ItemNotFound - -from bot import messages -from bot.utils.chat_queries import get_chat_groups_and_dictionaries -from models import Dictionary +from antispambot.bot import messages +from antispambot.bot.utils.chat_queries import get_chat_groups_and_dictionaries +from antispambot.storage.storages import dictionary_storage router = Router() @@ -15,12 +13,11 @@ async def list_words_handler(message: Message, state: FSMContext): await state.clear() - try: - default_dict: Dictionary = await Dictionary.get('default') - except ItemNotFound: + default_dict = dictionary_storage.get('default') + if default_dict is None: return - groups_and_dicts = await get_chat_groups_and_dictionaries(message.chat.id) + groups_and_dicts = get_chat_groups_and_dictionaries(message.chat.id) for group, dictionary in groups_and_dicts: full_words = sorted(set(dictionary.full_words)) partial_words = sorted(set(dictionary.partial_words)) diff --git a/antispambot/bot/handlers/private/profanity_filter.py b/antispambot/bot/handlers/private/profanity_filter.py index ad1895a..caaeed9 100644 --- a/antispambot/bot/handlers/private/profanity_filter.py +++ b/antispambot/bot/handlers/private/profanity_filter.py @@ -2,8 +2,10 @@ from aiogram.fsm.context import FSMContext from aiogram.types import Message -from bot import messages -from bot.utils.chat_queries import get_chat_groups_dictionaries +from antispambot.bot import messages +from antispambot.bot.utils.chat_queries import get_chat_groups_dictionaries + +from antispambot.storage.storages import dictionary_storage router = Router() @@ -12,10 +14,10 @@ async def activate_profanity_filter(message: Message, state: FSMContext): await state.clear() - chat_dicts = await get_chat_groups_dictionaries(message.chat.id) + chat_dicts = get_chat_groups_dictionaries(message.chat.id) for dictionary in chat_dicts: dictionary.profanity_filter = True - await dictionary.save() + dictionary_storage.save(dictionary) await message.answer(messages.SUCCESSFUL_ACTIVATE_FILTER) @@ -24,9 +26,9 @@ async def activate_profanity_filter(message: Message, state: FSMContext): async def deactivate_profanity_filter(message: Message, state: FSMContext): await state.clear() - chat_dicts = await get_chat_groups_dictionaries(message.chat.id) + chat_dicts = get_chat_groups_dictionaries(message.chat.id) for dictionary in chat_dicts: dictionary.profanity_filter = False - await dictionary.save() + dictionary_storage.save(dictionary) await message.answer(messages.SUCCESSFUL_DEACTIVATE_FILTER) diff --git a/antispambot/bot/handlers/private/start.py b/antispambot/bot/handlers/private/start.py index 4f9e4dc..1cb7feb 100644 --- a/antispambot/bot/handlers/private/start.py +++ b/antispambot/bot/handlers/private/start.py @@ -1,12 +1,11 @@ -from aiogram import Router, F +from aiogram import F, Router from aiogram.filters import CommandStart from aiogram.fsm.context import FSMContext -from aiogram.types import Message, KeyboardButton, ReplyKeyboardMarkup -from odetam.exceptions import ItemNotFound - -from bot import messages -from models import Chat +from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup +from antispambot.bot import messages +from antispambot.models.chat import Chat +from antispambot.storage.storages import chat_storage router = Router() @@ -62,12 +61,11 @@ async def start_handler(message: Message, state: FSMContext) -> None: if not message.from_user: return - try: - chat: Chat = await Chat.get(str(message.chat.id)) - except ItemNotFound: + chat = chat_storage.get(str(message.chat.id)) + if chat is None: chat = Chat( key=str(message.chat.id), username=message.from_user.full_name, groups=[] ) - await chat.save() + chat_storage.save(chat) diff --git a/antispambot/bot/handlers/private/strike_mode.py b/antispambot/bot/handlers/private/strike_mode.py index b579f17..b44a9d9 100644 --- a/antispambot/bot/handlers/private/strike_mode.py +++ b/antispambot/bot/handlers/private/strike_mode.py @@ -1,11 +1,11 @@ -from aiogram import Router, F +from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import Message -from bot import messages -from bot.utils.chat_queries import get_chat_groups -from bot.states.private import StrikeLimitState - +from antispambot.bot import messages +from antispambot.bot.states.private import StrikeLimitState +from antispambot.bot.utils.chat_queries import get_chat_groups +from antispambot.storage.storages import group_storage router = Router() @@ -14,22 +14,22 @@ async def strike_mode_handler(message: Message, state: FSMContext): await state.clear() - chat_groups = await get_chat_groups(message.chat.id) + chat_groups = get_chat_groups(message.chat.id) for group in chat_groups: group.strike_mode = message.text == 'Включить баны' - await group.save() - + group_storage.save(group) + if message.text == 'Включить баны': await message.answer(messages.STRIKES_ENABLED) elif message.text == 'Отключить баны': await message.answer(messages.STRIKES_DISABLED) - + @router.message(F.text == 'Установить лимит бана') async def strike_limit_handler(message: Message, state: FSMContext): await message.answer(messages.ASK_STRIKE_LIMIT) await state.set_state(StrikeLimitState.limit) - + @router.message(StrikeLimitState.limit, F.text) async def strike_limit_number_handler(message: Message, state: FSMContext): @@ -39,13 +39,13 @@ async def strike_limit_number_handler(message: Message, state: FSMContext): if not message.text.isdigit(): await message.answer(messages.STRIKE_LIMIT_NOT_DIGIT) return - + strike_limit = int(message.text) - - chat_groups = await get_chat_groups(message.chat.id) + + chat_groups = get_chat_groups(message.chat.id) for group in chat_groups: group.strike_limit = strike_limit - await group.save() - + group_storage.save(group) + await message.answer(messages.STRIKE_LIMIT_UPDATED) - await state.clear() \ No newline at end of file + await state.clear() diff --git a/antispambot/bot/messages.py b/antispambot/bot/messages.py index b58840a..af054c5 100644 --- a/antispambot/bot/messages.py +++ b/antispambot/bot/messages.py @@ -1,6 +1,7 @@ from typing import Iterable -from models import Dictionary, Group +from antispambot.models.dictionary import Dictionary +from antispambot.models.group import Group GREETING = 'Привет! Я помогу тебе удалять сообщения со стоп-словами.' diff --git a/antispambot/bot/middlewares/active_group.py b/antispambot/bot/middlewares/active_group.py index aca804c..261b516 100644 --- a/antispambot/bot/middlewares/active_group.py +++ b/antispambot/bot/middlewares/active_group.py @@ -1,16 +1,10 @@ -from typing import Any, Callable, Awaitable - -from aiogram.types import Message -from odetam.exceptions import ItemNotFound - -from bot.utils.group_utils import is_user_admin - -from models import Group - from typing import Any, Awaitable, Callable, Dict from aiogram.dispatcher.middlewares.base import BaseMiddleware -from aiogram.types import TelegramObject +from aiogram.types import Message, TelegramObject + +from antispambot.bot.utils.group_utils import is_user_admin +from antispambot.storage.storages import group_storage class ActiveGroupMiddleware(BaseMiddleware): @@ -18,7 +12,7 @@ async def __call__( self, handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any] + data: Dict[str, Any], ) -> Any: if isinstance(event, Message): if not event.from_user: @@ -26,23 +20,25 @@ async def __call__( if event.from_user.full_name == 'Telegram': return - - is_admin = await is_user_admin(event.from_user, event.chat) + + if event.bot is None: + return + + is_admin = await is_user_admin(event.from_user, event.chat, event.bot) if is_admin: return - + chat_id = event.chat.id - try: - group: Group = await Group.get(str(chat_id)) - except ItemNotFound: + group = group_storage.get(str(chat_id)) + if group is None: return - + if not group.active: return - + if event.from_user.full_name in group.ignored_users: return - + data['group'] = group return await handler(event, data) diff --git a/antispambot/bot/middlewares/logging.py b/antispambot/bot/middlewares/logging.py index 2816deb..da06868 100644 --- a/antispambot/bot/middlewares/logging.py +++ b/antispambot/bot/middlewares/logging.py @@ -1,31 +1,18 @@ -from datetime import datetime +import logging from typing import Any, Awaitable, Callable, Dict from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.types import TelegramObject -from deta import Base # type: ignore - -EXPIRE_IN = 604800 # week +logger = logging.getLogger(__name__) class LoggingMiddleware(BaseMiddleware): - def __init__(self, expire_in: int = EXPIRE_IN) -> None: - self.expire_in = expire_in - async def __call__( self, handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], event: TelegramObject, data: Dict[str, Any] ) -> Any: - time = datetime.now() - - logging_base = Base('logs') - logging_base.put( - key=str(2 * 10**9 - time.timestamp()), - data={'time': time.isoformat(), 'update': event.json()}, - expire_in=self.expire_in, - ) - + logger.debug(event) return await handler(event, data) diff --git a/antispambot/bot/utils/chat_queries.py b/antispambot/bot/utils/chat_queries.py index 449de55..7b67c76 100644 --- a/antispambot/bot/utils/chat_queries.py +++ b/antispambot/bot/utils/chat_queries.py @@ -1,20 +1,26 @@ -from odetam.exceptions import ItemNotFound +import logging +from antispambot.models.dictionary import Dictionary +from antispambot.models.group import Group +from antispambot.storage.storages import ( + chat_storage, + dictionary_storage, + group_storage, +) -from models import Chat, Group, Dictionary +logger = logging.getLogger(__name__) -async def get_chat_groups(chat_id: int) -> list[Group]: + +def get_chat_groups(chat_id: int) -> list[Group]: groups: list[Group] = [] - try: - chat: Chat = await Chat.get(str(chat_id)) - except ItemNotFound: + chat = chat_storage.get(str(chat_id)) + if chat is None: return groups for group_key in chat.groups: - try: - group: Group = await Group.get(group_key) - except ItemNotFound: + group = group_storage.get(group_key) + if group is None: continue groups.append(group) @@ -22,18 +28,16 @@ async def get_chat_groups(chat_id: int) -> list[Group]: return groups -async def get_chat_groups_dictionaries(chat_id: int) -> list[Dictionary]: +def get_chat_groups_dictionaries(chat_id: int) -> list[Dictionary]: dictionaries: list[Dictionary] = [] - try: - chat: Chat = await Chat.get(str(chat_id)) - except ItemNotFound: + chat = chat_storage.get(str(chat_id)) + if chat is None: return dictionaries for group_key in chat.groups: - try: - dictionary: Dictionary = await Dictionary.get(group_key) - except ItemNotFound: + dictionary = dictionary_storage.get(group_key) + if dictionary is None: continue dictionaries.append(dictionary) @@ -41,23 +45,20 @@ async def get_chat_groups_dictionaries(chat_id: int) -> list[Dictionary]: return dictionaries -async def get_chat_groups_and_dictionaries(chat_id: int) -> list[tuple[Group, Dictionary]]: +def get_chat_groups_and_dictionaries(chat_id: int) -> list[tuple[Group, Dictionary]]: groups_and_dicts: list[tuple[Group, Dictionary]] = [] - try: - chat: Chat = await Chat.get(str(chat_id)) - except ItemNotFound: + chat = chat_storage.get(str(chat_id)) + if chat is None: return groups_and_dicts for group_key in chat.groups: - try: - group: Group = await Group.get(group_key) - except ItemNotFound: + group = group_storage.get(group_key) + if group is None: continue - try: - dictionary: Dictionary = await Dictionary.get(group_key) - except ItemNotFound: + dictionary = dictionary_storage.get(group_key) + if dictionary is None: continue groups_and_dicts.append((group, dictionary)) diff --git a/antispambot/bot/utils/events.py b/antispambot/bot/utils/events.py index 9c10445..cc3f7f0 100644 --- a/antispambot/bot/utils/events.py +++ b/antispambot/bot/utils/events.py @@ -1,17 +1,37 @@ from datetime import datetime +import logging from aiogram import Bot from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message -from bot.callbacks.event_message import (BanMemberCallback, - DeleteMessageCallback, - UnbanMemberCallback) -from bot.messages import (DELETE_MESSAGE_EVENT, DELETE_MESSAGE_REASON, - PROFANITY_EVENT, STRIKE_MEMBER_EVENT) -from bot.utils.spread import SendMessage, forward_messages, spread_messages -from models import (Chat, DeleteMessageEvent, Group, Member, - StrikeMemberEvent) -from models.events import Event, ProfanityFilterEvent +from antispambot.bot.callbacks.event_message import ( + BanMemberCallback, + DeleteMessageCallback, + UnbanMemberCallback, +) +from antispambot.bot.messages import ( + DELETE_MESSAGE_EVENT, + DELETE_MESSAGE_REASON, + PROFANITY_EVENT, + STRIKE_MEMBER_EVENT, +) +from antispambot.bot.utils.spread import ( + SendMessage, + forward_messages, + spread_messages, +) +from antispambot.models.event import ( + DeleteMessageEvent, + Event, + ProfanityFilterEvent, + StrikeMemberEvent, +) +from antispambot.models.group import Group +from antispambot.models.member import Member +from antispambot.storage.storages import chat_storage, member_storage + + +logger = logging.getLogger(__name__) async def message_delete_event( @@ -21,6 +41,9 @@ async def message_delete_event( reason: str, bot: Bot ) -> Event: + assert message.from_user + logger.info(f'Deleting message {message.message_id} from {message.from_user.username} in group {group.key}: {reason}') + await message.delete() # send info to Recent Actions @@ -31,15 +54,13 @@ async def message_delete_event( # register event del_msg_event = DeleteMessageEvent( + key=str(message.message_id), username=message.from_user.username, full_name=message.from_user.full_name, message_text=message.text or 'error', reason=reason, time=datetime.now() ) - # history: History = await History.get(group.key) - # history.events.append(del_msg_event) - # await history.save() # type: ignore # send info to admins delete_event_message = SendMessage( @@ -71,7 +92,21 @@ async def message_delete_event( ] ]) ) - admins_chats: list[Chat] = await Chat.query(Chat.groups.contains(group.key)) + # admins_chats = [ + # chat + # for chat in chat_storage.get_all() + # if group.key in chat.groups + # ] + admins_chats = [] + logger.info(group.key) + for chat in chat_storage.get_all(): + if chat.groups: + logger.info(f'{chat.key}: {chat.groups}') + + if group.key in chat.groups: + admins_chats.append(chat) + + logger.info(f'Admins chats: {admins_chats}') admins_chats_ids = [int(chat.key) for chat in admins_chats] await spread_messages(admins_chats_ids, [delete_event_message], bot) @@ -80,6 +115,7 @@ async def message_delete_event( if member_striked: await strike_member_event(group, member, message, bot) + logger.info(f'Success: {del_msg_event}') return del_msg_event @@ -89,6 +125,9 @@ async def strike_member_event( message: Message, bot: Bot ) -> Event: + assert message.from_user + logger.info(f'Striking member {message.from_user.username} in group {group.key}') + # send info to Recent Actions await send_to_recent_actions( message, @@ -101,15 +140,13 @@ async def strike_member_event( # register event strike_member_event = StrikeMemberEvent( + key=str(message.message_id), username=message.from_user.username, full_name=message.from_user.full_name, message_text=None, reason=None, time=datetime.now() ) - # history: History = await History.get(group.key) - # history.events.append(strike_member_event) - # await history.save() # type: ignore # send event to admins strike_event_message = SendMessage( @@ -123,17 +160,22 @@ async def strike_member_event( InlineKeyboardButton( text='Разбанить', callback_data=UnbanMemberCallback( - chat_id=group.key, + chat_id=int(group.key), user_id=message.from_user.id ).pack() ) ] ]) ) - admins_chats: list[Chat] = await Chat.query(Chat.groups.contains(group.key)) + admins_chats = [ + chat + for chat in chat_storage.get_all() + if group.key in chat.groups + ] admins_chats_ids = [int(chat.key) for chat in admins_chats] await spread_messages(admins_chats_ids, [strike_event_message], bot) + logger.info(f'Success: {strike_member_event}') return strike_member_event @@ -144,17 +186,18 @@ async def profanity_filter_event( word: str, bot: Bot ) -> Event: + assert message.from_user + logger.info(f'Profanity filter event in group {group.key}: {word}') + # register event profanity_filter_event = ProfanityFilterEvent( + key=str(message.message_id), username=message.from_user.username, full_name=message.from_user.full_name, message_text=message.text or 'error', reason=word, time=datetime.now() ) - # history: History = await History.get(group.key) - # history.events.append(profanity_filter_event) - # await history.save() # type: ignore # send message to admins profanity_event_message = SendMessage( @@ -182,18 +225,23 @@ async def profanity_filter_event( ]) ) - admins_chats: list[Chat] = await Chat.query(Chat.groups.contains(group.key)) + admins_chats = [ + chat + for chat in chat_storage.get_all() + if group.key in chat.groups + ] admins_chats_ids = [int(chat.key) for chat in admins_chats] await spread_messages(admins_chats_ids, [profanity_event_message], bot) await forward_messages(admins_chats_ids, [message], bot) + logger.info(f'Success: {profanity_filter_event}') return profanity_filter_event async def update_member_strike(group: Group, member: Member) -> bool: member.strikes_count.setdefault(group.key, 0) member.strikes_count[group.key] += 1 - await member.save() + member_storage.save(member) if member.strikes_count[group.key] >= 3: return group.strike_mode diff --git a/antispambot/bot/utils/group_utils.py b/antispambot/bot/utils/group_utils.py index 2a347f1..ec86476 100644 --- a/antispambot/bot/utils/group_utils.py +++ b/antispambot/bot/utils/group_utils.py @@ -1,10 +1,9 @@ from aiogram import Bot from aiogram.exceptions import TelegramAPIError -from aiogram.types import User, Chat +from aiogram.types import Chat, User -async def is_user_admin(user: User, chat: Chat) -> bool: - bot = Bot.get_current() +async def is_user_admin(user: User, chat: Chat, bot: Bot) -> bool: if not bot: return False diff --git a/antispambot/bot/utils/spread.py b/antispambot/bot/utils/spread.py index 53b37a0..6c6c35e 100644 --- a/antispambot/bot/utils/spread.py +++ b/antispambot/bot/utils/spread.py @@ -15,7 +15,7 @@ async def spread_messages( chat_ids: list[int], messages: list[SendMessage], bot: Bot -) -> list[Union[Exception, Message]]: +) -> list[Union[BaseException, Message]]: targets = [ bot.send_message( chat_id, @@ -25,7 +25,7 @@ async def spread_messages( for chat_id in chat_ids for message in messages ] - results: list[Union[Exception, Message]] = await gather( + results: list[Union[BaseException, Message]] = await gather( *targets, return_exceptions=True ) @@ -36,7 +36,7 @@ async def forward_messages( chat_ids: list[int], messages: list[Message], bot: Bot -) -> list[Union[Exception, Message]]: +) -> list[Union[BaseException, Message]]: targets = [ bot.forward_message( chat_id, @@ -46,7 +46,7 @@ async def forward_messages( for chat_id in chat_ids for message in messages ] - results: list[Union[Exception, Message]] = await gather( + results: list[Union[BaseException, Message]] = await gather( *targets, return_exceptions=True ) diff --git a/antispambot/main.py b/antispambot/main.py deleted file mode 100644 index 729f44e..0000000 --- a/antispambot/main.py +++ /dev/null @@ -1,24 +0,0 @@ -from os import getenv - -from deta import Deta - -from bot.factory import create_bot, create_dispatcher -from web.factory import create_app - - -BOT_TOKEN = getenv('BOT_TOKEN') -assert BOT_TOKEN - - -deta = Deta() - -bot, webhook_secret = create_bot(BOT_TOKEN) -dispatcher = create_dispatcher(deta) - - -app = create_app( - deta, - bot, - dispatcher, - webhook_secret -) diff --git a/antispambot/models/__init__.py b/antispambot/models/__init__.py index e5f3670..e69de29 100644 --- a/antispambot/models/__init__.py +++ b/antispambot/models/__init__.py @@ -1,9 +0,0 @@ -from .chat import Chat -from .dictionaries import Dictionary -from .events import StrikeMemberEvent, DeleteMessageEvent -from .group import Group -from .history import History -from .member import Member - -__all__ = ['Chat', 'Dictionary', 'StrikeMemberEvent', - 'DeleteMessageEvent', 'Group', 'History', 'Member'] diff --git a/antispambot/models/chat.py b/antispambot/models/chat.py index 4b90139..4e49370 100644 --- a/antispambot/models/chat.py +++ b/antispambot/models/chat.py @@ -1,6 +1,6 @@ -from odetam.async_model import AsyncDetaModel +from antispambot.storage.base import BaseStorageModel -class Chat(AsyncDetaModel): - username: str +class Chat(BaseStorageModel): + username: str groups: list[str] diff --git a/antispambot/models/dictionaries.py b/antispambot/models/dictionary.py similarity index 57% rename from antispambot/models/dictionaries.py rename to antispambot/models/dictionary.py index 7525988..af1a11f 100644 --- a/antispambot/models/dictionaries.py +++ b/antispambot/models/dictionary.py @@ -1,14 +1,11 @@ -from typing import Optional -from odetam.async_model import AsyncDetaModel from pydantic import Field +from antispambot.storage.base import BaseStorageModel -class Dictionary(AsyncDetaModel): + +class Dictionary(BaseStorageModel): full_words: list[str] partial_words: list[str] regex_patterns: list[str] = Field(default_factory=list) stop_words: list[str] = Field(default_factory=list) profanity_filter: bool = False - - class Config: - table_name = 'dictionaries' diff --git a/antispambot/models/events.py b/antispambot/models/event.py similarity index 56% rename from antispambot/models/events.py rename to antispambot/models/event.py index c1e4d8c..53a8238 100644 --- a/antispambot/models/events.py +++ b/antispambot/models/event.py @@ -1,11 +1,11 @@ from datetime import datetime from typing import Optional -from pydantic import BaseModel +from antispambot.storage.base import BaseStorageModel -class Event(BaseModel): - event: str = '' +class Event(BaseStorageModel): + event: str username: Optional[str] full_name: str time: datetime @@ -14,12 +14,12 @@ class Event(BaseModel): class StrikeMemberEvent(Event): - event = 'ban_user' + event: str = 'ban_user' class DeleteMessageEvent(Event): - event = 'delete_message' + event: str = 'delete_message' class ProfanityFilterEvent(Event): - event = 'profanity_filter' + event: str = 'profanity_filter' diff --git a/antispambot/models/group.py b/antispambot/models/group.py index fe90788..e9dde1a 100644 --- a/antispambot/models/group.py +++ b/antispambot/models/group.py @@ -1,7 +1,7 @@ -from odetam.async_model import AsyncDetaModel +from antispambot.storage.base import BaseStorageModel -class Group(AsyncDetaModel): +class Group(BaseStorageModel): title: str active: bool strike_mode: bool diff --git a/antispambot/models/history.py b/antispambot/models/history.py deleted file mode 100644 index 90c298c..0000000 --- a/antispambot/models/history.py +++ /dev/null @@ -1,7 +0,0 @@ -from odetam.async_model import AsyncDetaModel - -from models.events import Event - - -class History(AsyncDetaModel): - events: list[Event] diff --git a/antispambot/models/member.py b/antispambot/models/member.py index 6b2696c..b670e9d 100644 --- a/antispambot/models/member.py +++ b/antispambot/models/member.py @@ -1,8 +1,9 @@ -from odetam.async_model import AsyncDetaModel from pydantic import Field +from antispambot.storage.base import BaseStorageModel -class Member(AsyncDetaModel): + +class Member(BaseStorageModel): strikes_count: dict[str, int] messages_count: dict[str, int] = Field(default_factory=dict) nickname_pass: dict[str, bool] = Field(default_factory=dict) diff --git a/antispambot/utils/logging.py b/antispambot/utils/logging.py deleted file mode 100644 index 9addb89..0000000 --- a/antispambot/utils/logging.py +++ /dev/null @@ -1,16 +0,0 @@ -from datetime import datetime -from typing import Any, Optional - -from deta import Base - - -def log(data: dict[str, Any], expire_in: Optional[int] = 60 * 60 * 2) -> None: - time = datetime.now() - data.update({'time': time.isoformat()}) - - logging_base = Base('logs') - logging_base.put( - key=str(2 * 10**9 - time.timestamp()), - data=data, - expire_in=expire_in - ) diff --git a/antispambot/vartrie.py b/antispambot/vartrie.py deleted file mode 100644 index ba8d8ff..0000000 --- a/antispambot/vartrie.py +++ /dev/null @@ -1,252 +0,0 @@ -"""VarTrie package. - -Provide prefix trie for words with letters that have variable forms. - -Example: - words = {'apple', 'banana', 'apricot'} - chars_table = {'a': {'á', '@-'}, 'e': {'e', 'é', 'É'}} - trie = VarTrie(chars_table, words) - - '@-pplÉ' in trie # True - 'apple' in trie # False (because 'a' is not in chars_table) - -Inspirations: - Search for words in different forms is common task in text processing. - For example, in a chat application, we may want to filter out bad words - in messages. However, some letters in bad words may be replaced with - similar-looking letters, such as 'a' with '@' or 'e' with 'é'. In this - case, we need to search for words in different forms. But if we have - a large number of words, it may be inefficient to search for each word - in all its forms. This is where VarTrie comes in handy. - - In one of my projects I process 47k words with about 20 forms each - with VarTrie in milliseconds. -""" - - -from collections import defaultdict -from email.policy import default -from typing import DefaultDict, Optional - - -class Node: - """Node of VarTrie. - - Consists of a dictionary of children nodes and a boolean value - indicating whether this node is the end of a word. - """ - - def __init__(self, is_end: bool = False): - """Initialize Node. - - Args: - is_end (bool): - Whether this node is the end of a word. Defaults to False. - """ - super().__init__() - self.is_end = is_end - self.children: DefaultDict[frozenset[str], Node] = defaultdict(Node) - - def __repr__(self) -> str: - """Return Node string representation as dict. - - Returns: - str: Node dictionary representation. - """ - return str(self.children) - - -class VarTrie: - """Prefix trie with letters that have variable forms. - - The trie is constructed using a set of words and a characters table, - which maps each letter to a set of its possible forms. - - Characters table rules: - If a letter is not in the characters table, it is assumed to have - only one form, itself. - - If letter in the characters table, but its forms do not include itself, - it will not be included in the trie. - - For chars_table = {'a': {'á', '@-'}, 'e': {'e', 'é', 'É'}} - 'a' will have three forms, 'á', '@-', but not 'a', - 'b' will have only one form, 'b', - 'e' will have three forms, 'e', 'é', 'É'. - - Example: - words = {'apple', 'banana', 'apricot'} - chars_table = {'a': {'á', '@-'}, 'e': {'e', 'é', 'É'}} - trie = VarTrie(chars_table, words) - - '@-pplÉ' in trie # True - 'apple' in trie # False (because 'a' is not in chars_table) - """ - - def __init__( - self, - chars_table: dict[str, set[str]], - words: Optional[set[str]] = None, - ): - """Initialize VarTrie from provided characters table and words. - - Args: - chars_table (dict[str, set[str]]): - Maps each letter to a set of its possible forms. - words (set[str]): - Words to be inserted into the trie. - Defaults empty trie created. - """ - self.root = Node() - self.chars_table = self._froze_chars_table(chars_table) - if words is not None: - self.insert_all(words) - - def insert(self, word: str) -> None: - """Insert word into the trie. - - Args: - word (str): Word to be inserted. - """ - node = self.root - while word: - char = word[0] - word = word[1:] - forms = self._get_char_forms(char) - node = node.children[forms] - - node.is_end = True - - def insert_all(self, words: set[str]) -> None: - """Insert all words into the trie. - - Args: - words (set[str]): Words to be inserted. - """ - for word in words: - self.insert(word) - - def search(self, word: str) -> bool: - """Return whether the word is in the trie. - - Args: - word (str): Word to be searched. - - Returns: - bool: Whether the word is in the trie. - """ - return self._search(self.root, word) - - def search_prefix(self, prefix: str) -> bool: - """Return whether the prefix is a prefix of a word in the trie. - - Args: - prefix (str): Prefix to be searched. - - Returns: - bool: Whether the prefix is a prefix of a word in the trie. - """ - return self._search_prefix(self.root, prefix) - - @classmethod - def _froze_chars_table( - cls, - chars_table: dict[str, set[str]], - ) -> dict[str, frozenset[str]]: - """Return frozenset version of chars_table. - - Args: - chars_table (dict[str, set[str]]): Characters table. - - Returns: - dict[str, frozenset[str]]: Frozenset version of chars_table. - """ - return {char: frozenset(forms) for char, forms in chars_table.items()} - - def _get_char_forms(self, char: str) -> frozenset[str]: - """Return set of forms of a character. - - If the character is not in the characters table, it is assumed to have - only one form, itself. - - Args: - char (str): Character to get forms of. - - Returns: - set[str]: Set of forms of the character. - """ - forms = self.chars_table.get(char) - if forms is None: - return frozenset((char, )) - - return forms - - def _get_descendants( - self, - node: Node, - word: str, - ) -> list[tuple[str, Node]]: - """Return list of descendants of node that match word. - - Find all forms that match word prefix and return their nodes. - - Args: - node (Node): Node to get descendants of. - word (str): Word to match descendants against. - - Returns: - list[tuple[str, Node]]: - List of descendants of node that match word. - """ - nodes = [] - for forms, forms_node in node.children.items(): - sorted_forms = sorted(forms, key=len, reverse=True) - for form in sorted_forms: - if word.startswith(form): - nodes.append((form, forms_node)) - - return nodes - - def _search(self, node: Node, word: str) -> bool: - """Return whether the word is in the trie. - - Args: - node (Node): Node to search word in. - word (str): Word to be searched. - - Returns: - bool: Whether the word is in the trie. - """ - descendants = self._get_descendants(node, word) - if not descendants: - return False - - if any(word == prefix and node.is_end for prefix, node in descendants): - return True - - return any( - self._search(node, word.removeprefix(prefix)) - for prefix, node in descendants - ) - - def _search_prefix(self, node: Node, prefix: str) -> bool: - """Return whether the prefix is a prefix of a word in the trie. - - Args: - node (Node): Node to search prefix in. - prefix (str): Prefix to be searched. - - Returns: - bool: Whether the prefix is a prefix of a word in the trie. - """ - descendants = self._get_descendants(node, prefix) - if not descendants: - return False - - if any(d_prefix == prefix for d_prefix, _ in descendants): - return True - - return any( - self._search_prefix(node, prefix.removeprefix(word)) - for word, node in descendants - ) diff --git a/antispambot/web/factory.py b/antispambot/web/factory.py deleted file mode 100644 index 084926f..0000000 --- a/antispambot/web/factory.py +++ /dev/null @@ -1,18 +0,0 @@ -from aiogram import Bot, Dispatcher -from deta import Deta -from fastapi import FastAPI - -from web.routers import root_router -from web.stubs import BotStub, DispatcherStub, SecretStub - - -def create_app(deta: Deta, bot: Bot, dispatcher: Dispatcher, webhook_secret: str) -> FastAPI: - app = FastAPI(title='Bot') - app.dependency_overrides.update({ - BotStub: lambda: bot, - DispatcherStub: lambda: dispatcher, - SecretStub: lambda: webhook_secret, - }) - - app.include_router(root_router) - return app diff --git a/antispambot/web/routers/__init__.py b/antispambot/web/routers/__init__.py deleted file mode 100644 index abe87f8..0000000 --- a/antispambot/web/routers/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import APIRouter - -from .develop import develop_router -from .webhook import webhook_router - -__all__ = ['root_router'] - - -root_router = APIRouter() -root_router.include_router(webhook_router) -root_router.include_router(develop_router) diff --git a/antispambot/web/routers/develop.py b/antispambot/web/routers/develop.py deleted file mode 100644 index 614676b..0000000 --- a/antispambot/web/routers/develop.py +++ /dev/null @@ -1,15 +0,0 @@ -from aiogram import Bot -from fastapi import APIRouter, Depends - -from web.stubs import SecretStub, BotStub - -develop_router = APIRouter(prefix='/develop', tags=['Develop']) - - -@develop_router.get('') -async def get_meta_info( - expected_secret: str = Depends(SecretStub), - bot: Bot = Depends(BotStub), -): - webhook_info = await bot.get_webhook_info() - return {'secret_token': expected_secret, 'webhook_info': webhook_info} diff --git a/antispambot/web/routers/webhook.py b/antispambot/web/routers/webhook.py deleted file mode 100644 index a00cefa..0000000 --- a/antispambot/web/routers/webhook.py +++ /dev/null @@ -1,27 +0,0 @@ -from aiogram import Bot, Dispatcher -from aiogram.types import Update -from aiogram.types.error_event import ErrorEvent -from fastapi import APIRouter, Depends, Header, HTTPException, status -from pydantic import SecretStr - -from web.stubs import BotStub, DispatcherStub, SecretStub - -webhook_router = APIRouter(prefix='/webhook', tags=['Webhook']) - - -@webhook_router.post('') -async def feed_update( - update: Update, - secret: SecretStr = Header(alias='X-Telegram-Bot-Api-Secret-Token'), - expected_secret: str = Depends(SecretStub), - bot: Bot = Depends(BotStub), - dispatcher: Dispatcher = Depends(DispatcherStub), -): - if secret.get_secret_value() != expected_secret: - raise HTTPException(detail='Invalid secret', status_code=status.HTTP_401_UNAUTHORIZED) - - result = await dispatcher.feed_update(bot, update=update) - if isinstance(result, ErrorEvent): - return {'ok': False, 'exception': result.exception, 'dispatcher': result} - - return {'ok': True, 'dispatcher': result} diff --git a/antispambot/web/stubs.py b/antispambot/web/stubs.py deleted file mode 100644 index d91f9eb..0000000 --- a/antispambot/web/stubs.py +++ /dev/null @@ -1,10 +0,0 @@ -class BotStub: - pass - - -class DispatcherStub: - pass - - -class SecretStub: - pass diff --git a/main.py b/main.py new file mode 100644 index 0000000..679d04d --- /dev/null +++ b/main.py @@ -0,0 +1,24 @@ +import asyncio +import logging +from os import getenv +import sys + +from antispambot.bot.factory import create_bot, create_dispatcher + + +async def main(bot_token: str) -> None: + bot = create_bot(bot_token) + await bot.delete_webhook(drop_pending_updates=True) + dispatcher = create_dispatcher() + await dispatcher.start_polling(bot) + + +if __name__ == '__main__': + if '--debug-log' in sys.argv: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + BOT_TOKEN = getenv('BOT_TOKEN') + assert BOT_TOKEN + asyncio.run(main(BOT_TOKEN)) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f0e6383 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[isort] +include_trailing_comma = true +use_parentheses = true +multi_line_output = 3 + +[pycodestyle] +max_line_length = 6969 From 56b2d2a038809ec06bd0f8fc52c8569839d1ec51 Mon Sep 17 00:00:00 2001 From: Mihail Butvin Date: Sat, 19 Oct 2024 12:07:54 +0300 Subject: [PATCH 4/6] Restore storage code --- .gitignore | 230 +++++++++++++++++- .spaceignore | 1 - antispambot/.gitignore | 228 ----------------- .../bot/handlers/private/profanity_filter.py | 1 - antispambot/bot/utils/chat_queries.py | 2 +- antispambot/bot/utils/events.py | 3 +- antispambot/storage/__init__.py | 0 antispambot/storage/base.py | 47 ++++ antispambot/storage/storages.py | 16 ++ predeploy.sh | 1 + 10 files changed, 294 insertions(+), 235 deletions(-) delete mode 100644 .spaceignore delete mode 100644 antispambot/.gitignore create mode 100644 antispambot/storage/__init__.py create mode 100644 antispambot/storage/base.py create mode 100644 antispambot/storage/storages.py create mode 100755 predeploy.sh diff --git a/.gitignore b/.gitignore index 202094f..7eaf573 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,229 @@ -.venv/ -storage/ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig +# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + +typings/ +poetry.lock +develop/ +/storage/* diff --git a/.spaceignore b/.spaceignore deleted file mode 100644 index 6fdd640..0000000 --- a/.spaceignore +++ /dev/null @@ -1 +0,0 @@ -.mypy_cache \ No newline at end of file diff --git a/antispambot/.gitignore b/antispambot/.gitignore deleted file mode 100644 index 7fd4f16..0000000 --- a/antispambot/.gitignore +++ /dev/null @@ -1,228 +0,0 @@ -# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig -# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python -# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,python - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - -### VisualStudioCode ### -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python - -# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) - -typings/ -poetry.lock -develop/ diff --git a/antispambot/bot/handlers/private/profanity_filter.py b/antispambot/bot/handlers/private/profanity_filter.py index caaeed9..c39543c 100644 --- a/antispambot/bot/handlers/private/profanity_filter.py +++ b/antispambot/bot/handlers/private/profanity_filter.py @@ -4,7 +4,6 @@ from antispambot.bot import messages from antispambot.bot.utils.chat_queries import get_chat_groups_dictionaries - from antispambot.storage.storages import dictionary_storage router = Router() diff --git a/antispambot/bot/utils/chat_queries.py b/antispambot/bot/utils/chat_queries.py index 7b67c76..2c0389c 100644 --- a/antispambot/bot/utils/chat_queries.py +++ b/antispambot/bot/utils/chat_queries.py @@ -1,4 +1,5 @@ import logging + from antispambot.models.dictionary import Dictionary from antispambot.models.group import Group from antispambot.storage.storages import ( @@ -7,7 +8,6 @@ group_storage, ) - logger = logging.getLogger(__name__) diff --git a/antispambot/bot/utils/events.py b/antispambot/bot/utils/events.py index cc3f7f0..181c397 100644 --- a/antispambot/bot/utils/events.py +++ b/antispambot/bot/utils/events.py @@ -1,5 +1,5 @@ -from datetime import datetime import logging +from datetime import datetime from aiogram import Bot from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message @@ -30,7 +30,6 @@ from antispambot.models.member import Member from antispambot.storage.storages import chat_storage, member_storage - logger = logging.getLogger(__name__) diff --git a/antispambot/storage/__init__.py b/antispambot/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/antispambot/storage/base.py b/antispambot/storage/base.py new file mode 100644 index 0000000..bf94721 --- /dev/null +++ b/antispambot/storage/base.py @@ -0,0 +1,47 @@ +from pathlib import Path +from typing import Generic, Type, TypeAlias, TypeVar + +from pydantic import BaseModel, TypeAdapter + + +class BaseStorageModel(BaseModel): + key: str + + +T = TypeVar('T', bound=BaseStorageModel) +Models: TypeAlias = dict[str, T] + + +class JsonStorage(Generic[T]): + def __init__(self, model_type: Type[T], storage_path: Path) -> None: + self.storage_path = storage_path + self.model_type = model_type + self.models_adapter = TypeAdapter(Models[model_type]) + + def get(self, key: str) -> T | None: + models = self._load() + return models.get(key) + + def get_all(self) -> list[T]: + models = self._load() + return list(models.values()) + + def save(self, model: T) -> None: + models = self._load() + models[model.key] = model + self._dump(models) + + def delete(self, key: str) -> T | None: + models = self._load() + model = models.pop(key, None) + self._dump(models) + return model + + def _load(self) -> Models[T]: + if not self.storage_path.exists(): + return {} + + return self.models_adapter.validate_json(self.storage_path.read_bytes()) + + def _dump(self, models: Models) -> None: + self.storage_path.write_bytes(self.models_adapter.dump_json(models)) diff --git a/antispambot/storage/storages.py b/antispambot/storage/storages.py new file mode 100644 index 0000000..26dc8b0 --- /dev/null +++ b/antispambot/storage/storages.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from antispambot.models.chat import Chat +from antispambot.models.dictionary import Dictionary +from antispambot.models.event import Event +from antispambot.models.group import Group +from antispambot.models.member import Member +from antispambot.storage.base import JsonStorage + +STORAGE_PATH = Path('./storage') + +chat_storage = JsonStorage(Chat, STORAGE_PATH.joinpath('chat.json')) +dictionary_storage = JsonStorage(Dictionary, STORAGE_PATH.joinpath('dictionary.json')) +event_storage = JsonStorage(Event, STORAGE_PATH.joinpath('event.json')) +group_storage = JsonStorage(Group, STORAGE_PATH.joinpath('group.json')) +member_storage = JsonStorage(Member, STORAGE_PATH.joinpath('member.json')) diff --git a/predeploy.sh b/predeploy.sh new file mode 100755 index 0000000..48de739 --- /dev/null +++ b/predeploy.sh @@ -0,0 +1 @@ +zip -r antispambot.zip . -x ".venv/*" "**.mypy_cache/*" "**__pycache__/*" ".git/*" From e1e657450331aa448a8d5cdf9bc9e8fe1362c185 Mon Sep 17 00:00:00 2001 From: Mihail Butvin Date: Sat, 19 Oct 2024 12:21:36 +0300 Subject: [PATCH 5/6] Provde bot token as cli arg, write logs to file --- antispambot/bot/utils/events.py | 90 +++------------------------------ antispambot/models/event.py | 4 -- main.py | 22 ++++++-- 3 files changed, 25 insertions(+), 91 deletions(-) diff --git a/antispambot/bot/utils/events.py b/antispambot/bot/utils/events.py index 181c397..9a38e80 100644 --- a/antispambot/bot/utils/events.py +++ b/antispambot/bot/utils/events.py @@ -6,24 +6,17 @@ from antispambot.bot.callbacks.event_message import ( BanMemberCallback, - DeleteMessageCallback, UnbanMemberCallback, ) from antispambot.bot.messages import ( DELETE_MESSAGE_EVENT, DELETE_MESSAGE_REASON, - PROFANITY_EVENT, STRIKE_MEMBER_EVENT, ) -from antispambot.bot.utils.spread import ( - SendMessage, - forward_messages, - spread_messages, -) +from antispambot.bot.utils.spread import SendMessage, spread_messages from antispambot.models.event import ( DeleteMessageEvent, Event, - ProfanityFilterEvent, StrikeMemberEvent, ) from antispambot.models.group import Group @@ -91,21 +84,12 @@ async def message_delete_event( ] ]) ) - # admins_chats = [ - # chat - # for chat in chat_storage.get_all() - # if group.key in chat.groups - # ] - admins_chats = [] - logger.info(group.key) - for chat in chat_storage.get_all(): - if chat.groups: - logger.info(f'{chat.key}: {chat.groups}') - - if group.key in chat.groups: - admins_chats.append(chat) - - logger.info(f'Admins chats: {admins_chats}') + admins_chats = [ + chat + for chat in chat_storage.get_all() + if group.key in chat.groups + ] + logger.info(f'Admins chats: {[chat.username for chat in admins_chats]}') admins_chats_ids = [int(chat.key) for chat in admins_chats] await spread_messages(admins_chats_ids, [delete_event_message], bot) @@ -171,6 +155,7 @@ async def strike_member_event( for chat in chat_storage.get_all() if group.key in chat.groups ] + logger.info(f'Admins chats: {[chat.username for chat in admins_chats]}') admins_chats_ids = [int(chat.key) for chat in admins_chats] await spread_messages(admins_chats_ids, [strike_event_message], bot) @@ -178,65 +163,6 @@ async def strike_member_event( return strike_member_event -async def profanity_filter_event( - group: Group, - member: Member, - message: Message, - word: str, - bot: Bot -) -> Event: - assert message.from_user - logger.info(f'Profanity filter event in group {group.key}: {word}') - - # register event - profanity_filter_event = ProfanityFilterEvent( - key=str(message.message_id), - username=message.from_user.username, - full_name=message.from_user.full_name, - message_text=message.text or 'error', - reason=word, - time=datetime.now() - ) - - # send message to admins - profanity_event_message = SendMessage( - text=PROFANITY_EVENT.format( - title=group.title, - word=profanity_filter_event.reason, - ), - reply_markup=InlineKeyboardMarkup(inline_keyboard=[ - [ - InlineKeyboardButton( - text='Удалить сообщение', - callback_data=DeleteMessageCallback( - chat_id=message.chat.id, - message_id=message.message_id - ).pack() - ), - InlineKeyboardButton( - text='Забанить', - callback_data=BanMemberCallback( - chat_id=message.chat.id, - user_id=message.from_user.id - ).pack() - ) - ] - ]) - ) - - admins_chats = [ - chat - for chat in chat_storage.get_all() - if group.key in chat.groups - ] - admins_chats_ids = [int(chat.key) for chat in admins_chats] - await spread_messages(admins_chats_ids, [profanity_event_message], bot) - await forward_messages(admins_chats_ids, [message], bot) - - logger.info(f'Success: {profanity_filter_event}') - return profanity_filter_event - - async def update_member_strike(group: Group, member: Member) -> bool: member.strikes_count.setdefault(group.key, 0) member.strikes_count[group.key] += 1 diff --git a/antispambot/models/event.py b/antispambot/models/event.py index 53a8238..f76c335 100644 --- a/antispambot/models/event.py +++ b/antispambot/models/event.py @@ -19,7 +19,3 @@ class StrikeMemberEvent(Event): class DeleteMessageEvent(Event): event: str = 'delete_message' - - -class ProfanityFilterEvent(Event): - event: str = 'profanity_filter' diff --git a/main.py b/main.py index 679d04d..58c781a 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,5 @@ import asyncio import logging -from os import getenv import sys from antispambot.bot.factory import create_bot, create_dispatcher @@ -14,11 +13,24 @@ async def main(bot_token: str) -> None: if __name__ == '__main__': + if len(sys.argv) < 2: + print('Usage: python main.py [--debug-log]') + exit(1) + + BOT_TOKEN = sys.argv[1] + if '--debug-log' in sys.argv: - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig( + filename='logs.log', + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s', + ) else: - logging.basicConfig(level=logging.INFO) + logging.basicConfig( + level=logging.INFO, + filename='logs.log', + format='%(asctime)s - %(levelname)s - %(message)s', + ) + logging.getLogger('aiogram').setLevel(logging.WARNING) - BOT_TOKEN = getenv('BOT_TOKEN') - assert BOT_TOKEN asyncio.run(main(BOT_TOKEN)) From 700e968392806ad2470258664d17839397d868da Mon Sep 17 00:00:00 2001 From: Mihail Butvin Date: Sat, 19 Oct 2024 12:21:59 +0300 Subject: [PATCH 6/6] Stuff --- .gitignore | 1 + predeploy.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7eaf573..c0eb429 100644 --- a/.gitignore +++ b/.gitignore @@ -227,3 +227,4 @@ typings/ poetry.lock develop/ /storage/* +*.zip diff --git a/predeploy.sh b/predeploy.sh index 48de739..f3afc07 100755 --- a/predeploy.sh +++ b/predeploy.sh @@ -1 +1 @@ -zip -r antispambot.zip . -x ".venv/*" "**.mypy_cache/*" "**__pycache__/*" ".git/*" +zip -r antispambot.zip . -x ".venv/*" "**.mypy_cache/*" "**__pycache__/*" ".git/*" "./storage/*"