commit e367a380655fb409dfa78a554d599763013dbfc7 Author: Felix Förtsch Date: Wed Feb 18 10:50:25 2026 +0100 snapshot current state before gitea sync diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..aa288bb Binary files /dev/null and b/.DS_Store differ diff --git a/StiggerDLBot/.claude/settings.local.json b/StiggerDLBot/.claude/settings.local.json new file mode 100644 index 0000000..84b82dd --- /dev/null +++ b/StiggerDLBot/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(/Users/felixfoertsch/Developer/TelegramStickersDownloaderHTTP/.venv/bin/pip list:*)", + "Bash(.venv/bin/pip install:*)", + "Bash(python3:*)", + "Bash(.venv/bin/python:*)" + ] + } +} diff --git a/StiggerDLBot/.gitignore b/StiggerDLBot/.gitignore new file mode 100644 index 0000000..6eecaf3 --- /dev/null +++ b/StiggerDLBot/.gitignore @@ -0,0 +1,10 @@ +.env +.venv/ +cache/ +downloads/ +__pycache__/ +*.pyc +.DS_Store +.idea/ +.cache/ +.vscode/ diff --git a/StiggerDLBot/app/__init__.py b/StiggerDLBot/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/StiggerDLBot/app/config.py b/StiggerDLBot/app/config.py new file mode 100644 index 0000000..db152a9 --- /dev/null +++ b/StiggerDLBot/app/config.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from dotenv import load_dotenv +from pydantic_settings import BaseSettings + +load_dotenv() + + +class Settings(BaseSettings): + bot_token: str + downloads_dir: Path = Path("downloads") + + +settings = Settings() diff --git a/StiggerDLBot/app/main.py b/StiggerDLBot/app/main.py new file mode 100644 index 0000000..cd3ce9a --- /dev/null +++ b/StiggerDLBot/app/main.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.routers import stickersets + +app = FastAPI(title="Telegram Stickers API", root_path="/StiggerDLBot") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["GET"], + allow_headers=["*"], +) + +app.include_router(stickersets.router) + + +@app.get("/health") +def health(): + return {"status": "ok"} diff --git a/StiggerDLBot/app/models.py b/StiggerDLBot/app/models.py new file mode 100644 index 0000000..666c33c --- /dev/null +++ b/StiggerDLBot/app/models.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel + + +class StickerResponse(BaseModel): + id: str + emoji: str + emoji_name: str + is_animated: bool + png_url: str + gif_url: str | None = None + + +class StickerSetResponse(BaseModel): + name: str + title: str + sticker_count: int + stickers: list[StickerResponse] diff --git a/StiggerDLBot/app/routers/__init__.py b/StiggerDLBot/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/StiggerDLBot/app/routers/stickersets.py b/StiggerDLBot/app/routers/stickersets.py new file mode 100644 index 0000000..50c4c35 --- /dev/null +++ b/StiggerDLBot/app/routers/stickersets.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse + +from app.models import StickerSetResponse +from app.services import sticker_service + +router = APIRouter(prefix="/api/stickersets") + + +@router.get("/{pack_name}", response_model=StickerSetResponse) +def get_sticker_set(pack_name: str): + result = sticker_service.get_sticker_set(pack_name) + if result is None: + raise HTTPException(status_code=404, detail="Sticker pack not found") + return result + + +@router.get("/{pack_name}/stickers/{sticker_id}.png") +def get_sticker_png(pack_name: str, sticker_id: str): + path = sticker_service.get_sticker_file(pack_name, sticker_id, "png") + if path is None: + raise HTTPException(status_code=404, detail="Sticker not found") + return FileResponse(path, media_type="image/png") + + +@router.get("/{pack_name}/stickers/{sticker_id}.gif") +def get_sticker_gif(pack_name: str, sticker_id: str): + path = sticker_service.get_sticker_file(pack_name, sticker_id, "gif") + if path is None: + raise HTTPException(status_code=404, detail="Sticker not found") + return FileResponse(path, media_type="image/gif") diff --git a/StiggerDLBot/app/services/__init__.py b/StiggerDLBot/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/StiggerDLBot/app/services/sticker_service.py b/StiggerDLBot/app/services/sticker_service.py new file mode 100644 index 0000000..f61b984 --- /dev/null +++ b/StiggerDLBot/app/services/sticker_service.py @@ -0,0 +1,125 @@ +from pathlib import Path + +from tstickers.convert import Backend +from tstickers.manager import StickerManager + +from app.config import settings +from app.models import StickerResponse, StickerSetResponse + +_manager: StickerManager | None = None + + +def _get_manager() -> StickerManager: + global _manager + if _manager is None: + _manager = StickerManager(settings.bot_token) + return _manager + + +def _pack_is_cached(pack_name: str) -> bool: + png_dir = settings.downloads_dir / pack_name / "png" + gif_dir = settings.downloads_dir / pack_name / "gif" + has_png = png_dir.exists() and any(png_dir.iterdir()) + has_gif = gif_dir.exists() and any(gif_dir.iterdir()) + return has_png or has_gif + + +def get_sticker_set(pack_name: str) -> StickerSetResponse | None: + """Fetch a sticker pack, download and convert if not cached.""" + manager = _get_manager() + + # Get pack metadata (uses requests-cache internally) + pack = manager.getPack(pack_name) + if pack is None: + return None + + actual_name = pack["name"] + + if not _pack_is_cached(actual_name): + # downloadPack calls getPack internally again, but it's cached + manager.downloadPack(actual_name) + manager.convertPack(actual_name, formats={"gif", "png"}, backend=Backend.RLOTTIE_PYTHON) + + # Build response from files on disk + png_dir = settings.downloads_dir / actual_name / "png" + gif_dir = settings.downloads_dir / actual_name / "gif" + + # Build a lookup of sticker metadata from the pack files + sticker_lookup: dict[str, dict] = {} + for sticker in pack["files"]: + sticker_id = sticker.name.split("_")[-1].split(".")[0] + sticker_lookup[sticker_id] = { + "emoji": sticker.emoji, + "emoji_name": sticker.emojiName(), + "is_animated": sticker.fileType == "tgs", + } + + stickers: list[StickerResponse] = [] + if png_dir.exists(): + for png_file in sorted(png_dir.iterdir()): + sticker_id = png_file.stem.split("+")[0] + emoji_name = png_file.stem.split("+")[1] if "+" in png_file.stem else "" + + meta = sticker_lookup.get(sticker_id, {}) + emoji = meta.get("emoji", "") + is_animated = meta.get("is_animated", False) + if not emoji_name: + emoji_name = meta.get("emoji_name", "") + + gif_url = None + gif_file = gif_dir / f"{png_file.stem}.gif" + if gif_file.exists(): + gif_url = f"/api/stickersets/{actual_name}/stickers/{sticker_id}.gif" + + stickers.append( + StickerResponse( + id=sticker_id, + emoji=emoji, + emoji_name=emoji_name, + is_animated=is_animated, + png_url=f"/api/stickersets/{actual_name}/stickers/{sticker_id}.png", + gif_url=gif_url, + ) + ) + + # Also check for GIF-only stickers (animated TGS that don't produce PNG) + if gif_dir.exists(): + existing_ids = {s.id for s in stickers} + for gif_file in sorted(gif_dir.iterdir()): + sticker_id = gif_file.stem.split("+")[0] + if sticker_id in existing_ids: + continue + emoji_name = gif_file.stem.split("+")[1] if "+" in gif_file.stem else "" + meta = sticker_lookup.get(sticker_id, {}) + + stickers.append( + StickerResponse( + id=sticker_id, + emoji=meta.get("emoji", ""), + emoji_name=emoji_name or meta.get("emoji_name", ""), + is_animated=True, + png_url=f"/api/stickersets/{actual_name}/stickers/{sticker_id}.png", + gif_url=f"/api/stickersets/{actual_name}/stickers/{sticker_id}.gif", + ) + ) + + return StickerSetResponse( + name=actual_name, + title=pack["title"], + sticker_count=len(stickers), + stickers=stickers, + ) + + +def get_sticker_file(pack_name: str, sticker_id: str, fmt: str) -> Path | None: + """Return the path to a converted sticker file, or None if not found.""" + fmt_dir = settings.downloads_dir / pack_name / fmt + if not fmt_dir.exists(): + return None + + # Files are named like: {id}+{emoji_name}.{ext} + for f in fmt_dir.iterdir(): + if f.stem.split("+")[0] == sticker_id: + return f + + return None diff --git a/StiggerDLBot/botfather.md b/StiggerDLBot/botfather.md new file mode 100644 index 0000000..c85c174 --- /dev/null +++ b/StiggerDLBot/botfather.md @@ -0,0 +1,10 @@ +Name: StiggerDLBot +Username: StiggerDLBot + +Done! Congratulations on your new bot. You will find it at t.me/StiggerDLBot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. + +Use this token to access the HTTP API: +8247171504:AAE5o32Zv1B6N5VXh5S9m6H97LTB6c7Vu1s +Keep your token secure and store it safely, it can be used by anyone to control your bot. + +For a description of the Bot API, see this page: https://core.telegram.org/bots/api diff --git a/StiggerDLBot/requirements.txt b/StiggerDLBot/requirements.txt new file mode 100644 index 0000000..391c82f --- /dev/null +++ b/StiggerDLBot/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +pydantic-settings +python-dotenv +tstickers +Pillow