Compare commits
No commits in common. "373aa8f6c2ba51c72cd2d66bc59d5ddd76cfbc5b" and "73e539b4c4bc6829e64da8325b2101d352698994" have entirely different histories.
373aa8f6c2
...
73e539b4c4
6 changed files with 6 additions and 250 deletions
22
README.md
22
README.md
|
|
@ -4,11 +4,7 @@
|
||||||
- вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`);
|
- вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`);
|
||||||
- делает мемы из присланных фото (`/gen_mem Up:... Down:...`);
|
- делает мемы из присланных фото (`/gen_mem Up:... Down:...`);
|
||||||
- подсказывает, сколько минут до перекура по заданному расписанию (`/break`);
|
- подсказывает, сколько минут до перекура по заданному расписанию (`/break`);
|
||||||
- умеет базарить (`/talk`) с контекстом 100 сообщений;
|
- умеет базарить (`/talk`).
|
||||||
- авто-ответы в чате (10% шанс + триггер-слова);
|
|
||||||
- миниигра `/penis` + `/top_penis`;
|
|
||||||
- казино на размер (`/penis_casino`);
|
|
||||||
- гадание на фене с мемом (`/gadanie`).
|
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|
@ -26,13 +22,7 @@ docker-compose up --build
|
||||||
- `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240).
|
- `/maket <id> <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240).
|
||||||
- `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down.
|
- `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down.
|
||||||
- `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур.
|
- `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур.
|
||||||
- `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений).
|
- `/talk <сообщение>` — побазарить с ИИ. Работает через llama.cpp сервер.
|
||||||
- `/penis` — миниигра, 1 раз в 24 часа.
|
|
||||||
- `/top_penis` — топ по длине.
|
|
||||||
- `/penis_casino <ставка>` — казино на размер (макс 1 см, казино всегда в выигрыше).
|
|
||||||
- `/gadanie <тема>` — ИИ-гадание на фене + авто-мем.
|
|
||||||
- `/ebalnik` — вкл/выкл авто-ответы бота в чате.
|
|
||||||
- `/svodka` — сводка СВО.
|
|
||||||
|
|
||||||
## Макеты и шрифт
|
## Макеты и шрифт
|
||||||
|
|
||||||
|
|
@ -49,14 +39,12 @@ docker-compose up --build
|
||||||
|
|
||||||
- `/maket` подбирает размер текста, чтобы уместить его в область макета.
|
- `/maket` подбирает размер текста, чтобы уместить его в область макета.
|
||||||
- `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой.
|
- `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой.
|
||||||
- `/break` считает по расписанию в `config.py`.
|
- `/break` считает по расписанию в `main.py` (словарь `SCHEDULE`).
|
||||||
- `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite).
|
- `/talk` шлёт запрос к llama.cpp серверу (`LLAMA_API_URL`), системный промпт задаёт стиль фени(Можно изменить).
|
||||||
- `/penis_casino` — слоты с множителем x1.2–x3 (`games/casino.py`).
|
|
||||||
- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`).
|
|
||||||
|
|
||||||
## ИИ-модуль (`AI/talk_handler.py`)
|
## ИИ-модуль (`AI/talk_handler.py`)
|
||||||
|
|
||||||
Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`). Хранит контекст 100 сообщений в SQLite.
|
Отдельный модуль, не трогает основной код. Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`).
|
||||||
|
|
||||||
Настройка сервера:
|
Настройка сервера:
|
||||||
1. Скачать модель: `wget https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf`
|
1. Скачать модель: `wget https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf`
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,6 @@ PENIS_COOLDOWN = timedelta(hours=24)
|
||||||
PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds())
|
PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds())
|
||||||
PENIS_TOP_LIMIT = 10
|
PENIS_TOP_LIMIT = 10
|
||||||
|
|
||||||
# --- Казино ---
|
|
||||||
CASINO_MAX_BET = 1.0
|
|
||||||
|
|
||||||
# --- Конфигурация макетов ---
|
# --- Конфигурация макетов ---
|
||||||
TEMPLATES = {
|
TEMPLATES = {
|
||||||
"1": {
|
"1": {
|
||||||
|
|
@ -89,7 +86,7 @@ KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'}
|
||||||
# --- Авто-ответы ИИ ---
|
# --- Авто-ответы ИИ ---
|
||||||
AUTOREPLY_CHANCE = 0.10
|
AUTOREPLY_CHANCE = 0.10
|
||||||
AUTOREPLY_COOLDOWN = 120
|
AUTOREPLY_COOLDOWN = 120
|
||||||
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'ваномас'}
|
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'пиздато', 'пиздато', 'пиздато', 'ваномас'}
|
||||||
|
|
||||||
def get_hint_text(templates_str: str) -> str:
|
def get_hint_text(templates_str: str) -> str:
|
||||||
return (
|
return (
|
||||||
|
|
@ -100,8 +97,6 @@ def get_hint_text(templates_str: str) -> str:
|
||||||
"• /penis — миниигра 1 раз в 24 часа.\n"
|
"• /penis — миниигра 1 раз в 24 часа.\n"
|
||||||
"• /top_penis — топ по длине.\n"
|
"• /top_penis — топ по длине.\n"
|
||||||
"• /talk [текст] — ИИ базарит на фене.\n"
|
"• /talk [текст] — ИИ базарит на фене.\n"
|
||||||
"• /penis_casino [ставка] — казино на размер.\n"
|
|
||||||
"• /gadanie [тема] — гадание на фене с мемом.\n"
|
|
||||||
"• /ebalnik — включить/выключить автоответы в чате.\n"
|
"• /ebalnik — включить/выключить автоответы в чате.\n"
|
||||||
"• /prices /fetch — цены.\n"
|
"• /prices /fetch — цены.\n"
|
||||||
"Подсказка: в группах пиши /команда@username_бота."
|
"Подсказка: в группах пиши /команда@username_бота."
|
||||||
|
|
|
||||||
127
games/casino.py
127
games/casino.py
|
|
@ -1,127 +0,0 @@
|
||||||
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} см"
|
|
||||||
)
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
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
43
main.py
|
|
@ -19,8 +19,6 @@ from aiogram.client.default import DefaultBotProperties
|
||||||
|
|
||||||
# Локальные импорты
|
# Локальные импорты
|
||||||
from AI.talk_handler import handle_talk, generate_autoreply, push_message
|
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
|
from zparser import get_military_data
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
|
@ -713,43 +711,6 @@ async def handle_keywords(message: Message):
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Autoreply failed")
|
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():
|
def main():
|
||||||
|
|
@ -774,8 +735,6 @@ def main():
|
||||||
dp.message.register(handle_talk, Command("talk"))
|
dp.message.register(handle_talk, Command("talk"))
|
||||||
dp.message.register(handle_svodka_cmd, Command("svodka"))
|
dp.message.register(handle_svodka_cmd, Command("svodka"))
|
||||||
dp.message.register(handle_ebalnik_cmd, Command("ebalnik"))
|
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_gen_mem, Command("gen_mem"))
|
||||||
dp.message.register(handle_keywords, F.text)
|
dp.message.register(handle_keywords, F.text)
|
||||||
|
|
||||||
|
|
@ -795,8 +754,6 @@ def main():
|
||||||
BotCommand(command="zvetok", description="Цветянский бля"),
|
BotCommand(command="zvetok", description="Цветянский бля"),
|
||||||
BotCommand(command="talk", description="Побазарить"),
|
BotCommand(command="talk", description="Побазарить"),
|
||||||
BotCommand(command="ebalnik", description="вкл/выкл автответов"),
|
BotCommand(command="ebalnik", description="вкл/выкл автответов"),
|
||||||
BotCommand(command="penis_casino", description="казино на размер"),
|
|
||||||
BotCommand(command="gadanie", description="гадание на фене"),
|
|
||||||
BotCommand(command="svodka", description="СВО: итоги"),
|
BotCommand(command="svodka", description="СВО: итоги"),
|
||||||
]
|
]
|
||||||
scopes = (
|
scopes = (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue