diff --git a/api_client.py b/api_client.py
new file mode 100644
index 0000000..80b4f9a
--- /dev/null
+++ b/api_client.py
@@ -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'
', '\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
+ )
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..4079028
--- /dev/null
+++ b/app.py
@@ -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=(
+ "🤖 QIYANA AUTO-BUMP BOT\n\n"
+ "Бот для автоматического поднятия тем на Lolz.live\n\n"
+ f"⏰ Автоподнятие каждые {self.config.bump_interval_hours}ч"
+ ),
+ 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"
+ "Пример: 12345, 67890, 11111",
+ 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 += "✅ Добавлено:\n" + "\n".join(added) + "\n\n"
+
+ if errors:
+ result += "❌ Ошибки:\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"📋 Список тем ({len(threads)}):\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"{thread.id} - {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"📊 Поднятие завершено!\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 = (
+ "📊 Статистика бота\n\n"
+ f"📋 Темы:\n"
+ f"• Всего в списке: {len(threads)}\n"
+ f"• Готовы к поднятию: {len(threads_ready)}\n"
+ f"• Ожидают времени: {len(threads) - len(threads_ready)}\n\n"
+ f"🚀 Поднятия:\n"
+ f"• Всего попыток: {self.total_bumps}\n"
+ f"• Успешных: {self.successful_bumps}\n"
+ f"• Успешность: {success_rate:.1f}%\n\n"
+ f"⚙️ Настройки:\n"
+ f"• Интервал: {self.config.bump_interval_hours}ч\n"
+ f"• Задержка между темами: 2с\n\n"
+ f"⏱️ Работа:\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")
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..7480b25
--- /dev/null
+++ b/config.json
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/config_manager.py b/config_manager.py
new file mode 100644
index 0000000..f9118e6
--- /dev/null
+++ b/config_manager.py
@@ -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"]
+ )
diff --git a/database.py b/database.py
new file mode 100644
index 0000000..2b87a71
--- /dev/null
+++ b/database.py
@@ -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
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..688eb9e
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+aiogram==3.4.1
+aiohttp>=3.9.0
+aiosqlite>=0.19.0