363 lines
14 KiB
Python
363 lines
14 KiB
Python
|
import asyncio
|
|||
|
from asyncio import Lock
|
|||
|
import asyncpg
|
|||
|
from asyncpg import create_pool
|
|||
|
from aiogram import Bot, Dispatcher, types, F
|
|||
|
from aiogram.filters import Command
|
|||
|
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove
|
|||
|
from dotenv import load_dotenv
|
|||
|
import uuid
|
|||
|
import hashlib
|
|||
|
import ast
|
|||
|
import os
|
|||
|
import logging
|
|||
|
|
|||
|
logging.basicConfig(level=logging.DEBUG) # DEBUG показывает максимум информации
|
|||
|
|
|||
|
# Загрузка данных из .env
|
|||
|
load_dotenv()
|
|||
|
|
|||
|
db_lock = Lock()
|
|||
|
|
|||
|
db_pool2 = None
|
|||
|
|
|||
|
|
|||
|
# Асинхронная функция для логирования состояния пула
|
|||
|
async def log_pool_state():
|
|||
|
try:
|
|||
|
active_connections = len(db_pool2._holders) # Занятые соединения
|
|||
|
free_connections = db_pool2._queue.qsize() # Свободные соединения
|
|||
|
logging.info(
|
|||
|
f"Пул соединений db_pool2: Активных соединений: {active_connections}, Свободных соединений: {free_connections}"
|
|||
|
)
|
|||
|
except Exception as e:
|
|||
|
logging.error(f"Ошибка при логировании состояния пула db_pool2: {e}")
|
|||
|
|
|||
|
|
|||
|
DATABASE_URL = os.getenv("DATABASE_URL")
|
|||
|
|
|||
|
|
|||
|
# Асинхронная функция для создания пула подключения
|
|||
|
async def get_db_pool2():
|
|||
|
try:
|
|||
|
if not DATABASE_URL:
|
|||
|
raise ValueError("DATABASE_URL не задана.")
|
|||
|
|
|||
|
return await asyncpg.create_pool(DATABASE_URL, max_size=10)
|
|||
|
except Exception as e:
|
|||
|
logging.error(f"Ошибка при подключении к базе данных: {e}")
|
|||
|
raise
|
|||
|
|
|||
|
|
|||
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
|||
|
GROUP_ID = os.getenv("GROUP_ID") # Telegram ID администратора
|
|||
|
|
|||
|
# Инициализация бота и диспетчера
|
|||
|
bot = Bot(token=BOT_TOKEN)
|
|||
|
dp = Dispatcher()
|
|||
|
|
|||
|
|
|||
|
# Регистрация пользователя с созданием нового топика
|
|||
|
async def register_user(telegram_id):
|
|||
|
"""
|
|||
|
Регистрирует пользователя, создавая анонимный ID и топик для взаимодействия.
|
|||
|
"""
|
|||
|
try:
|
|||
|
logging.info(f"Регистрация пользователя с Telegram ID: {telegram_id}")
|
|||
|
async with db_pool2.acquire() as conn:
|
|||
|
# Проверяем, зарегистрирован ли пользователь
|
|||
|
result = await conn.fetchrow(
|
|||
|
"SELECT anon_id, topic_id FROM an_users WHERE telegram_id = $1",
|
|||
|
telegram_id,
|
|||
|
)
|
|||
|
if result:
|
|||
|
logging.info(f"Пользователь {telegram_id} уже зарегистрирован.")
|
|||
|
return result["anon_id"], result["topic_id"]
|
|||
|
|
|||
|
# Генерация анонимного ID
|
|||
|
anon_id = str(uuid.uuid4())
|
|||
|
|
|||
|
# Создание нового топика
|
|||
|
topic_title = f"Чат {anon_id[:4]}"
|
|||
|
topic_result = await bot.create_forum_topic(
|
|||
|
chat_id=GROUP_ID, name=topic_title
|
|||
|
)
|
|||
|
topic_id = topic_result.message_thread_id
|
|||
|
|
|||
|
# Сохранение в базе данных
|
|||
|
await conn.execute(
|
|||
|
"INSERT INTO an_users (telegram_id, anon_id, topic_id) VALUES ($1, $2, $3)",
|
|||
|
telegram_id,
|
|||
|
anon_id,
|
|||
|
topic_id,
|
|||
|
)
|
|||
|
logging.info(
|
|||
|
f"Создан новый топик с ID {topic_id} для пользователя {telegram_id}."
|
|||
|
)
|
|||
|
return anon_id, topic_id
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logging.error(f"Ошибка при регистрации пользователя {telegram_id}: {e}")
|
|||
|
return None, None
|
|||
|
|
|||
|
|
|||
|
# Получение Telegram ID по анонимному ID
|
|||
|
async def get_telegram_id(anon_id):
|
|||
|
async with db_pool2.acquire() as conn:
|
|||
|
result = await conn.fetchrow(
|
|||
|
"SELECT telegram_id FROM an_users WHERE anon_id = $1", anon_id
|
|||
|
)
|
|||
|
return result["telegram_id"] if result else None
|
|||
|
|
|||
|
|
|||
|
@dp.message(Command("start"))
|
|||
|
async def start_command(message: types.Message):
|
|||
|
|
|||
|
anon_id, topic_id = await register_user(message.from_user.id)
|
|||
|
|
|||
|
# Создаем кнопки
|
|||
|
keyboard = ReplyKeyboardMarkup(
|
|||
|
keyboard=[
|
|||
|
[KeyboardButton(text="Написать волонтер_ке")],
|
|||
|
[KeyboardButton(text="Заполнить форму")],
|
|||
|
],
|
|||
|
resize_keyboard=True,
|
|||
|
)
|
|||
|
|
|||
|
await message.answer(
|
|||
|
"Привет 👋! Выбери ниже то, что тебе нужно. ✨\n Если мы не ответили тебе втечение 24 ч, то напиши нам на почту: rloveplus@proton.me или в Matrix Element: @br:bark.lgbt \n А так же желаем хорошего тебе дня 💖",
|
|||
|
reply_markup=keyboard,
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
# Хэндлер для кнопки "Заполнить форму"
|
|||
|
@dp.message(F.text == "Заполнить форму")
|
|||
|
async def fill_form(message: types.Message):
|
|||
|
form_url = "https://t.me/InstantFormsBot/form?startapp=9dd0908d-3c39-45cb-afe1-7f4004aa8fc6&startApp=9dd0908d-3c39-45cb-afe1-7f4004aa8fc6" # Ссылка на вашу форму
|
|||
|
await message.answer(f"[Заполнить форму]({form_url})", parse_mode="Markdown")
|
|||
|
|
|||
|
|
|||
|
@dp.message(F.text == "Написать волонтер_ке")
|
|||
|
async def contact_volunteer(message: types.Message):
|
|||
|
try:
|
|||
|
|
|||
|
# Получаем номер строки в базе
|
|||
|
async with db_pool2.acquire() as conn:
|
|||
|
result = await conn.fetchrow(
|
|||
|
"SELECT id FROM an_users WHERE telegram_id = $1", message.from_user.id
|
|||
|
)
|
|||
|
|
|||
|
if result:
|
|||
|
user_number = result["id"]
|
|||
|
topic_name = f"{user_number}"
|
|||
|
|
|||
|
await message.answer(
|
|||
|
f"Добро пожаловать! Здесь ты можешь написать нам. Ты будешь общаться с администраторами через бота, поэтому ты останешься для них анонимными.",
|
|||
|
reply_markup=ReplyKeyboardRemove(),
|
|||
|
)
|
|||
|
except Exception as e:
|
|||
|
logging.error(f"Ошибка при создании топика: {e}")
|
|||
|
|
|||
|
|
|||
|
# Обработчик сообщений от пользователя
|
|||
|
@dp.message(F.chat.type == "private")
|
|||
|
async def handle_user_message(message: types.Message):
|
|||
|
logging.info(
|
|||
|
f"Сообщение от {message.from_user.id}: {message.text or 'мультимедиа'}"
|
|||
|
)
|
|||
|
|
|||
|
# Регистрируем пользователя и получаем данные
|
|||
|
anon_id, topic_id = await register_user(message.from_user.id)
|
|||
|
|
|||
|
try:
|
|||
|
# Формируем идентификатор для анонимности
|
|||
|
user_tag = f"Сообщение от {str(anon_id)[:4]}:"
|
|||
|
|
|||
|
# Отправляем разные типы сообщений
|
|||
|
if message.text:
|
|||
|
forward_message = f"{user_tag}\n{message.text}"
|
|||
|
await bot.send_message(
|
|||
|
chat_id=GROUP_ID, message_thread_id=topic_id, text=forward_message
|
|||
|
)
|
|||
|
elif message.photo:
|
|||
|
await bot.send_photo(
|
|||
|
chat_id=GROUP_ID,
|
|||
|
message_thread_id=topic_id,
|
|||
|
photo=message.photo[-1].file_id,
|
|||
|
caption=f"{user_tag}\n{message.caption or ''}",
|
|||
|
)
|
|||
|
elif message.video:
|
|||
|
await bot.send_video(
|
|||
|
chat_id=GROUP_ID,
|
|||
|
message_thread_id=topic_id,
|
|||
|
video=message.video.file_id,
|
|||
|
caption=f"{user_tag}\n{message.caption or ''}",
|
|||
|
)
|
|||
|
elif message.document:
|
|||
|
await bot.send_document(
|
|||
|
chat_id=GROUP_ID,
|
|||
|
message_thread_id=topic_id,
|
|||
|
document=message.document.file_id,
|
|||
|
caption=f"{user_tag}\n{message.caption or ''}",
|
|||
|
)
|
|||
|
elif message.audio:
|
|||
|
await bot.send_audio(
|
|||
|
chat_id=GROUP_ID,
|
|||
|
message_thread_id=topic_id,
|
|||
|
audio=message.audio.file_id,
|
|||
|
caption=f"{user_tag}\n{message.caption or ''}",
|
|||
|
)
|
|||
|
elif message.voice:
|
|||
|
await bot.send_voice(
|
|||
|
chat_id=GROUP_ID,
|
|||
|
message_thread_id=topic_id,
|
|||
|
voice=message.voice.file_id,
|
|||
|
caption=user_tag,
|
|||
|
)
|
|||
|
else:
|
|||
|
await bot.send_message(
|
|||
|
chat_id=GROUP_ID,
|
|||
|
message_thread_id=topic_id,
|
|||
|
text=f"{user_tag}\nТип сообщения пока не поддерживается.",
|
|||
|
)
|
|||
|
|
|||
|
# Уведомляем пользователя
|
|||
|
await message.answer("Сообщение отправлено!")
|
|||
|
logging.info(f"Сообщение от {message.from_user.id} успешно переслано в топик.")
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logging.error(f"Ошибка при обработке сообщения от пользователя: {e}")
|
|||
|
await message.answer("Произошла ошибка при отправке сообщения.")
|
|||
|
|
|||
|
|
|||
|
# Обработка новых сообщений администратора
|
|||
|
@dp.message(F.chat.type.in_(["group", "supergroup"]) & ~F.text.startswith("/"))
|
|||
|
async def handle_admin_reply(message: types.Message):
|
|||
|
"""
|
|||
|
Обрабатывает только текстовые сообщения от администратора в топиках группы,
|
|||
|
игнорируя команды.
|
|||
|
"""
|
|||
|
if not any(
|
|||
|
[
|
|||
|
message.text,
|
|||
|
message.photo,
|
|||
|
message.video,
|
|||
|
message.document,
|
|||
|
message.audio,
|
|||
|
message.voice,
|
|||
|
]
|
|||
|
):
|
|||
|
logging.info(f"Игнорирование пустого сообщения или команды: {message.text}")
|
|||
|
return
|
|||
|
|
|||
|
# Обрабатываем сообщение
|
|||
|
await process_admin_message(message)
|
|||
|
|
|||
|
|
|||
|
# Обработка редактирования сообщений администратора
|
|||
|
@dp.edited_message(F.chat.type.in_(["group", "supergroup"]))
|
|||
|
async def handle_admin_edited_message(message: types.Message):
|
|||
|
"""
|
|||
|
Обрабатывает редактированные сообщения от администратора в топиках группы.
|
|||
|
"""
|
|||
|
topic_id = message.message_thread_id
|
|||
|
|
|||
|
# Проверяем, существует ли topic_id в базе
|
|||
|
async with db_pool2.acquire() as conn:
|
|||
|
result = await conn.fetchrow(
|
|||
|
"SELECT telegram_id FROM an_users WHERE topic_id = $1", topic_id
|
|||
|
)
|
|||
|
|
|||
|
if not result:
|
|||
|
logging.warning(
|
|||
|
f"Редактирование сообщения в несуществующем топике: {topic_id}. Игнорируем."
|
|||
|
)
|
|||
|
return # Игнорируем редактирование, если пользователь не зарегистрирован
|
|||
|
|
|||
|
# Если пользователь найден, продолжаем обработку
|
|||
|
await process_admin_message(message)
|
|||
|
|
|||
|
|
|||
|
# Общая функция обработки сообщений
|
|||
|
async def process_admin_message(message: types.Message):
|
|||
|
"""
|
|||
|
Обрабатывает как новые, так и редактированные сообщения от администратора.
|
|||
|
"""
|
|||
|
logging.info(
|
|||
|
f"Обработка сообщения от администратора: {message.text}, чат: {message.chat.id}"
|
|||
|
)
|
|||
|
# Получаем topic_id из текущего чата
|
|||
|
topic_id = message.message_thread_id
|
|||
|
|
|||
|
# Находим telegram_id пользователя, связанного с этим topic_id
|
|||
|
async with db_pool2.acquire() as conn:
|
|||
|
result = await conn.fetchrow(
|
|||
|
"SELECT telegram_id FROM an_users WHERE topic_id = $1", topic_id
|
|||
|
)
|
|||
|
|
|||
|
try:
|
|||
|
telegram_id = result["telegram_id"]
|
|||
|
|
|||
|
# Пересылаем сообщение пользователю в зависимости от типа контента
|
|||
|
if message.photo:
|
|||
|
await bot.send_photo(
|
|||
|
chat_id=telegram_id,
|
|||
|
photo=message.photo[-1].file_id,
|
|||
|
caption=message.caption,
|
|||
|
)
|
|||
|
elif message.video:
|
|||
|
await bot.send_video(
|
|||
|
chat_id=telegram_id,
|
|||
|
video=message.video.file_id,
|
|||
|
caption=message.caption,
|
|||
|
)
|
|||
|
elif message.document:
|
|||
|
await bot.send_document(
|
|||
|
chat_id=telegram_id,
|
|||
|
document=message.document.file_id,
|
|||
|
caption=message.caption,
|
|||
|
)
|
|||
|
elif message.audio:
|
|||
|
await bot.send_audio(
|
|||
|
chat_id=telegram_id,
|
|||
|
audio=message.audio.file_id,
|
|||
|
caption=message.caption,
|
|||
|
)
|
|||
|
elif message.voice:
|
|||
|
await bot.send_voice(
|
|||
|
chat_id=telegram_id,
|
|||
|
voice=message.voice.file_id,
|
|||
|
caption=message.caption,
|
|||
|
)
|
|||
|
elif message.text:
|
|||
|
await bot.send_message(
|
|||
|
chat_id=telegram_id, text=f"Ответ волонтёр_ки:\n\n{message.text}"
|
|||
|
)
|
|||
|
|
|||
|
logging.info(f"Ответ успешно отправлен пользователю с ID {telegram_id}")
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logging.error(f"Ошибка при обработке ответа администратора: {e}")
|
|||
|
await message.reply("Произошла ошибка при отправке ответа пользователю.")
|
|||
|
|
|||
|
|
|||
|
# Инициализация пула и запуск бота
|
|||
|
async def main():
|
|||
|
global db_pool2
|
|||
|
try:
|
|||
|
db_pool2 = await get_db_pool2() # Создание пула
|
|||
|
logging.info("Пул db_pool2 успешно создан")
|
|||
|
|
|||
|
# Тестовый запрос
|
|||
|
async with db_pool2.acquire() as conn:
|
|||
|
await conn.execute("SELECT 1")
|
|||
|
logging.info("Подключение к базе данных успешно установлено")
|
|||
|
|
|||
|
await log_pool_state() # Логирование состояния пула
|
|||
|
await dp.start_polling(bot)
|
|||
|
except Exception as e:
|
|||
|
logging.error(f"Ошибка при подключении к базе данных: {e}")
|
|||
|
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
asyncio.run(main())
|