forked from Lolz/Auto-Pump
Add files via upload
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
|||||||
|
import aiohttp
|
||||||
|
import re
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BumpResult:
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
thread_id: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ThreadInfo:
|
||||||
|
thread_id: str
|
||||||
|
title: str
|
||||||
|
|
||||||
|
|
||||||
|
class APIClient:
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, auth_token: str):
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
self.auth_token = auth_token
|
||||||
|
self.session: Optional[aiohttp.ClientSession] = None
|
||||||
|
|
||||||
|
def _clean_error_message(self, error_msg: str) -> str:
|
||||||
|
error_msg = re.sub(r'<br\s*/?>', '\n', error_msg)
|
||||||
|
error_msg = re.sub(r'<[^>]+>', '', error_msg)
|
||||||
|
|
||||||
|
parts = [p.strip() for p in error_msg.split('\n') if p.strip()]
|
||||||
|
|
||||||
|
if len(parts) > 1:
|
||||||
|
for part in parts:
|
||||||
|
if 'должны подождать' in part.lower() or 'должен подождать' in part.lower():
|
||||||
|
return part
|
||||||
|
return parts[-1]
|
||||||
|
|
||||||
|
return error_msg.strip()
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
if self.session is None or self.session.closed:
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": f"Bearer {self.auth_token}",
|
||||||
|
"User-Agent": "AutoBumpBot/3.0",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
timeout = aiohttp.ClientTimeout(total=30)
|
||||||
|
self.session = aiohttp.ClientSession(headers=headers, timeout=timeout)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
if self.session and not self.session.closed:
|
||||||
|
await self.session.close()
|
||||||
|
|
||||||
|
async def get_thread_info(self, thread_id: str) -> Optional[ThreadInfo]:
|
||||||
|
if not self.session:
|
||||||
|
await self.start()
|
||||||
|
|
||||||
|
url = f"{self.base_url}/threads/{thread_id}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self.session.get(url) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
thread_data = data.get("thread", {})
|
||||||
|
return ThreadInfo(
|
||||||
|
thread_id=thread_id,
|
||||||
|
title=thread_data.get("thread_title", "Unknown")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting thread info {thread_id}: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def bump_thread(self, thread_id: str) -> BumpResult:
|
||||||
|
if not self.session:
|
||||||
|
await self.start()
|
||||||
|
|
||||||
|
url = f"{self.base_url}/threads/{thread_id}/bump"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self.session.post(url) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
if "errors" in data and data["errors"]:
|
||||||
|
error_msg = data["errors"][0]
|
||||||
|
cleaned_msg = self._clean_error_message(error_msg)
|
||||||
|
return BumpResult(
|
||||||
|
success=False,
|
||||||
|
message=f"Тема {thread_id}: {cleaned_msg}",
|
||||||
|
thread_id=thread_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.status == 200:
|
||||||
|
return BumpResult(
|
||||||
|
success=True,
|
||||||
|
message=f"✅ Тема {thread_id} поднята успешно",
|
||||||
|
thread_id=thread_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return BumpResult(
|
||||||
|
success=False,
|
||||||
|
message=f"Тема {thread_id}: HTTP {resp.status}",
|
||||||
|
thread_id=thread_id
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return BumpResult(
|
||||||
|
success=False,
|
||||||
|
message=f"Тема {thread_id}: Ошибка - {str(e)}",
|
||||||
|
thread_id=thread_id
|
||||||
|
)
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from aiogram import Bot, Dispatcher, F
|
||||||
|
from aiogram.filters import Command, StateFilter
|
||||||
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
|
|
||||||
|
from config_manager import Config
|
||||||
|
from api_client import APIClient
|
||||||
|
from database import Database
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
logging.FileHandler('bot.log', encoding='utf-8')
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BotStates(StatesGroup):
|
||||||
|
waiting_for_threads = State()
|
||||||
|
|
||||||
|
|
||||||
|
class AutoBumpBot:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.config = Config.load()
|
||||||
|
self.bot = Bot(token=self.config.bot_token)
|
||||||
|
self.storage = MemoryStorage()
|
||||||
|
self.dp = Dispatcher(storage=self.storage)
|
||||||
|
self.api = APIClient(self.config.api_base_url, self.config.api_token)
|
||||||
|
self.db = Database(self.config.db_path)
|
||||||
|
self.is_running = False
|
||||||
|
self.total_bumps = 0
|
||||||
|
self.successful_bumps = 0
|
||||||
|
self.start_time = None
|
||||||
|
|
||||||
|
self.dp.message.register(self.cmd_start, Command("start"))
|
||||||
|
self.dp.callback_query.register(self.cb_add_thread, F.data == "add_thread")
|
||||||
|
self.dp.callback_query.register(self.cb_list_threads, F.data == "list_threads")
|
||||||
|
self.dp.callback_query.register(self.cb_delete_menu, F.data == "delete_menu")
|
||||||
|
self.dp.callback_query.register(self.cb_delete_thread, F.data.startswith("delete_"))
|
||||||
|
self.dp.callback_query.register(self.cb_bump_now, F.data == "bump_now")
|
||||||
|
self.dp.callback_query.register(self.cb_stats, F.data == "stats")
|
||||||
|
self.dp.message.register(self.msg_add_threads, StateFilter(BotStates.waiting_for_threads))
|
||||||
|
|
||||||
|
def get_main_keyboard(self) -> InlineKeyboardMarkup:
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="➕ Добавить темы", callback_data="add_thread"),
|
||||||
|
InlineKeyboardButton(text="📋 Список тем", callback_data="list_threads")
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="🗑️ Удалить тему", callback_data="delete_menu"),
|
||||||
|
InlineKeyboardButton(text="🚀 Поднять темы", callback_data="bump_now")
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="📊 Статистика", callback_data="stats"),
|
||||||
|
InlineKeyboardButton(text="👤 Автор", url="https://lolz.live/kqlol/")
|
||||||
|
]
|
||||||
|
])
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
async def send_main_menu(self, chat_id: int, text: str = None):
|
||||||
|
if text is None:
|
||||||
|
text = "Выберите действие:"
|
||||||
|
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id,
|
||||||
|
text,
|
||||||
|
reply_markup=self.get_main_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def cmd_start(self, message: Message):
|
||||||
|
await message.answer_photo(
|
||||||
|
photo="https://wallpapers-clan.com/wp-content/uploads/2024/04/dark-anime-girl-with-red-eyes-desktop-wallpaper-preview.jpg",
|
||||||
|
caption=(
|
||||||
|
"🤖 <b>QIYANA AUTO-BUMP BOT</b>\n\n"
|
||||||
|
"Бот для автоматического поднятия тем на Lolz.live\n\n"
|
||||||
|
f"⏰ Автоподнятие каждые <b>{self.config.bump_interval_hours}ч</b>"
|
||||||
|
),
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
await self.send_main_menu(message.chat.id)
|
||||||
|
|
||||||
|
async def cb_add_thread(self, callback: CallbackQuery, state: FSMContext):
|
||||||
|
await callback.answer()
|
||||||
|
await callback.message.answer(
|
||||||
|
"📝 Введите ID тем через запятую для добавления:\n"
|
||||||
|
"Пример: <code>12345, 67890, 11111</code>",
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
await state.set_state(BotStates.waiting_for_threads)
|
||||||
|
|
||||||
|
async def msg_add_threads(self, message: Message, state: FSMContext):
|
||||||
|
try:
|
||||||
|
thread_ids = [tid.strip() for tid in message.text.split(',') if tid.strip()]
|
||||||
|
|
||||||
|
if not thread_ids:
|
||||||
|
await message.answer("❌ Не указаны ID тем")
|
||||||
|
return
|
||||||
|
|
||||||
|
status_msg = await message.answer(f"⏳ Добавляю {len(thread_ids)} тем...")
|
||||||
|
|
||||||
|
added = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for thread_id in thread_ids:
|
||||||
|
if not thread_id.isdigit():
|
||||||
|
errors.append(f"❌ {thread_id} - неверный формат")
|
||||||
|
continue
|
||||||
|
|
||||||
|
thread_info = await self.api.get_thread_info(thread_id)
|
||||||
|
|
||||||
|
if not thread_info:
|
||||||
|
errors.append(f"❌ {thread_id} - не найдена")
|
||||||
|
continue
|
||||||
|
|
||||||
|
success = await self.db.add_thread(thread_id, thread_info.title)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
added.append(f"✅ {thread_id} - {thread_info.title}")
|
||||||
|
else:
|
||||||
|
errors.append(f"⚠️ {thread_id} - уже добавлена")
|
||||||
|
|
||||||
|
result = f"📊 Результат добавления:\n\n"
|
||||||
|
|
||||||
|
if added:
|
||||||
|
result += "✅ <b>Добавлено:</b>\n" + "\n".join(added) + "\n\n"
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
result += "❌ <b>Ошибки:</b>\n" + "\n".join(errors)
|
||||||
|
|
||||||
|
await status_msg.edit_text(result, parse_mode="HTML")
|
||||||
|
await self.send_main_menu(message.chat.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding threads: {e}", exc_info=True)
|
||||||
|
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||||
|
finally:
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
async def cb_list_threads(self, callback: CallbackQuery):
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
threads = await self.db.get_all_threads()
|
||||||
|
|
||||||
|
if not threads:
|
||||||
|
await callback.message.answer("📭 Список тем пуст\n\nДобавьте темы через кнопку ➕")
|
||||||
|
await self.send_main_menu(callback.message.chat.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = f"📋 <b>Список тем ({len(threads)}):</b>\n\n"
|
||||||
|
|
||||||
|
for thread in threads:
|
||||||
|
status = "🆕 Новая"
|
||||||
|
if thread.last_bumped:
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(thread.last_bumped.replace('Z', '+00:00'))
|
||||||
|
status = f"✅ {dt.strftime('%d.%m.%Y %H:%M')}"
|
||||||
|
except:
|
||||||
|
status = f"✅ {thread.last_bumped[:16]}"
|
||||||
|
|
||||||
|
text += f"<b>{thread.id}</b> - {thread.title}\n{status}\n\n"
|
||||||
|
|
||||||
|
await callback.message.answer(text, parse_mode="HTML")
|
||||||
|
await self.send_main_menu(callback.message.chat.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing threads: {e}", exc_info=True)
|
||||||
|
await callback.message.answer(f"❌ Ошибка: {str(e)}")
|
||||||
|
|
||||||
|
async def cb_delete_menu(self, callback: CallbackQuery):
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
threads = await self.db.get_all_threads()
|
||||||
|
|
||||||
|
if not threads:
|
||||||
|
await callback.message.answer("📭 Список тем пуст")
|
||||||
|
await self.send_main_menu(callback.message.chat.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
buttons = []
|
||||||
|
for thread in threads:
|
||||||
|
button_text = f"{thread.id} - {thread.title[:30]}"
|
||||||
|
buttons.append([
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=button_text,
|
||||||
|
callback_data=f"delete_{thread.id}"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
|
||||||
|
await callback.message.answer(
|
||||||
|
"🗑️ Выберите тему для удаления:",
|
||||||
|
reply_markup=keyboard
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error showing delete menu: {e}", exc_info=True)
|
||||||
|
await callback.message.answer(f"❌ Ошибка: {str(e)}")
|
||||||
|
|
||||||
|
async def cb_delete_thread(self, callback: CallbackQuery):
|
||||||
|
try:
|
||||||
|
thread_id = callback.data.split('_')[1]
|
||||||
|
success = await self.db.delete_thread(thread_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await callback.answer(f"✅ Тема {thread_id} удалена", show_alert=True)
|
||||||
|
await callback.message.answer(f"✅ Тема {thread_id} успешно удалена из базы данных")
|
||||||
|
await self.send_main_menu(callback.message.chat.id)
|
||||||
|
else:
|
||||||
|
await callback.answer(f"❌ Тема {thread_id} не найдена", show_alert=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting thread: {e}", exc_info=True)
|
||||||
|
await callback.answer(f"❌ Ошибка: {str(e)}", show_alert=True)
|
||||||
|
|
||||||
|
async def cb_bump_now(self, callback: CallbackQuery):
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
threads = await self.db.get_threads_to_bump(0)
|
||||||
|
|
||||||
|
if not threads:
|
||||||
|
await callback.message.answer("📭 Нет тем для поднятия\n\nДобавьте темы через кнопку ➕")
|
||||||
|
await self.send_main_menu(callback.message.chat.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
status_msg = await callback.message.answer(
|
||||||
|
f"🚀 Начинаю поднятие {len(threads)} тем...\n\n"
|
||||||
|
"Подождите, это может занять некоторое время..."
|
||||||
|
)
|
||||||
|
|
||||||
|
results = await self.bump_threads(threads, send_to_chat=callback.message.chat.id)
|
||||||
|
|
||||||
|
success_count = sum(1 for r in results if r.success)
|
||||||
|
|
||||||
|
await status_msg.edit_text(
|
||||||
|
f"📊 <b>Поднятие завершено!</b>\n\n"
|
||||||
|
f"✅ Успешно: {success_count}\n"
|
||||||
|
f"❌ Ошибок: {len(threads) - success_count}\n"
|
||||||
|
f"📝 Всего тем: {len(threads)}",
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.send_main_menu(callback.message.chat.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error bumping threads: {e}", exc_info=True)
|
||||||
|
await callback.message.answer(f"❌ Ошибка: {str(e)}")
|
||||||
|
|
||||||
|
async def cb_stats(self, callback: CallbackQuery):
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
threads = await self.db.get_all_threads()
|
||||||
|
threads_ready = await self.db.get_threads_to_bump(self.config.bump_interval_hours)
|
||||||
|
|
||||||
|
uptime = "0 минут"
|
||||||
|
if self.start_time:
|
||||||
|
delta = datetime.now() - self.start_time
|
||||||
|
hours = delta.seconds // 3600
|
||||||
|
minutes = (delta.seconds % 3600) // 60
|
||||||
|
if hours > 0:
|
||||||
|
uptime = f"{hours}ч {minutes}м"
|
||||||
|
else:
|
||||||
|
uptime = f"{minutes}м"
|
||||||
|
|
||||||
|
success_rate = 0
|
||||||
|
if self.total_bumps > 0:
|
||||||
|
success_rate = (self.successful_bumps / self.total_bumps) * 100
|
||||||
|
|
||||||
|
text = (
|
||||||
|
"📊 <b>Статистика бота</b>\n\n"
|
||||||
|
f"📋 <b>Темы:</b>\n"
|
||||||
|
f"• Всего в списке: {len(threads)}\n"
|
||||||
|
f"• Готовы к поднятию: {len(threads_ready)}\n"
|
||||||
|
f"• Ожидают времени: {len(threads) - len(threads_ready)}\n\n"
|
||||||
|
f"🚀 <b>Поднятия:</b>\n"
|
||||||
|
f"• Всего попыток: {self.total_bumps}\n"
|
||||||
|
f"• Успешных: {self.successful_bumps}\n"
|
||||||
|
f"• Успешность: {success_rate:.1f}%\n\n"
|
||||||
|
f"⚙️ <b>Настройки:</b>\n"
|
||||||
|
f"• Интервал: {self.config.bump_interval_hours}ч\n"
|
||||||
|
f"• Задержка между темами: 2с\n\n"
|
||||||
|
f"⏱️ <b>Работа:</b>\n"
|
||||||
|
f"• Время работы: {uptime}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await callback.message.answer(text, parse_mode="HTML")
|
||||||
|
await self.send_main_menu(callback.message.chat.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error showing stats: {e}", exc_info=True)
|
||||||
|
await callback.message.answer(f"❌ Ошибка: {str(e)}")
|
||||||
|
|
||||||
|
async def bump_threads(self, threads, send_to_chat=None):
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, thread in enumerate(threads, 1):
|
||||||
|
result = await self.api.bump_thread(thread.id)
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
self.total_bumps += 1
|
||||||
|
if result.success:
|
||||||
|
self.successful_bumps += 1
|
||||||
|
await self.db.update_last_bumped(thread.id)
|
||||||
|
|
||||||
|
if send_to_chat:
|
||||||
|
await self.bot.send_message(
|
||||||
|
send_to_chat,
|
||||||
|
f"[{i}/{len(threads)}] {result.message}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Bump result: {result.message}")
|
||||||
|
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def auto_bump_loop(self):
|
||||||
|
logger.info(f"Auto-bump scheduler started (interval: {self.config.bump_interval_hours}h)")
|
||||||
|
logger.info("Waiting for manual first bump or timer...")
|
||||||
|
|
||||||
|
while self.is_running:
|
||||||
|
try:
|
||||||
|
sleep_seconds = self.config.bump_interval_hours * 3600
|
||||||
|
logger.info(f"Next scheduled bump in {self.config.bump_interval_hours} hours")
|
||||||
|
await asyncio.sleep(sleep_seconds)
|
||||||
|
|
||||||
|
if not self.is_running:
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info("Starting scheduled bump...")
|
||||||
|
|
||||||
|
threads = await self.db.get_threads_to_bump(self.config.bump_interval_hours)
|
||||||
|
|
||||||
|
if threads:
|
||||||
|
logger.info(f"Found {len(threads)} threads to bump")
|
||||||
|
results = await self.bump_threads(threads)
|
||||||
|
success_count = sum(1 for r in results if r.success)
|
||||||
|
logger.info(f"Scheduled bump completed: {success_count}/{len(threads)} successful")
|
||||||
|
else:
|
||||||
|
logger.info("No threads ready for scheduled bump")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in auto_bump_loop: {e}", exc_info=True)
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
try:
|
||||||
|
await self.db.init()
|
||||||
|
logger.info("Database initialized")
|
||||||
|
|
||||||
|
await self.api.start()
|
||||||
|
logger.info("API client started")
|
||||||
|
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
|
||||||
|
self.is_running = True
|
||||||
|
asyncio.create_task(self.auto_bump_loop())
|
||||||
|
|
||||||
|
logger.info("Bot started successfully!")
|
||||||
|
await self.dp.start_polling(self.bot)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting bot: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
logger.info("Stopping bot...")
|
||||||
|
self.is_running = False
|
||||||
|
await self.api.close()
|
||||||
|
await self.bot.session.close()
|
||||||
|
logger.info("Bot stopped")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
bot = AutoBumpBot()
|
||||||
|
try:
|
||||||
|
await bot.start()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Bot interrupted by user")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Bot crashed: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
await bot.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Application terminated")
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"bot": {
|
||||||
|
"api_token": "",
|
||||||
|
"img_url": "https://wallpapers-clan.com/wp-content/uploads/2024/04/dark-anime-girl-with-red-eyes-desktop-wallpaper-preview.jpg",
|
||||||
|
"author_url": "https://lolz.live/kqlol/"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"base_url": "https://api.zelenka.guru",
|
||||||
|
"auth_token": ""
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"path": "threads.db"
|
||||||
|
},
|
||||||
|
"scheduling": {
|
||||||
|
"bump_interval_hours": 12,
|
||||||
|
"enable_auto_bump": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
bot_token: str
|
||||||
|
api_base_url: str
|
||||||
|
api_token: str
|
||||||
|
db_path: str
|
||||||
|
bump_interval_hours: float
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, config_path: str = "config.json") -> "Config":
|
||||||
|
path = Path(config_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Config file not found: {config_path}")
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
bot_token=data["bot"]["api_token"],
|
||||||
|
api_base_url=data["api"]["base_url"],
|
||||||
|
api_token=data["api"]["auth_token"],
|
||||||
|
db_path=data["database"]["path"],
|
||||||
|
bump_interval_hours=data["scheduling"]["bump_interval_hours"]
|
||||||
|
)
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
import aiosqlite
|
||||||
|
from typing import List, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Thread:
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
last_bumped: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
|
||||||
|
def __init__(self, db_path: str):
|
||||||
|
self.db_path = db_path
|
||||||
|
|
||||||
|
async def init(self):
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS threads (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
last_bumped TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
async def add_thread(self, thread_id: str, title: str) -> bool:
|
||||||
|
try:
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
await db.execute(
|
||||||
|
"INSERT OR IGNORE INTO threads (id, title) VALUES (?, ?)",
|
||||||
|
(thread_id, title)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding thread {thread_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_all_threads(self) -> List[Thread]:
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
async with db.execute(
|
||||||
|
"SELECT id, title, last_bumped FROM threads ORDER BY id"
|
||||||
|
) as cursor:
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return [Thread(id=r[0], title=r[1], last_bumped=r[2]) for r in rows]
|
||||||
|
|
||||||
|
async def get_threads_to_bump(self, interval_hours: float) -> List[Thread]:
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
async with db.execute(f"""
|
||||||
|
SELECT id, title, last_bumped FROM threads
|
||||||
|
WHERE last_bumped IS NULL
|
||||||
|
OR datetime(last_bumped, '+{interval_hours} hours') <= datetime('now')
|
||||||
|
ORDER BY last_bumped ASC NULLS FIRST
|
||||||
|
""") as cursor:
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return [Thread(id=r[0], title=r[1], last_bumped=r[2]) for r in rows]
|
||||||
|
|
||||||
|
async def update_last_bumped(self, thread_id: str):
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE threads SET last_bumped = datetime('now') WHERE id = ?",
|
||||||
|
(thread_id,)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
async def delete_thread(self, thread_id: str) -> bool:
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
cursor = await db.execute(
|
||||||
|
"DELETE FROM threads WHERE id = ?",
|
||||||
|
(thread_id,)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
aiogram==3.4.1
|
||||||
|
aiohttp>=3.9.0
|
||||||
|
aiosqlite>=0.19.0
|
||||||
Reference in New Issue
Block a user