1
0
Fork 0
forked from zovos/bot_tg
This commit is contained in:
Omelechko Danil 2026-04-12 04:11:08 +03:00
parent b47e6bee81
commit 298a5d7d59
7 changed files with 450 additions and 32 deletions

View file

@ -33,6 +33,7 @@ async def handle_uwu_cmd(message: types.Message):
tags += " order:random score:>300" tags += " order:random score:>300"
async def fetch_uwu_post(tags: str) -> dict:
url = "https://e621.net/posts.json" url = "https://e621.net/posts.json"
params = { params = {
"tags": tags, "tags": tags,
@ -47,28 +48,102 @@ async def handle_uwu_cmd(message: types.Message):
"User-Agent": f"Sex Bomba TG Bot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)" "User-Agent": f"Sex Bomba TG Bot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)"
} }
try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, auth=auth) as resp: async with session.get(url, params=params, headers=headers, auth=auth) as resp:
if resp.status != 200: if resp.status != 200:
logger.error(f"e621 API Error: {resp.status}") logger.error(f"e621 API Error: {resp.status}")
await message.reply("Не удалось получить картинку от сервера :(") raise Exception("Не удалось получить картинку от сервера :(")
return
data = await resp.json() data = await resp.json()
posts = data.get("posts", []) posts = data.get("posts", [])
if not posts: if not posts:
await message.reply("Ничего не найдено по этим тегам :(") raise Exception("Ничего не найдено по этим тегам :(")
return
post = posts[0] post = posts[0]
file_url = post.get("file", {}).get("url") file_url = post.get("file", {}).get("url")
ext = post.get("file", {}).get("ext", "png") ext = post.get("file", {}).get("ext", "png")
if not file_url: if not file_url:
await message.reply("У найденного поста нет прямого URL изображения :(") raise Exception("У найденного поста нет прямого URL изображения :(")
return
caption = random.choice(CAPTIONS) caption = random.choice(CAPTIONS)
return {
"url": file_url,
"ext": ext,
"caption": caption
}
async def fetch_furtok_feed(safe: bool = True, page: int = 1) -> list[dict]:
rating = "rating:safe" if safe else "-rating:safe"
tags = f"{rating} animated order:random score:>200"
url = "https://e621.net/posts.json"
params = {
"tags": tags,
"limit": 10,
"page": page,
}
auth = None
if config.E621_LOGIN and config.E621_API_KEY:
auth = aiohttp.BasicAuth(config.E621_LOGIN, config.E621_API_KEY)
headers = {
"User-Agent": f"Sex Bomba TG Bot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)"
}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, auth=auth) as resp:
if resp.status != 200:
logger.error(f"e621 API Error: {resp.status}")
raise Exception("Не удалось загрузить ленту :(")
data = await resp.json()
posts = data.get("posts", [])
feed = []
for post in posts:
file_url = post.get("file", {}).get("url")
ext = post.get("file", {}).get("ext", "")
if not file_url or ext not in ("gif", "webm", "mp4"):
continue
sample_url = post.get("sample", {}).get("url") or file_url
feed.append({
"url": file_url,
"sample": sample_url,
"ext": ext,
"score": post.get("score", {}).get("total", 0),
"fav_count": post.get("fav_count", 0),
})
return feed
async def handle_uwu_cmd(message: types.Message):
args = message.text.split(maxsplit=1)
if len(args) > 1 and args[1].strip() == "-h":
help_msg = (
"🐈 <b>Справка по тегам e621:</b>\n\n"
"Перечислите любые теги через пробел после команды.\n"
"• <b>rating</b>: <code>rating:safe</code> (безопасные), <code>rating:questionable</code>, <code>rating:explicit</code> (NSFW)\n"
"• <b>score</b>: <code>score:>500</code> (искать популярные посты с рейтингом больше 500)\n"
"• Исключить тег: поставьте минус, например <code>-fox</code>\n\n"
"<i>Пример:</i> <code>/uwu wolf -rating:explicit score:>100</code>\n"
"<i>По умолчанию:</i> <code>rating:safe score:>500 -animated</code>"
)
await message.reply(help_msg, parse_mode="HTML")
return
tags = "rating:safe score:>500 -animated"
if len(args) > 1:
tags = args[1].strip()
tags += " order:random score:>300"
try:
post = await fetch_uwu_post(tags)
file_url = post["url"]
ext = post["ext"]
caption = post["caption"]
if ext in ("gif", "webm", "mp4"): if ext in ("gif", "webm", "mp4"):
# Для анимаций: отправляем caption отдельным сообщением, # Для анимаций: отправляем caption отдельным сообщением,
# а саму гифку/видео через URL напрямую — так Telegram # а саму гифку/видео через URL напрямую — так Telegram
@ -84,4 +159,7 @@ async def handle_uwu_cmd(message: types.Message):
except Exception as e: except Exception as e:
logger.exception("Error in uwu command") logger.exception("Error in uwu command")
if str(e) in ["Не удалось получить картинку от сервера :(", "Ничего не найдено по этим тегам :(", "У найденного поста нет прямого URL изображения :("]:
await message.reply(str(e))
else:
await message.reply("Произошла ошибка при обращении к API :(") await message.reply("Произошла ошибка при обращении к API :(")

View file

@ -951,23 +951,30 @@ async def handle_help(message: Message):
async def handle_start(message: Message): async def handle_start(message: Message):
webapp_url = os.getenv("WEBAPP_URL", "") webapp_url = os.getenv("WEBAPP_URL", "")
if webapp_url: if webapp_url:
if message.chat.type == "private":
kb = InlineKeyboardMarkup(inline_keyboard=[ kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))] [InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))]
]) ])
await message.answer("Йо, кент! Жми кнопку 👇", reply_markup=kb) await message.answer("Йо, кент! Жми кнопку 👇", reply_markup=kb)
else:
await message.answer("Йо, кент! Mini App доступно только в личных сообщениях. Пиши мне в личку!")
else: else:
await message.answer("Соси") await message.answer("Соси")
async def handle_app_cmd(message: Message): async def handle_app_cmd(message: Message):
"""Открыть Mini App — работает и в группах.""" """Открыть Mini App."""
webapp_url = os.getenv("WEBAPP_URL", "") webapp_url = os.getenv("WEBAPP_URL", "")
if not webapp_url: if not webapp_url:
await message.answer("Mini App не настроен.") await message.answer("Mini App не настроен.")
return return
if message.chat.type == "private":
kb = InlineKeyboardMarkup(inline_keyboard=[ kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))] [InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))]
]) ])
await message.answer("⬇️ Жми кнопку чтобы открыть приложение", reply_markup=kb) await message.answer("⬇️ Жми кнопку чтобы открыть приложение", reply_markup=kb)
else:
await message.answer("Братуха, встроенная кнопка Mini App пока работает только в личке (ограничение Telegram). Перейди в личные сообщения с ботом или используй прямую ссылку: https://t.me/FenyaBotTest_bot/app (если она настроена).")
async def handle_schedule_cmd(message: Message): async def handle_schedule_cmd(message: Message):
now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) now = datetime.now(ZoneInfo(config.DEFAULT_TZ))

View file

@ -25,10 +25,13 @@ from games.casino import play_casino, get_user_length, spin_slots, MULTIPLIERS,
from games.betting import ( from games.betting import (
fetch_matches, fetch_matches,
place_bet, place_bet,
get_match_by_index,
resolve_match_outcome,
get_user_bets as _get_user_bets_raw, get_user_bets as _get_user_bets_raw,
get_user_bet_history as _get_user_bet_history_raw, get_user_bet_history as _get_user_bet_history_raw,
) )
from webapp.auth import validate_init_data from webapp.auth import validate_init_data
from games.uwu import fetch_uwu_post, fetch_furtok_feed
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -223,8 +226,19 @@ async def create_bet(
): ):
"""Разместить ставку на матч.""" """Разместить ставку на матч."""
user_id = user["user_id"] user_id = user["user_id"]
display_name = user["username"] or user["first_name"]
result = await place_bet(user_id, display_name, req.league, req.match_index, req.outcome, req.amount) # 1. Найти матч по индексу
match = await get_match_by_index(req.league, req.match_index)
if not match:
raise HTTPException(status_code=404, detail="Матч не найден")
# 2. Преобразовать код исхода (1/X/2) в название команды
team = resolve_match_outcome(match, req.outcome)
if not team:
raise HTTPException(status_code=400, detail="Неверный исход ставки")
# 3. Разместить ставку (синхронная функция)
result = await asyncio.to_thread(place_bet, user_id, match, req.league, team, req.amount)
return {"message": result} return {"message": result}
@ -352,6 +366,40 @@ async def get_schedule(day: int | None = None) -> list[SchedulePair]:
return result return result
# --- UwU Арты ---
@app.get("/api/uwu")
async def get_uwu_api(
tags: str = "rating:safe score:>500 -animated",
user: dict = Depends(get_current_user),
):
"""Отправить случайный uwu арт по тегам."""
try:
tags += " order:random score:>300"
post = await fetch_uwu_post(tags)
return {"response": post}
except Exception as e:
logger.exception("UwU API failed")
raise HTTPException(status_code=500, detail=str(e))
# --- FurTok Лента ---
@app.get("/api/furtok")
async def get_furtok_feed_api(
safe: bool = True,
page: int = 1,
user: dict = Depends(get_current_user),
):
"""Получить ленту анимированных постов для FurTok."""
try:
feed = await fetch_furtok_feed(safe=safe, page=page)
return {"feed": feed}
except Exception as e:
logger.exception("FurTok API failed")
raise HTTPException(status_code=500, detail=str(e))
# --- ИИ Чат --- # --- ИИ Чат ---
@app.post("/api/talk") @app.post("/api/talk")

View file

@ -871,3 +871,137 @@ body::before {
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
} }
/* ── FurTok Feed ── */
.furtok-wrapper {
position: fixed;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 480px;
height: calc(100vh - var(--nav-height));
overflow: hidden;
z-index: 10;
}
.furtok-feed {
height: 100%;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scrollbar-width: none;
}
.furtok-feed::-webkit-scrollbar { display: none; }
.furtok-card {
height: 100%;
scroll-snap-align: start;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: #000;
overflow: hidden;
}
.furtok-card video,
.furtok-card img {
width: 100%;
height: 100%;
object-fit: contain;
}
.furtok-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
display: flex;
align-items: flex-end;
justify-content: space-between;
pointer-events: none;
}
.furtok-stats {
display: flex;
gap: 16px;
font-size: 13px;
font-weight: 600;
color: rgba(255,255,255,0.85);
}
.furtok-header {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: linear-gradient(rgba(0,0,0,0.5), transparent);
z-index: 5;
}
.furtok-title {
font-size: 18px;
font-weight: 800;
color: white;
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.furtok-toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
color: rgba(255,255,255,0.8);
}
.furtok-toggle input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 40px;
height: 22px;
background: rgba(255,255,255,0.2);
border-radius: 11px;
position: relative;
cursor: pointer;
transition: background 0.3s;
}
.furtok-toggle input[type="checkbox"]::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background: white;
border-radius: 50%;
transition: transform 0.3s;
}
.furtok-toggle input[type="checkbox"]:checked {
background: var(--green-500);
}
.furtok-toggle input[type="checkbox"]:checked::after {
transform: translateX(18px);
}
.furtok-loader {
height: 100%;
scroll-snap-align: start;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 14px;
}

View file

@ -31,6 +31,10 @@
<span class="nav-icon">📅</span> <span class="nav-icon">📅</span>
<span class="nav-label">Пары</span> <span class="nav-label">Пары</span>
</button> </button>
<button class="nav-btn" data-page="furtok">
<span class="nav-icon">🐺</span>
<span class="nav-label">FurTok</span>
</button>
<button class="nav-btn" data-page="chat"> <button class="nav-btn" data-page="chat">
<span class="nav-icon">💬</span> <span class="nav-icon">💬</span>
<span class="nav-label">Чат</span> <span class="nav-label">Чат</span>

View file

@ -40,5 +40,7 @@ export const API = {
playPenis: () => api('/api/penis', { method: 'POST' }), playPenis: () => api('/api/penis', { method: 'POST' }),
getTop: () => api('/api/top'), getTop: () => api('/api/top'),
getSchedule: (day) => api(`/api/schedule${day != null ? `?day=${day}` : ''}`), getSchedule: (day) => api(`/api/schedule${day != null ? `?day=${day}` : ''}`),
getUwu: (tags) => api(`/api/uwu?tags=${encodeURIComponent(tags || 'rating:safe score:>500 -animated')}`),
getFurtokFeed: (safe = true, page = 1) => api(`/api/furtok?safe=${safe}&page=${page}`),
talk: (text) => api('/api/talk', { method: 'POST', body: { text } }), talk: (text) => api('/api/talk', { method: 'POST', body: { text } }),
}; };

View file

@ -35,6 +35,10 @@ const navBtns = document.querySelectorAll('.nav-btn');
function navigate(page, data = null) { function navigate(page, data = null) {
currentPage = page; currentPage = page;
haptic(); haptic();
// Убираем оверлеи при смене страницы
document.querySelector('.bet-slip')?.remove();
document.querySelector('.result-overlay')?.remove();
document.querySelector('.furtok-wrapper')?.remove();
navBtns.forEach(b => b.classList.toggle('active', b.dataset.page === page)); navBtns.forEach(b => b.classList.toggle('active', b.dataset.page === page));
container.style.animation = 'none'; container.style.animation = 'none';
container.offsetHeight; container.offsetHeight;
@ -45,6 +49,7 @@ function navigate(page, data = null) {
case 'betting': renderBetting(data); break; case 'betting': renderBetting(data); break;
case 'casino': renderCasino(); break; case 'casino': renderCasino(); break;
case 'schedule': renderSchedule(); break; case 'schedule': renderSchedule(); break;
case 'furtok': renderFurtok(); break;
case 'chat': renderChat(); break; case 'chat': renderChat(); break;
default: renderHome(); default: renderHome();
} }
@ -491,6 +496,146 @@ async function renderSchedule() {
loadDay(todayIdx); loadDay(todayIdx);
} }
// ══════════════════════════════════════════
// PAGE: FURTOK
// ══════════════════════════════════════════
let furtokSafe = true;
let furtokPage = 1;
let furtokLoading = false;
let furtokWrapper = null;
function renderFurtok() {
// Очищаем основной контейнер и создаём полноэкранную обёртку
container.innerHTML = '';
// Удаляем старый wrapper если есть
document.querySelector('.furtok-wrapper')?.remove();
furtokPage = 1;
furtokLoading = false;
furtokWrapper = document.createElement('div');
furtokWrapper.className = 'furtok-wrapper';
furtokWrapper.innerHTML = `
<div class="furtok-header">
<div class="furtok-title">🐺 FurTok</div>
<label class="furtok-toggle">
<span>Safe</span>
<input type="checkbox" id="furtok-safe" ${furtokSafe ? 'checked' : ''}>
</label>
</div>
<div class="furtok-feed" id="furtok-feed"></div>
`;
document.body.appendChild(furtokWrapper);
const feed = document.getElementById('furtok-feed');
const safeToggle = document.getElementById('furtok-safe');
safeToggle.addEventListener('change', () => {
furtokSafe = safeToggle.checked;
furtokPage = 1;
feed.innerHTML = '';
loadFurtokPage(feed);
});
// Infinite scroll
feed.addEventListener('scroll', () => {
if (furtokLoading) return;
if (feed.scrollTop + feed.clientHeight >= feed.scrollHeight - 200) {
furtokPage++;
loadFurtokPage(feed);
}
});
// Первая загрузка
loadFurtokPage(feed);
}
async function loadFurtokPage(feedEl) {
if (furtokLoading) return;
furtokLoading = true;
// Лоадер
const loader = document.createElement('div');
loader.className = 'furtok-loader';
loader.textContent = '⏳ Загрузка...';
feedEl.appendChild(loader);
try {
const res = await API.getFurtokFeed(furtokSafe, furtokPage);
loader.remove();
if (!res.feed || res.feed.length === 0) {
if (furtokPage === 1) {
feedEl.innerHTML = `<div class="furtok-loader">Ничего не найдено</div>`;
}
furtokLoading = false;
return;
}
res.feed.forEach(post => {
const card = document.createElement('div');
card.className = 'furtok-card';
let mediaHtml = '';
if (post.ext === 'gif') {
mediaHtml = `<img src="${post.url}" loading="lazy">`;
} else {
mediaHtml = `<video src="${post.url}" loop playsinline preload="metadata" muted></video>`;
}
card.innerHTML = `
${mediaHtml}
<div class="furtok-overlay">
<div class="furtok-stats">
<span> ${post.score}</span>
<span> ${post.fav_count}</span>
</div>
</div>
`;
// Тап по видео = unmute + play/pause
const video = card.querySelector('video');
if (video) {
card.addEventListener('click', () => {
video.muted = false;
if (video.paused) video.play();
else video.pause();
});
}
feedEl.appendChild(card);
});
// IntersectionObserver для автоплейа
setupAutoplay(feedEl);
haptic('success');
} catch (e) {
loader.remove();
haptic('error');
if (furtokPage === 1) {
feedEl.innerHTML = `<div class="furtok-loader">❌ ${e.message}</div>`;
}
}
furtokLoading = false;
}
function setupAutoplay(feedEl) {
const videos = feedEl.querySelectorAll('.furtok-card video');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.play().catch(() => {});
} else {
entry.target.pause();
}
});
}, { root: feedEl, threshold: 0.6 });
videos.forEach(v => observer.observe(v));
}
// ══════════════════════════════════════════ // ══════════════════════════════════════════
// PAGE: CHAT // PAGE: CHAT
// ══════════════════════════════════════════ // ══════════════════════════════════════════