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 @@
+