forked from zovos/bot_tg
Compare commits
1 commit
07f6efe7b1
...
100fbf7051
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
100fbf7051 |
2 changed files with 141 additions and 137 deletions
105
config.py
Normal file
105
config.py
Normal file
|
|
@ -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)."
|
||||
)
|
||||
173
main.py
173
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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue