1
0
Fork 0
forked from zovos/bot_tg

Merge pull request 'feature/FenyaBot' (#12) from Dan4ick/bot_tg:feature/FenyaBot into main

Reviewed-on: zovos/bot_tg#12
This commit is contained in:
q 2026-03-07 13:06:19 +00:00
commit 373aa8f6c2
6 changed files with 250 additions and 6 deletions

View file

@ -4,7 +4,11 @@
- вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`);
- делает мемы из присланных фото (`/gen_mem Up:... Down:...`);
- подсказывает, сколько минут до перекура по заданному расписанию (`/break`);
- умеет базарить (`/talk`).
- умеет базарить (`/talk`) с контекстом 100 сообщений;
- авто-ответы в чате (10% шанс + триггер-слова);
- миниигра `/penis` + `/top_penis`;
- казино на размер (`/penis_casino`);
- гадание на фене с мемом (`/gadanie`).
## Запуск
@ -22,7 +26,13 @@ docker-compose up --build
- `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240).
- `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down.
- `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур.
- `/talk <сообщение>` — побазарить с ИИ. Работает через llama.cpp сервер.
- `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений).
- `/penis` — миниигра, 1 раз в 24 часа.
- `/top_penis` — топ по длине.
- `/penis_casino <ставка>` — казино на размер (макс 1 см, казино всегда в выигрыше).
- `/gadanie <тема>` — ИИ-гадание на фене + авто-мем.
- `/ebalnik` — вкл/выкл авто-ответы бота в чате.
- `/svodka` — сводка СВО.
## Макеты и шрифт
@ -39,12 +49,14 @@ docker-compose up --build
- `/maket` подбирает размер текста, чтобы уместить его в область макета.
- `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой.
- `/break` считает по расписанию в `main.py` (словарь `SCHEDULE`).
- `/talk` шлёт запрос к llama.cpp серверу (`LLAMA_API_URL`), системный промпт задаёт стиль фени(Можно изменить).
- `/break` считает по расписанию в `config.py`.
- `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite).
- `/penis_casino` — слоты с множителем x1.2x3 (`games/casino.py`).
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).
## ИИ-модуль (`AI/talk_handler.py`)
Отдельный модуль, не трогает основной код. Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`).
Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`). Хранит контекст 100 сообщений в SQLite.
Настройка сервера:
1. Скачать модель: `wget https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf`

View file

@ -18,6 +18,9 @@ PENIS_COOLDOWN = timedelta(hours=24)
PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds())
PENIS_TOP_LIMIT = 10
# --- Казино ---
CASINO_MAX_BET = 1.0
# --- Конфигурация макетов ---
TEMPLATES = {
"1": {
@ -86,7 +89,7 @@ KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'}
# --- Авто-ответы ИИ ---
AUTOREPLY_CHANCE = 0.10
AUTOREPLY_COOLDOWN = 120
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'пиздато', 'пиздато', 'пиздато', 'ваномас'}
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'ваномас'}
def get_hint_text(templates_str: str) -> str:
return (
@ -97,6 +100,8 @@ def get_hint_text(templates_str: str) -> str:
"• /penis — миниигра 1 раз в 24 часа.\n"
"• /top_penis — топ по длине.\n"
"• /talk [текст] — ИИ базарит на фене.\n"
"• /penis_casino [ставка] — казино на размер.\n"
"• /gadanie [тема] — гадание на фене с мемом.\n"
"• /ebalnik — включить/выключить автоответы в чате.\n"
"• /prices /fetch — цены.\n"
"Подсказка: в группах пиши /команда@username_бота."

0
games/__init__.py Normal file
View file

127
games/casino.py Normal file
View file

@ -0,0 +1,127 @@
import random
import sqlite3
import logging
import config
logger = logging.getLogger(__name__)
# Символы слотов и их веса (чем выше вес — тем чаще выпадает)
SLOT_SYMBOLS = ["🍒", "🍋", "🔔", "💎", "7", "🍀"]
SLOT_WEIGHTS = [30, 25, 20, 12, 8, 5] # 🍒 чаще всего, 🍀 реже всего
# Множители для выигрышей: {кол-во совпадений: (мин, макс)}
MULTIPLIERS = {
2: (1.2, 1.8), # две одинаковых
3: (2.0, 3.0), # три одинаковых (джекпот, редко)
}
CASINO_WIN_CHANCE = 0.35
def spin_slots() -> tuple[list[str], int]:
reels = random.choices(SLOT_SYMBOLS, weights=SLOT_WEIGHTS, k=3)
if random.random() > CASINO_WIN_CHANCE:
while len(set(reels)) < 3:
reels = random.choices(SLOT_SYMBOLS, weights=SLOT_WEIGHTS, k=3)
return reels, 0
counts = {}
for s in reels:
counts[s] = counts.get(s, 0) + 1
max_match = max(counts.values())
return reels, max_match
def get_user_length(user_id: int) -> float | None:
try:
with sqlite3.connect(config.PENIS_DB_PATH) as conn:
row = conn.execute(
"SELECT length FROM penis_stats WHERE user_id = ?",
(user_id,),
).fetchone()
return float(row[0]) if row else None
except sqlite3.Error:
logger.exception("casino: failed to get length")
return None
def update_user_length(user_id: int, delta: float) -> float | None:
try:
with sqlite3.connect(config.PENIS_DB_PATH) as conn:
row = conn.execute(
"SELECT length FROM penis_stats WHERE user_id = ?",
(user_id,),
).fetchone()
if row is None:
return None
new_length = round(float(row[0]) + delta, 1)
conn.execute(
"UPDATE penis_stats SET length = ? WHERE user_id = ?",
(new_length, user_id),
)
conn.commit()
return new_length
except sqlite3.Error:
logger.exception("casino: failed to update length")
return None
def play_casino(user_id: int, bet: float) -> str:
current = get_user_length(user_id)
if current is None:
return "Ты ещё ни разу не крутил /penis, братуха. Сначала заведи счёт."
# Проверяем отрицательный баланс
if current <= 0:
return "🚫 С кредитом в казино не впускают, братуха. Иди наращивай через /penis."
# Проверяем максимум ставки
if bet > config.CASINO_MAX_BET:
return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай."
if bet <= 0:
return "Ставка должна быть больше нуля, фраер."
# Проверяем хватает ли
if bet > current:
return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер."
# Крутим
reels, matches = spin_slots()
slots_display = " | ".join(reels)
if matches >= 2:
min_mult, max_mult = MULTIPLIERS[matches]
multiplier = round(random.uniform(min_mult, max_mult), 1)
winnings = round(bet * multiplier, 1)
new_length = update_user_length(user_id, winnings)
if new_length is None:
return "Ошибка БД. Попробуй позже."
if matches == 3:
return (
f"🎰 {slots_display}\n\n"
f"🎉 ДЖЕКПОТ!!! Три одинаковых!\n"
f"Множитель: x{multiplier}\n"
f"Выигрыш: +{winnings:.1f} см\n"
f"📏 Текущий размер: {new_length:.1f} см"
)
return (
f"🎰 {slots_display}\n\n"
f"✅ Выигрыш! Две совпали.\n"
f"Множитель: x{multiplier}\n"
f"Выигрыш: +{winnings:.1f} см\n"
f"📏 Текущий размер: {new_length:.1f} см"
)
else:
new_length = update_user_length(user_id, -bet)
if new_length is None:
return "Ошибка БД. Попробуй позже."
return (
f"🎰 {slots_display}\n\n"
f"❌ Мимо, братуха.\n"
f"Проигрыш: -{bet:.1f} см\n"
f"📏 Текущий размер: {new_length:.1f} см"
)

57
games/fortune.py Normal file
View file

@ -0,0 +1,57 @@
import logging
import aiohttp
import os
logger = logging.getLogger(__name__)
LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-ai/")
FORTUNE_PROMPT = (
"Ты — уличный гадатель с района, базаришь на фене. "
"Пользователь хочет погадать на тему. Выдай шуточное гадание, "
"коротко и дерзко, 2-3 предложения максимум. "
"В конце ОБЯЗАТЕЛЬНО напиши на отдельной строке в формате:\n"
"MEM_UP: <текст сверху для мема, максимум 4 слова>\n"
"MEM_DOWN: <текст снизу для мема, максимум 4 слова>\n"
"Тексты для мема должны быть смешными и короткими."
)
async def generate_fortune(topic: str) -> tuple[str, str, str]:
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
payload = {
"messages": [
{"role": "system", "content": FORTUNE_PROMPT},
{"role": "user", "content": f"Погадай мне на: {topic}"},
],
"max_tokens": 200,
"temperature": 0.9,
"top_p": 0.9,
}
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
data = await resp.json()
text = data["choices"][0]["message"]["content"].strip()
up_text = ""
down_text = ""
fortune_lines = []
for line in text.split("\n"):
stripped = line.strip()
if stripped.upper().startswith("MEM_UP:"):
up_text = stripped.split(":", 1)[1].strip()
elif stripped.upper().startswith("MEM_DOWN:"):
down_text = stripped.split(":", 1)[1].strip()
else:
fortune_lines.append(line)
fortune_text = "\n".join(fortune_lines).strip()
if not up_text:
up_text = "Гадание"
if not down_text:
down_text = "по фене"
return fortune_text, up_text, down_text

43
main.py
View file

@ -19,6 +19,8 @@ from aiogram.client.default import DefaultBotProperties
# Локальные импорты
from AI.talk_handler import handle_talk, generate_autoreply, push_message
from games.casino import play_casino
from games.fortune import generate_fortune
from zparser import get_military_data
import config
@ -711,6 +713,43 @@ async def handle_keywords(message: Message):
except Exception:
logger.exception("Autoreply failed")
async def handle_penis_casino_cmd(message: Message):
if not message.from_user:
await message.answer("Команда доступна только пользователям.")
return
parts = message.text.split(maxsplit=1)
if len(parts) < 2:
await message.reply("Формат: /penis_casino <ставка>\nПример: /penis_casino 0.5")
return
try:
bet = round(float(parts[1].replace(",", ".")), 1)
except ValueError:
await message.reply("Ставка должна быть числом, фраер. Пример: /penis_casino 0.5")
return
result = await asyncio.to_thread(play_casino, message.from_user.id, bet)
await message.reply(result, parse_mode=None)
async def handle_gadanie_cmd(message: Message):
parts = message.text.split(maxsplit=1)
if len(parts) < 2 or not parts[1].strip():
await message.reply("Напиши на что гадать, кент. Пример: /gadanie любовь")
return
topic = parts[1].strip()
await message.bot.send_chat_action(chat_id=message.chat.id, action="typing")
try:
fortune_text, up_text, down_text = await generate_fortune(topic)
await message.reply(f"🔮 Гадание на «{topic}»:\n\n{fortune_text}", parse_mode=None)
photo_path = config.BASE_DIR / "maket" / "vape.jpg"
if photo_path.exists():
img = Image.open(photo_path).convert("RGB")
meme_output = await asyncio.to_thread(generate_meme, img, up_text, down_text)
await message.reply_photo(
BufferedInputFile(meme_output.getvalue(), filename="gadanie.jpg")
)
except Exception:
logger.exception("Gadanie failed")
await message.reply("Бля, карты рассыпались, попробуй позже.")
# --- Запуск ---
def main():
@ -735,6 +774,8 @@ def main():
dp.message.register(handle_talk, Command("talk"))
dp.message.register(handle_svodka_cmd, Command("svodka"))
dp.message.register(handle_ebalnik_cmd, Command("ebalnik"))
dp.message.register(handle_penis_casino_cmd, Command("penis_casino"))
dp.message.register(handle_gadanie_cmd, Command("gadanie"))
dp.message.register(handle_gen_mem, Command("gen_mem"))
dp.message.register(handle_keywords, F.text)
@ -754,6 +795,8 @@ def main():
BotCommand(command="zvetok", description="Цветянский бля"),
BotCommand(command="talk", description="Побазарить"),
BotCommand(command="ebalnik", description="вкл/выкл автответов"),
BotCommand(command="penis_casino", description="казино на размер"),
BotCommand(command="gadanie", description="гадание на фене"),
BotCommand(command="svodka", description="СВО: итоги"),
]
scopes = (