Compare commits

...

3 commits

4 changed files with 107 additions and 48 deletions

0
.codex Normal file
View file

View file

@ -2,6 +2,7 @@ import asyncio
import os import os
import logging import logging
import sqlite3 import sqlite3
import re
from pathlib import Path from pathlib import Path
import aiohttp import aiohttp
@ -69,6 +70,8 @@ _CONTEXT_SIZE = 20
_MAX_PROMPT_CHARS = 8_000 _MAX_PROMPT_CHARS = 8_000
# Путь к SQLite базе (берётся из переменной окружения или из config) # Путь к SQLite базе (берётся из переменной окружения или из config)
_DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH", "/db/chat_history.sqlite3") _DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH", "/db/chat_history.sqlite3")
_MAX_REPLY_SENTENCES = 3
_MAX_REPLY_CHARS = 280
def _init_db() -> None: def _init_db() -> None:
@ -139,20 +142,49 @@ def _build_messages(chat_id: int, system_prompt: str, current_text: str, current
return messages return messages
def _shorten_reply(text: str, max_sentences: int = _MAX_REPLY_SENTENCES, max_chars: int = _MAX_REPLY_CHARS) -> str:
"""Ограничивает ответ до 1-3 предложений и разумной длины."""
cleaned = " ".join((text or "").replace("\n", " ").split()).strip()
if not cleaned:
return "Братуха, коротко: чёт пусто получилось."
# Делим по окончаниям предложений, сохраняя знаки.
parts = re.split(r"(?<=[.!?…])\s+", cleaned)
sentences = [p.strip() for p in parts if p.strip()]
if not sentences:
# На случай ответа без пунктуации.
words = cleaned.split()
return " ".join(words[:25]).strip()
short = " ".join(sentences[:max_sentences]).strip()
# Дополнительный предохранитель по символам.
if len(short) > max_chars:
clipped = short[:max_chars].rstrip()
last_break = max(clipped.rfind("."), clipped.rfind("!"), clipped.rfind("?"), clipped.rfind(""))
if last_break >= 40:
clipped = clipped[: last_break + 1].rstrip()
short = clipped
return short
async def _generate_response(chat_id: int, user_text: str, user_name: str) -> str: async def _generate_response(chat_id: int, user_text: str, user_name: str) -> str:
# Формируем запрос к /v1/chat/completions с историей чата и возвращаем текст первого ответа. # Формируем запрос к /v1/chat/completions с историей чата и возвращаем текст первого ответа.
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions" url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
msgs = await asyncio.to_thread(_build_messages, chat_id, SYSTEM_PROMPT, user_text, user_name) msgs = await asyncio.to_thread(_build_messages, chat_id, SYSTEM_PROMPT, user_text, user_name)
payload = { payload = {
"messages": msgs, "messages": msgs,
"max_tokens": 200, "max_tokens": 90,
"temperature": 0.8, "temperature": 0.8,
"top_p": 0.9, "top_p": 0.9,
} }
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp: async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
data = await resp.json() data = await resp.json()
return data["choices"][0]["message"]["content"].strip() content = data["choices"][0]["message"]["content"].strip()
return _shorten_reply(content)
async def handle_talk(message: Message): async def handle_talk(message: Message):
@ -230,14 +262,15 @@ async def generate_autoreply(chat_id: int, text: str, user_name: str) -> str:
msgs = await asyncio.to_thread(_build_messages, chat_id, AUTOREPLY_SYSTEM_PROMPT, text, user_name) msgs = await asyncio.to_thread(_build_messages, chat_id, AUTOREPLY_SYSTEM_PROMPT, text, user_name)
payload = { payload = {
"messages": msgs, "messages": msgs,
"max_tokens": 150, "max_tokens": 70,
"temperature": 0.9, "temperature": 0.9,
"top_p": 0.9, "top_p": 0.9,
} }
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp: async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
data = await resp.json() data = await resp.json()
return data["choices"][0]["message"]["content"].strip() content = data["choices"][0]["message"]["content"].strip()
return _shorten_reply(content)
# Инициализация БД при импорте модуля # Инициализация БД при импорте модуля

View file

@ -3,13 +3,14 @@ from aiogram import types
import config import config
import logging import logging
import random import random
import json import re
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CAPTIONS = [ CAPTIONS = [
"Hot stuff 🔥", "Hot stuff 🔥",
"Sexy content 😏" "Sexy content 😏",
"ШПАЧИХА"
] ]
async def handle_nude_cmd(message: types.Message): async def handle_nude_cmd(message: types.Message):
@ -19,66 +20,90 @@ async def handle_nude_cmd(message: types.Message):
help_msg = ( help_msg = (
"🔥 <b>Справка по команде /nude:</b>\n\n" "🔥 <b>Справка по команде /nude:</b>\n\n"
"Получает случайные изображения взрослых женщин из открытых источников.\n" "Получает случайные изображения взрослых женщин из открытых источников.\n"
"• Использует Pornhub API для поиска контента\n" "• Использует прямое сканирование сайтов для поиска контента\n"
"• По умолчанию ищет популярные видео с высоким рейтингом\n" "• По умолчанию ищет популярные изображения\n"
"• Отправляет превью видео как изображение\n\n" "• Отправляет изображения напрямую\n\n"
"<i>Пример:</i> <code>/nude</code>\n" "<i>Пример:</i> <code>/nude</code>\n"
"<i>По умолчанию:</i> Популярный контент для взрослых" "<i>По умолчанию:</i> Случайные изображения с популярных сайтов"
) )
await message.reply(help_msg, parse_mode="HTML") await message.reply(help_msg, parse_mode="HTML")
return return
# Используем Pornhub API для поиска видео # Список сайтов для сканирования
url = "https://www.pornhub.com/webmasters/search" sites = [
"https://www.imagefap.com/gallery.php?pg=random",
"https://xhamster.com/photos?sort=random",
"https://spankbang.com/random"
]
params = { random_site = random.choice(sites)
"category": "straight",
"ordering": "mostviewed",
"period": "week",
"thumbsize": "medium"
}
headers = { headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1"
} }
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers) as resp: async with session.get(random_site, headers=headers) as resp:
if resp.status != 200: if resp.status != 200:
logger.error(f"Pornhub API Error: {resp.status}") logger.error(f"Site access error: {resp.status}")
await message.reply("Не удалось получить контент от сервера :(") await message.reply("Не удалось получить доступ к сайту :(")
return return
# Pornhub API возвращает HTML, нужно парсить html = await resp.text()
text = await resp.text()
# Ищем URL превью в HTML # Ищем URL изображений в HTML
import re img_patterns = [
thumb_pattern = r'<img[^>]+src="([^"]+)"[^>]+class="thumb"' r'<img[^>]+src="([^"]+)"',
matches = re.findall(thumb_pattern, text) r'<a[^>]+href="([^"]+\.(jpg|jpeg|png|gif|webp))"',
r'data-src="([^"]+)"'
]
if not matches: img_urls = []
await message.reply("Не удалось найти контент :(") for pattern in img_patterns:
matches = re.findall(pattern, html, re.IGNORECASE)
img_urls.extend(matches)
if not img_urls:
await message.reply("Изображения не найдены на странице :(")
return return
# Берем случайное превью # Фильтруем и выбираем случайное изображение
thumb_url = random.choice(matches) valid_imgs = []
for img_url in img_urls:
if img_url.startswith('//'):
img_url = 'https:' + img_url
elif img_url.startswith('/'):
if 'imagefap.com' in random_site:
img_url = 'https://www.imagefap.com' + img_url
elif 'xhamster.com' in random_site:
img_url = 'https://xhamster.com' + img_url
elif 'spankbang.com' in random_site:
img_url = 'https://spankbang.com' + img_url
if any(ext in img_url.lower() for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
valid_imgs.append(img_url)
# Убедимся что URL полный if not valid_imgs:
if not thumb_url.startswith('http'): await message.reply("Подходящие изображения не найдены :(")
thumb_url = f"https://thumb.pornhub.com{thumb_url}" return
caption = random.choice(CAPTIONS) final_img_url = random.choice(valid_imgs)
caption = random.choice(CAPTIONS)
# Отправляем как фото
try: # Отправляем изображение
input_file = types.URLInputFile(thumb_url) try:
await message.answer_photo(photo=input_file, caption=caption) input_file = types.URLInputFile(final_img_url)
except Exception as e: await message.answer_photo(photo=input_file, caption=caption)
logger.error(f"Error sending photo: {e}") except Exception as e:
await message.answer(f"{caption}\n{thumb_url}") logger.error(f"Error sending photo: {e}")
await message.answer(f"{caption}\n{final_img_url}")
except Exception as e: except Exception as e:
logger.exception("Error in nude command") logger.exception("Error in nude command")
await message.reply("Произошла ошибка при обращении к API :(") await message.reply("Произошла ошибка при обработке запроса :(")

View file

@ -1145,6 +1145,10 @@ async def handle_keywords(message: Message):
chat_id = message.chat.id chat_id = message.chat.id
user_name = (message.from_user.first_name or "кент") if message.from_user else "кент" user_name = (message.from_user.first_name or "кент") if message.from_user else "кент"
# /ebalnik должен глушить все авто-реакции в чате, включая медиа/фото-ветки.
if chat_id in _autoreply_disabled_chats:
return
if any(re.search(rf'\b{word}\b', text_lower) for word in config.KEYWORDS): if any(re.search(rf'\b{word}\b', text_lower) for word in config.KEYWORDS):
audio_path = config.BASE_DIR / "maket" / "Radiohead - Creep.mp3" audio_path = config.BASE_DIR / "maket" / "Radiohead - Creep.mp3"
if audio_path.exists(): if audio_path.exists():
@ -1171,9 +1175,6 @@ async def handle_keywords(message: Message):
# Записываем каждое обычное сообщение в историю чата # Записываем каждое обычное сообщение в историю чата
await asyncio.to_thread(push_message, chat_id, "user", user_name, message.text) await asyncio.to_thread(push_message, chat_id, "user", user_name, message.text)
if chat_id in _autoreply_disabled_chats:
return
import time as _time import time as _time
now = _time.time() now = _time.time()
triggered = any(trigger in text_lower for trigger in config.AUTOREPLY_TRIGGERS) triggered = any(trigger in text_lower for trigger in config.AUTOREPLY_TRIGGERS)