diff --git a/games/nude.py b/games/nude.py
index 280bbf4..71ac438 100644
--- a/games/nude.py
+++ b/games/nude.py
@@ -1,108 +1,159 @@
-import aiohttp
-from aiogram import types
-import config
import logging
import random
-import re
+from pathlib import PurePosixPath
+from urllib.parse import urlparse
+
+import aiohttp
+from aiogram import types
+
logger = logging.getLogger(__name__)
CAPTIONS = [
"Hot stuff π₯",
- "Sexy content π"
+ "Sexy content π",
]
+LESAINT_POSTS_API = "https://lesaintdesseins.fr/wp-json/wp/v2/posts"
+LESAINT_REFERER = "https://lesaintdesseins.fr/"
+LATEST_POSTS_LIMIT = 30
+REQUEST_HEADERS = {
+ "User-Agent": (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/124.0.0.0 Safari/537.36"
+ ),
+ "Accept": "application/json,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Referer": LESAINT_REFERER,
+}
+
+
+def _extract_photo_candidates(posts: list[dict]) -> list[dict]:
+ candidates = []
+
+ for post in posts:
+ embedded = post.get("_embedded") or {}
+ media_items = embedded.get("wp:featuredmedia") or []
+
+ for media in media_items:
+ if media.get("media_type") != "image":
+ continue
+
+ media_details = media.get("media_details") or {}
+ sizes = media_details.get("sizes") or {}
+ image_url = (
+ (sizes.get("full") or {}).get("source_url")
+ or media.get("source_url")
+ )
+
+ if not image_url:
+ continue
+
+ candidates.append(
+ {
+ "image_url": image_url,
+ "post_url": post.get("link", LESAINT_REFERER),
+ "title": (post.get("title") or {}).get("rendered", "").strip(),
+ }
+ )
+ break
+
+ return candidates
+
+
+def _guess_filename(image_url: str) -> str:
+ parsed = urlparse(image_url)
+ filename = PurePosixPath(parsed.path).name or "nude.jpg"
+
+ if "." not in filename:
+ filename = f"{filename}.jpg"
+
+ return filename
+
+
+async def _fetch_latest_photo_candidates(session: aiohttp.ClientSession) -> list[dict]:
+ params = {
+ "per_page": str(LATEST_POSTS_LIMIT),
+ "orderby": "date",
+ "order": "desc",
+ "_embed": "wp:featuredmedia",
+ }
+
+ async with session.get(LESAINT_POSTS_API, params=params, headers=REQUEST_HEADERS) as resp:
+ if resp.status != 200:
+ body = await resp.text()
+ raise RuntimeError(f"API returned {resp.status}: {body[:300]}")
+
+ posts = await resp.json(content_type=None)
+
+ if not isinstance(posts, list):
+ raise RuntimeError("Unexpected API response format")
+
+ return _extract_photo_candidates(posts)
+
+
+async def _download_photo(
+ session: aiohttp.ClientSession, image_url: str
+) -> tuple[bytes | None, str]:
+ async with session.get(image_url, headers=REQUEST_HEADERS) as resp:
+ if resp.status != 200:
+ logger.warning("Image download failed for %s: %s", image_url, resp.status)
+ return None, _guess_filename(image_url)
+
+ content_type = resp.headers.get("Content-Type", "")
+ if "image" not in content_type.lower():
+ logger.warning(
+ "Unexpected content type for %s: %s", image_url, content_type
+ )
+ return None, _guess_filename(image_url)
+
+ return await resp.read(), _guess_filename(image_url)
+
+
async def handle_nude_cmd(message: types.Message):
args = message.text.split(maxsplit=1)
-
+
if len(args) > 1 and args[1].strip() == "-h":
help_msg = (
"π₯ Π‘ΠΏΡΠ°Π²ΠΊΠ° ΠΏΠΎ ΠΊΠΎΠΌΠ°Π½Π΄Π΅ /nude:\n\n"
- "ΠΠΎΠ»ΡΡΠ°Π΅Ρ ΡΠ»ΡΡΠ°ΠΉΠ½ΡΠ΅ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ Π²Π·ΡΠΎΡΠ»ΡΡ
ΠΆΠ΅Π½ΡΠΈΠ½ ΠΈΠ· ΠΎΡΠΊΡΡΡΡΡ
ΠΈΡΡΠΎΡΠ½ΠΈΠΊΠΎΠ².\n"
- "β’ ΠΡΠΏΠΎΠ»ΡΠ·ΡΠ΅Ρ ΠΏΡΡΠΌΠΎΠ΅ ΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠ°ΠΉΡΠΎΠ² Π΄Π»Ρ ΠΏΠΎΠΈΡΠΊΠ° ΠΊΠΎΠ½ΡΠ΅Π½ΡΠ°\n"
- "β’ ΠΠΎ ΡΠΌΠΎΠ»ΡΠ°Π½ΠΈΡ ΠΈΡΠ΅Ρ ΠΏΠΎΠΏΡΠ»ΡΡΠ½ΡΠ΅ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ\n"
- "β’ ΠΡΠΏΡΠ°Π²Π»ΡΠ΅Ρ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ Π½Π°ΠΏΡΡΠΌΡΡ\n\n"
- "ΠΡΠΈΠΌΠ΅Ρ: /nude\n"
- "ΠΠΎ ΡΠΌΠΎΠ»ΡΠ°Π½ΠΈΡ: Π‘Π»ΡΡΠ°ΠΉΠ½ΡΠ΅ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ Ρ ΠΏΠΎΠΏΡΠ»ΡΡΠ½ΡΡ
ΡΠ°ΠΉΡΠΎΠ²"
+ "ΠΠ΅ΡΠ΅Ρ ΡΠ»ΡΡΠ°ΠΉΠ½ΡΡ ΡΠΎΡΠΊΡ ΠΈΠ· ΠΏΠΎΡΠ»Π΅Π΄Π½ΠΈΡ
ΠΏΠΎΡΡΠΎΠ² ΡΠ°ΠΉΡΠ° le saint des seins.\n"
+ "β’ ΠΡΡΠΎΡΠ½ΠΈΠΊ: https://lesaintdesseins.fr/\n"
+ "β’ ΠΠΎΡΡΠ°Π΅Ρ ΠΏΠΎΡΠ»Π΅Π΄Π½ΠΈΠ΅ ΠΏΠΎΡΡΡ ΡΠ΅ΡΠ΅Π· WordPress REST API\n"
+ "β’ ΠΡΠ±ΠΈΡΠ°Π΅Ρ ΡΠ»ΡΡΠ°ΠΉΠ½ΡΡ ΠΊΠ°ΡΡΠΈΠ½ΠΊΡ ΠΈ ΠΎΡΠΏΡΠ°Π²Π»ΡΠ΅Ρ Π΅Π΅ ΠΏΡΡΠΌΠΎ Π² Telegram\n\n"
+ "ΠΡΠΈΠΌΠ΅Ρ: /nude"
)
await message.reply(help_msg, parse_mode="HTML")
return
- # Π‘ΠΏΠΈΡΠΎΠΊ ΡΠ°ΠΉΡΠΎΠ² Π΄Π»Ρ ΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΡ
- sites = [
- "https://www.imagefap.com/gallery.php?pg=random",
- "https://xhamster.com/photos?sort=random",
- "https://spankbang.com/random"
- ]
-
- random_site = random.choice(sites)
-
- headers = {
- "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"
- }
-
+ timeout = aiohttp.ClientTimeout(total=30)
+
try:
- async with aiohttp.ClientSession() as session:
- async with session.get(random_site, headers=headers) as resp:
- if resp.status != 200:
- logger.error(f"Site access error: {resp.status}")
- await message.reply("ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΠΏΠΎΠ»ΡΡΠΈΡΡ Π΄ΠΎΡΡΡΠΏ ΠΊ ΡΠ°ΠΉΡΡ :(")
- return
-
- html = await resp.text()
-
- # ΠΡΠ΅ΠΌ URL ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ Π² HTML
- img_patterns = [
- r'
]+src="([^"]+)"',
- r']+href="([^"]+\.(jpg|jpeg|png|gif|webp))"',
- r'data-src="([^"]+)"'
- ]
-
- img_urls = []
- for pattern in img_patterns:
- matches = re.findall(pattern, html, re.IGNORECASE)
- img_urls.extend(matches)
-
- if not img_urls:
- await message.reply("ΠΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ Π½Π° ΡΡΡΠ°Π½ΠΈΡΠ΅ :(")
- return
-
- # Π€ΠΈΠ»ΡΡΡΡΠ΅ΠΌ ΠΈ Π²ΡΠ±ΠΈΡΠ°Π΅ΠΌ ΡΠ»ΡΡΠ°ΠΉΠ½ΠΎΠ΅ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅
- 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)
-
- if not valid_imgs:
- await message.reply("ΠΠΎΠ΄Ρ
ΠΎΠ΄ΡΡΠΈΠ΅ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ :(")
- return
-
- final_img_url = random.choice(valid_imgs)
- caption = random.choice(CAPTIONS)
-
- # ΠΡΠΏΡΠ°Π²Π»ΡΠ΅ΠΌ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅
- try:
- input_file = types.URLInputFile(final_img_url)
- await message.answer_photo(photo=input_file, caption=caption)
- except Exception as e:
- logger.error(f"Error sending photo: {e}")
- await message.answer(f"{caption}\n{final_img_url}")
-
- except Exception as e:
+ async with aiohttp.ClientSession(timeout=timeout) as session:
+ candidates = await _fetch_latest_photo_candidates(session)
+
+ if not candidates:
+ await message.reply("ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π½Π°ΠΉΡΠΈ ΡΠ²Π΅ΠΆΠΈΠ΅ ΡΠΎΡΠΊΠΈ :(")
+ return
+
+ selected = random.choice(candidates)
+ caption = random.choice(CAPTIONS)
+ image_bytes, filename = await _download_photo(session, selected["image_url"])
+
+ if image_bytes:
+ photo = types.BufferedInputFile(image_bytes, filename=filename)
+ await message.answer_photo(photo=photo, caption=caption)
+ return
+
+ try:
+ photo = types.URLInputFile(selected["image_url"])
+ await message.answer_photo(photo=photo, caption=caption)
+ except Exception:
+ logger.exception("Failed to send photo by URL")
+ await message.answer(
+ f"{caption}\n{selected['image_url']}\n{selected['post_url']}"
+ )
+
+ except Exception:
logger.exception("Error in nude command")
- await message.reply("ΠΡΠΎΠΈΠ·ΠΎΡΠ»Π° ΠΎΡΠΈΠ±ΠΊΠ° ΠΏΡΠΈ ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠ΅ Π·Π°ΠΏΡΠΎΡΠ° :(")
+ await message.reply("ΠΡΠΎΠΈΠ·ΠΎΡΠ»Π° ΠΎΡΠΈΠ±ΠΊΠ° ΠΏΡΠΈ ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΠΈ ΡΠΎΡΠΎ :(")