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)