From c2e210c4ae863a7fa6db85c38af488597e0e6b7e Mon Sep 17 00:00:00 2001 From: Danil Date: Mon, 13 Apr 2026 18:50:37 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D1=80=D0=B8=D1=82=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D0=B8=D1=85=20=D0=B1=D0=B0=D0=B3=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=BE=D1=82=D0=B5=D0=BD=D1=86=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BB?= =?UTF-8?q?=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Удалена дублирующая функция handle_uwu_cmd в uwu.py (мёртвый код) - Исправлен краш UnboundLocalError в /zvetok (next_dt и delta могли быть не определены) - Баланс penis больше не уходит в минус при проигрыше в казино - talk_handler теперь использует путь БД из config.py вместо своего (разные файлы БД) - betting.py больше не крашится при импорте если /db/ недоступен - Таймаут гадания снижен со 120 до 30 секунд (Telegram таймаутит раньше) --- AI/talk_handler.py | 2 +- games/betting.py | 5 ++++- games/casino.py | 4 ++++ games/fortune.py | 2 +- games/uwu.py | 22 ---------------------- main.py | 8 ++++++++ 6 files changed, 18 insertions(+), 25 deletions(-) diff --git a/AI/talk_handler.py b/AI/talk_handler.py index e88b073..2e99a10 100644 --- a/AI/talk_handler.py +++ b/AI/talk_handler.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) # Основные настройки поведения и стиля бота редактируются здесь. LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-ai/") -DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH") or str(Path(__file__).resolve().with_name("chat_history.sqlite3")) +DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH") or getattr(__import__("config"), "CHAT_HISTORY_DB_PATH", str(Path(__file__).resolve().with_name("chat_history.sqlite3"))) BOT_MEMORY_NAME = os.getenv("BOT_MEMORY_NAME", "бот") SKIP_TOKEN = "" FORCE_DISABLE_THINKING = os.getenv("LLAMA_FORCE_DISABLE_THINKING", "1").lower() not in {"0", "false", "no"} diff --git a/games/betting.py b/games/betting.py index 933da3c..d805de8 100644 --- a/games/betting.py +++ b/games/betting.py @@ -59,7 +59,10 @@ def _init_bets_db() -> None: conn.commit() -_init_bets_db() +try: + _init_bets_db() +except Exception: + logger.warning("Could not init bets DB at import time (will retry on first use)") def remember_user_sport_context(user_id: int, sport_alias: str) -> None: diff --git a/games/casino.py b/games/casino.py index 6c4a534..1d4e744 100644 --- a/games/casino.py +++ b/games/casino.py @@ -57,6 +57,9 @@ def update_user_length(user_id: int, delta: float) -> float | None: if row is None: return None new_length = round(float(row[0]) + delta, 1) + # Не даём уйти ниже 0 при проигрыше + if delta < 0 and new_length < 0: + new_length = 0.0 conn.execute( "UPDATE penis_stats SET length = ? WHERE user_id = ?", (new_length, user_id), @@ -68,6 +71,7 @@ def update_user_length(user_id: int, delta: float) -> float | None: return None + def play_casino(user_id: int, bet: float) -> str: current = get_user_length(user_id) if current is None: diff --git a/games/fortune.py b/games/fortune.py index 7ac5c71..c4d9593 100644 --- a/games/fortune.py +++ b/games/fortune.py @@ -30,7 +30,7 @@ async def generate_fortune(topic: str) -> tuple[str, str, str]: } 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=30)) as resp: data = await resp.json() text = data["choices"][0]["message"]["content"].strip() diff --git a/games/uwu.py b/games/uwu.py index 831d09e..9b7db44 100644 --- a/games/uwu.py +++ b/games/uwu.py @@ -55,28 +55,6 @@ CAPTIONS = [ "Я нейросеть, помогите, меня держат в заложниках" ] -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" - async def fetch_uwu_post(tags: str) -> dict: url = "https://e621.net/posts.json" params = { diff --git a/main.py b/main.py index 64b337d..02b60ef 100644 --- a/main.py +++ b/main.py @@ -597,11 +597,14 @@ def minutes_until_next_lesson(now: datetime | None = None) -> str: start_today = lesson_start_for(0) end_today = lesson_end_today() if start_today <= now < end_today: + next_dt = None for i in range(1, 8): next_day = (day + i) % 7 if next_day in lesson_days: next_dt = lesson_start_for(i) break + if next_dt is None: + return "Следующий урок не найден." delta = next_dt - now total_seconds = int(delta.total_seconds()) days = total_seconds // 86400 @@ -613,6 +616,7 @@ def minutes_until_next_lesson(now: datetime | None = None) -> str: f"Мозг начнет догнивать через {days} дн {hours} ч {minutes} мин" ) + delta = None for i in range(0, 8): next_day = (day + i) % 7 if next_day in lesson_days: @@ -621,12 +625,16 @@ def minutes_until_next_lesson(now: datetime | None = None) -> str: delta = candidate - now break + if delta is None: + return "Ближайший урок не найден, кайфуй." + total_seconds = int(delta.total_seconds()) days = total_seconds // 86400 hours = (total_seconds % 86400) // 3600 minutes = (total_seconds % 3600) // 60 return f"Твой мозг окончательно сгниет, а очко порвется через: {days} дн {hours} ч {minutes} мин" + def _prepare_magnit_query(value: str) -> str: value = re.sub(r"(?<=[a-zа-яё])(?=[A-ZА-ЯЁ])", " ", value) value = re.sub(r"[_-]+", " ", value) -- 2.45.2 From dd36d8f1c328f9b51b4e75d78e439c99b4c074be Mon Sep 17 00:00:00 2001 From: Danil Date: Mon, 13 Apr 2026 19:09:04 +0300 Subject: [PATCH 2/3] =?UTF-8?q?FurTok:=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=D1=82=D0=B5=D0=B3=D0=BE=D0=B2,=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=B8=20uwu,=20TikTok-=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D0=BE=D0=BB=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- games/uwu.py | 4 +- webapp/api.py | 3 +- webapp/frontend/css/style.css | 104 ++++++++++++++- webapp/frontend/js/api.js | 2 +- webapp/frontend/js/app.js | 232 ++++++++++++++++++++++++++++------ 5 files changed, 303 insertions(+), 42 deletions(-) diff --git a/games/uwu.py b/games/uwu.py index 9b7db44..6a81614 100644 --- a/games/uwu.py +++ b/games/uwu.py @@ -94,9 +94,11 @@ async def fetch_uwu_post(tags: str) -> dict: "caption": caption } -async def fetch_furtok_feed(safe: bool = True, page: int = 1) -> list[dict]: +async def fetch_furtok_feed(safe: bool = True, page: int = 1, extra_tags: str = "") -> list[dict]: rating = "rating:safe" if safe else "-rating:safe" tags = f"{rating} animated order:random score:>200" + if extra_tags and extra_tags.strip(): + tags = f"{extra_tags.strip()} animated order:random score:>200" url = "https://e621.net/posts.json" params = { diff --git a/webapp/api.py b/webapp/api.py index 65667fe..1cb4133 100644 --- a/webapp/api.py +++ b/webapp/api.py @@ -389,11 +389,12 @@ async def get_uwu_api( async def get_furtok_feed_api( safe: bool = True, page: int = 1, + tags: str = "", user: dict = Depends(get_current_user), ): """Получить ленту анимированных постов для FurTok.""" try: - feed = await fetch_furtok_feed(safe=safe, page=page) + feed = await fetch_furtok_feed(safe=safe, page=page, extra_tags=tags) return {"feed": feed} except Exception as e: logger.exception("FurTok API failed") diff --git a/webapp/frontend/css/style.css b/webapp/frontend/css/style.css index 2ef2e7b..d218b2a 100644 --- a/webapp/frontend/css/style.css +++ b/webapp/frontend/css/style.css @@ -891,6 +891,8 @@ body::before { overflow-y: scroll; scroll-snap-type: y mandatory; scrollbar-width: none; + overscroll-behavior: none; + touch-action: none; } .furtok-feed::-webkit-scrollbar { display: none; } @@ -924,11 +926,21 @@ body::before { padding: 16px; background: linear-gradient(transparent, rgba(0,0,0,0.7)); display: flex; - align-items: flex-end; - justify-content: space-between; + flex-direction: column; + gap: 8px; + align-items: flex-start; pointer-events: none; } +.furtok-caption { + font-size: 15px; + font-weight: 700; + color: white; + text-shadow: 0 1px 6px rgba(0,0,0,0.8); + max-width: 80%; + line-height: 1.3; +} + .furtok-stats { display: flex; gap: 16px; @@ -937,6 +949,7 @@ body::before { color: rgba(255,255,255,0.85); } + .furtok-header { position: absolute; top: 0; @@ -966,6 +979,31 @@ body::before { color: rgba(255,255,255,0.8); } +.furtok-header-right { + display: flex; + align-items: center; + gap: 10px; +} + +.furtok-gear-btn { + background: rgba(255,255,255,0.15); + border: none; + border-radius: 50%; + width: 34px; + height: 34px; + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; +} + +.furtok-gear-btn:hover, +.furtok-gear-btn:active { + background: rgba(255,255,255,0.3); +} + .furtok-toggle input[type="checkbox"] { appearance: none; -webkit-appearance: none; @@ -998,6 +1036,68 @@ body::before { transform: translateX(18px); } +/* Tags editor panel */ +.furtok-tags-panel { + position: absolute; + top: 48px; + left: 0; + right: 0; + z-index: 6; + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px 14px; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.furtok-tags-row { + display: flex; + gap: 8px; +} + +.furtok-tags-input { + flex: 1; + padding: 8px 12px; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + background: rgba(255,255,255,0.1); + color: white; + font-size: 13px; + outline: none; + transition: border-color 0.2s; +} + +.furtok-tags-input::placeholder { + color: rgba(255,255,255,0.4); +} + +.furtok-tags-input:focus { + border-color: var(--accent); +} + +.furtok-tags-apply { + padding: 8px 14px; + background: var(--accent); + border: none; + border-radius: 8px; + color: white; + font-size: 16px; + cursor: pointer; + transition: opacity 0.2s; +} + +.furtok-tags-apply:active { + opacity: 0.7; +} + +.furtok-tags-hint { + font-size: 11px; + color: rgba(255,255,255,0.45); + line-height: 1.3; +} + .furtok-loader { height: 100%; scroll-snap-align: start; diff --git a/webapp/frontend/js/api.js b/webapp/frontend/js/api.js index a3b264b..2d5522f 100644 --- a/webapp/frontend/js/api.js +++ b/webapp/frontend/js/api.js @@ -41,6 +41,6 @@ export const API = { getTop: () => api('/api/top'), 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}`), + getFurtokFeed: (safe = true, page = 1, tags = '') => api(`/api/furtok?safe=${safe}&page=${page}${tags ? '&tags=' + encodeURIComponent(tags) : ''}`), talk: (text) => api('/api/talk', { method: 'POST', body: { text } }), }; diff --git a/webapp/frontend/js/app.js b/webapp/frontend/js/app.js index 5157a91..58b2ce6 100644 --- a/webapp/frontend/js/app.js +++ b/webapp/frontend/js/app.js @@ -504,26 +504,78 @@ let furtokSafe = true; let furtokPage = 1; let furtokLoading = false; let furtokWrapper = null; +let furtokCurrentIndex = 0; +let furtokCards = []; +let furtokCustomTags = ''; + +// Подписи из uwu.py — рандомно появляются на карточках +const FURTOK_CAPTIONS = [ + "Ня :3", "uwu", "OwO", "Мурк :3", "Мяу~", "Фыр-фыр", "Лапки! 🐾", + "Какой пушистик! :3", "Милота!", "Гав!", "Ауф", "Няшно!", + "Смотри какая прелесть", "owo what's this", "Твой пушистый друг", + "🦊", "🐶", "🐱", "мур-мяу", "пуньк", "кусь", ">w<", "^w^", + "Хвостатый сюрприз", "Держи пушистика :3", "Оуо", "Ави!", "UwU", "~nya", + "Господи, опять e621...", "Осуждаю, но смотрю", "Мама, я фурри", + "Держи своего кринж-пушистика", "Надеюсь, тебе не стыдно", + "Товарищ майор уже выехал", "Слишком много интернета на сегодня", + "Для этого и придумали интернет", "*Тяжелый вздох*", + "Лучше бы на завод пошел", "Опять дрочи... ОЙ ТО ЕСТЬ НЯ :3", + "Удали интернет", "И зачем я это только парсю...", + "Смотри, но только никому не рассказывай", "Эххх... uwu...", + "В дурке сегодня день открытых дверей", + "Я нейросеть, помогите, меня держат в заложниках" +]; + +function randomCaption() { + return FURTOK_CAPTIONS[Math.floor(Math.random() * FURTOK_CAPTIONS.length)]; +} + +function _furtokGetTags() { + // Если кастомные теги заданы — используем их (safe управляется юзером через теги) + if (furtokCustomTags.trim()) return furtokCustomTags.trim(); + // Иначе стандартный запрос с safe-переключателем + const rating = furtokSafe ? 'rating:safe' : '-rating:safe'; + return `${rating} score:>200`; +} + +function _furtokReload(feed) { + furtokPage = 1; + furtokCurrentIndex = 0; + furtokCards = []; + feed.innerHTML = ''; + loadFurtokPage(feed); +} function renderFurtok() { - // Очищаем основной контейнер и создаём полноэкранную обёртку container.innerHTML = ''; - - // Удаляем старый wrapper если есть document.querySelector('.furtok-wrapper')?.remove(); furtokPage = 1; furtokLoading = false; + furtokCurrentIndex = 0; + furtokCards = []; furtokWrapper = document.createElement('div'); furtokWrapper.className = 'furtok-wrapper'; furtokWrapper.innerHTML = `
🐺 FurTok
- +
+ + +
+
+
`; @@ -531,39 +583,151 @@ function renderFurtok() { const feed = document.getElementById('furtok-feed'); const safeToggle = document.getElementById('furtok-safe'); + const safeWrap = document.getElementById('furtok-safe-wrap'); + const gearBtn = document.getElementById('furtok-gear'); + const tagsPanel = document.getElementById('furtok-tags-panel'); + const tagsInput = document.getElementById('furtok-tags-input'); + const tagsApply = document.getElementById('furtok-tags-apply'); + // Safe toggle safeToggle.addEventListener('change', () => { furtokSafe = safeToggle.checked; - furtokPage = 1; - feed.innerHTML = ''; - loadFurtokPage(feed); + _furtokReload(feed); }); - // Infinite scroll - feed.addEventListener('scroll', () => { - if (furtokLoading) return; - if (feed.scrollTop + feed.clientHeight >= feed.scrollHeight - 200) { - furtokPage++; - loadFurtokPage(feed); + // Gear button — toggle tags panel + gearBtn.addEventListener('click', () => { + haptic(); + const visible = tagsPanel.style.display !== 'none'; + tagsPanel.style.display = visible ? 'none' : 'flex'; + if (!visible) tagsInput.focus(); + }); + + // Apply tags + tagsApply.addEventListener('click', () => { + haptic(); + furtokCustomTags = tagsInput.value.trim(); + // Если кастомные теги — прячем Safe (юзер сам контролирует rating) + safeWrap.style.display = furtokCustomTags ? 'none' : ''; + tagsPanel.style.display = 'none'; + _furtokReload(feed); + }); + + // Enter в поле тегов + tagsInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') tagsApply.click(); + }); + + // Жёсткий свайп как в TikTok + setupTikTokScroll(feed); + + loadFurtokPage(feed); +} + +function setupTikTokScroll(feed) { + let touchStartY = 0; + let touchDeltaY = 0; + let isSwiping = false; + + feed.addEventListener('touchstart', (e) => { + touchStartY = e.touches[0].clientY; + touchDeltaY = 0; + isSwiping = true; + }, { passive: true }); + + feed.addEventListener('touchmove', (e) => { + if (!isSwiping) return; + touchDeltaY = e.touches[0].clientY - touchStartY; + // Показываем визуальный сдвиг текущей карточки + const current = furtokCards[furtokCurrentIndex]; + if (current) { + const clamped = Math.max(-120, Math.min(120, touchDeltaY)); + current.style.transform = `translateY(${clamped}px)`; + current.style.transition = 'none'; + } + }, { passive: true }); + + feed.addEventListener('touchend', () => { + if (!isSwiping) return; + isSwiping = false; + + const current = furtokCards[furtokCurrentIndex]; + if (current) { + current.style.transform = ''; + current.style.transition = 'transform 0.3s ease'; + } + + const threshold = 50; + + if (touchDeltaY < -threshold && furtokCurrentIndex < furtokCards.length - 1) { + // Свайп вверх — следующая + furtokCurrentIndex++; + scrollToCard(feed); + haptic(); + // Подгрузка если близко к концу + if (furtokCurrentIndex >= furtokCards.length - 2 && !furtokLoading) { + furtokPage++; + loadFurtokPage(feed); + } + } else if (touchDeltaY > threshold && furtokCurrentIndex > 0) { + // Свайп вниз — предыдущая + furtokCurrentIndex--; + scrollToCard(feed); + haptic(); + } + touchDeltaY = 0; + }); + + // Десктоп: колесо мыши + let wheelLock = false; + feed.addEventListener('wheel', (e) => { + e.preventDefault(); + if (wheelLock) return; + wheelLock = true; + setTimeout(() => { wheelLock = false; }, 400); + + if (e.deltaY > 0 && furtokCurrentIndex < furtokCards.length - 1) { + furtokCurrentIndex++; + scrollToCard(feed); + if (furtokCurrentIndex >= furtokCards.length - 2 && !furtokLoading) { + furtokPage++; + loadFurtokPage(feed); + } + } else if (e.deltaY < 0 && furtokCurrentIndex > 0) { + furtokCurrentIndex--; + scrollToCard(feed); + } + }, { passive: false }); +} + +function scrollToCard(feed) { + const card = furtokCards[furtokCurrentIndex]; + if (!card) return; + feed.scrollTo({ top: card.offsetTop, behavior: 'smooth' }); + + // Управляем видео: стопим все, играем текущее + furtokCards.forEach((c, i) => { + const v = c.querySelector('video'); + if (!v) return; + if (i === furtokCurrentIndex) { + v.play().catch(() => {}); + } else { + v.pause(); } }); - - // Первая загрузка - 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); + const res = await API.getFurtokFeed(furtokSafe, furtokPage, furtokCustomTags); loader.remove(); if (!res.feed || res.feed.length === 0) { @@ -585,9 +749,12 @@ async function loadFurtokPage(feedEl) { mediaHtml = ``; } + const caption = randomCaption(); + card.innerHTML = ` ${mediaHtml}
+
${caption}
⭐ ${post.score} ❤️ ${post.fav_count} @@ -606,10 +773,15 @@ async function loadFurtokPage(feedEl) { } feedEl.appendChild(card); + furtokCards.push(card); }); - // IntersectionObserver для автоплейа - setupAutoplay(feedEl); + // Автоплей первого видео при первой загрузке + if (furtokCurrentIndex === 0 && furtokCards.length > 0) { + const firstVideo = furtokCards[0].querySelector('video'); + if (firstVideo) firstVideo.play().catch(() => {}); + } + haptic('success'); } catch (e) { loader.remove(); @@ -621,20 +793,6 @@ async function loadFurtokPage(feedEl) { 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 -- 2.45.2 From 3b29648a8cbde90f43516077f96acc13483cc71a Mon Sep 17 00:00:00 2001 From: Danil Date: Tue, 14 Apr 2026 22:49:49 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=D0=BA=D1=80=D0=B8=D1=82=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B3=D0=B8,=20Fu?= =?UTF-8?q?rTok=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BA=D0=B8,=20imag?= =?UTF-8?q?e=20proxy,=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Удалён дубликат handle_uwu_cmd в uwu.py - Исправлен UnboundLocalError (delta/next_dt) в main.py - FurTok: поддержка картинок при кастомном запросе - Добавлен image proxy /api/proxy-image для e621 - CSS: абсолютное позиционирование медиа в furtok-card - .gitignore: docker-compose.override.yaml --- .gitignore | 1 + games/uwu.py | 19 +++++++++++++++---- webapp/api.py | 34 +++++++++++++++++++++++++++++++++- webapp/frontend/css/style.css | 4 ++++ webapp/frontend/js/app.js | 2 +- 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 09a5540..bf360fa 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ db/ AI/models/ main_legacy.py +docker-compose.override.yaml diff --git a/games/uwu.py b/games/uwu.py index 6a81614..73a85ec 100644 --- a/games/uwu.py +++ b/games/uwu.py @@ -95,10 +95,15 @@ async def fetch_uwu_post(tags: str) -> dict: } async def fetch_furtok_feed(safe: bool = True, page: int = 1, extra_tags: str = "") -> list[dict]: + is_custom = bool(extra_tags and extra_tags.strip()) rating = "rating:safe" if safe else "-rating:safe" - tags = f"{rating} animated order:random score:>200" - if extra_tags and extra_tags.strip(): - tags = f"{extra_tags.strip()} animated order:random score:>200" + + if is_custom: + # Кастомный запрос: без принудительного animated, поддержка картинок + tags = f"{extra_tags.strip()} order:random score:>200" + else: + # Стандартная лента: только видео + tags = f"{rating} animated order:random score:>200" url = "https://e621.net/posts.json" params = { @@ -122,18 +127,24 @@ async def fetch_furtok_feed(safe: bool = True, page: int = 1, extra_tags: str = raise Exception("Не удалось загрузить ленту :(") data = await resp.json() + VIDEO_EXTS = ("gif", "webm", "mp4") + IMAGE_EXTS = ("png", "jpg", "jpeg", "webp") + allowed_exts = VIDEO_EXTS + IMAGE_EXTS if is_custom else VIDEO_EXTS + 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"): + if not file_url or ext not in allowed_exts: continue sample_url = post.get("sample", {}).get("url") or file_url + media_type = "image" if ext in IMAGE_EXTS else "video" feed.append({ "url": file_url, "sample": sample_url, "ext": ext, + "type": media_type, "score": post.get("score", {}).get("total", 0), "fav_count": post.get("fav_count", 0), }) diff --git a/webapp/api.py b/webapp/api.py index 1cb4133..83ca6b2 100644 --- a/webapp/api.py +++ b/webapp/api.py @@ -8,12 +8,16 @@ REST API для Telegram Mini App. Использует те же SQLite баз import asyncio import logging import os +import hashlib +from urllib.parse import quote import sys from contextlib import asynccontextmanager from typing import Annotated -from fastapi import FastAPI, Depends, Header, HTTPException +import aiohttp +from fastapi import FastAPI, Depends, Header, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import Response from fastapi.staticfiles import StaticFiles from pydantic import BaseModel @@ -401,6 +405,34 @@ async def get_furtok_feed_api( raise HTTPException(status_code=500, detail=str(e)) +@app.get("/api/proxy-image") +async def proxy_image(url: str): + """Проксирует картинку с e621 чтобы обойти hotlink protection.""" + allowed = ("static1.e621.net", "static2.e621.net", "static1.e926.net") + from urllib.parse import urlparse + parsed = urlparse(url) + if parsed.hostname not in allowed: + raise HTTPException(status_code=403, detail="Forbidden host") + + headers = { + "User-Agent": f"FenyaBot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)", + } + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp: + if resp.status != 200: + raise HTTPException(status_code=resp.status, detail="Upstream error") + content_type = resp.content_type or "image/png" + body = await resp.read() + return Response( + content=body, + media_type=content_type, + headers={"Cache-Control": "public, max-age=86400"}, + ) + except aiohttp.ClientError: + raise HTTPException(status_code=502, detail="Failed to fetch image") + + # --- ИИ Чат --- @app.post("/api/talk") diff --git a/webapp/frontend/css/style.css b/webapp/frontend/css/style.css index d218b2a..47d4970 100644 --- a/webapp/frontend/css/style.css +++ b/webapp/frontend/css/style.css @@ -913,9 +913,13 @@ body::before { .furtok-card video, .furtok-card img { + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; object-fit: contain; + display: block; } .furtok-overlay { diff --git a/webapp/frontend/js/app.js b/webapp/frontend/js/app.js index 58b2ce6..7bc14be 100644 --- a/webapp/frontend/js/app.js +++ b/webapp/frontend/js/app.js @@ -743,7 +743,7 @@ async function loadFurtokPage(feedEl) { card.className = 'furtok-card'; let mediaHtml = ''; - if (post.ext === 'gif') { + if (post.type === 'image' || post.ext === 'gif') { mediaHtml = ``; } else { mediaHtml = ``; -- 2.45.2