Initial commit
This commit is contained in:
+13
@@ -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"]
|
||||
@@ -1,3 +1,35 @@
|
||||
# chat_moderator_bot
|
||||
## Бот транскрибатор
|
||||
|
||||
Удаляет сообщения из группы с бан-вордами. После, скидывает изменяемое в админ-панели сообщение
|
||||
### Конфигурация .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
|
||||
```
|
||||
|
||||
@@ -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='Меню'
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
from aiogram import Bot
|
||||
|
||||
from config import BOT_TOKEN
|
||||
|
||||
bot = Bot(
|
||||
token=BOT_TOKEN,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
Binary file not shown.
@@ -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:
|
||||
@@ -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]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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={}
|
||||
)
|
||||
@@ -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())
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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 = """
|
||||
Сообщение включено: <b>{include}</b>
|
||||
|
||||
Выберите действие:
|
||||
"""
|
||||
|
||||
send_message_text = """
|
||||
Отправьте сообщение с прикреплением до одного фото:
|
||||
"""
|
||||
|
||||
send_buttons_text = """
|
||||
Отправьте кнопки в таком формате:
|
||||
|
||||
<b>Кнопка в первом ряду - http://example.com</b>
|
||||
<b>Кнопка во втором ряду - http://example.com</b>
|
||||
|
||||
Используйте разделитель " | ", чтобы добавить до 8 кнопок в один ряд (допустимо 6 рядов):
|
||||
<b>Кнопка в ряду - http://example.com | Другая кнопка в ряду - http://example.com</b>
|
||||
"""
|
||||
|
||||
incorrect_data_text = """
|
||||
<b>Не верный формат данных</b>
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -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 = """
|
||||
<b>Автор:</b> {author_name}
|
||||
<b>Дата:</b> {creation_date}
|
||||
<b>Подразделение:</b> {department}
|
||||
<b>Категория:</b> {category}
|
||||
<b>Город:</b> {city}
|
||||
<b>Название:</b> «{idea_name}»
|
||||
<b>Содержание:</b> {idea_content}
|
||||
"""
|
||||
|
||||
choose_action_text = """
|
||||
Пожалуйста, выберите одно из следующих действий:
|
||||
"""
|
||||
|
||||
confirm_action_text = """
|
||||
<b>Вы согласовываете НеоИдею:</b>
|
||||
«{idea_name}».
|
||||
|
||||
<b>Автор:</b> {author_name}
|
||||
"""
|
||||
|
||||
choose_responsible_text = """
|
||||
Пожалуйста, выберите <b>Ответственного за категорию</b> для передачи НеоИдеи в работу.
|
||||
"""
|
||||
|
||||
send_date_text = """
|
||||
В календаре укажите крайние сроки выполнения:
|
||||
"""
|
||||
|
||||
confirm_responsible_date_text = """
|
||||
Вы передаёте НеоИдею в работу.
|
||||
<b>Название:</b> «{idea_name}».
|
||||
|
||||
<b>Автор:</b> {author_name}
|
||||
<b>Ответственный за категорию:</b> {responsible_name}
|
||||
<b>Крайние сроки выполнения:</b> {date}
|
||||
"""
|
||||
|
||||
confirm_action_send_text = """
|
||||
Спасибо! Вы передали НеоИдею в работу.
|
||||
"""
|
||||
|
||||
revision_action_text = """
|
||||
<b>Вы возвращаете на доработку НеоИдею:</b>
|
||||
«{idea_name}».
|
||||
<b>Автор:</b> {author_name}
|
||||
|
||||
Оставьте комментарий для того, чтобы отправить НеоИдею на доработку.
|
||||
"""
|
||||
|
||||
send_revision_comment_text = """
|
||||
Оставьте комментарий для того, чтобы отправить НеоИдею на доработку
|
||||
"""
|
||||
|
||||
send_comment_text = """
|
||||
Пожалуйста, оставьте свой комментарий:
|
||||
"""
|
||||
|
||||
check_comment = """
|
||||
Проверьте правильность введенных данных:
|
||||
|
||||
{comment}
|
||||
"""
|
||||
|
||||
revision_action_send_text = """
|
||||
Вы отправили НеоИдею на доработку.
|
||||
"""
|
||||
|
||||
grade_action_text = """
|
||||
<b>Вы направляете НеоИдею на оценку:</b>
|
||||
«{idea_name}».
|
||||
<b>Автор:</b> {author_name}
|
||||
|
||||
НеоИдея будет передана Куратору.
|
||||
"""
|
||||
|
||||
grade_action_send_text = """
|
||||
Спасибо! НеоИдея передана Куратору.
|
||||
"""
|
||||
|
||||
reject_action_text = """
|
||||
<b>Вы отклоняете НеоИдею:</b>
|
||||
«{idea_name}».
|
||||
<b>Автор:</b> {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}')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+250
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user