From e367a380655fb409dfa78a554d599763013dbfc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Wed, 18 Feb 2026 10:50:25 +0100 Subject: [PATCH] snapshot current state before gitea sync --- .DS_Store | Bin 0 -> 6148 bytes StiggerDLBot/.claude/settings.local.json | 10 ++ StiggerDLBot/.gitignore | 10 ++ StiggerDLBot/app/__init__.py | 0 StiggerDLBot/app/config.py | 14 +++ StiggerDLBot/app/main.py | 20 +++ StiggerDLBot/app/models.py | 17 +++ StiggerDLBot/app/routers/__init__.py | 0 StiggerDLBot/app/routers/stickersets.py | 31 +++++ StiggerDLBot/app/services/__init__.py | 0 StiggerDLBot/app/services/sticker_service.py | 125 +++++++++++++++++++ StiggerDLBot/botfather.md | 10 ++ StiggerDLBot/requirements.txt | 6 + 13 files changed, 243 insertions(+) create mode 100644 .DS_Store create mode 100644 StiggerDLBot/.claude/settings.local.json create mode 100644 StiggerDLBot/.gitignore create mode 100644 StiggerDLBot/app/__init__.py create mode 100644 StiggerDLBot/app/config.py create mode 100644 StiggerDLBot/app/main.py create mode 100644 StiggerDLBot/app/models.py create mode 100644 StiggerDLBot/app/routers/__init__.py create mode 100644 StiggerDLBot/app/routers/stickersets.py create mode 100644 StiggerDLBot/app/services/__init__.py create mode 100644 StiggerDLBot/app/services/sticker_service.py create mode 100644 StiggerDLBot/botfather.md create mode 100644 StiggerDLBot/requirements.txt diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..aa288bbc0b391cbafe3efb629abdff524b3f7998 GIT binary patch literal 6148 zcmeHKy-tKc5T4~IFj`!x9j%QCtu{!oG1^FJV>k?&K=z{8;~Q968(+rQ`3jakgN@(p zZtg%lVr4F7hRlA;?9Bdr5QZTlmE08((Ugb+D5JN5<_F<%)`57=!f}e{sLCjt>3Tl! z6vMhQz|YR7Ju2y%n*Q^9JnW9uI!*IR=dcCQ`SIEN^UJsKVsw0d)p<9(2j*joGHS4H ziR+SXD5VohNK;eX&Z=DNYSgK_*~#n2{@8@w&7XeW@9aLA&9B@*t(S-SjeqxW{=pmX zTSOQT284k%W&k;xCD^nmtuP=A2m=cS`1=q-8Dod7Mf2%EV@d#^53>>Uxt8FV&|&Pb zwTKajvY|j5s$7YoY&h(p^NSs}7Hv2wS9~b9vT_xQGOJ_$(72O|ElMj42m^fvCiZs9 z`TuBj{ofCgJ7GW=_*V=lKh4qvM{>D!a5&DjA@l;u!hWsAMF=`36~mWP@ix>5>>+o6 UvBTCPED-q-Ff>Rb4E!krU-o-hL;wH) literal 0 HcmV?d00001 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