diff --git a/config.py b/config.py new file mode 100644 index 0000000..7fa783a --- /dev/null +++ b/config.py @@ -0,0 +1,105 @@ +# ВСЕ КОНСТАНТНЫЕ ЗНАЧЕНИЯ + +import os +from pathlib import Path +from datetime import timedelta, time + +def make_time(h: int, m: int) -> time: + return time(hour=h, minute=m) + +# Базовые пути +BASE_DIR = Path(__file__).resolve().parent +FONT_PATH = BASE_DIR / "impact.ttf" +DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow") + +# Состояние мини-игры +PENIS_STATE: dict[int, dict[str, object]] = {} +PENIS_START_LENGTH = 10.0 +PENIS_MIN_DELTA = 0.1 +PENIS_MAX_DELTA = 7.0 +PENIS_COOLDOWN = timedelta(hours=24) +PENIS_TOP_LIMIT = 10 + +# Твои шаблоны и тот самый огромный кортеж +TEMPLATES = { + "1": { + "path": BASE_DIR / "maket" / "1.png", + "anchor": (104, 240), + "padding": 20, + "label": "Владимир Владимирович Путин самый лучший президент", + }, + # Сюда вставь весь остальной огромный массив +} + +SCHEDULE: dict[int, list[tuple[time, time, int | None]]] = { + 0: [ # Monday + (make_time(9, 50), make_time(10, 35), 5), + (make_time(10, 40), make_time(11, 25), 30), + (make_time(11, 55), make_time(12, 40), 5), + (make_time(12, 45), make_time(13, 30), 15), + (make_time(13, 45), make_time(14, 30), 5), + (make_time(14, 35), make_time(15, 20), 30), + (make_time(15, 50), make_time(16, 35), 5), + (make_time(16, 40), make_time(17, 25), None), + ], + 1: [ # Tuesday + (make_time(9, 50), make_time(10, 35), 5), + (make_time(10, 40), make_time(11, 25), 30), + (make_time(11, 55), make_time(12, 40), 5), + (make_time(12, 45), make_time(13, 30), 15), + (make_time(13, 45), make_time(14, 30), 5), + (make_time(14, 35), make_time(15, 20), None), + ], + 2: [], # Wednesday — кайфуем дома + 3: [ # Thursday + (make_time(8, 0), make_time(8, 45), 5), + (make_time(8, 50), make_time(9, 35), 15), + (make_time(9, 50), make_time(10, 35), 5), + (make_time(10, 40), make_time(11, 25), 30), + (make_time(11, 55), make_time(12, 40), 5), + (make_time(12, 45), make_time(13, 30), 15), + (make_time(13, 45), make_time(14, 30), 5), + (make_time(14, 35), make_time(15, 20), 30), + (make_time(15, 50), make_time(16, 35), 5), + (make_time(16, 40), make_time(17, 25), None), + ], + 4: [ # Friday + (make_time(9, 50), make_time(10, 35), 5), + (make_time(10, 40), make_time(11, 25), 30), + (make_time(11, 55), make_time(12, 40), 5), + (make_time(12, 45), make_time(13, 30), 15), + (make_time(13, 45), make_time(14, 30), 5), + (make_time(14, 35), make_time(15, 20), None), + ], + 5: [], # Saturday — выходной + 6: [], # Sunday — выходной +} + +# Ключевые слова, на которые бот кидает трек Creep. +KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'} + + +def templates_list_text() -> str: + parts = [] + for tid in sorted(TEMPLATES.keys()): + label = TEMPLATES[tid].get("label", "") + if label: + parts.append(f"{tid} — {label}") + else: + parts.append(tid) + return ", ".join(parts) if parts else "нет макетов" + + +HINT_TEXT = ( + # Общая подсказка по командам. + "Команды:\n" + f"• /maket «id» «текст» — доступные: {templates_list_text()} (1: 737x414, точка 104,240).\n" + "• /gen_mem Up:верх Down:низ — отправь с фото (как подпись) или ответом на фото.\n" + "• /break — узнать, сколько минут до перекура.\n" + "• /penis — миниигра: +/− от 0.1 до 7 см, раз в 24 часа.\n" + "• /top_penis — динамический топ по текущей длине.\n" + "• /talk [сообщение] — побазарить с ИИ на фене.\n" + "• /prices — старый способ цен.\n" + "• /fetch — свежие цены с x-server.\n" + "Подсказка: в группах команда должна быть слитно с ботом (/gen_mem@username)." +) diff --git a/main.py b/main.py index d591d18..b3db276 100644 --- a/main.py +++ b/main.py @@ -1,126 +1,40 @@ -import asyncio -from AI.talk_handler import handle_talk -import logging -import os -import re -import requests +# Сделал чуть компактнее и читабельнее +import asyncio, logging, os, re, requests, random from io import BytesIO from pathlib import Path from textwrap import wrap -import random +from datetime import datetime, time, timedelta +from zoneinfo import ZoneInfo from aiogram import Bot, Dispatcher, F, types from aiogram.enums import ParseMode from aiogram.filters import Command, CommandStart from aiogram.types import BufferedInputFile, Message, BotCommand, FSInputFile from aiogram.client.default import DefaultBotProperties + from PIL import Image, ImageDraw, ImageFont -from datetime import datetime, time, timedelta -from zoneinfo import ZoneInfo +from AI.talk_handler import handle_talk -# Базовые пути/настройки проекта. -BASE_DIR = Path(__file__).parent -FONT_PATH = BASE_DIR / "impact.ttf" -DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow") - -# In-memory состояние миниигры /penis. -# Важно: после перезапуска процесса всё обнуляется. -PENIS_STATE: dict[int, dict[str, object]] = {} -PENIS_START_LENGTH = 10.0 -PENIS_MIN_DELTA = 0.1 -PENIS_MAX_DELTA = 7.0 -PENIS_COOLDOWN = timedelta(hours=24) -PENIS_TOP_LIMIT = 10 - -# Конфигурация текстовых макетов /maket. -TEMPLATES = { - "1": { - "path": BASE_DIR / "maket" / "1.png", - "anchor": (104, 240), - "padding": 20, # right/bottom padding from edges - "label": "Владимир Владимирович Путин самый лучший президент", - }, -} - -def templates_list_text() -> str: - # Собираем человекочитаемый список доступных макетов. - parts = [] - for tid in sorted(TEMPLATES.keys()): - label = TEMPLATES[tid].get("label", "") - if label: - parts.append(f"{tid} — {label}") - else: - parts.append(tid) - return ", ".join(parts) if parts else "нет макетов" +# Конфигурационный файл, куда я вынес все константы +import config # ---- Schedule & breaks ---- -def make_time(h: int, m: int) -> time: - # Короткий хелпер для заполнения расписания. - return time(hour=h, minute=m) - - -# Schedule format: list of (lesson_start, lesson_end, break_minutes_after) -SCHEDULE: dict[int, list[tuple[time, time, int | None]]] = { - 0: [ # Monday - (make_time(9, 50), make_time(10, 35), 5), - (make_time(10, 40), make_time(11, 25), 30), - (make_time(11, 55), make_time(12, 40), 5), - (make_time(12, 45), make_time(13, 30), 15), - (make_time(13, 45), make_time(14, 30), 5), - (make_time(14, 35), make_time(15, 20), 30), - (make_time(15, 50), make_time(16, 35), 5), - (make_time(16, 40), make_time(17, 25), None), - ], - 1: [ # Tuesday - (make_time(9, 50), make_time(10, 35), 5), - (make_time(10, 40), make_time(11, 25), 30), - (make_time(11, 55), make_time(12, 40), 5), - (make_time(12, 45), make_time(13, 30), 15), - (make_time(13, 45), make_time(14, 30), 5), - (make_time(14, 35), make_time(15, 20), None), - ], - 2: [], # Wednesday — кайфуем дома - 3: [ # Thursday - (make_time(8, 0), make_time(8, 45), 5), - (make_time(8, 50), make_time(9, 35), 15), - (make_time(9, 50), make_time(10, 35), 5), - (make_time(10, 40), make_time(11, 25), 30), - (make_time(11, 55), make_time(12, 40), 5), - (make_time(12, 45), make_time(13, 30), 15), - (make_time(13, 45), make_time(14, 30), 5), - (make_time(14, 35), make_time(15, 20), 30), - (make_time(15, 50), make_time(16, 35), 5), - (make_time(16, 40), make_time(17, 25), None), - ], - 4: [ # Friday - (make_time(9, 50), make_time(10, 35), 5), - (make_time(10, 40), make_time(11, 25), 30), - (make_time(11, 55), make_time(12, 40), 5), - (make_time(12, 45), make_time(13, 30), 15), - (make_time(13, 45), make_time(14, 30), 5), - (make_time(14, 35), make_time(15, 20), None), - ], - 5: [], # Saturday — выходной - 6: [], # Sunday — выходной -} - - def minutes_until_break(now: datetime | None = None) -> str: # Возвращает статус: до перекура / идёт перекур / пары окончены. if now is None: - now = datetime.now(ZoneInfo(DEFAULT_TZ)) + now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) elif now.tzinfo is None: - now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ)) + now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ)) day = now.weekday() - today_schedule = SCHEDULE.get(day, []) - if not today_schedule: + today_SCHEDULE = config.SCHEDULE.get(day, []) + if not today_SCHEDULE: return "Сегодня кайфуем дома, перекур не нужен." def dt_for(t: time) -> datetime: return now.replace(hour=t.hour, minute=t.minute, second=0, microsecond=0) - for start_t, end_t, br_minutes in today_schedule: + for start_t, end_t, br_minutes in today_SCHEDULE: start_dt = dt_for(start_t) end_dt = dt_for(end_t) @@ -143,19 +57,6 @@ def minutes_until_break(now: datetime | None = None) -> str: return "Пары закончились, перекур только завтра." -HINT_TEXT = ( - # Общая подсказка по командам. - "Команды:\n" - f"• /maket «id» «текст» — доступные: {templates_list_text()} (1: 737x414, точка 104,240).\n" - "• /gen_mem Up:верх Down:низ — отправь с фото (как подпись) или ответом на фото.\n" - "• /break — узнать, сколько минут до перекура.\n" - "• /penis — миниигра: +/− от 0.1 до 7 см, раз в 24 часа.\n" - "• /top_penis — динамический топ по текущей длине.\n" - "• /talk [сообщение] — побазарить с ИИ на фене.\n" - "• /prices — старый способ цен.\n" - "• /fetch — свежие цены с x-server.\n" - "Подсказка: в группах команда должна быть слитно с ботом (/gen_mem@username)." -) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -163,9 +64,9 @@ logger = logging.getLogger(__name__) def load_font(size: int) -> ImageFont.FreeTypeFont: # Грузим Impact нужного размера. - if not FONT_PATH.exists(): + if not config.FONT_PATH.exists(): raise FileNotFoundError("impact.ttf not found. Place the font in the project root.") - return ImageFont.truetype(str(FONT_PATH), size=size) + return ImageFont.truetype(str(config.FONT_PATH), size=size) def wrap_text_to_width(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.ImageDraw, stroke_width: int) -> list[str]: @@ -228,7 +129,7 @@ def draw_multiline(draw: ImageDraw.ImageDraw, lines: list[str], position: tuple[ def generate_maket(text: str, template_id: str = "1") -> BytesIO: # Генерируем картинку макета с текстом в заранее заданной области. - tpl = TEMPLATES.get(template_id) + tpl = config.TEMPLATES.get(template_id) if not tpl: raise FileNotFoundError(f"Template {template_id} is missing") template_path = tpl["path"] @@ -327,9 +228,9 @@ def minutes_until_next_lesson(now: datetime | None = None) -> str: import random if now is None: - now = datetime.now(ZoneInfo(DEFAULT_TZ)) + now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) elif now.tzinfo is None: - now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ)) + now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ)) LESSON_DAYS = [1, 3] # 1 = вторник, 3 = четверг LESSON_START = time(9, 50) @@ -400,14 +301,14 @@ def minutes_until_next_lesson(now: datetime | None = None) -> str: def play_penis(user_id: int, user_name: str | None = None, now: datetime | None = None) -> tuple[bool, str]: # Миниигра /penis: изменение длины и кулдаун 24 часа от последнего броска. if now is None: - now = datetime.now(ZoneInfo(DEFAULT_TZ)) + now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) elif now.tzinfo is None: - now = now.replace(tzinfo=ZoneInfo(DEFAULT_TZ)) + now = now.replace(tzinfo=ZoneInfo(config.DEFAULT_TZ)) - state = PENIS_STATE.get(user_id) + state = config.PENIS_STATE.get(user_id) if state is None: - state = {"length": PENIS_START_LENGTH, "last_used_at": None} - PENIS_STATE[user_id] = state + state = {"length": config.PENIS_START_LENGTH, "last_used_at": None} + config.PENIS_STATE[user_id] = state # Обновляем отображаемое имя для топа. if user_name: @@ -415,7 +316,7 @@ def play_penis(user_id: int, user_name: str | None = None, now: datetime | None last_used_at = state.get("last_used_at") if isinstance(last_used_at, datetime): - next_time = last_used_at + PENIS_COOLDOWN + next_time = last_used_at + config.PENIS_COOLDOWN if now < next_time: # Если рано, считаем оставшееся время до следующей попытки. left = next_time - now @@ -424,8 +325,8 @@ def play_penis(user_id: int, user_name: str | None = None, now: datetime | None minutes = (total_seconds % 3600) // 60 return False, f"Сегодня уже кидал. Попробуй через {hours} ч {minutes} мин." - current_length = float(state.get("length", PENIS_START_LENGTH)) - delta = round(random.uniform(PENIS_MIN_DELTA, PENIS_MAX_DELTA), 1) + current_length = float(state.get("length", config.PENIS_START_LENGTH)) + delta = round(random.uniform(config.PENIS_MIN_DELTA, config.PENIS_MAX_DELTA), 1) sign = random.choice([-1, 1]) # Не даём длине уйти в отрицательные значения. new_length = round(max(0.1, current_length + sign * delta), 1) @@ -451,10 +352,10 @@ def format_penis_user_name(user: types.User) -> str: return f"user_{user.id}" -def build_penis_top(limit: int = PENIS_TOP_LIMIT) -> str: +def build_penis_top(limit: int = config.PENIS_TOP_LIMIT) -> str: # Формируем динамический топ по текущим значениям /penis. rows: list[tuple[float, str]] = [] - for user_id, state in PENIS_STATE.items(): + for user_id, state in config.PENIS_STATE.items(): length = state.get("length") if not isinstance(length, (int, float)): continue @@ -481,7 +382,7 @@ async def handle_start(message: Message): async def handle_help(message: Message): # Вывод подсказки по возможностям бота. - await message.answer(HINT_TEXT) + await message.answer(config.HINT_TEXT) async def handle_break(message: Message): # Команда расписания перекура. @@ -492,14 +393,14 @@ async def handle_maket(message: Message): # Генерация текста в подготовленном шаблоне /maket. parts = message.text.split(maxsplit=2) if len(parts) == 1: - await message.reply(f"Доступные макеты: {templates_list_text()}\nФормат: /maket «id» «текст»") + await message.reply(f"Доступные макеты: {config.templates_list_text()}\nФормат: /maket «id» «текст»") return if len(parts) < 3: - await message.reply(f"Формат: /maket «id» «текст»\nДоступные макеты: {templates_list_text()}\n\n" + HINT_TEXT) + await message.reply(f"Формат: /maket «id» «текст»\nДоступные макеты: {config.templates_list_text()}\n\n" + config.HINT_TEXT) return template_id = parts[1] - if template_id not in TEMPLATES: - await message.reply(f"Такого макета нет. Доступные: {templates_list_text()}") + if template_id not in config.TEMPLATES: + await message.reply(f"Такого макета нет. Доступные: {config.templates_list_text()}") return text = parts[2].strip() if not text: @@ -592,9 +493,7 @@ async def handle_top_penis(message: Message): await message.answer(build_penis_top()) -# Ключевые слова, на которые бот кидает трек Creep. -KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'} -# I'M CREEP +# Смотреть config.py async def handle_keywords(message: types.Message): # Не реагируем на команды, только на обычный текст. @@ -603,8 +502,8 @@ async def handle_keywords(message: types.Message): text = message.text.lower() - if any(re.search(rf'\b{word}\b', text) for word in KEYWORDS): - audio_path = BASE_DIR / "maket" / "Radiohead - Creep.mp3" + if any(re.search(rf'\b{word}\b', text) for word in config.KEYWORDS): + audio_path = config.BASE_DIR / "maket" / "Radiohead - Creep.mp3" if audio_path.exists(): await message.answer_audio(