Compare commits

...

4 Commits

12 changed files with 216 additions and 42 deletions
+10
View File
@@ -0,0 +1,10 @@
TELEGRAM_BOT_TOKEN=your_token_here
# Полное отключение логов и любого хранения (БД, файлы логов, сохранение message_id)
# DISABLE_PERSISTENCE=1
# Или по отдельности:
# DISABLE_LOGGING=1 — нет логов в консоль и в файл
# DISABLE_STORAGE=1 — нет SQLite, /del не работает, группы /set не сохраняются
SCHEDULE_DRIVE_FOLDER_ID=1WhUFHGkS4qC_e84KRArF4ooXHJr8mL5T
+8 -2
View File
@@ -12,6 +12,8 @@ logger = getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
async def init_db():
if Config.DISABLE_STORAGE:
return
async with aiosqlite.connect(Config.DAYS_TO_DB_PATH) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS days_to_new_year (
@@ -24,6 +26,8 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
logger.info("База данных инициализирована")
async def save_days_to_db(user_id: int, days: int):
if Config.DISABLE_STORAGE:
return
logger.debug(f"Сохраняем user_id={user_id}, days={days}")
async with aiosqlite.connect(Config.DAYS_TO_DB_PATH) as db:
await db.execute("""
@@ -34,6 +38,8 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
logger.info(f"Запись сохранена: user_id={user_id}, days={days}")
async def get_last_days(user_id: int) -> int | None:
if Config.DISABLE_STORAGE:
return None
async with aiosqlite.connect(Config.DAYS_TO_DB_PATH) as db:
async with db.execute(
"SELECT days FROM days_to_new_year WHERE user_id = ?", (int(user_id),)
@@ -110,8 +116,8 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
save_message(msg.chat.id, msg.message_id)
except Exception as e:
logger.exception(f"Ошибка при обработке uid={uid}: {e}")
logger.info("Завершён цикл periodic_task, спим 6 часов")
await asyncio.sleep(21600) # каждые 6 часов
logger.info("Завершён цикл periodic_task, спим 24 часов")
await asyncio.sleep(86400) # каждые 24 часов
asyncio.create_task(periodic_task())
return 0
+27 -4
View File
@@ -1,11 +1,20 @@
import os
from pathlib import Path
from dotenv import load_dotenv
from typing import Dict
load_dotenv()
def _env_bool(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in ("1", "true", "yes", "on")
class Config:
# Загружаем .env
load_dotenv()
# API
API_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
@@ -40,9 +49,23 @@ class Config:
"SCHEDULE_DRIVE_FOLDER_ID", "1WhUFHGkS4qC_e84KRArF4ooXHJr8mL5T"
)
# Отключение логов и хранения (см. .env.example)
# DISABLE_PERSISTENCE=1 — выключает и логи, и все БД/файлы сразу
_NO_PERSISTENCE = _env_bool("DISABLE_PERSISTENCE")
DISABLE_LOGGING = (
_env_bool("DISABLE_LOGGING")
if os.getenv("DISABLE_LOGGING") is not None
else _NO_PERSISTENCE
)
DISABLE_STORAGE = (
_env_bool("DISABLE_STORAGE")
if os.getenv("DISABLE_STORAGE") is not None
else _NO_PERSISTENCE
)
# Пути
LOG_FILE = "storage/log/bot.log"
DAYS_TO_DB_PATH = "addons/x_days_to/days_to_new_year.db"
LOG_FILE = Path("storage/log/bot.log")
DAYS_TO_DB_PATH = Path("addons/x_days_to/days_to_new_year.db")
if __name__ == "__main__":
+45 -6
View File
@@ -17,8 +17,14 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@saving
@admin_required(3)
async def send_log(message: Message):
if Config.DISABLE_LOGGING:
await message.answer("📝 Логирование отключено (DISABLE_LOGGING=1).")
return
if Config.DISABLE_STORAGE:
await message.answer("📝 Файл логов не ведётся (DISABLE_STORAGE=1).")
return
try:
log_file = types.FSInputFile(Config.LOG_FILE)
log_file = types.FSInputFile(str(Config.LOG_FILE))
await message.answer_document(log_file, caption="📑 Логи бота")
except FileNotFoundError:
await message.answer("Файл логов пока не создан.")
@@ -31,7 +37,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
from utils.mac_metrics import get_macbook_battery_level, get_process_usage
try:
stats = analyze_bot_logs(Config.LOG_FILE)
if Config.DISABLE_LOGGING or Config.DISABLE_STORAGE:
await message.answer(
"📊 Аналитика по логам недоступна: логирование или хранение отключено в .env"
)
return
stats = analyze_bot_logs(str(Config.LOG_FILE))
batt = await get_macbook_battery_level()
usage = await get_process_usage()
status_text = (
@@ -53,7 +64,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
async def stat(message: Message):
from utils.analytics import analyze_bot_logs
stats = analyze_bot_logs(Config.LOG_FILE)
if Config.DISABLE_LOGGING or Config.DISABLE_STORAGE:
await message.answer(
"📊 Аналитика по логам недоступна: логирование или хранение отключено в .env"
)
return
stats = analyze_bot_logs(str(Config.LOG_FILE))
await message.answer(
create_statistics_text(stats), reply_to_message_id=message.message_id
)
@@ -61,6 +77,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("del"))
@admin_required(1)
async def delete_all_messages(message: Message):
if Config.DISABLE_STORAGE:
await message.answer(
"📭 Хранение сообщений отключено (DISABLE_STORAGE=1).",
reply_to_message_id=message.message_id,
)
return
messages = load_messages()
if not messages:
sent = await message.answer(
@@ -91,16 +113,33 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
async def power_control(message: types.Message):
args = message.text.split()
if len(args) < 2:
days = state.watcher_days_ahead
status = "включена" if state.watcher_work else "выключена"
await message.answer(f"⏱️ Слежка расписания: {status}")
await message.answer(f"⏱️ Слежка расписания: {status} (на {days} дн.)")
return
command = args[1].lower()
watcher_service = WatcherService(state, bot)
if command == "on" and not state.watcher_work:
if command == "on":
# Проверяем, есть ли параметр количества дней
days = 1
if len(args) > 2:
try:
days = int(args[2])
if days < 1:
await message.answer("❌ Количество дней должно быть >= 1")
return
except ValueError:
await message.answer("❌ Неверный формат дней. Используйте: /power on 3")
return
state.watcher_days_ahead = days
if not state.watcher_work:
await watcher_service.start()
await message.answer("✅ Слежка расписания включена")
await message.answer(f"✅ Слежка расписания включена (на {days} дн.)")
else:
await message.answer(f"✅ Количество дней изменено на {days} дн.")
elif command == "off" and state.watcher_work:
await watcher_service.stop()
await message.answer("❌ Слежка расписания выключена")
+4 -10
View File
@@ -1,17 +1,11 @@
from asyncio import run
from logging import basicConfig, FileHandler, StreamHandler, INFO, getLogger
from logging import getLogger
from bot.core import TelegramBot
from config import Config
from utils.logging_config import setup_logging
# Настройка логирования
basicConfig(
level=INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[FileHandler(Config.LOG_FILE, encoding="utf-8"), StreamHandler()],
force=True,
)
setup_logging()
logger = getLogger(__name__)
+1
View File
@@ -11,6 +11,7 @@ class BotState:
last_pinned: Dict[str, int] = None
watcher_work: bool = False
watcher_task: Optional[Task] = None
watcher_days_ahead: int = 1
file_id_cache: Dict[str, str] = None
last_day: Dict[str, int] = None
last_clip_hash: Dict[str, str] = None
+26 -1
View File
@@ -36,6 +36,12 @@ class ScheduleService:
target = target.replace(day=int(day_offset))
return target.replace(hour=0, minute=0, second=0, microsecond=0)
def _next_target_date(self, day_offset: int = 0) -> datetime:
return (datetime.now() + timedelta(days=day_offset)).replace(hour=0, minute=0, second=0, microsecond=0)
async def _load_pdf_for_date(
self, day_offset: int = 0
) -> Tuple[Optional[bytes], Optional[str], int, int]:
@@ -54,6 +60,25 @@ class ScheduleService:
url = f"https://drive.google.com/file/d/{drive_file.file_id}/view"
return self._pdf_cache[drive_file.file_id], url, day, month
async def _load_pdf_for_watcher(
self, day_offset: int = 1
) -> Tuple[Optional[bytes], Optional[str], int, int]:
target = self._next_target_date(day_offset)
day, month = target.day, target.month
drive_file = await self.drive.find_for_date(target)
if not drive_file:
return None, None, day, month
if drive_file.file_id not in self._pdf_cache:
self._pdf_cache[drive_file.file_id] = await self.drive.download_pdf(
drive_file.file_id
)
url = f"https://drive.google.com/file/d/{drive_file.file_id}/view"
return self._pdf_cache[drive_file.file_id], url, day, month
@staticmethod
def exact_group_regex(group: str) -> re.Pattern:
pattern = rf"(^|{BOUNDARY}){re.escape(group)}({BOUNDARY}|$)"
@@ -105,7 +130,7 @@ class ScheduleService:
return f"📅 Расписание для {day} числа:\n<pre>{body}</pre>"
async def is_published_for(self, day_offset: int = 0) -> bool:
target = self._resolve_target_date(day_offset)
target = self._next_target_date(day_offset)
return await self.drive.find_for_date(target) is not None
async def get_schedule(
+23 -6
View File
@@ -1,8 +1,8 @@
import asyncio
from random import randint
from datetime import datetime, timedelta
from aiogram import Bot, types
from config import Config
from logging import getLogger
from models.state import BotState
@@ -47,6 +47,20 @@ class WatcherService:
Config.WATCHER_RANDOM_DELAY_MAX,
)
@staticmethod
def _get_target_date_with_weekend_handling(days_ahead: int) -> datetime:
"""
Получить целевую дату с учетом выходных.
Если целевая дата - воскресенье, переносится на понедельник.
"""
target = (datetime.now() + timedelta(days=days_ahead)).replace(
hour=0, minute=0, second=0, microsecond=0
)
# weekday() returns 6 for Sunday
if target.weekday() == 6:
target += timedelta(days=1)
return target
async def _watcher_loop(self):
"""Основной цикл слежки за появлением PDF на Google Drive."""
while self.state.watcher_work:
@@ -71,12 +85,14 @@ class WatcherService:
Возвращает True, если расписание ещё недоступно ни для одной группы.
Возвращает False, если хотя бы одной группе отправили расписание.
"""
target = self.schedule_service._resolve_target_date(0)
days_ahead = self.state.watcher_days_ahead
target = self._get_target_date_with_weekend_handling(days_ahead)
logger.info(
f"Проверяем Google Drive на расписание за {target.strftime('%d.%m.%Y')} "
f"(дней вперед: {days_ahead})"
)
if not await self.schedule_service.is_published_for(0):
if not await self.schedule_service.is_published_for(days_ahead):
return True
found_any = False
@@ -84,14 +100,15 @@ class WatcherService:
logger.info(
f"Проверяем расписание для {group} на {target.strftime('%d.%m.%Y')}"
)
if await self._check_group_schedule(group, chat_id):
if await self._check_group_schedule(group, chat_id, days_ahead):
found_any = True
return not found_any
async def _check_group_schedule(self, group: str, chat_id: int) -> bool:
async def _check_group_schedule(self, group: str, chat_id: int, days_ahead: int) -> bool:
target = self._get_target_date_with_weekend_handling(days_ahead)
text, url, data_day, data_month = await self.schedule_service.get_schedule(
group, 0
group, target.day
)
if not self.schedule_service.is_schedule_missing(text):
msg = await self.bot.send_message(
+4 -1
View File
@@ -1,6 +1,9 @@
import sqlite3
from pathlib import Path
DIR = "/Users/mac/myfirstprogramm/storage/message.db"
from config import Config
DIR = Path(__file__).resolve().parent / "message.db"
if __name__ == "__main__":
db = sqlite3.connect(DIR)
+19 -1
View File
@@ -1,7 +1,13 @@
from .DB import get_db
from config import Config
def save_message(chat_id: int, message_id: int):
if Config.DISABLE_STORAGE:
return
if True:
return
from .DB import get_db
db = get_db()
cur = db.cursor()
cur.execute("INSERT INTO message VALUES (?, ?)", (int(chat_id), int(message_id)))
@@ -11,6 +17,12 @@ def save_message(chat_id: int, message_id: int):
def load_messages():
if Config.DISABLE_STORAGE:
return []
if True:
return []
from .DB import get_db
db = get_db()
cur = db.cursor()
cur.execute("SELECT * FROM message")
@@ -21,6 +33,12 @@ def load_messages():
def clear_messages():
if Config.DISABLE_STORAGE:
return
if True:
return
from .DB import get_db
db = get_db()
cur = db.cursor()
cur.execute("DELETE FROM message")
+13 -8
View File
@@ -1,6 +1,11 @@
from config import Config
_DEFAULT_GROUP = "30тс"
def save_user(user_id: int, group: str = _DEFAULT_GROUP):
from .DB import get_db
def save_user(user_id: int, group: str = "30тс"):
db = get_db()
cur = db.cursor()
cur.execute("INSERT INTO users (user_id, user_group) VALUES (?, ?)", (user_id, group))
@@ -8,19 +13,18 @@ def save_user(user_id: int, group: str = "30тс"):
cur.close()
db.close()
def set_group(user_id: int, group: str = "30тс"):
def set_group(user_id: int, group: str = _DEFAULT_GROUP):
from .DB import get_db
db = get_db()
cur = db.cursor()
# проверяем, есть ли пользователь
cur.execute("SELECT 1 FROM users WHERE user_id = ?", (user_id,))
exists = cur.fetchone()
if exists:
# если есть — обновляем группу
cur.execute("UPDATE users SET user_group = ? WHERE user_id = ?", (group, user_id))
else:
# если нет — регистрируем нового пользователя
cur.execute("INSERT INTO users (user_id, user_group) VALUES (?, ?)", (user_id, group))
db.commit()
@@ -28,7 +32,9 @@ def set_group(user_id: int, group: str = "30тс"):
db.close()
def get_group(user_id: int, default: str = "30тс") -> str:
def get_group(user_id: int, default: str = _DEFAULT_GROUP) -> str:
from .DB import get_db
db = get_db()
cur = db.cursor()
cur.execute("SELECT user_group FROM users WHERE user_id = ?", (user_id,))
@@ -36,7 +42,6 @@ def get_group(user_id: int, default: str = "30тс") -> str:
if row:
group = row[0]
else:
# если пользователя нет — регистрируем с дефолтной группой
cur.execute("INSERT INTO users (user_id, user_group) VALUES (?, ?)", (user_id, default))
db.commit()
group = default
+33
View File
@@ -0,0 +1,33 @@
import logging
from logging import CRITICAL, NullHandler, getLogger
from config import Config
def setup_logging() -> None:
"""Настройка логирования. При DISABLE_LOGGING — полное отключение."""
root = getLogger()
if Config.DISABLE_LOGGING:
root.handlers.clear()
root.addHandler(NullHandler())
root.setLevel(CRITICAL)
logging.disable(CRITICAL)
return
from logging import INFO, FileHandler, StreamHandler, basicConfig
handlers: list[logging.Handler] = [StreamHandler()]
if not Config.DISABLE_STORAGE:
log_path = Config.LOG_FILE
log_path.parent.mkdir(parents=True, exist_ok=True)
handlers.append(FileHandler(log_path, encoding="utf-8"))
basicConfig(
level=INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=handlers,
force=True,
)