diff --git a/games/uwu.py b/games/uwu.py index 3a51d52..6cd9077 100644 --- a/games/uwu.py +++ b/games/uwu.py @@ -33,6 +33,7 @@ async def handle_uwu_cmd(message: types.Message): tags += " order:random score:>300" +async def fetch_uwu_post(tags: str) -> dict: url = "https://e621.net/posts.json" params = { "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)" } - try: - 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}") - await message.reply("Не удалось получить картинку от сервера :(") - return - data = await resp.json() - - posts = data.get("posts", []) - if not posts: - await message.reply("Ничего не найдено по этим тегам :(") - return + 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() - post = posts[0] + posts = data.get("posts", []) + if not posts: + raise Exception("Ничего не найдено по этим тегам :(") + + post = posts[0] + file_url = post.get("file", {}).get("url") + ext = post.get("file", {}).get("ext", "png") + if not file_url: + raise Exception("У найденного поста нет прямого URL изображения :(") + + 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", "png") - if not file_url: - await message.reply("У найденного поста нет прямого URL изображения :(") - return - - caption = random.choice(CAPTIONS) + 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 = ( + "🐈 Справка по тегам e621:\n\n" + "Перечислите любые теги через пробел после команды.\n" + "• rating: rating:safe (безопасные), rating:questionable, rating:explicit (NSFW)\n" + "• score: score:>500 (искать популярные посты с рейтингом больше 500)\n" + "• Исключить тег: поставьте минус, например -fox\n\n" + "Пример: /uwu wolf -rating:explicit score:>100\n" + "По умолчанию: rating:safe score:>500 -animated" + ) + 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"): # Для анимаций: отправляем caption отдельным сообщением, # а саму гифку/видео через URL напрямую — так Telegram @@ -84,4 +159,7 @@ async def handle_uwu_cmd(message: types.Message): except Exception as e: logger.exception("Error in uwu command") - await message.reply("Произошла ошибка при обращении к API :(") + if str(e) in ["Не удалось получить картинку от сервера :(", "Ничего не найдено по этим тегам :(", "У найденного поста нет прямого URL изображения :("]: + await message.reply(str(e)) + else: + await message.reply("Произошла ошибка при обращении к API :(") diff --git a/main.py b/main.py index 82fc5d0..5fb712c 100644 --- a/main.py +++ b/main.py @@ -951,23 +951,30 @@ async def handle_help(message: Message): async def handle_start(message: Message): webapp_url = os.getenv("WEBAPP_URL", "") if webapp_url: - kb = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))] - ]) - await message.answer("Йо, кент! Жми кнопку 👇", reply_markup=kb) + if message.chat.type == "private": + kb = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))] + ]) + await message.answer("Йо, кент! Жми кнопку 👇", reply_markup=kb) + else: + await message.answer("Йо, кент! Mini App доступно только в личных сообщениях. Пиши мне в личку!") else: await message.answer("Соси") async def handle_app_cmd(message: Message): - """Открыть Mini App — работает и в группах.""" + """Открыть Mini App.""" webapp_url = os.getenv("WEBAPP_URL", "") if not webapp_url: await message.answer("Mini App не настроен.") return - kb = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))] - ]) - await message.answer("⬇️ Жми кнопку чтобы открыть приложение", reply_markup=kb) + + if message.chat.type == "private": + kb = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="🎮 Открыть Mini App", web_app=WebAppInfo(url=webapp_url))] + ]) + 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): now = datetime.now(ZoneInfo(config.DEFAULT_TZ)) diff --git a/webapp/api.py b/webapp/api.py index d8009d4..a54f0a9 100644 --- a/webapp/api.py +++ b/webapp/api.py @@ -25,10 +25,13 @@ from games.casino import play_casino, get_user_length, spin_slots, MULTIPLIERS, from games.betting import ( fetch_matches, place_bet, + get_match_by_index, + resolve_match_outcome, get_user_bets as _get_user_bets_raw, get_user_bet_history as _get_user_bet_history_raw, ) from webapp.auth import validate_init_data +from games.uwu import fetch_uwu_post, fetch_furtok_feed logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -223,8 +226,19 @@ async def create_bet( ): """Разместить ставку на матч.""" 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} @@ -352,6 +366,40 @@ async def get_schedule(day: int | None = None) -> list[SchedulePair]: 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") diff --git a/webapp/frontend/css/style.css b/webapp/frontend/css/style.css index c227cd1..8347cf0 100644 --- a/webapp/frontend/css/style.css +++ b/webapp/frontend/css/style.css @@ -871,3 +871,137 @@ body::before { font-size: 14px; 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; +} diff --git a/webapp/frontend/index.html b/webapp/frontend/index.html index b88bcc0..02a7753 100644 --- a/webapp/frontend/index.html +++ b/webapp/frontend/index.html @@ -31,6 +31,10 @@ 📅 Пары +