diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..79369da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11 + +WORKDIR /moderaotrbot +COPY ./ ./ + +RUN rm -rf /etc/localtime +RUN ln -s /usr/share/zoneinfo/Europe/Moscow /etc/localtime +RUN echo "Europe/Moscow" > /etc/timezone + +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["python","-u", "main.py"] diff --git a/README.md b/README.md index 32a8c09..dca4506 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ -# chat_moderator_bot +## Бот транскрибатор -Удаляет сообщения из группы с бан-вордами. После, скидывает изменяемое в админ-панели сообщение \ No newline at end of file +### Конфигурация .env + +``` +BOT_TOKEN - Токен бота + +ADMINS - Админы, для которых работает /start. Записывать через запятую: user_id1,user_id2,user_id3 и т.д. + +POSTGRES_NAME - Имя postgres бд +POSTGRES_HOST - Хост postgres бд +POSTGRES_PORT - Порт postgres бд +POSTGRES_PASSWORD - Пароль от postgres бд +POSTGRES_USER - Пользовтаель postgres бд + +REDIS_NAME - Имя redis бд +REDIS_HOST - Хост redis бд +REDIS_PORT - Порт redis бд +REDIS_PASSWORD - Пароль redis бд +``` + +## Шаг 1: Получение Токена бота + +1. Перейдите в [BotFather](https://t.me/BotFather) +2. Скопируйте Токен вашего телеграмм бота + +## Шаг 2: Загрузка переменных в окружение + +1. Создайте файл `.env` в корневой папке проекта и заполните его значениями выше. + +2. Скачайте необходимые проекту библиотеки командой: + + ``` + pip install -r requirements.txt + ``` diff --git a/config.py b/config.py new file mode 100644 index 0000000..566a21e --- /dev/null +++ b/config.py @@ -0,0 +1,27 @@ +from os import getenv +from aiogram.types import BotCommand +from dotenv import load_dotenv + +load_dotenv() + +BOT_TOKEN: str = getenv('BOT_TOKEN') + +ADMINS: list = [int(admin) for admin in getenv('ADMINS').split(',')] + +POSTGRES_NAME: str = getenv('POSTGRES_NAME') +POSTGRES_HOST: str = getenv('POSTGRES_HOST') +POSTGRES_PORT: int = int(getenv('POSTGRES_PORT')) +POSTGRES_PASSWORD: str = getenv('POSTGRES_PASSWORD') +POSTGRES_USER: str = getenv('POSTGRES_USER') + +REDIS_NAME: int = int(getenv('REDIS_NAME')) +REDIS_HOST: str = getenv('REDIS_HOST') +REDIS_PORT: int =int(getenv('REDIS_PORT')) +REDIS_PASSWORD: str = getenv('REDIS_PASSWORD') + +commands = [ + BotCommand( + command='start', + description='Меню' + ) +] \ No newline at end of file diff --git a/core.py b/core.py new file mode 100644 index 0000000..4b67fa8 --- /dev/null +++ b/core.py @@ -0,0 +1,8 @@ +from aiogram import Bot + +from config import BOT_TOKEN + +bot = Bot( + token=BOT_TOKEN, + parse_mode="HTML" +) diff --git a/data/ban_words.xlsx b/data/ban_words.xlsx new file mode 100644 index 0000000..5cf9c43 Binary files /dev/null and b/data/ban_words.xlsx differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..275e466 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + bot: + build: + context: . + restart: always + depends_on: + - db + env_file: + - .env + + db: + image: postgres + restart: always + environment: + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_NAME} + expose: + - '5432' + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:latest + restart: always + command: [ "redis-server", "--requirepass", "4CEqaD0JL8gTM4XWVt8K" ] + expose: + - '6379' + volumes: + - redis_data:/data + +volumes: + postgres_data: + redis_data: diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..93a84bc --- /dev/null +++ b/handlers/__init__.py @@ -0,0 +1,8 @@ +from handlers.ban_words import router as rban_words +from handlers.commands import router as rcommands +from handlers.come_back import router as rcome_back +from handlers.message import router as rmessage +from handlers.group import router as rgroup +from handlers.ban_media import router as rban_media + +routers = [rcommands, rban_words, rcome_back, rmessage, rgroup, rban_media] \ No newline at end of file diff --git a/handlers/__pycache__/__init__.cpython-310.pyc b/handlers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1645c6e Binary files /dev/null and b/handlers/__pycache__/__init__.cpython-310.pyc differ diff --git a/handlers/__pycache__/__init__.cpython-311.pyc b/handlers/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f8f3af1 Binary files /dev/null and b/handlers/__pycache__/__init__.cpython-311.pyc differ diff --git a/handlers/__pycache__/ban_media.cpython-311.pyc b/handlers/__pycache__/ban_media.cpython-311.pyc new file mode 100644 index 0000000..c15e9a5 Binary files /dev/null and b/handlers/__pycache__/ban_media.cpython-311.pyc differ diff --git a/handlers/__pycache__/ban_words.cpython-311.pyc b/handlers/__pycache__/ban_words.cpython-311.pyc new file mode 100644 index 0000000..c40e65d Binary files /dev/null and b/handlers/__pycache__/ban_words.cpython-311.pyc differ diff --git a/handlers/__pycache__/come_back.cpython-311.pyc b/handlers/__pycache__/come_back.cpython-311.pyc new file mode 100644 index 0000000..a552263 Binary files /dev/null and b/handlers/__pycache__/come_back.cpython-311.pyc differ diff --git a/handlers/__pycache__/commands.cpython-310.pyc b/handlers/__pycache__/commands.cpython-310.pyc new file mode 100644 index 0000000..7641694 Binary files /dev/null and b/handlers/__pycache__/commands.cpython-310.pyc differ diff --git a/handlers/__pycache__/commands.cpython-311.pyc b/handlers/__pycache__/commands.cpython-311.pyc new file mode 100644 index 0000000..b8c29a2 Binary files /dev/null and b/handlers/__pycache__/commands.cpython-311.pyc differ diff --git a/handlers/__pycache__/group.cpython-311.pyc b/handlers/__pycache__/group.cpython-311.pyc new file mode 100644 index 0000000..3931dc9 Binary files /dev/null and b/handlers/__pycache__/group.cpython-311.pyc differ diff --git a/handlers/__pycache__/message.cpython-311.pyc b/handlers/__pycache__/message.cpython-311.pyc new file mode 100644 index 0000000..de0cbd6 Binary files /dev/null and b/handlers/__pycache__/message.cpython-311.pyc differ diff --git a/handlers/ban_media.py b/handlers/ban_media.py new file mode 100644 index 0000000..805022f --- /dev/null +++ b/handlers/ban_media.py @@ -0,0 +1,66 @@ +from aiogram import Router, F +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery + +from templates import commands as tcommands +from utils.db import Postgres, Redis + +router = Router() + + +@router.callback_query(F.data.startswith('enable_ban_media_')) +async def enable_ban_media_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку Включить + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + p = Postgres() + r = Redis() + + await p.update_data( + table_name='ban_media', + new_data={call.data[17:]: True}, + query_filter={} + ) + await r.update_dict( + key='ban_media', + value={call.data[17:]: 'yes'} + ) + print('new redis data', {call.data[17:]: 'yes'}) + + await state_data['last_msg'].edit_text( + text=tcommands.start_text, + reply_markup=await tcommands.start_ikb() + ) + + +@router.callback_query(F.data.startswith('disable_ban_media_')) +async def disable_ban_media_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку Выключить + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + p = Postgres() + r = Redis() + + await p.update_data( + table_name='ban_media', + new_data={call.data[18:]: False}, + query_filter={} + ) + await r.update_dict( + key='ban_media', + value={call.data[18:]: ''} + ) + print('new redis data', {call.data[18:]: ''}) + + await state_data['last_msg'].edit_text( + text=tcommands.start_text, + reply_markup=await tcommands.start_ikb() + ) \ No newline at end of file diff --git a/handlers/ban_words.py b/handlers/ban_words.py new file mode 100644 index 0000000..b5aae9c --- /dev/null +++ b/handlers/ban_words.py @@ -0,0 +1,134 @@ +import logging + +from aiogram import Router, F +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, FSInputFile, Message + +from templates import ban_words as tban_words +from templates import commands as tcommands +from utils.defs import delete_msg, create_xlsx +from utils.db import Redis, Postgres + +router = Router() + + +@router.callback_query(F.data == 'ban_words') +async def ban_words_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку 🚫 Стоп слова + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + document_path = await create_xlsx() + + last_msg = await call.message.answer_document( + document=FSInputFile(document_path), + reply_markup=tban_words.actions_ikb() + ) + + await delete_msg( + msg=state_data.get('last_msg') + ) + await state.update_data( + last_msg=last_msg + ) + + +@router.callback_query(F.data.startswith('ban_words_action_')) +async def ban_words_action_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопки ➕ Добавить ➖ Удалить + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + last_msg = await call.message.answer( + text=tban_words.send_words_text, + reply_markup=tban_words.send_words_ikb() + ) + + await state.update_data( + last_msg=last_msg, + ban_words_action=call.data[17:] + ) + + await state.set_state( + state=tban_words.SendState.words + ) + + await delete_msg( + msg=state_data.get('last_msg') + ) + + +@router.message(tban_words.SendState.words, F.text) +async def get_words(msg: Message, state: FSMContext): + """ + Ловит новое сообщение + :param msg: Message + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + words = msg.text.split('\n') + + r = Redis() + p = Postgres() + + redis_ban_words = await r.get_list( + key='ban_words' + ) + + if state_data['ban_words_action'] == 'add': + msg_text = tban_words.success_text.format( + action='добавлены' + ) + for word in words: + await p.create_row( + table_name='ban_words', + data_to_insert={'word': word.lower()} + ) + redis_ban_words.append(word.lower()) + + else: + msg_text = tban_words.success_text.format( + action='удалены' + ) + not_deleted = [] + for word in words: + try: + await p.query( + query_text=f"DELETE FROM ban_words WHERE LOWER(word)='{word.lower()}'" + ) + redis_ban_words.remove(word.lower()) + except Exception as e: + logging.error(f'get_words {e}') + not_deleted.append(word) + + if not_deleted: + msg_text = tban_words.not_success_remove_text.format( + words='\n'.join(not_deleted) + ) + + await state_data['last_msg'].edit_text( + text=msg_text, + reply_markup=await tcommands.start_ikb() + ) + await msg.delete() + + await r.delete_key( + 'ban_words' + ) + await r.update_list( + 'ban_words', + *redis_ban_words + ) + + await state.set_state( + state=None + ) diff --git a/handlers/come_back.py b/handlers/come_back.py new file mode 100644 index 0000000..82ff44d --- /dev/null +++ b/handlers/come_back.py @@ -0,0 +1,115 @@ +from aiogram import Router, F +from aiogram.types import CallbackQuery +from aiogram.fsm.context import FSMContext + +from handlers.ban_words import ban_words_btn +from templates import message as tmessage +from templates import commands as tcommands +from handlers.message import message_btn +from utils.defs import delete_msg + +router = Router() + + +@router.callback_query(F.data == 'come_back_buttons') +async def come_back_buttons_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку ⬅️ Назад + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + await state_data['last_msg'].edit_text( + text=tmessage.send_buttons_text, + reply_markup=tmessage.send_buttons_ikb() + ) + + await delete_msg( + msg=state_data.get('preview_msg') + ) + + state_data['edited_message_data']['buttons'] = [] + await state.update_data( + edited_message_data=state_data['edited_message_data'] + ) + + await state.set_state( + state=tmessage.SendState.buttons + ) + + +@router.callback_query(F.data == 'come_back_message') +async def come_back_message_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку ⬅️ Назад + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + await state_data['last_msg'].edit_text( + text=tmessage.send_message_text, + reply_markup=tmessage.send_message_ikb() + ) + + await state.set_state( + state=tmessage.SendState.message + ) + + +@router.callback_query(F.data == 'come_back_preview') +async def come_back_preview_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку ⬅️ Назад + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + await message_btn( + call=call, + state=state + ) + + +@router.callback_query(F.data == 'come_back_menu') +async def come_back_preview_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку ⬅️ Назад + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + last_msg = await call.message.answer( + text=tcommands.start_text, + reply_markup=await tcommands.start_ikb() + ) + + await state.update_data( + last_msg=last_msg + ) + + await delete_msg( + msg=state_data.get('preview_msg') + ) + await delete_msg( + msg=state_data.get('last_msg') + ) + + +@router.callback_query(F.data == 'come_back_ban_words') +async def come_back_ban_words_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку ⬅️ Назад + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + await ban_words_btn( + call=call, + state=state + ) diff --git a/handlers/commands.py b/handlers/commands.py new file mode 100644 index 0000000..26d6eab --- /dev/null +++ b/handlers/commands.py @@ -0,0 +1,39 @@ +from aiogram import Router +from aiogram.types import Message, BotCommandScopeChat +from aiogram.filters import Command +from aiogram.fsm.context import FSMContext + +from core import bot +from templates import commands as tcommands +from config import ADMINS, commands + +router = Router() + + +@router.message(Command("start")) +async def start_command(msg: Message, state: FSMContext): + """ + Ловит команду /start + :param msg: Message + :param state: FSMContext + :return: + """ + await msg.delete() + if not msg.from_user.id in ADMINS: + return + + last_msg = await msg.answer( + text=tcommands.start_text, + reply_markup=await tcommands.start_ikb() + ) + + await state.update_data( + last_msg=last_msg + ) + + await bot.set_my_commands( + commands=commands, + scope=BotCommandScopeChat( + chat_id=msg.from_user.id + ) + ) diff --git a/handlers/group.py b/handlers/group.py new file mode 100644 index 0000000..ee25315 --- /dev/null +++ b/handlers/group.py @@ -0,0 +1,92 @@ + +from aiogram import Router, F +from aiogram.fsm.context import FSMContext +from aiogram.types import Message + +from templates.message import send_preview +from utils.db import Redis, Postgres +from utils.defs import delete_msg + +router = Router() + + +@router.message(F.chat.func(lambda chat: chat.type in ('group', 'supergroup'))) +async def get_all_messages(msg: Message, state: FSMContext): + """ + Ловит все сообщения в чате + :param msg: Message + :param state: FSMContext + :return: + """ + ban = False + + msg_text = msg.text + if not msg_text: + msg_text = msg.caption + + r = Redis() + p = Postgres() + + if msg_text: + ban_words = await r.get_list( + key='ban_words' + ) + if not ban_words: + ban_words = await p.get_data( + table_name='ban_words' + ) + ban_words = [word['word'] for word in ban_words] + await r.delete_key( + 'ban_words' + ) + await r.update_list( + 'ban_words', + *ban_words + ) + + for ban_word in ban_words: + if ban_word in msg_text.lower(): + ban = True + + else: + ban_media = await r.get_dict( + key='ban_media' + ) + print(ban_media) + if not ban_media.get('video') or ban_media.get('photo'): + postgres_ban_media = await p.get_data( + table_name='ban_media' + ) + ban_media = {} + for key, value in postgres_ban_media[0].items(): + ban_media[key] = 'yes' if value else '' + + await r.update_dict( + 'ban_media', + value=ban_media + ) + + if ban_media.get('video') and msg.video: + ban = True + if ban_media.get('photo') and msg.photo: + ban = True + + if ban: + await delete_msg( + msg=msg + ) + message_data = await p.get_data( + table_name='message' + ) + + if msg.from_user.username: + username = f'@{msg.from_user.username}' + else: + username = msg.from_user.full_name + + if message_data[0]['included']: + await send_preview( + chat_id=msg.chat.id, + message_data=message_data[0], + username=username + ) \ No newline at end of file diff --git a/handlers/message.py b/handlers/message.py new file mode 100644 index 0000000..0da3794 --- /dev/null +++ b/handlers/message.py @@ -0,0 +1,252 @@ +import json + +from aiogram import Router, F +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message + +from templates import commands as tcommands +from templates import message as tmessage +from utils.db import Postgres +from utils.defs import delete_msg + +router = Router() + + +@router.callback_query(F.data == 'message') +async def message_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку 💬 Стоп сообщение + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + p = Postgres() + message_data = await p.get_data( + table_name='message' + ) + + preview_msg = await tmessage.send_preview( + message_data=message_data[0], + chat_id=call.from_user.id + ) + + last_msg = await call.message.answer( + text=tmessage.actions_text.format( + include='Да' if message_data[0]['included'] else 'Нет' + ), + reply_markup=tmessage.actions_ikb( + included=message_data[0]['included'] + ) + ) + await delete_msg( + msg=state_data.get('last_msg') + ) + await state.update_data( + preview_msg=preview_msg, + last_msg=last_msg + ) + + +@router.callback_query(F.data == 'edit_message') +async def edit_message_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку 📝 Редактировать + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + await state_data['last_msg'].edit_text( + text=tmessage.send_message_text, + reply_markup=tmessage.send_message_ikb() + ) + await state.set_state( + state=tmessage.SendState.message + ) + await delete_msg( + msg=state_data.get('preview_msg') + ) + + await state.update_data( + edited_message_data={ + 'text': '', + 'buttons': [], + 'media': '' + } + ) + + +@router.message(tmessage.SendState.message, F.content_type.in_({'text', 'photo'})) +async def get_edit_message(msg: Message, state: FSMContext): + """ + Ловит новое сообщение + :param msg: Message + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + await state_data['last_msg'].edit_text( + text=tmessage.send_buttons_text, + reply_markup=tmessage.send_buttons_ikb() + ) + await msg.delete() + + await state.set_state( + state=tmessage.SendState.buttons + ) + + if msg.photo: + state_data['edited_message_data']['text'] = msg.caption + state_data['edited_message_data']['media'] = msg.photo[-1].file_id + else: + state_data['edited_message_data']['text'] = msg.text + state_data['edited_message_data']['media'] = None + + await state.update_data( + edited_message_data=state_data['edited_message_data'] + ) + + +@router.message(tmessage.SendState.buttons, F.text) +async def get_edit_buttons(msg: Message, state: FSMContext): + """ + Ловит новое url кнопки к сообщению + :param msg: Message + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + buttons = tmessage.build_url_ikb(msg.text) + + await msg.delete() + + if not buttons: + await state_data['last_msg'].edit_text( + text=tmessage.incorrect_data_text + '\n\n' + tmessage.send_buttons_text, + reply_markup=tmessage.send_buttons_ikb() + ) + return + + await delete_msg( + msg=state_data.get('last_msg') + ) + + state_data['edited_message_data']['buttons'] = buttons + + preview_msg = await tmessage.send_preview( + message_data=state_data['edited_message_data'], + chat_id=msg.from_user.id + ) + last_msg = await msg.answer( + text=tmessage.check_data_text, + reply_markup=tmessage.check_data_ikb() + ) + + await state.update_data( + edited_message_data=state_data['edited_message_data'], + preview_msg=preview_msg, + last_msg=last_msg + ) + await state.set_state( + state=None + ) + + +@router.callback_query(F.data == 'pass_buttons') +async def pass_buttons_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку ➡️ Пропустить + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + await delete_msg( + msg=state_data.get('last_msg') + ) + + preview_msg = await tmessage.send_preview( + message_data=state_data['edited_message_data'], + chat_id=call.from_user.id + ) + + last_msg = await call.message.answer( + text=tmessage.check_data_text, + reply_markup=tmessage.check_data_ikb() + ) + + await state.update_data( + preview_msg=preview_msg, + last_msg=last_msg + ) + + await state.set_state( + state=None + ) + + +@router.callback_query(F.data == 'publish_message') +async def publish_message_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку ✅ Опубликовать + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + state_data['edited_message_data']['buttons'] = json.dumps( + state_data['edited_message_data']['buttons'] + ) + + p = Postgres() + await p.update_data( + table_name='message', + new_data=state_data['edited_message_data'], + query_filter={} + ) + + await state_data['last_msg'].edit_text( + text=tmessage.publish_message_text, + reply_markup=await tcommands.start_ikb() + ) + + await delete_msg( + msg=state_data.get('preview_msg') + ) + + +@router.callback_query(F.data.startswith('included_message_')) +async def included_message_btn(call: CallbackQuery, state: FSMContext): + """ + Ловит кнопку 📝 Редактировать + :param call: CallbackQuery + :param state: FSMContext + :return: + """ + state_data = await state.get_data() + + if call.data[17:] == 'true': + included = True + else: + included = False + + await state_data['last_msg'].edit_text( + text=tmessage.actions_text.format( + include='Да' if included else 'Нет' + ), + reply_markup=tmessage.actions_ikb( + included=included + ) + ) + + p = Postgres() + await p.update_data( + table_name='message', + new_data={'included': included}, + query_filter={} + ) diff --git a/main.py b/main.py new file mode 100644 index 0000000..4812aa2 --- /dev/null +++ b/main.py @@ -0,0 +1,36 @@ +import asyncio +import logging +import sys + +from aiogram import Dispatcher + +from core import bot +from handlers import routers +from utils.db import Postgres +# from utils.middleware import DeleteMessage + +dp = Dispatcher() +dp.include_routers(*routers) +# dp.update.middleware.register( +# middleware=DeleteMessage() +# ) + + +async def start(): + """ + Запускает бота + :return: + """ + p = Postgres() + await p.create_tables() + + await bot.delete_webhook() + await dp.start_polling(bot) + + +if __name__ == "__main__": + logging.basicConfig( + level=logging.INFO, + stream=sys.stdout + ) + asyncio.run(start()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dfc578b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +aiogram==3.6.0 +aiohttp==3.9.5 +python-dotenv==1.0.1 +openpyxl~=3.1.5 +pandas~=2.2.2 +redis~=5.0.7 +asyncpg~=0.29.0 \ No newline at end of file diff --git a/templates/__init__.py b/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/__pycache__/__init__.cpython-311.pyc b/templates/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..3e3f29b Binary files /dev/null and b/templates/__pycache__/__init__.cpython-311.pyc differ diff --git a/templates/__pycache__/ban_words.cpython-311.pyc b/templates/__pycache__/ban_words.cpython-311.pyc new file mode 100644 index 0000000..812e5dc Binary files /dev/null and b/templates/__pycache__/ban_words.cpython-311.pyc differ diff --git a/templates/__pycache__/commands.cpython-311.pyc b/templates/__pycache__/commands.cpython-311.pyc new file mode 100644 index 0000000..94dabb3 Binary files /dev/null and b/templates/__pycache__/commands.cpython-311.pyc differ diff --git a/templates/__pycache__/message.cpython-311.pyc b/templates/__pycache__/message.cpython-311.pyc new file mode 100644 index 0000000..37c1cf8 Binary files /dev/null and b/templates/__pycache__/message.cpython-311.pyc differ diff --git a/templates/ban_words.py b/templates/ban_words.py new file mode 100644 index 0000000..0e16766 --- /dev/null +++ b/templates/ban_words.py @@ -0,0 +1,64 @@ +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.fsm.state import StatesGroup, State + + +class SendState(StatesGroup): + words = State() + + +send_words_text = """ +Отправь список слов по одному или столбцом +""" + +success_text = """ +Слова {action}! +""" + +not_success_remove_text = """ +Что-то пошло не так. Не удалил слова: + +{words} +""" + + +def send_words_ikb() -> InlineKeyboardMarkup: + """ + -⬅️ Назад + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + builder.add( + InlineKeyboardButton( + text='⬅️ Назад', + callback_data='come_back_ban_words' + ) + ) + builder.adjust(1) + return builder.as_markup() + + +def actions_ikb() -> InlineKeyboardMarkup: + """ + -➕ Добавить + -➖ Удалить + -⬅️ Назад + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + builder.add( + InlineKeyboardButton( + text='➕ Добавить', + callback_data='ban_words_action_add' + ), + InlineKeyboardButton( + text='➖ Удалить', + callback_data='ban_words_action_remove' + ), + InlineKeyboardButton( + text='⬅️ Назад', + callback_data='come_back_menu' + ) + ) + builder.adjust(1) + return builder.as_markup() \ No newline at end of file diff --git a/templates/commands.py b/templates/commands.py new file mode 100644 index 0000000..f516707 --- /dev/null +++ b/templates/commands.py @@ -0,0 +1,62 @@ +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from utils.db import Postgres + +start_text = """ +Привет, Админ! +""" + + +async def start_ikb() -> InlineKeyboardMarkup: + """ + -🚫 Стоп слова + -💬 Стоп сообщение + -Запретить / Разрешить видео без опис. + -Запретить / Разрешить фото без опис. + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + + ban_media_photo = {'text': '', 'callback_data': ''} + ban_media_video = {'text': '', 'callback_data': ''} + + ban_media = await Postgres().get_data( + table_name='ban_media' + ) + + if not ban_media[0]['photo']: + ban_media_photo['text'] = 'Запретить фото без описания' + ban_media_photo['callback_data'] = 'enable_ban_media_photo' + else: + ban_media_photo['text'] = 'Разрешить фото без описания' + ban_media_photo['callback_data'] = 'disable_ban_media_photo' + + if not ban_media[0]['video']: + ban_media_video['text'] = 'Запретить видео без описания' + ban_media_video['callback_data'] = 'enable_ban_media_video' + else: + ban_media_video['text'] = 'Разрешить видео без описания' + ban_media_video['callback_data'] = 'disable_ban_media_video' + + + builder.add( + InlineKeyboardButton( + text='🚫 Стоп слова', + callback_data='ban_words' + ), + InlineKeyboardButton( + text='💬 Стоп сообщение', + callback_data='message' + ), + InlineKeyboardButton( + text=ban_media_video['text'], + callback_data=ban_media_video['callback_data'] + ), + InlineKeyboardButton( + text=ban_media_photo['text'], + callback_data=ban_media_photo['callback_data'] + ) + ) + builder.adjust(1) + return builder.as_markup() \ No newline at end of file diff --git a/templates/message.py b/templates/message.py new file mode 100644 index 0000000..ab61c5d --- /dev/null +++ b/templates/message.py @@ -0,0 +1,226 @@ +import json + +from aiogram.fsm.state import StatesGroup, State +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.types import Message +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from core import bot + + +class SendState(StatesGroup): + message = State() + buttons = State() + + +actions_text = """ +Сообщение включено: {include} + +Выберите действие: +""" + +send_message_text = """ +Отправьте сообщение с прикреплением до одного фото: +""" + +send_buttons_text = """ +Отправьте кнопки в таком формате: + +Кнопка в первом ряду - http://example.com +Кнопка во втором ряду - http://example.com + +Используйте разделитель " | ", чтобы добавить до 8 кнопок в один ряд (допустимо 6 рядов): +Кнопка в ряду - http://example.com | Другая кнопка в ряду - http://example.com +""" + +incorrect_data_text = """ +Не верный формат данных +""" + +check_data_text = """ +Проверьте введённые данные +""" + +publish_message_text = """ +Обуликовано успешно +""" + + +def check_data_ikb() -> InlineKeyboardMarkup: + """ + -✅ Опубликовать + -⬅️ Назад + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + builder.add( + InlineKeyboardButton( + text='✅ Опубликовать', + callback_data='publish_message' + ), + InlineKeyboardButton( + text='⬅️ Назад', + callback_data='come_back_buttons' + ) + ) + builder.adjust(1) + return builder.as_markup() + + +async def send_preview(message_data: dict, chat_id: int, username='') -> Message | None: + """ + Присылает превью сообщения + :param message_data: Данные сообщения из бд + :param chat_id: ID телеграм чата куда надо прислать превью + :param username: Username пользователя + :return: + """ + msg_text = message_data['text'] + if username: + msg_text = f'{username}\n\n' + message_data['text'] + + if message_data['media']: + preview_msg = await bot.send_photo( + chat_id=chat_id, + caption=msg_text, + photo=message_data['media'], + reply_markup=url_ikb( + row_buttons=message_data['buttons'] + ) + ) + else: + preview_msg = await bot.send_message( + chat_id=chat_id, + text=msg_text, + reply_markup=url_ikb( + row_buttons=message_data['buttons'] + ) + ) + + return preview_msg + + +def send_buttons_ikb() -> InlineKeyboardMarkup: + """ + -➡️ Пропустить + -⬅️ Назад + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + builder.add( + InlineKeyboardButton( + text='➡️ Пропустить', + callback_data='pass_buttons' + ), + InlineKeyboardButton( + text='⬅️ Назад', + callback_data='come_back_message' + ) + ) + builder.adjust(1) + return builder.as_markup() + + +def send_message_ikb() -> InlineKeyboardMarkup: + """ + -⬅️ Назад + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + builder.add( + InlineKeyboardButton( + text='⬅️ Назад', + callback_data='come_back_preview' + ) + ) + builder.adjust(1) + return builder.as_markup() + + +def actions_ikb(included: bool) -> InlineKeyboardMarkup: + """ + -Вкл✅ \ Выкл ❌ + -📝 Редактировать + -⬅️ Назад + :param included: bool включено ли сообщение или нет + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + + if included: + included_btn = InlineKeyboardButton( + text='Выкл ❌', + callback_data='included_message_false' + ) + else: + included_btn = InlineKeyboardButton( + text='Вкл ✅', + callback_data='included_message_true' + ) + + builder.add( + included_btn, + InlineKeyboardButton( + text='📝 Редактировать', + callback_data='edit_message' + ), + InlineKeyboardButton( + text='⬅️ Назад', + callback_data='come_back_menu' + ) + ) + builder.adjust(1) + return builder.as_markup() + + +def build_url_ikb(msg_text: str) -> list: + """ + Создаёт клавиатуру с url кнопками пользователя + :param msg_text: Текст сообщения с заданными параметры клавиатуру + :return: list с кнопками клавиатуры + """ + try: + paragraphs = msg_text.split('\n') + row_buttons = [] + for paragraph in paragraphs: + row = [] + for button_text_data in paragraph.split(' | '): + row.append(button_text_data.strip().split(' - ')) + row_buttons.append(row) + + return row_buttons + except: + return [] + + +def url_ikb(row_buttons: list | str) -> InlineKeyboardMarkup | None: + """ + Создаёт клавиатуру с url кнопками пользователя + :param row_buttons: List с кнопками клавиатуры + :return: В случае ошибки False, если норм то объект клавиатуры для параметра reply_markup + """ + try: + if isinstance(row_buttons, str): + row_buttons = json.loads(row_buttons) + + builder = InlineKeyboardBuilder() + rows_len = [0, 0, 0, 0, 0, 0] + i = 0 + + for row in row_buttons: + rows_len[i] = len(row) + for button in row: + builder.add( + InlineKeyboardButton( + text=button[0], + url=button[1] + ) + ) + i += 1 + + builder.adjust(*rows_len) + if row_buttons: + return builder.as_markup() + return None + except: + return None diff --git a/templates/moderation.py b/templates/moderation.py new file mode 100644 index 0000000..6b8123a --- /dev/null +++ b/templates/moderation.py @@ -0,0 +1,458 @@ +import datetime +import logging + +from aiogram.fsm.state import StatesGroup, State +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, FSInputFile +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from config import FILES_PATH, ROLE_MODERATOR_ID, ROLE_CURATOR_ID +from core import bot +from utils.bitrix import notify +from utils.db import get_data, query + + +class ModerationState(StatesGroup): + confirm = State() + confirm_confirm = State() + revision = State() + grade = State() + reject = State() + check_revision_comment = State() + check_reject_comment = State() + date = State() + + +new_idea_text = """ +{name}, пожалуйста, ознакомьтесь с новой НеоИдеей. +""" + +new_idea_data_text = """ +Автор: {author_name} +Дата: {creation_date} +Подразделение: {department} +Категория: {category} +Город: {city} +Название: «{idea_name}» +Содержание: {idea_content} +""" + +choose_action_text = """ +Пожалуйста, выберите одно из следующих действий: +""" + +confirm_action_text = """ +Вы согласовываете НеоИдею: +«{idea_name}». + +Автор: {author_name} +""" + +choose_responsible_text = """ +Пожалуйста, выберите Ответственного за категорию для передачи НеоИдеи в работу. +""" + +send_date_text = """ +В календаре укажите крайние сроки выполнения: +""" + +confirm_responsible_date_text = """ +Вы передаёте НеоИдею в работу. +Название: «{idea_name}». + +Автор: {author_name} +Ответственный за категорию: {responsible_name} +Крайние сроки выполнения: {date} +""" + +confirm_action_send_text = """ +Спасибо! Вы передали НеоИдею в работу. +""" + +revision_action_text = """ +Вы возвращаете на доработку НеоИдею: +«{idea_name}». +Автор: {author_name} + +Оставьте комментарий для того, чтобы отправить НеоИдею на доработку. +""" + +send_revision_comment_text = """ +Оставьте комментарий для того, чтобы отправить НеоИдею на доработку +""" + +send_comment_text = """ +Пожалуйста, оставьте свой комментарий: +""" + +check_comment = """ +Проверьте правильность введенных данных: + +{comment} +""" + +revision_action_send_text = """ +Вы отправили НеоИдею на доработку. +""" + +grade_action_text = """ +Вы направляете НеоИдею на оценку: +«{idea_name}». +Автор: {author_name} + +НеоИдея будет передана Куратору. +""" + +grade_action_send_text = """ +Спасибо! НеоИдея передана Куратору. +""" + +reject_action_text = """ +Вы отклоняете НеоИдею: +«{idea_name}». +Автор: {author_name} + +Пожалуйста, оставьте свой комментарий. +""" + +reject_action_send_text = """ +Спасибо! НеоИдея отклонена. +""" + +you_rejected = """ +Здравствуйте, {name}. + +Ваша НеоИдея "{idea_name}" была отклонена с комментарием: +"{comment}" +""" + +you_revision = """ +Ваша НеоИдея возвращена на доработку, пожалуйста проработайте комментарии от генерального директора и отправьте заявку заново. + +НеоИдея: "{idea_name}" +Комментарий: "{comment}" +""" + +accept_notify_author_text = """ +Поздравляем! Ваша НеоИдея передана к внедрению +""" + +accept_notify_curator_text = """ +Быстрая НеоИдея автора {author_name} передана к внедрению {responsible_name} +""" + +grade_curator_text = """ +Вам на оценку передана НеоИдея "{idea_name}". +""" + +confirm_notify_curator_text = """ +Быстрая НеоИдея автора {author_name} передана к ответственному за категорию {responsible_name} +""" + +confirm_notify_author_text = """ +Ваша НеоИдея передана ответственному за категорию +""" + + +def confirm_action_ikb() -> InlineKeyboardMarkup: + """ + -Да, продолжить ✅ + -Нет, вернуться назад ❌ + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + builder.add( + InlineKeyboardButton( + text='Да, продолжить ✅', + callback_data='confirm_action_confirm' + ), + InlineKeyboardButton( + text='Нет, вернуться назад ❌', + callback_data='confirm_action_reject' + ) + ) + builder.adjust(1) + return builder.as_markup() + + +def choose_action_ikb(idea_id: str | int, msg_id: str | int) -> InlineKeyboardMarkup: + """ + -Согласовать и передать в работу + -Вернуть Автору на доработку + -Направить на оценку + -Отклонить + :param idea_id: ID идеи + :param msg_id: ID сообщения с клавиатурой + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + builder.add( + InlineKeyboardButton( + text='Согласовать и передать в работу', + callback_data=f'moderation_confirm_{idea_id}_{msg_id}' + ), + InlineKeyboardButton( + text='Вернуть Автору на доработку', + callback_data=f'moderation_revision_{idea_id}_{msg_id}' + ), + InlineKeyboardButton( + text='Направить на оценку', + callback_data=f'moderation_grade_{idea_id}_{msg_id}' + ), + InlineKeyboardButton( + text='Отклонить', + callback_data=f'moderation_reject_{idea_id}_{msg_id}' + ) + ) + builder.adjust(1) + return builder.as_markup() + + +async def choose_responsible_ikb(category_id: int) -> InlineKeyboardMarkup: + """ + -Ответственный 1 + -Ответственный 2 + :param category_id: ID категории идеи + :return: объект клавиатуры для параметра reply_markup + """ + builder = InlineKeyboardBuilder() + responsible_list = await get_data( + table_name='user_to_categories', + query_filter={'category_id': category_id} + ) + for responsible in responsible_list: + responsible_data = await get_data( + table_name='users', + query_filter={'id': responsible['user_id']} + ) + builder.add( + InlineKeyboardButton( + text=responsible_data[0]['full_name'], + callback_data=f'choose_responsible_{responsible_data[0]["id"]}', + ) + ) + builder.adjust(1) + return builder.as_markup() + + +async def send_new_idea_on_moderation( + idea_id: int, user_name: str, department: str, idea_title: str, + idea_content: str, relation_id: int, city_id: int, category_id: int, creation_date: datetime.datetime): + """ + Присылает новую идею модераторам из списка + :param idea_id: Данные идеи + :param user_name: Имя пользователя + :param department: Отдел пользователя + :param idea_title: Название идеи + :param idea_content: Описание идеи + :param relation_id: id отношения к идее + :param city_id: id города, где работает пользователь + :param category_id: id категории + :param creation_date: Дата создания идеи + :return: + """ + moderators = await get_data( + table_name='user_to_roles', + query_filter={'role_id': ROLE_MODERATOR_ID} + ) + city = await get_data( + table_name='cities', + query_filter={'id': city_id} + ) + files = await get_data( + table_name='idea_to_files', + query_filter={'idea_id': idea_id} + ) + category = await get_data( + table_name='categories', + query_filter={'id': category_id} + ) + notify_text = new_idea_data_text.format( + author_name=user_name, + creation_date=creation_date, + department=department, + idea_name=idea_title, + idea_content=idea_content, + city=city[0]['name'], + category=category[0]['name'] + ) + + for moderator_id in moderators: + try: + moderator = await get_data( + table_name='users', + query_filter={'id': moderator_id['user_id']} + ) + formatted_new_idea_text = new_idea_text.format( + name=moderator[0]['full_name'], + ) + await bot.send_message( + chat_id=moderator[0]['telegram_id'], + text=formatted_new_idea_text + ) + await bot.send_message( + chat_id=moderator[0]['telegram_id'], + text=notify_text + ) + for file in files: + file_data = await get_data( + table_name='files', + query_filter={'id': file['file_id']} + ) + await bot.send_document( + chat_id=moderator[0]['telegram_id'], + document=FSInputFile(FILES_PATH + file_data[0]['file']) + ) + last_msg = await bot.send_message( + chat_id=moderator[0]['telegram_id'], + text=choose_action_text, + reply_markup=choose_action_ikb( + idea_id=idea_id, + msg_id=0 + ) + ) + await bot.edit_message_reply_markup( + message_id=last_msg.message_id, + chat_id=moderator[0]['telegram_id'], + reply_markup=choose_action_ikb( + idea_id=idea_id, + msg_id=last_msg.message_id + ) + ) + moderator_user_data = await get_data( + table_name='users', + query_filter={'id': moderator_id['user_id']} + ) + await notify( + user_id=moderator_user_data[0]['external_id'], + message='Новая быстрая НеоИдея!' + ) + + except Exception as e: + logging.error(e) + + curators = await get_data( + table_name='user_to_roles', + query_filter={'role_id': ROLE_CURATOR_ID} + ) + for curator in curators: + try: + user_data = await get_data( + table_name='users', + query_filter={'id': curator['user_id']} + ) + await notify( + user_id=user_data[0]['external_id'], + message='Новая быстрая НеоИдея!' + ) + except Exception as e: + logging.error(f'не могу отправить уведомление куратору о новой идее {e}') + + +async def accept_notify(author_name: str, responsible_id: str, author_id: int) -> None: + """ + Уведомляет куратора и автора о принятии идеи + :param responsible_id: ID кому передают идею + :param author_name: Имя автора идеи + :param author_id: ID автора + :return: + """ + responsible_data = await get_data( + table_name='users', + query_filter={'id': responsible_id} + ) + notify_text = notify_curator_text.format( + author_name=author_name, + responsible_name=responsible_data[0]['full_name'] + ) + curators = await get_data( + table_name='user_to_roles', + query_filter={'role_id': ROLE_CURATOR_ID} + ) + + for curator in curators: + try: + user_data = await get_data( + table_name='users', + query_filter={'id': curator['user_id']} + ) + await notify( + user_id=user_data[0]['external_id'], + message=notify_text + ) + await bot.send_message( + chat_id=user_data[0]['telegram_id'], + text=notify_text + ) + except Exception as e: + logging.error(f'не могу отправить уведомление куратору о принятии идеи {e}') + + try: + author_data = await get_data( + table_name='users', + query_filter={'id': author_id} + ) + await notify( + user_id=author_data[0]['external_id'], + message=notify_author_text + ) + await bot.send_message( + chat_id=author_data[0]['telegram_id'], + text=notify_author_text + ) + except Exception as e: + logging.error(f'Не смог отправить уведомление автору идеи {e}') + + +async def confirm_notify(author_name: str, responsible_id: str, author_id: int) -> None: + """ + Уведомляет куратора и автора о согласовании идеи + :param responsible_id: ID кому передают идею + :param author_name: Имя автора идеи + :param author_id: ID автора + :return: + """ + responsible_data = await get_data( + table_name='users', + query_filter={'id': responsible_id} + ) + notify_text = confirm_notify_curator_text.format( + author_name=author_name, + responsible_name=responsible_data[0]['full_name'] + ) + curators = await get_data( + table_name='user_to_roles', + query_filter={'role_id': ROLE_CURATOR_ID} + ) + + for curator in curators: + try: + user_data = await get_data( + table_name='users', + query_filter={'id': curator['user_id']} + ) + await notify( + user_id=user_data[0]['external_id'], + message=notify_text + ) + await bot.send_message( + chat_id=user_data[0]['telegram_id'], + text=notify_text + ) + except Exception as e: + logging.error(f'не могу отправить уведомление куратору о принятии идеи {e}') + + try: + author_data = await get_data( + table_name='users', + query_filter={'id': author_id} + ) + await notify( + user_id=author_data[0]['external_id'], + message=notify_author_text + ) + await bot.send_message( + chat_id=author_data[0]['telegram_id'], + text=notify_author_text + ) + except Exception as e: + logging.error(f'Не смог отправить уведомление автору идеи {e}') \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f91d11f Binary files /dev/null and b/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/utils/__pycache__/__init__.cpython-311.pyc b/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1988dde Binary files /dev/null and b/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/utils/__pycache__/db.cpython-311.pyc b/utils/__pycache__/db.cpython-311.pyc new file mode 100644 index 0000000..a038000 Binary files /dev/null and b/utils/__pycache__/db.cpython-311.pyc differ diff --git a/utils/__pycache__/defs.cpython-311.pyc b/utils/__pycache__/defs.cpython-311.pyc new file mode 100644 index 0000000..e4d44af Binary files /dev/null and b/utils/__pycache__/defs.cpython-311.pyc differ diff --git a/utils/__pycache__/middleware.cpython-311.pyc b/utils/__pycache__/middleware.cpython-311.pyc new file mode 100644 index 0000000..80bed69 Binary files /dev/null and b/utils/__pycache__/middleware.cpython-311.pyc differ diff --git a/utils/db.py b/utils/db.py new file mode 100644 index 0000000..8d73723 --- /dev/null +++ b/utils/db.py @@ -0,0 +1,250 @@ +import logging + +import asyncpg +import redis.asyncio as redis + +from config import POSTGRES_NAME, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT, REDIS_NAME, \ + REDIS_HOST, REDIS_PORT, REDIS_PASSWORD + +create_tables_query = """ + CREATE TABLE IF NOT EXISTS ban_words + (id SERIAL PRIMARY KEY, + word TEXT NOT NULL); + + CREATE TABLE IF NOT EXISTS message + (id SERIAL PRIMARY KEY, + text TEXT NOT NULL, + media TEXT, + buttons TEXT NOT NULL, + included BOOL NOT NULL); + + CREATE TABLE IF NOT EXISTS ban_media + (id SERIAL PRIMARY KEY, + video BOOL NOT NULL, + photo BOOL NOT NULL); +""" + +exist_query = """ +SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1) +""" + + +class Postgres: + def __init__(self): + self.conn = None + + async def connect(self): + self.conn = await asyncpg.connect( + database=POSTGRES_NAME, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD, + host=POSTGRES_HOST, + port=POSTGRES_PORT, + ) + + async def create_tables(self) -> None: + """ + Создаёт таблицы если их нет + :return: + """ + await self.connect() + try: + async with self.conn.transaction(): + message_exist = await self.conn.fetchval( + exist_query, 'message' + ) + ban_media_exist = await self.conn.fetchval( + exist_query, 'ban_media' + ) + await self.conn.execute( + create_tables_query + ) + + if not message_exist: + await self.create_row( + table_name='message', + data_to_insert={ + 'text': 'Сообщение', + 'media': '', + 'buttons': '', + 'included': False + } + ) + + if not ban_media_exist: + await self.create_row( + table_name='ban_media', + data_to_insert={ + 'photo': True, + 'video': True, + } + ) + + except Exception as e: + logging.error(f'Ошибка в create_tables {e}') + finally: + await self.conn.close() + + async def get_data(self, table_name: str, columns='*', query_filter=None) -> list: + """ + Получить данные нужной таблицы по указанным фильтрам + :param columns: Название колонн с нужными данными + :param query_filter: Фильтры запроса в формате {колонка: её значение} + :param table_name: Название таблицы для запроса + :return: False в случае ошибки, словарь с данными в случае успеха + """ + if query_filter is None: + query_filter = {} + await self.connect() + try: + if isinstance(columns, str): + columns = [columns] + full_query = f"SELECT {','.join(columns)} FROM {table_name}" + + if query_filter: + query_filter = ' AND '.join( + [f"{key} = '{value}'" for key, value in query_filter.items()] + ) + full_query += f' WHERE {query_filter}' + + async with self.conn.transaction(): + result = await self.conn.fetch(full_query) + return result + + except Exception as e: + logging.error(f'Ошибка в get_data {e}') + finally: + await self.conn.close() + return [] + + async def update_data(self, new_data: dict, query_filter: dict, table_name: str) -> bool: + """ + Обновляет данные по заданным фильтрам + :param new_data: Новые данные в формате {Колонка: новое значение} + :param query_filter: Фильтры запроса в формате {колонка: её значение} + :param table_name: Название таблицы для запроса + :return: True в случае успеха, False в случае ошибки + """ + await self.connect() + try: + dollar_data = {key: f"${i + 1}" for i, key in enumerate(new_data)} + values = ', '.join(f'{key} = {value}' for key, value in dollar_data.items()) + full_query = f"UPDATE {table_name} SET {values}" + + if query_filter: + query_filter = ' AND '.join([f"{key} = '{value}'" for key, value in query_filter.items()]) + full_query += f' WHERE {query_filter}' + + async with self.conn.transaction(): + await self.conn.execute(full_query, *new_data.values()) + return True + + except Exception as e: + logging.error(f'Ошибка в update_data {e}') + return False + finally: + await self.conn.close() + + async def create_row(self, data_to_insert: dict, table_name: str) -> bool: + """ + Создаёт новую строку с данными + :param data_to_insert: Список, где ключ - название столбика, значение - значение столбика в новой строчке + :param table_name: Название таблицы, куда вставляем данные + :return: id последней вставленной строки + """ + await self.connect() + try: + dollars = [f"${i + 1}" for i in range(len(data_to_insert))] + full_query = f"INSERT INTO {table_name} ({', '.join(data_to_insert.keys())}) VALUES ({', '.join(dollars)})" + async with self.conn.transaction(): + await self.conn.execute(full_query, *data_to_insert.values()) + return True + + except Exception as e: + logging.error(f'Ошибка в create_row {e}') + return False + finally: + await self.conn.close() + + async def query(self, query_text: str): + """ + Прямой запрос к бд + :param query_text: sql запрос + :return: Результат sql запроса + """ + await self.connect() + try: + async with self.conn.transaction(): + await self.conn.execute(query_text) + + except Exception as e: + logging.error(f'Ошибка в query {e}') + finally: + await self.conn.close() + + +class Redis: + def __init__(self): + self.conn = None + + async def connect(self): + try: + self.conn = await redis.Redis( + host=REDIS_HOST, + port=REDIS_PORT, + db=REDIS_NAME, + password=REDIS_PASSWORD, + decode_responses=True, + encoding='utf-8' + ) + except Exception as e: + logging.error('redis connect', e) + + async def delete_key(self, *keys: str | int) -> str | int: + await self.connect() + try: + return await self.conn.delete(*keys) + except Exception as e: + logging.error('redis delete_key', e) + finally: + await self.conn.close() + + async def update_list(self, key: str | int, *values) -> str | int: + await self.connect() + try: + return await self.conn.rpush(key, *values) + except Exception as e: + logging.error('redis update_data', e) + finally: + await self.conn.close() + + async def get_list(self, key: str | int) -> list: + await self.connect() + try: + data = await self.conn.lrange(name=str(key), start=0, end=-1) + return data + except Exception as e: + logging.error('redis get_data', e) + return [] + finally: + await self.conn.close() + + async def update_dict(self, key: str | int, value: dict) -> str | int: + await self.connect() + try: + return await self.conn.hset(name=str(key), mapping=value) + except Exception as e: + logging.error('redis update', e) + finally: + await self.conn.close() + + async def get_dict(self, key: str | int) -> dict: + await self.connect() + try: + data = await self.conn.hgetall(name=str(key)) + return data + except Exception as e: + logging.error('redis get', e) + return [] + finally: + await self.conn.close() diff --git a/utils/defs.py b/utils/defs.py new file mode 100644 index 0000000..12c9254 --- /dev/null +++ b/utils/defs.py @@ -0,0 +1,44 @@ +import logging + +from aiogram.types import Message +import pandas + +from utils.db import Postgres + + +async def delete_msg(msg: Message) -> None: + """ + Безопасно удаляет сообщение + :param msg: Message + :return: True, если текст является ссылкой, иначе False. + """ + try: + await msg.delete() + except: + pass + + +async def create_xlsx() -> str: + """ + Составляет xlsx файл с данными бан слова + :return: Путь к готовому xlsx файлу + """ + try: + p = Postgres() + ban_words = await p.get_data( + table_name='ban_words' + ) + table_dict = {'ID': [], 'Слово': []} + + for ban_word in ban_words: + table_dict['ID'].append(ban_word['id']) + table_dict['Слово'].append(ban_word['word']) + + df = pandas.DataFrame(table_dict) + file_path = 'data/ban_words.xlsx' + df.to_excel(file_path, index=False) + + return file_path + + except Exception as e: + logging.error('Ошибка в create_xlsx', e) \ No newline at end of file diff --git a/utils/middleware.py b/utils/middleware.py new file mode 100644 index 0000000..44c8b83 --- /dev/null +++ b/utils/middleware.py @@ -0,0 +1,73 @@ +# from aiogram import BaseMiddleware +# from aiogram.types import TelegramObject, Update +# +# from typing import Callable, Dict, Any, Awaitable +# from utils.defs import delete_msg +# from utils.db import Postgres, Redis +# from templates.message import send_preview +# +# +# class DeleteMessage(BaseMiddleware): +# """ +# Мидлвари удаляющая сообщения с бан вордами +# """ +# +# async def __call__( +# self, +# handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], +# event: Update, +# data: Dict[str, Any] +# ) -> Any: +# if not event.message: +# return await handler(event, data) +# if not (event.message.chat.type == 'group' or +# event.message.chat.type == 'supergroup'): +# return await handler(event, data) +# +# ban = False +# r = Redis() +# p = Postgres() +# +# ban_words = await r.get_list( +# key='ban_words' +# ) +# if not ban_words: +# ban_words = await p.get_data( +# table_name='ban_words' +# ) +# ban_words = [word['word'] for word in ban_words] +# await r.delete_key( +# 'ban_words' +# ) +# await r.update_list( +# 'ban_words', +# *ban_words +# ) +# +# for ban_word in ban_words: +# print(ban_word) +# if ban_word in event.message.text.lower(): +# print(event.message.text.lower(), 'нашёл') +# ban = True +# +# if ban: +# await delete_msg( +# msg=event.message +# ) +# message_data = await p.get_data( +# table_name='message' +# ) +# +# if event.message.from_user.username: +# username = f'@{event.message.from_user.username}' +# else: +# username = event.message.from_user.full_name +# +# if message_data[0]['included']: +# await send_preview( +# chat_id=event.message.chat.id, +# message_data=message_data[0], +# username=username +# ) +# +# return await handler(event, data)