├── tests ├── __init__.py └── test_main.py ├── readme-assets └── icons │ ├── name.png │ ├── TStickers.xcf │ └── proj-icon.png ├── documentation ├── tutorials │ ├── assets │ │ ├── step1.png │ │ ├── step3_0.png │ │ └── step3_1.png │ ├── README.md │ ├── backends.md │ └── getting-started.md └── reference │ ├── tstickers │ ├── module.md │ ├── cli.md │ ├── index.md │ ├── convert_pyrlottie.md │ ├── convert_rlottie_python.md │ ├── caching.md │ ├── convert.md │ └── manager.md │ └── README.md ├── tstickers ├── __main__.py ├── __init__.py ├── convert_pyrlottie.py ├── caching.py ├── convert_rlottie_python.py ├── cli.py ├── convert.py └── manager.py ├── move_webp_stickers.py ├── .github └── workflows │ ├── mirror-to-codeberg.yaml │ └── test-lint.yaml ├── requirements.txt ├── LICENSE.md ├── .pre-commit-config.yaml ├── pyproject.toml ├── .gitignore ├── CHANGELOG.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /readme-assets/icons/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/readme-assets/icons/name.png -------------------------------------------------------------------------------- /readme-assets/icons/TStickers.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/readme-assets/icons/TStickers.xcf -------------------------------------------------------------------------------- /readme-assets/icons/proj-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/readme-assets/icons/proj-icon.png -------------------------------------------------------------------------------- /documentation/tutorials/assets/step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/documentation/tutorials/assets/step1.png -------------------------------------------------------------------------------- /documentation/tutorials/assets/step3_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/documentation/tutorials/assets/step3_0.png -------------------------------------------------------------------------------- /documentation/tutorials/assets/step3_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/documentation/tutorials/assets/step3_1.png -------------------------------------------------------------------------------- /tstickers/__main__.py: -------------------------------------------------------------------------------- 1 | """entry point for python -m tstickers.""" 2 | 3 | from __future__ import annotations 4 | 5 | from tstickers import cli 6 | 7 | cli() 8 | -------------------------------------------------------------------------------- /documentation/reference/tstickers/module.md: -------------------------------------------------------------------------------- 1 | # Module 2 | 3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Module 4 | 5 | > Auto-generated documentation for [tstickers.__main__](../../../tstickers/__main__.py) module. 6 | - [Module](#module) 7 | -------------------------------------------------------------------------------- /tstickers/__init__.py: -------------------------------------------------------------------------------- 1 | """Entry point for python -m sigstickers.""" 2 | 3 | import sys 4 | 5 | from loguru import logger 6 | 7 | from tstickers.cli import cli 8 | 9 | _ = cli 10 | 11 | 12 | logger.remove(0) 13 | logger.add( 14 | sys.stderr, 15 | format="{level: <8} | {message}", 16 | ) 17 | -------------------------------------------------------------------------------- /documentation/reference/tstickers/cli.md: -------------------------------------------------------------------------------- 1 | # Cli 2 | 3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Cli 4 | 5 | > Auto-generated documentation for [tstickers.cli](../../../tstickers/cli.py) module. 6 | 7 | - [Cli](#cli) 8 | - [cli](#cli) 9 | 10 | ## cli 11 | 12 | [Show source in cli.py:20](../../../tstickers/cli.py#L20) 13 | 14 | Cli entry point. 15 | 16 | #### Signature 17 | 18 | ```python 19 | def cli() -> None: ... 20 | ``` -------------------------------------------------------------------------------- /move_webp_stickers.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | 4 | source_dir = Path("./downloads") 5 | 6 | packnames = [name.name for name in source_dir.iterdir() if name.is_dir()] 7 | 8 | for packname in packnames: 9 | source_path = source_dir / packname / "webp" 10 | dest_path = Path("./sorted") / packname 11 | 12 | print(packname) 13 | 14 | if not dest_path.exists(): 15 | dest_path.mkdir(parents=True) 16 | 17 | for file_path in source_path.iterdir(): 18 | shutil.copy(file_path, dest_path) 19 | -------------------------------------------------------------------------------- /documentation/reference/tstickers/index.md: -------------------------------------------------------------------------------- 1 | # Tstickers 2 | 3 | [Tstickers Index](../README.md#tstickers-index) / Tstickers 4 | 5 | > Auto-generated documentation for [tstickers](../../../tstickers/__init__.py) module. 6 | 7 | - [Tstickers](#tstickers) 8 | - [Modules](#modules) 9 | 10 | ## Modules 11 | 12 | - [Module](./module.md) 13 | - [Caching](./caching.md) 14 | - [Cli](./cli.md) 15 | - [Convert](./convert.md) 16 | - [Convert Pyrlottie](./convert_pyrlottie.md) 17 | - [Convert Rlottie Python](./convert_rlottie_python.md) 18 | - [Manager](./manager.md) -------------------------------------------------------------------------------- /.github/workflows/mirror-to-codeberg.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Sync repo to the Codeberg mirror 3 | name: Repo sync GitHub -> Codeberg 4 | on: 5 | push: 6 | branches: 7 | - '**' 8 | 9 | jobs: 10 | codeberg: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - uses: spyoungtech/mirror-action@v0.7.0 17 | with: 18 | REMOTE: "https://codeberg.org/FredHappyface/TStickers.git" 19 | GIT_USERNAME: FredHappyface 20 | GIT_PASSWORD: ${{ secrets.CODEBERG_PASSWORD }} 21 | -------------------------------------------------------------------------------- /documentation/reference/README.md: -------------------------------------------------------------------------------- 1 | # Tstickers Index 2 | 3 | > Auto-generated documentation index. 4 | 5 | A full list of `Tstickers` project modules. 6 | 7 | - [Tstickers](tstickers/index.md#tstickers) 8 | - [Module](tstickers/module.md#module) 9 | - [Caching](tstickers/caching.md#caching) 10 | - [Cli](tstickers/cli.md#cli) 11 | - [Convert](tstickers/convert.md#convert) 12 | - [Convert Pyrlottie](tstickers/convert_pyrlottie.md#convert-pyrlottie) 13 | - [Convert Rlottie Python](tstickers/convert_rlottie_python.md#convert-rlottie-python) 14 | - [Manager](tstickers/manager.md#manager) 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv export --no-hashes --no-dev -o requirements.txt 3 | -e . 4 | attrs==23.2.0 5 | cattrs==24.1.2 6 | certifi==2025.1.31 7 | charset-normalizer==3.4.1 8 | colorama==0.4.6 ; sys_platform == 'win32' 9 | emoji==2.14.1 10 | exceptiongroup==1.2.2 ; python_full_version < '3.11' 11 | idna==3.10 12 | loguru==0.7.3 13 | pillow==10.4.0 14 | platformdirs==4.3.6 15 | requests==2.32.3 16 | requests-cache==1.2.1 17 | rlottie-python==1.3.6 18 | six==1.17.0 19 | typing-extensions==4.12.2 ; python_full_version < '3.11' 20 | url-normalize==1.4.3 21 | urllib3==2.3.0 22 | win32-setctime==1.2.0 ; sys_platform == 'win32' 23 | -------------------------------------------------------------------------------- /documentation/tutorials/README.md: -------------------------------------------------------------------------------- 1 | 2 | # TStickers Tutorials and User Guides 3 | 4 | Welcome to the TStickers tutorials. This section provides a handful of tutorials 5 | to help you get started with TStickers and make the most of its features. 6 | 7 | ## Resource Table 8 | 9 | | Guide | Description | Link | 10 | |-----------------------|---------------------|---------------------------------------| 11 | | **getting-started.md** | This guide will walk you through the initial setup of TStickers, including installation and basic usage. | [View getting-started.md](./getting-started.md) | 12 | | **backends.md** | Learn about the different backend options available with TStickers. | [View backends.md](./backends.md) | 13 | -------------------------------------------------------------------------------- /.github/workflows/test-lint.yaml: -------------------------------------------------------------------------------- 1 | name: Python Test and Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Python Test and Lint 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | python-version: 19 | - '3.9' 20 | - '3.10' 21 | - '3.11' 22 | - '3.12' 23 | - '3.13' 24 | 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | 34 | - name: Install UV 35 | run: | 36 | curl -LsSf https://astral.sh/uv/install.sh | sh 37 | 38 | - name: Install dependencies 39 | run: uv sync 40 | 41 | - name: Run pytest 42 | run: uv run pytest 43 | 44 | - name: Run ruff 45 | run: uv run ruff check --output-format=github 46 | continue-on-error: true 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Kieran W 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /documentation/reference/tstickers/convert_pyrlottie.md: -------------------------------------------------------------------------------- 1 | # Convert Pyrlottie 2 | 3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Convert Pyrlottie 4 | 5 | > Auto-generated documentation for [tstickers.convert_pyrlottie](../../../tstickers/convert_pyrlottie.py) module. 6 | 7 | - [Convert Pyrlottie](#convert-pyrlottie) 8 | - [convertAnimated](#convertanimated) 9 | 10 | ## convertAnimated 11 | 12 | [Show source in convert_pyrlottie.py:17](../../../tstickers/convert_pyrlottie.py#L17) 13 | 14 | Convert animated stickers, over a number of threads, at a given framerate, scale and to a 15 | set of formats. 16 | 17 | #### Arguments 18 | 19 | - `swd` *Path* - The sticker working directory (e.g., downloads/packName). 20 | - `_threads` *int* - This is ignored for the pyrlottie backend 21 | - `fps` *int* - framerate of the converted sticker, affecting optimization and 22 | quality (default: 20) 23 | - `scale` *float* - Scale factor for up/downscaling images, affecting optimization and 24 | quality (default: 1). 25 | :param set[str] | None _formats: This is ignored for the pyrlottie backend 26 | 27 | #### Returns 28 | 29 | Type: *int* 30 | Number of stickers successfully converted. 31 | 32 | #### Signature 33 | 34 | ```python 35 | def convertAnimated( 36 | swd: Path, 37 | _threads: int = 4, 38 | fps: int = 20, 39 | scale: float = 1, 40 | _formats: set[str] | None = None, 41 | ) -> int: ... 42 | ``` -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | """tests""" 2 | 3 | from __future__ import annotations 4 | 5 | import sys 6 | from pathlib import Path 7 | 8 | THISDIR = str(Path(__file__).resolve().parent) 9 | PROJECT_DIR = Path(THISDIR).parent 10 | sys.path.insert(0, str(PROJECT_DIR)) 11 | 12 | from tstickers.convert import Backend 13 | from tstickers.manager import StickerManager 14 | 15 | token = "" 16 | for candidate in [PROJECT_DIR / "env.txt", PROJECT_DIR / "env"]: 17 | if candidate.exists(): 18 | token = candidate.read_text(encoding="utf-8").strip() 19 | if not token: 20 | msg = ( 21 | '!! Generate a bot token and paste in a file called "env". Send a ' 22 | "message to @BotFather to get started" 23 | ) 24 | raise RuntimeError(msg) 25 | 26 | 27 | stickerManager = StickerManager(token) 28 | stickerManager.cwd = Path(THISDIR) / "data" 29 | 30 | packs = [{"pack": "DonutTheDog", "len": 31}] 31 | 32 | 33 | def test_getPack() -> None: 34 | stickerPack = stickerManager.getPack(packs[0]["pack"]) 35 | assert stickerPack is not None 36 | assert len(stickerPack["files"]) == packs[0]["len"] 37 | 38 | 39 | def test_downloadPack() -> None: 40 | stickerManager.downloadPack(packs[0]["pack"]) 41 | assert len(list(Path(f"{stickerManager.cwd}/donutthedog/tgs").iterdir())) == packs[0]["len"] 42 | 43 | 44 | def test_convertPack() -> None: 45 | stickerManager.downloadPack(packs[0]["pack"]) 46 | stickerManager.convertPack( 47 | packs[0]["pack"], scale=0.05, noCache=True, backend=Backend.RLOTTIE_PYTHON 48 | ) 49 | assert len(list(Path(f"{stickerManager.cwd}/donutthedog/webp").iterdir())) == packs[0]["len"] 50 | 51 | 52 | # def test_convertPack_slow() -> None: 53 | # stickerManager.downloadPack(packs[0]["pack"]) 54 | # stickerManager.convertPack(packs[0]["pack"], scale=1, noCache=True, backend=Backend.PYRLOTTIE) 55 | # assert len(list(Path(f"{stickerManager.cwd}/donutthedog/webp").iterdir())) == packs[0]["len"] 56 | -------------------------------------------------------------------------------- /documentation/tutorials/backends.md: -------------------------------------------------------------------------------- 1 | 2 | # Using Backends in TStickers 3 | 4 | TStickers supports multiple backends for converting sticker formats. You can choose between 5 | `rlottie-python` and `pyrlottie` depending on your needs and system compatibility. Here’s a brief 6 | guide on how to use and select these backends. 7 | 8 | ## Available Backends 9 | 10 | - **pyrlottie**: in my testing this seems to be a little faster at converting telegram stickers 11 | - **rlottie-python**: this backend has broader system compatibility, though seems to be a little 12 | slower on my machine™ 13 | 14 | ## How to Specify a Backend 15 | 16 | You can specify the backend you want to use by using the `-b` or `--backend` option in your 17 | TStickers command. 18 | 19 | ### Command Syntax 20 | 21 | ```bash 22 | tstickers -b BACKEND [other options] 23 | ``` 24 | 25 | ### Examples 26 | 27 | 1. **Using `rlottie-python` Backend:** 28 | 29 | If you want to use the `rlottie-python` backend, run: 30 | 31 | ```bash 32 | tstickers -b rlottie-python -t YOUR_BOT_TOKEN -p https://t.me/addstickers/YourStickerPack 33 | ``` 34 | 35 | This command uses the `rlottie-python` backend to process the sticker pack specified by the URL. 36 | 37 | 2. **Using `pyrlottie` Backend:** 38 | 39 | To use the `pyrlottie` backend, execute: 40 | 41 | ```bash 42 | tstickers -b pyrlottie -t YOUR_BOT_TOKEN -p https://t.me/addstickers/YourStickerPack 43 | ``` 44 | 45 | This command processes the sticker pack using the `pyrlottie` backend. 46 | 47 | ## Choosing the Right Backend 48 | 49 | - **Performance:** Test both backends to see which one performs better for your specific needs. 50 | - **Compatibility:** If you are having trouble with the default then try `rlottie-python`. 51 | 52 | Note if performance is important then you may want to explore the other options `--frameskip` and 53 | `--scale`. These will change the quality of the output image though! 54 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.9.6 4 | hooks: 5 | - id: ruff 6 | args: [ --fix ] 7 | - id: ruff-format 8 | 9 | - repo: https://github.com/RobertCraigie/pyright-python 10 | rev: v1.1.394 11 | hooks: 12 | - id: pyright 13 | 14 | - repo: local 15 | hooks: 16 | - id: generate requirements 17 | name: generate requirements 18 | entry: uv export --no-hashes --no-dev -o requirements.txt 19 | language: system 20 | pass_filenames: false 21 | - id: safety 22 | name: safety 23 | entry: uv run safety 24 | language: system 25 | pass_filenames: false 26 | - id: make docs 27 | name: make docs 28 | entry: uv run handsdown --cleanup -o documentation/reference 29 | language: system 30 | pass_filenames: false 31 | - id: build package 32 | name: build package 33 | entry: uv build 34 | language: system 35 | pass_filenames: false 36 | - id: pytest 37 | name: pytest 38 | entry: uv run pytest 39 | language: system 40 | pass_filenames: false 41 | 42 | - repo: https://github.com/pre-commit/pre-commit-hooks 43 | rev: v5.0.0 44 | hooks: 45 | - id: trailing-whitespace 46 | - id: end-of-file-fixer 47 | - id: check-case-conflict 48 | - id: check-executables-have-shebangs 49 | - id: check-json 50 | - id: check-merge-conflict 51 | - id: check-shebang-scripts-are-executable 52 | - id: check-symlinks 53 | - id: check-toml 54 | - id: check-vcs-permalinks 55 | - id: check-yaml 56 | - id: detect-private-key 57 | - id: mixed-line-ending 58 | 59 | - repo: https://github.com/boidolr/pre-commit-images 60 | rev: v1.8.4 61 | hooks: 62 | - id: optimize-jpg 63 | - id: optimize-png 64 | - id: optimize-svg 65 | - id: optimize-webp 66 | 67 | exclude: "tests/data|documentation/reference" 68 | -------------------------------------------------------------------------------- /documentation/reference/tstickers/convert_rlottie_python.md: -------------------------------------------------------------------------------- 1 | # Convert Rlottie Python 2 | 3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Convert Rlottie Python 4 | 5 | > Auto-generated documentation for [tstickers.convert_rlottie_python](../../../tstickers/convert_rlottie_python.py) module. 6 | 7 | - [Convert Rlottie Python](#convert-rlottie-python) 8 | - [convertAnimated](#convertanimated) 9 | - [convert_single_tgs](#convert_single_tgs) 10 | 11 | ## convertAnimated 12 | 13 | [Show source in convert_rlottie_python.py:59](../../../tstickers/convert_rlottie_python.py#L59) 14 | 15 | Convert animated stickers, over a number of threads, at a given framerate, scale and to a 16 | set of formats. 17 | 18 | #### Arguments 19 | 20 | - `swd` *Path* - The sticker working directory (e.g., downloads/packName). 21 | - `threads` *int* - Number of threads for ProcessPoolExecutor (default: number of 22 | logical processors). 23 | - `fps` *int* - framerate of the converted sticker, affecting optimization and 24 | quality (default: 20) 25 | - `scale` *float* - Scale factor for up/downscaling images, affecting optimization and 26 | quality (default: 1). 27 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to 28 | (default: {"gif", "webp", "apng"}) 29 | 30 | #### Returns 31 | 32 | Type: *int* 33 | Number of stickers successfully converted. 34 | 35 | #### Signature 36 | 37 | ```python 38 | def convertAnimated( 39 | swd: Path, 40 | threads: int = multiprocessing.cpu_count(), 41 | fps: int = 20, 42 | scale: float = 1, 43 | formats: set[str] | None = None, 44 | ) -> int: ... 45 | ``` 46 | 47 | 48 | 49 | ## convert_single_tgs 50 | 51 | [Show source in convert_rlottie_python.py:18](../../../tstickers/convert_rlottie_python.py#L18) 52 | 53 | Convert a single tgs file. 54 | 55 | #### Arguments 56 | 57 | - `stckr` *Path* - Path to the sticker 58 | - `fps` *int* - framerate of the converted sticker, affecting optimization and 59 | quality (default: 20) 60 | - `scale` *float* - Scale factor for up/downscaling images, affecting optimization and 61 | quality (default: 1). 62 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to 63 | (default: {"gif", "webp", "apng"}) 64 | 65 | #### Returns 66 | 67 | Type: *int* 68 | 1 if success 69 | 70 | #### Signature 71 | 72 | ```python 73 | def convert_single_tgs( 74 | stckr: Path, fps: int, scale: float = 1.0, formats: set[str] | None = None 75 | ) -> int: ... 76 | ``` -------------------------------------------------------------------------------- /tstickers/convert_pyrlottie.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conversion functionality for animated stickers. 3 | 4 | implements the conversion functionality for the pyrlottie backend. exposing a 5 | public function called `convertAnimated`, which is used to perform the conversion. 6 | 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import asyncio 12 | from pathlib import Path 13 | 14 | import pyrlottie 15 | 16 | 17 | def convertAnimated( 18 | swd: Path, 19 | _threads: int = 4, 20 | fps: int = 20, 21 | scale: float = 1, 22 | _formats: set[str] | None = None, 23 | ) -> int: 24 | """Convert animated stickers, over a number of threads, at a given framerate, scale and to a 25 | set of formats. 26 | 27 | :param Path swd: The sticker working directory (e.g., downloads/packName). 28 | :param int _threads: This is ignored for the pyrlottie backend 29 | :param int fps: framerate of the converted sticker, affecting optimization and 30 | quality (default: 20) 31 | :param float scale: Scale factor for up/downscaling images, affecting optimization and 32 | quality (default: 1). 33 | :param set[str] | None _formats: This is ignored for the pyrlottie backend 34 | :return int: Number of stickers successfully converted. 35 | 36 | """ 37 | converted = 0 38 | 39 | (swd / "webp").mkdir(parents=True, exist_ok=True) 40 | (swd / "gif").mkdir(parents=True, exist_ok=True) 41 | 42 | # here we are going to assume that the raw image is 60 fps as pyrlottie does not have a way 43 | # of setting the fps 44 | frameSkip = 0 45 | if fps < 45: # bisector of 30 and 60 46 | frameSkip = 1 # ~30fps 47 | if fps < 25: # bisector of 20 and 30 48 | frameSkip = 2 # ~20fps 49 | if fps < 18: # bisector of 15 and 20 50 | frameSkip = 3 # ~15fps 51 | if fps < 13: # 12fps and below 52 | frameSkip = 4 # ~12fps 53 | 54 | def doConvMultLottie(filemaps: list[pyrlottie.FileMap], frameSkip: int, scale: float) -> int: 55 | return ( 56 | len( 57 | asyncio.get_event_loop().run_until_complete( 58 | pyrlottie.convMultLottie(filemaps, frameSkip=frameSkip, scale=scale) 59 | ) 60 | ) 61 | // 2 62 | ) 63 | 64 | converted += doConvMultLottie( 65 | filemaps=[ 66 | pyrlottie.FileMap( 67 | pyrlottie.LottieFile(stckr.absolute().as_posix()), 68 | { 69 | stckr.absolute().as_posix().replace("tgs", "gif"), 70 | stckr.absolute().as_posix().replace("tgs", "webp"), 71 | }, 72 | ) 73 | for stckr in swd.glob("**/*.tgs") 74 | ], 75 | frameSkip=frameSkip, 76 | scale=scale, 77 | ) 78 | return converted 79 | -------------------------------------------------------------------------------- /tstickers/caching.py: -------------------------------------------------------------------------------- 1 | """Sticker caching functionality used by the downloader.""" 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | from pathlib import Path 7 | from typing import Any, Callable 8 | 9 | from loguru import logger 10 | from requests_cache.session import CachedSession 11 | 12 | # requests_cache 13 | cachedSession = CachedSession( 14 | ".cache/tstickers.requests", 15 | backend="filesystem", 16 | expire_after=60 * 60 * 12, 17 | allowable_codes=(200,), 18 | allowable_methods=("GET", "POST"), 19 | ) 20 | 21 | 22 | CACHE_DIR = Path(".cache") 23 | if not CACHE_DIR.exists(): 24 | CACHE_DIR.mkdir() 25 | 26 | 27 | def verify_converted(pack_name: str) -> bool: 28 | """Verify the cache for a packName eg. "DonutTheDog". Uses the cache "version" 29 | to call the verify function for that version. 30 | 31 | Args: 32 | ---- 33 | pack_name (str): name of the sticker pack eg. "DonutTheDog" 34 | 35 | Returns: 36 | ------- 37 | bool: if the converted cache has been verified 38 | 39 | """ 40 | cache = CACHE_DIR / pack_name 41 | if cache.exists(): 42 | data = json.loads(cache.read_text(encoding="utf-8")) 43 | verify_func = _get_verify_function(data.get("version", 1)) 44 | if verify_func(data): 45 | logger.info(f"-> Cache hit for {pack_name}!") 46 | return True 47 | logger.info(f"-> Cache miss for {pack_name}!") 48 | return False 49 | 50 | 51 | def _verify_converted_v1(data: dict[str, Any]) -> bool: 52 | """Verify the cache for a packName using cache data. 53 | 54 | Args: 55 | ---- 56 | data (dict[Path, Any]): packName cache data to verify 57 | 58 | Returns: 59 | ------- 60 | bool: if the converted cache has been verified 61 | 62 | """ 63 | return ( 64 | len(list(Path(f"{data['info']['swd']}").glob("**/*"))) > 0 65 | and data["converted"]["static"] + data["converted"]["animated"] 66 | >= data["converted"]["total"] 67 | ) 68 | 69 | 70 | def create_converted(pack_name: str, data: dict) -> None: 71 | """Write cache data to a file identified by packName. 72 | 73 | Args: 74 | ---- 75 | pack_name (str): name of the sticker pack eg. "DonutTheDog" 76 | data (dict): packName cache data to write to cache 77 | 78 | """ 79 | cache = CACHE_DIR / pack_name 80 | cache.write_text(json.dumps(data), encoding="utf-8") 81 | 82 | 83 | def _get_verify_function(version: int) -> Callable[[dict[str, Any]], bool]: 84 | """Get the appropriate cache verification function based on version. 85 | 86 | Args: 87 | ---- 88 | version (int): Cache version 89 | 90 | Returns: 91 | ------- 92 | Callable[[dict[str, Any]], bool]: Cache verification function 93 | 94 | """ 95 | return { 96 | 1: _verify_converted_v1, 97 | }.get(version, _verify_converted_v1) 98 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tstickers" 3 | version = "2025" 4 | description = "Download sticker packs from Telegram" 5 | authors = [{ name = "FredHappyface" }] 6 | requires-python = ">=3.9" 7 | readme = "README.md" 8 | license = "mit" 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Intended Audience :: Developers", 12 | "Intended Audience :: Education", 13 | "Natural Language :: English", 14 | "Operating System :: OS Independent", 15 | "Programming Language :: Python :: Implementation :: CPython", 16 | "Topic :: Software Development :: Libraries :: Python Modules", 17 | "Topic :: Utilities", 18 | "Topic :: Multimedia :: Graphics", 19 | ] 20 | dependencies = [ 21 | "emoji>=2.14.1", 22 | "loguru>=0.7.3", 23 | "pillow>=10.4.0", 24 | "requests>=2.32.3", 25 | "requests-cache>=1.2.1", 26 | "rlottie-python>=1.3.6", 27 | ] 28 | 29 | [project.optional-dependencies] 30 | pyrlottie = ["pyrlottie<2026,>=2024.0.1"] 31 | 32 | [project.urls] 33 | Homepage = "https://github.com/FHPythonUtils/TStickers" 34 | Repository = "https://github.com/FHPythonUtils/TStickers" 35 | Documentation = "https://github.com/FHPythonUtils/TStickers/blob/master/README.md" 36 | 37 | [project.scripts] 38 | tstickers = "tstickers:cli" 39 | 40 | [dependency-groups] 41 | dev = [ 42 | "coverage>=7.6.12", 43 | "handsdown>=2.1.0", 44 | "pyright>=1.1.394", 45 | "pytest>=8.3.4", 46 | "ruff>=0.9.6", 47 | "safety>=3.3.0", 48 | ] 49 | 50 | [tool.ruff] 51 | line-length = 100 52 | indent-width = 4 53 | target-version = "py38" 54 | 55 | [tool.ruff.lint] 56 | select = ["ALL"] 57 | ignore = [ 58 | "ANN101", # type annotation for self in method 59 | "COM812", # enforce trailing comma 60 | "D2", # pydocstyle formatting 61 | "ISC001", 62 | "N", # pep8 naming 63 | "PLR09", # pylint refactor too many 64 | "TCH", # type check blocks 65 | "W191", # ignore this to allow tabs 66 | ] 67 | fixable = ["ALL"] 68 | 69 | [tool.ruff.lint.per-file-ignores] 70 | "**/{tests,docs,tools}/*" = ["D", "S101", "E402"] 71 | 72 | [tool.ruff.lint.flake8-tidy-imports] 73 | ban-relative-imports = "all" # Disallow all relative imports. 74 | 75 | [tool.ruff.format] 76 | indent-style = "tab" 77 | docstring-code-format = true 78 | line-ending = "lf" 79 | 80 | [tool.pyright] 81 | venvPath = "." 82 | venv = ".venv" 83 | 84 | [tool.coverage.run] 85 | branch = true 86 | 87 | [tool.tox] 88 | legacy_tox_ini = """ 89 | [tox] 90 | env_list = 91 | py313 92 | py312 93 | py311 94 | py310 95 | py39 96 | 97 | [testenv] 98 | deps = 99 | pytest 100 | commands = pytest tests 101 | """ 102 | 103 | [build-system] 104 | requires = ["hatchling"] 105 | build-backend = "hatchling.build" 106 | -------------------------------------------------------------------------------- /documentation/reference/tstickers/caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Caching 4 | 5 | > Auto-generated documentation for [tstickers.caching](../../../tstickers/caching.py) module. 6 | 7 | #### Attributes 8 | 9 | - `cachedSession` - requests_cache: CachedSession('.cache/tstickers.requests.sqlite', backend='sqlite', expire_after=60 * 60 * 12, allowable_codes=(200), allowable_methods=('GET', 'POST')) 10 | 11 | 12 | - [Caching](#caching) 13 | - [_get_verify_function](#_get_verify_function) 14 | - [_verify_converted_v1](#_verify_converted_v1) 15 | - [create_converted](#create_converted) 16 | - [verify_converted](#verify_converted) 17 | 18 | ## _get_verify_function 19 | 20 | [Show source in caching.py:83](../../../tstickers/caching.py#L83) 21 | 22 | Get the appropriate cache verification function based on version. 23 | 24 | #### Arguments 25 | 26 | ---- 27 | - `version` *int* - Cache version 28 | 29 | #### Returns 30 | 31 | ------- 32 | Callable[[dict[str, Any]], bool]: Cache verification function 33 | 34 | #### Signature 35 | 36 | ```python 37 | def _get_verify_function(version: int) -> Callable[[dict[str, Any]], bool]: ... 38 | ``` 39 | 40 | 41 | 42 | ## _verify_converted_v1 43 | 44 | [Show source in caching.py:51](../../../tstickers/caching.py#L51) 45 | 46 | Verify the cache for a packName using cache data. 47 | 48 | #### Arguments 49 | 50 | ---- 51 | data (dict[Path, Any]): packName cache data to verify 52 | 53 | #### Returns 54 | 55 | ------- 56 | - `bool` - if the converted cache has been verified 57 | 58 | #### Signature 59 | 60 | ```python 61 | def _verify_converted_v1(data: dict[str, Any]) -> bool: ... 62 | ``` 63 | 64 | 65 | 66 | ## create_converted 67 | 68 | [Show source in caching.py:70](../../../tstickers/caching.py#L70) 69 | 70 | Write cache data to a file identified by packName. 71 | 72 | #### Arguments 73 | 74 | ---- 75 | - `pack_name` *str* - name of the sticker pack eg. "DonutTheDog" 76 | - `data` *dict* - packName cache data to write to cache 77 | 78 | #### Signature 79 | 80 | ```python 81 | def create_converted(pack_name: str, data: dict) -> None: ... 82 | ``` 83 | 84 | 85 | 86 | ## verify_converted 87 | 88 | [Show source in caching.py:27](../../../tstickers/caching.py#L27) 89 | 90 | Verify the cache for a packName eg. "DonutTheDog". Uses the cache "version" 91 | to call the verify function for that version. 92 | 93 | #### Arguments 94 | 95 | ---- 96 | - `pack_name` *str* - name of the sticker pack eg. "DonutTheDog" 97 | 98 | #### Returns 99 | 100 | ------- 101 | - `bool` - if the converted cache has been verified 102 | 103 | #### Signature 104 | 105 | ```python 106 | def verify_converted(pack_name: str) -> bool: ... 107 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | downloads/ 2 | env* 3 | tests/data 4 | sticker*.txt 5 | sorted 6 | perf.py 7 | /*.tgs 8 | 9 | uv.lock 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | cover/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | # For a library or package, you might want to ignore these files since the code is 97 | # intended to run in multiple environments; otherwise, check them in: 98 | # .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | # pytype static type analyzer 145 | .pytype/ 146 | -------------------------------------------------------------------------------- /tstickers/convert_rlottie_python.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conversion functionality for animated stickers. 3 | 4 | implements the conversion functionality for the rlottie_python backend. exposing a 5 | public function called `convertAnimated`, which is used to perform the conversion. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import concurrent.futures 11 | import multiprocessing 12 | from pathlib import Path 13 | 14 | from loguru import logger 15 | from rlottie_python import LottieAnimation 16 | 17 | 18 | def convert_single_tgs( 19 | stckr: Path, 20 | fps: int, 21 | scale: float = 1.0, 22 | formats: set[str] | None = None, 23 | ) -> int: 24 | """Convert a single tgs file. 25 | 26 | :param Path stckr: Path to the sticker 27 | :param int fps: framerate of the converted sticker, affecting optimization and 28 | quality (default: 20) 29 | :param float scale: Scale factor for up/downscaling images, affecting optimization and 30 | quality (default: 1). 31 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to 32 | (default: {"gif", "webp", "apng"}) 33 | :return int: 1 if success 34 | 35 | """ 36 | tgs_file = stckr.absolute().as_posix() 37 | if formats is None: 38 | formats = {"gif", "webp", "apng"} 39 | 40 | # Read the animation outside the context manager to avoid issues with pickling 41 | anim = LottieAnimation.from_tgs(path=tgs_file) 42 | 43 | try: 44 | width, height = anim.lottie_animation_get_size() 45 | fps_orig = anim.lottie_animation_get_framerate() 46 | fps = min(fps, fps_orig) 47 | width = int(width * scale) 48 | height = int(height * scale) 49 | 50 | for fmt in formats: 51 | anim.save_animation(tgs_file.replace("tgs", fmt), fps, width=width, height=height) 52 | 53 | finally: 54 | anim.lottie_animation_destroy() 55 | 56 | return 1 57 | 58 | 59 | def convertAnimated( 60 | swd: Path, 61 | threads: int = multiprocessing.cpu_count(), 62 | fps: int = 20, 63 | scale: float = 1, 64 | formats: set[str] | None = None, 65 | ) -> int: 66 | """Convert animated stickers, over a number of threads, at a given framerate, scale and to a 67 | set of formats. 68 | 69 | :param Path swd: The sticker working directory (e.g., downloads/packName). 70 | :param int threads: Number of threads for ProcessPoolExecutor (default: number of 71 | logical processors). 72 | :param int fps: framerate of the converted sticker, affecting optimization and 73 | quality (default: 20) 74 | :param float scale: Scale factor for up/downscaling images, affecting optimization and 75 | quality (default: 1). 76 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to 77 | (default: {"gif", "webp", "apng"}) 78 | :return int: Number of stickers successfully converted. 79 | 80 | """ 81 | if formats is None: 82 | formats = {"gif", "webp", "apng"} 83 | converted = 0 84 | 85 | with concurrent.futures.ProcessPoolExecutor(max_workers=threads) as executor: 86 | # Using list comprehension to submit tasks to the executor 87 | future_to_variable = { 88 | executor.submit(convert_single_tgs, stckr, fps, scale, formats): stckr 89 | for stckr in swd.glob("**/*.tgs") 90 | } 91 | 92 | # Wait for all tasks to complete and retrieve results 93 | for future in concurrent.futures.as_completed(future_to_variable): 94 | variable = future_to_variable[future] 95 | try: 96 | converted += future.result() 97 | except Exception as e: 98 | logger.error(f"Error processing {variable}: {e}") 99 | 100 | return converted 101 | -------------------------------------------------------------------------------- /tstickers/cli.py: -------------------------------------------------------------------------------- 1 | """Download sticker packs from Telegram.""" 2 | 3 | from __future__ import annotations 4 | 5 | import argparse 6 | import functools 7 | import importlib.util 8 | import operator 9 | from pathlib import Path 10 | from sys import exit as sysexit 11 | 12 | from loguru import logger 13 | 14 | from tstickers.convert import Backend 15 | from tstickers.manager import StickerManager 16 | 17 | allowed_formats = {"gif", "png", "webp", "apng"} 18 | 19 | 20 | def cli() -> None: # pragma: no cover 21 | """Cli entry point.""" 22 | parser = argparse.ArgumentParser("Welcome to TStickers, providing all of your sticker needs") 23 | parser.add_argument( 24 | "-t", 25 | "--token", 26 | help="Pass in a bot token inline", 27 | ) 28 | parser.add_argument( 29 | "-p", 30 | "--pack", 31 | action="append", 32 | nargs="+", 33 | help="Pass in a pack url, or pack name", 34 | ) 35 | parser.add_argument( 36 | "--fmt", 37 | action="append", 38 | nargs="+", 39 | choices=allowed_formats, 40 | help=f"Formats to convert to {allowed_formats}", 41 | ) 42 | parser.add_argument( 43 | "-f", 44 | "--file", 45 | help="Path to file containing pack urls", 46 | ) 47 | parser.add_argument( 48 | "--fps", 49 | default=20, 50 | type=int, 51 | help="Set fps for converted animated stickers (this does not affect " 52 | "static images). default=20", 53 | ) 54 | parser.add_argument( 55 | "--scale", 56 | default=1, 57 | type=float, 58 | help="Set scale for converted animated stickers (this does not affect " 59 | "static images). default=1.0", 60 | ) 61 | parser.add_argument( 62 | "-b", 63 | "--backend", 64 | choices={"rlottie_python", "pyrlottie"}, 65 | default="rlottie_python", 66 | help="Specify the convert backend", 67 | ) 68 | args = parser.parse_args() 69 | 70 | # Get the token 71 | token = args.token 72 | if token is None: 73 | token = "" 74 | for candidate in [Path.cwd() / "env.txt", Path.cwd() / "env"]: 75 | if candidate.exists(): 76 | token = candidate.read_text(encoding="utf-8").strip() 77 | if not token: 78 | logger.error( 79 | '!! Generate a bot token and paste in a file called "env". Send a ' 80 | "message to @BotFather to get started" 81 | ) 82 | sysexit(1) 83 | # Get the backend 84 | backend = args.backend 85 | 86 | if importlib.util.find_spec(backend) is None: 87 | logger.error(f'!! {backend} is not installed! Install with "pip install {backend}"') 88 | sysexit(2) 89 | 90 | # Get the packs 91 | 92 | packs = [] 93 | if args.file: 94 | fp = Path(args.file) 95 | if fp.is_file(): 96 | packs = fp.read_text("utf-8").strip().splitlines() 97 | 98 | packs.extend(functools.reduce(operator.iadd, args.pack or [[]], [])) 99 | if len(packs) == 0: 100 | logger.info("No packs provided, entering interactive mode...") 101 | while True: 102 | name = input("Enter pack url, or name (hit enter to stop):>").strip() 103 | if name == "": 104 | break 105 | packs.append(name) 106 | packs = [name.split("/")[-1] for name in packs] 107 | 108 | formats = {fmt for sublist in (args.fmt or []) for fmt in sublist} 109 | if len(formats) == 0: 110 | formats = {"png", "webp"} 111 | 112 | downloader = StickerManager(token) 113 | for pack in packs: 114 | logger.info("-" * 60) 115 | _ = downloader.downloadPack(pack) 116 | logger.info("-" * 60) 117 | 118 | backend_map = {"rlottie_python": Backend.RLOTTIE_PYTHON, "pyrlottie": Backend.PYRLOTTIE} 119 | 120 | downloader.convertPack( 121 | pack, 122 | args.fps, 123 | args.scale, 124 | backend=backend_map.get(args.backend, Backend.PYRLOTTIE), 125 | formats=formats, 126 | ) 127 | -------------------------------------------------------------------------------- /documentation/reference/tstickers/convert.md: -------------------------------------------------------------------------------- 1 | # Convert 2 | 3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Convert 4 | 5 | > Auto-generated documentation for [tstickers.convert](../../../tstickers/convert.py) module. 6 | 7 | - [Convert](#convert) 8 | - [Backend](#backend) 9 | - [convertAnimated](#convertanimated) 10 | - [convertAnimatedFunc](#convertanimatedfunc) 11 | - [convertStatic](#convertstatic) 12 | - [convertWithPIL](#convertwithpil) 13 | 14 | ## Backend 15 | 16 | [Show source in convert.py:16](../../../tstickers/convert.py#L16) 17 | 18 | Represents different conversion libraries such as pyrlottie, and rlottie-python. 19 | 20 | #### Signature 21 | 22 | ```python 23 | class Backend(IntEnum): ... 24 | ``` 25 | 26 | 27 | 28 | ## convertAnimated 29 | 30 | [Show source in convert.py:93](../../../tstickers/convert.py#L93) 31 | 32 | Convert animated stickers, over a number of threads, at a given framerate, scale and to a 33 | set of formats. 34 | 35 | #### Arguments 36 | 37 | - `swd` *Path* - The sticker working directory (e.g., downloads/packName). 38 | - `threads` *int* - Number of threads for ProcessPoolExecutor (default: number of 39 | logical processors). 40 | - `fps` *int* - framerate of the converted sticker, affecting optimization and 41 | quality (default: 20) 42 | - `scale` *float* - Scale factor for up/downscaling images, affecting optimization and 43 | quality (default: 1). 44 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to 45 | (default: {"gif", "webp", "apng"}) 46 | 47 | #### Returns 48 | 49 | Type: *int* 50 | Number of stickers successfully converted. 51 | 52 | #### Signature 53 | 54 | ```python 55 | def convertAnimated( 56 | swd: Path, 57 | threads: int = multiprocessing.cpu_count(), 58 | fps: int = 20, 59 | scale: float = 1, 60 | backend: Backend = Backend.UNDEFINED, 61 | formats: set[str] | None = None, 62 | ) -> int: ... 63 | ``` 64 | 65 | #### See also 66 | 67 | - [Backend](#backend) 68 | 69 | 70 | 71 | ## convertAnimatedFunc 72 | 73 | [Show source in convert.py:24](../../../tstickers/convert.py#L24) 74 | 75 | Convert animated stickers with (Base/Backend.UNDEFINED). 76 | 77 | #### Signature 78 | 79 | ```python 80 | def convertAnimatedFunc( 81 | _swd: Path, _threads: int, _fps: int, _scale: float, _formats: set[str] | None 82 | ) -> int: ... 83 | ``` 84 | 85 | 86 | 87 | ## convertStatic 88 | 89 | [Show source in convert.py:62](../../../tstickers/convert.py#L62) 90 | 91 | Convert static stickers to specified formats. 92 | 93 | #### Arguments 94 | 95 | - `swd` *Path* - The sticker working directory (e.g., downloads/packName). 96 | - `threads` *int* - Number of threads for ProcessPoolExecutor (default: number of 97 | logical processors). 98 | :param set[str]|None formats: Set of formats to convert telegram webp stickers to 99 | (default: {"gif", "png", "webp", "apng"}) 100 | 101 | #### Returns 102 | 103 | Type: *int* 104 | Number of stickers successfully converted. 105 | 106 | #### Signature 107 | 108 | ```python 109 | def convertStatic( 110 | swd: Path, threads: int = 4, formats: set[str] | None = None 111 | ) -> int: ... 112 | ``` 113 | 114 | 115 | 116 | ## convertWithPIL 117 | 118 | [Show source in convert.py:44](../../../tstickers/convert.py#L44) 119 | 120 | Convert a webp file to specified formats. 121 | 122 | #### Arguments 123 | 124 | - `input_file` *Path* - path to the input image/ sticker 125 | :param set[str] formats: set of formats 126 | 127 | #### Returns 128 | 129 | Type: *Path* 130 | path of the original image/sticker file 131 | 132 | #### Signature 133 | 134 | ```python 135 | def convertWithPIL(input_file: Path, formats: set[str]) -> Path: ... 136 | ``` -------------------------------------------------------------------------------- /tstickers/convert.py: -------------------------------------------------------------------------------- 1 | """Sticker convert functions used by the downloader.""" 2 | 3 | from __future__ import annotations 4 | 5 | import contextlib 6 | import multiprocessing 7 | import time 8 | from concurrent.futures import ThreadPoolExecutor 9 | from enum import IntEnum, auto 10 | from pathlib import Path 11 | 12 | from loguru import logger 13 | from PIL import Image 14 | 15 | 16 | class Backend(IntEnum): 17 | """Represents different conversion libraries such as pyrlottie, and rlottie-python.""" 18 | 19 | UNDEFINED = -1 20 | PYRLOTTIE = auto() 21 | RLOTTIE_PYTHON = auto() 22 | 23 | 24 | def convertAnimatedFunc( 25 | _swd: Path, 26 | _threads: int, 27 | _fps: int, 28 | _scale: float, 29 | _formats: set[str] | None, 30 | ) -> int: 31 | """Convert animated stickers with (Base/Backend.UNDEFINED).""" 32 | msg = "Backend could not be loaded" 33 | raise RuntimeError(msg) 34 | 35 | 36 | convertRlottiePython = convertPyRlottie = convertAnimatedFunc 37 | 38 | with contextlib.suppress(ModuleNotFoundError): 39 | from tstickers.convert_rlottie_python import convertAnimated as convertRlottiePython 40 | with contextlib.suppress(ModuleNotFoundError): 41 | from tstickers.convert_pyrlottie import convertAnimated as convertPyRlottie 42 | 43 | 44 | def convertWithPIL(input_file: Path, formats: set[str]) -> Path: 45 | """Convert a webp file to specified formats. 46 | 47 | :param Path input_file: path to the input image/ sticker 48 | :param set[str] formats: set of formats 49 | :return Path: path of the original image/sticker file 50 | """ 51 | 52 | img = Image.open(input_file) 53 | 54 | for fmt in formats: 55 | output_file = Path(input_file.as_posix().replace("webp", fmt)) 56 | if not output_file.exists(): 57 | img.save(output_file) 58 | 59 | return input_file 60 | 61 | 62 | def convertStatic( 63 | swd: Path, 64 | threads: int = 4, 65 | formats: set[str] | None = None, 66 | ) -> int: 67 | """Convert static stickers to specified formats. 68 | 69 | 70 | :param Path swd: The sticker working directory (e.g., downloads/packName). 71 | :param int threads: Number of threads for ProcessPoolExecutor (default: number of 72 | logical processors). 73 | :param set[str]|None formats: Set of formats to convert telegram webp stickers to 74 | (default: {"gif", "png", "webp", "apng"}) 75 | :return int: Number of stickers successfully converted. 76 | 77 | """ 78 | if formats is None: 79 | formats = {"gif", "png", "webp", "apng"} 80 | webp_files = list((swd / "webp").glob("**/*.webp")) 81 | converted = 0 82 | start = time.time() 83 | 84 | with ThreadPoolExecutor(max_workers=threads) as executor: 85 | results = executor.map(lambda f: convertWithPIL(f, formats), webp_files) 86 | converted = sum(1 for _ in results) 87 | 88 | end = time.time() 89 | logger.info(f"Converted {converted} stickers (static) in {end - start:.3f}s\n") 90 | return converted 91 | 92 | 93 | def convertAnimated( 94 | swd: Path, 95 | threads: int = multiprocessing.cpu_count(), 96 | fps: int = 20, 97 | scale: float = 1, 98 | backend: Backend = Backend.UNDEFINED, 99 | formats: set[str] | None = None, 100 | ) -> int: 101 | """Convert animated stickers, over a number of threads, at a given framerate, scale and to a 102 | set of formats. 103 | 104 | :param Path swd: The sticker working directory (e.g., downloads/packName). 105 | :param int threads: Number of threads for ProcessPoolExecutor (default: number of 106 | logical processors). 107 | :param int fps: framerate of the converted sticker, affecting optimization and 108 | quality (default: 20) 109 | :param float scale: Scale factor for up/downscaling images, affecting optimization and 110 | quality (default: 1). 111 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to 112 | (default: {"gif", "webp", "apng"}) 113 | :return int: Number of stickers successfully converted. 114 | 115 | """ 116 | if formats is None: 117 | formats = {"gif", "webp", "apng"} 118 | if backend == Backend.UNDEFINED: 119 | msg = "You must specify a conversion backend" 120 | raise RuntimeError(msg) 121 | start = time.time() 122 | 123 | convertMap = { 124 | Backend.UNDEFINED: convertAnimatedFunc, 125 | Backend.PYRLOTTIE: convertPyRlottie, 126 | Backend.RLOTTIE_PYTHON: convertRlottiePython, 127 | } 128 | 129 | converted = convertMap[backend](swd, threads, fps, scale, formats) 130 | 131 | end = time.time() 132 | logger.info(f"Time taken to convert {converted} stickers (tgs) - {end - start:.3f}s") 133 | logger.info("") 134 | return converted 135 | -------------------------------------------------------------------------------- /documentation/tutorials/getting-started.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Getting Started with TStickers 4 | 5 | Welcome to the TStickers tutorial! Follow these steps to get up and running with TStickers. This 6 | guide will walk you through setting up a Telegram bot, retrieving sticker pack URLs, and using 7 | TStickers to download and convert stickers. 8 | 9 | 10 | ## Table of Contents 11 | 12 | - [Step 1 - Send a message to @BotFather](#step-1---send-a-message-to-botfather) 13 | - [Step 2 - Create a File Called 'env'](#step-2---create-a-file-called-env) 14 | - [Step 3 - Get the URL of the Telegram Sticker Pack(s)](#step-3---get-the-url-of-the-telegram-sticker-packs) 15 | - [Option 1 - Use a Browser and Search for the Pack](#option-1---use-a-browser-and-search-for-the-pack) 16 | - [Option 2 - Use Telegram](#option-2---use-telegram) 17 | - [Step 4 - Use TStickers](#step-4---use-tstickers) 18 | 19 | ## Step 1 - Send a message to @BotFather 20 | 21 | To start using TStickers, you need to create a Telegram bot. Follow these steps to obtain your bot token: 22 | 23 | 1. **Create a Telegram Account:** If you don’t already have one, download the Telegram app and sign up. 24 | 25 | 2. **Contact @BotFather:** Open Telegram and search for the user `@BotFather`. This is the official bot for managing other bots on Telegram. 26 | 27 | 3. **Create a New Bot:** 28 | - Send the command `/newbot` to @BotFather. 29 | - Follow the prompts to provide a name for your bot (e.g., `TestBot`) and a username (e.g., `test_bot`). 30 | 31 | 4. **Receive Your Token:** @BotFather will reply with a message containing your bot’s API token. Keep this token safe, as you'll need it for the next steps. 32 | 33 | Step 1 34 | 35 | ## Step 2 - Create a File Called 'env' 36 | 37 | To store your bot token securely, you need to create a configuration file: 38 | 39 | 1. **Create the File:** 40 | - Create a new text file in the same directory where you'll run TStickers. 41 | - Name the file `env` or `env.txt`. 42 | 43 | 2. **Add Your Token:** 44 | - Open the file and paste your bot token into it. 45 | 46 | Example `env.txt`: 47 | ```txt 48 | 14************ 49 | ``` 50 | 51 | ## Step 3 - Get the URL of the Telegram Sticker Pack(s) 52 | 53 | To use TStickers, you'll need the URL of the sticker pack(s) you want to download. You can get this URL in two ways: 54 | 55 | ### Option 1 - Use a Browser and Search for the Pack 56 | 57 | 1. **Search for the Sticker Pack:** 58 | - Open your web browser and search for the sticker pack by name (e.g., `Telegram Donut The Dog`). 59 | 60 | Step 3: Part 1 61 | 62 | 2. **Copy the URL:** 63 | - Find the sticker pack link and copy its URL. It should look something like `https://t.me/addstickers/DonutTheDog`. 64 | 65 | ### Option 2 - Use Telegram 66 | 67 | 1. **Find the Sticker Pack:** 68 | - Open the Telegram app, search for the sticker pack, and open it. 69 | 70 | 2. **Copy the Link:** 71 | - Tap on the sticker pack’s name or menu options and select "Share" or "Copy Link" (on mobile devices). The URL will be copied to your clipboard. 72 | 73 | Example URL: `https://t.me/addstickers/DonutTheDog` 74 | 75 | Step 3: Part 2 76 | 77 | ## Step 4 - Use TStickers 78 | 79 | Now you’re ready to use TStickers to download and convert stickers from the URL you obtained: 80 | 81 | 1. **Install TStickers:** 82 | - Run the following command in your terminal: 83 | ```bash 84 | python3 -m pip install tstickers 85 | ``` 86 | 87 | 2. **Run TStickers:** 88 | - Start the program by executing: 89 | ```bash 90 | python3 -m tstickers 91 | ``` 92 | 93 | 3. **Enter the Sticker Pack URL:** 94 | - When prompted, paste the URL of the sticker pack and press Enter. 95 | 96 | 4. **Check the Output:** 97 | - TStickers will download and convert the stickers. The output will be saved in the `downloads` folder. 98 | 99 | Example output: 100 | ```bash 101 | $ tstickers 102 | Enter sticker_set url (leave blank to stop): https://t.me/addstickers/DonutTheDog 103 | Enter sticker_set url (leave blank to stop): 104 | INFO | ============================================================ 105 | INFO | Starting to scrape "DonutTheDog" .. 106 | INFO | Time taken to scrape 31 stickers - 0.044s 107 | INFO | 108 | INFO | ------------------------------------------------------------ 109 | INFO | Starting download of "donutthedog" into downloads\donutthedog 110 | INFO | Time taken to download 31 stickers - 0.157s 111 | INFO | 112 | INFO | ------------------------------------------------------------ 113 | INFO | -> Cache miss for DonutTheDog! 114 | INFO | Converting stickers "DonutTheDog"... 115 | INFO | Time taken to convert 31 stickers (tgs) - 60.970s 116 | INFO | 117 | INFO | Time taken to convert 31 stickers (webp) - 0.447s 118 | INFO | 119 | INFO | Time taken to convert 62 stickers (total) - 61.434s 120 | INFO | 121 | ``` 122 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All major and minor version changes will be documented in this file. Details of 4 | patch-level version changes can be found in [commit messages](../../commits/master). 5 | 6 | ## 2025 - 2025/02/16 7 | 8 | - be opinionated and install `rlottie-python` by default/ regardless 9 | - set default backend to `rlottie-python` in the cli 10 | - add `--fmt` to the cli, where the user can select a list of formats to convert to (default is png 11 | and webp formats) 12 | - replace `--frameskip` with `--fps` as this is more intuitive 13 | 14 | ## 2024.1.3 - 2024/08/26 15 | 16 | - clearly notify user if backend is not installed 17 | 18 | ## 2024.1.2 - 2024/03/25 19 | 20 | - revert 'fix' 21 | 22 | ## 2024.1.1 - 2024/03/24 23 | 24 | - fix `convert_rlottie_python.py` 25 | 26 | ## 2024.1 - 2024/03/22 27 | 28 | - Add '--file' arg for passing in a list of packs 29 | - Implement a custom `demojize` function similar to the `emoji.demojize` function. 30 | However, returns a string of unique keywords in alphabetical order seperated by "_" 31 | 32 | ## 2024 - 2024/03/17 33 | 34 | - add convert backends to give the user a choice of using their preferred tool 35 | - ruff 36 | - code quality improvements 37 | 38 | ## 2022.1.1 - 2022/06/25 39 | 40 | - Fix: add `parents=True` to `Path.mkdir()` 41 | - Update pre-commit 42 | 43 | ## 2022.1 - 2022/02/01 44 | 45 | - Refactor 46 | - Add support for webm stickers 47 | 48 | ## 2022 - 2022/01/24 49 | 50 | - Bump pillow version (CVE-2022-22815, CVE-2022-22816, CVE-2022-22817) 51 | - Update deps 52 | - Add formal tests 53 | 54 | ## 2021.4.4 - 2021/12/10 55 | 56 | - Fix https://github.com/FHPythonUtils/SigStickers/issues/1 57 | - More meaningful error messages 58 | 59 | ## 2021.4.2 - 2021/10/08 60 | 61 | - Implement action='extend' for pre 3.7 eg. `python3 -m tstickers -p pack1 pack2 -p pack3` 62 | 63 | ## 2021.4.1 - 2021/10/04 64 | 65 | - Update function names and docs 66 | 67 | ## 2021.4 - 2021/10/04 68 | 69 | - Added caching functionality using requests_cache and to the converter - 70 | output cache hit/miss to stdout for converter 71 | 72 | ## 2021.3.3 - 2021/10/03 73 | 74 | - Use `asyncio.get_event_loop().run_until_complete` in place of `asyncio.run` for compat 75 | with pyrlottie 2021.1 76 | - Marginal performance improvements with pyrlottie 2021.1 (~3% so may be a fluke?) 77 | 78 | ```txt 79 | Performance testing with https://t.me/addstickers/DonutTheDog on: 80 | OS: Windows 10 (2021/10/03) 81 | CPU: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz 82 | RAM: 16gb 83 | 84 | using pyrlottie (lottie2gif.exe + gif2webp.exe) 85 | ~85s (frameskip=0, scale=1) (-5s) 86 | ~47s (frameskip=1, scale=1) (0s) 87 | ~33s (frameskip=2, scale=1) (-1s) 88 | ``` 89 | 90 | ## 2021.3.2 - 2021/10/03 91 | 92 | - Produce pngs for animated stickers as in SigStickers 93 | - Tidy up 94 | 95 | ## 2021.3.1 - 2021/10/02 96 | 97 | - Bugfixes in dependency (pyrlottie) for linux/ wsl - so now runs 98 | 99 | ```txt 100 | Performance testing with https://t.me/addstickers/DonutTheDog on: 101 | OS: Windows 10 WSL Ubuntu (2021/10/02) 102 | CPU: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz 103 | RAM: 16gb 104 | 105 | using pyrlottie (lottie2gif + gif2webp) 106 | ~61s (frameskip=0, scale=1) 107 | ~27s (frameskip=1, scale=1) 108 | ~18s (frameskip=2, scale=1) 109 | 110 | => Approximately a 3.5x speed improvement for like-to-like image quality 111 | => Approximately a 2.4x speed improvement for improved image quality 112 | ``` 113 | 114 | ## 2021.3 - 2021/10/02 115 | 116 | - code quality improvements (eg readability) 117 | - significant performance improvements 118 | 119 | ```txt 120 | Performance testing with https://t.me/addstickers/DonutTheDog on: 121 | OS: Windows 10 (2021/10/02) 122 | CPU: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz 123 | RAM: 16gb 124 | 125 | using pylottie (pyppeteer backend) 126 | ~72s quality=1 (equal to frameskip=2) 127 | 128 | using pyrlottie (lottie2gif.exe + gif2webp.exe) 129 | ~90s (frameskip=0, scale=1) 130 | ~47s (frameskip=1, scale=1) 131 | ~34s (frameskip=2, scale=1) 132 | ~32s (frameskip=1, scale=0.5) 133 | 134 | => Approximately a 2x speed improvement for like-to-like image quality 135 | => Approximately a 1.4x speed improvement for improved image quality 136 | ``` 137 | 138 | ## 2021.2.1 - 2021/02/21 139 | 140 | - Fix `ResourceWarning: unclosed ssl.SSLSocket` 141 | 142 | ## 2021.2 - 2021/01/19 143 | 144 | - File names are now the emoji as text followed by the emoji glyph e.g. 145 | "647+smiling_face_with_3_hearts+🥰" followed by the file extension (requires 146 | `emoji` for this) 147 | - If no animated stickers then puppeteer is not launched resulting in a small 148 | speed increase 149 | - Strings double-quoted 150 | 151 | ## 2021.1.5 - 2021/01/13 152 | 153 | - Update `pylottie` for significant speed improvements (animation renders take 154 | approx 2/3 as long) 155 | - Leverage the quality setting exposed by `pylottie` to further improve speed if 156 | desired (quality 0 is fastest, quality 3 is best quality) 157 | 158 | ## 2021.1.3 - 2021/01/13 159 | 160 | - Use `pylottie` to convert animated stickers increasing processing speed by about 10% 161 | - Can pass in packs with `-p` or `--pack` 162 | - Can also pass in the bot token with `-t` or `--token` 163 | 164 | ## 2021.1.2 - 2021/01/07 165 | 166 | - Static stickers are saved as gif in addition to png and webp 167 | 168 | ## 2021.1.1 - 2021/01/07 169 | 170 | - Save animated stickers as webp 171 | - Fixed animation times 172 | 173 | ## 2021.1 - 2021/01/06 174 | 175 | - Added animated sticker support 176 | - These are converted to gif 177 | - No transparency support at this time 178 | 179 | ## 2021.0.1 - 2021/01/04 180 | 181 | - Using pillow for conversions 182 | 183 | ## 2021 - 2021/01/04 184 | 185 | - First release 186 | -------------------------------------------------------------------------------- /documentation/reference/tstickers/manager.md: -------------------------------------------------------------------------------- 1 | # Manager 2 | 3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Manager 4 | 5 | > Auto-generated documentation for [tstickers.manager](../../../tstickers/manager.py) module. 6 | 7 | - [Manager](#manager) 8 | - [Sticker](#sticker) 9 | - [Sticker().__repr__](#sticker()__repr__) 10 | - [Sticker().emojiName](#sticker()emojiname) 11 | - [StickerManager](#stickermanager) 12 | - [StickerManager().convertPack](#stickermanager()convertpack) 13 | - [StickerManager().doAPIReq](#stickermanager()doapireq) 14 | - [StickerManager().downloadPack](#stickermanager()downloadpack) 15 | - [StickerManager().downloadSticker](#stickermanager()downloadsticker) 16 | - [StickerManager().getPack](#stickermanager()getpack) 17 | - [StickerManager().getSticker](#stickermanager()getsticker) 18 | - [demojize](#demojize) 19 | 20 | ## Sticker 21 | 22 | [Show source in manager.py:56](../../../tstickers/manager.py#L56) 23 | 24 | Sticker instance attributes. 25 | 26 | #### Signature 27 | 28 | ```python 29 | class Sticker: ... 30 | ``` 31 | 32 | ### Sticker().__repr__ 33 | 34 | [Show source in manager.py:64](../../../tstickers/manager.py#L64) 35 | 36 | Get Sticker representation in the form . 37 | 38 | #### Returns 39 | 40 | Type: *str* 41 | representation 42 | 43 | #### Signature 44 | 45 | ```python 46 | def __repr__(self) -> str: ... 47 | ``` 48 | 49 | ### Sticker().emojiName 50 | 51 | [Show source in manager.py:71](../../../tstickers/manager.py#L71) 52 | 53 | Get the emoji as a string. 54 | 55 | #### Signature 56 | 57 | ```python 58 | def emojiName(self) -> str: ... 59 | ``` 60 | 61 | 62 | 63 | ## StickerManager 64 | 65 | [Show source in manager.py:76](../../../tstickers/manager.py#L76) 66 | 67 | The StickerManager sets up the api and makes requests. 68 | 69 | #### Signature 70 | 71 | ```python 72 | class StickerManager: 73 | def __init__( 74 | self, token: str, session: caching.CachedSession | None = None, threads: int = 4 75 | ) -> None: ... 76 | ``` 77 | 78 | ### StickerManager().convertPack 79 | 80 | [Show source in manager.py:248](../../../tstickers/manager.py#L248) 81 | 82 | Convert a downloaded sticker pack given by packName to other formats specified. 83 | 84 | #### Arguments 85 | 86 | - `packName` *str* - name of the pack to convert 87 | - `fps` *int* - framerate of animated stickers, affecting optimization and 88 | quality (default: 20) 89 | - `scale` *float* - Scale factor of animated stickers, for up/downscaling images, 90 | affecting optimization and quality (default: 1). 91 | - `noCache` *bool* - set to true to disable cache. Defaults to False. 92 | - `backend` *Backend* - select the backend to use to convert animated stickers 93 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to 94 | (default: {"gif", "webp", "apng"}) 95 | 96 | #### Signature 97 | 98 | ```python 99 | def convertPack( 100 | self, 101 | packName: str, 102 | fps: int = 20, 103 | scale: float = 1, 104 | noCache: bool = False, 105 | backend: Backend = Backend.UNDEFINED, 106 | formats: set[str] | None = None, 107 | ) -> None: ... 108 | ``` 109 | 110 | #### See also 111 | 112 | - [Backend](./convert.md#backend) 113 | 114 | ### StickerManager().doAPIReq 115 | 116 | [Show source in manager.py:107](../../../tstickers/manager.py#L107) 117 | 118 | Use the telegram api. 119 | 120 | #### Arguments 121 | 122 | ---- 123 | - `function` *str* - function to execute 124 | params (dict[Any, Any]): function parameters 125 | 126 | #### Raises 127 | 128 | ------ 129 | - `RuntimeError` - In the event of a failure 130 | 131 | #### Returns 132 | 133 | ------- 134 | - `Optional[dict[Any,` *Any]]* - api response 135 | 136 | #### Signature 137 | 138 | ```python 139 | def doAPIReq(self, function: str, params: dict[Any, Any]) -> dict[Any, Any] | None: ... 140 | ``` 141 | 142 | ### StickerManager().downloadPack 143 | 144 | [Show source in manager.py:207](../../../tstickers/manager.py#L207) 145 | 146 | Download a sticker pack. 147 | 148 | #### Arguments 149 | 150 | - `packName` *str* - name of the pack 151 | 152 | #### Returns 153 | 154 | Type: *bool* 155 | success 156 | 157 | #### Signature 158 | 159 | ```python 160 | def downloadPack(self, packName: str) -> bool: ... 161 | ``` 162 | 163 | ### StickerManager().downloadSticker 164 | 165 | [Show source in manager.py:195](../../../tstickers/manager.py#L195) 166 | 167 | Download a sticker from the server. 168 | 169 | #### Arguments 170 | 171 | - `path` *Path* - the path to write to 172 | - `link` *str* - the url to the file on the server 173 | 174 | #### Returns 175 | 176 | Type: *int* 177 | path.write_bytes(res.content) 178 | 179 | #### Signature 180 | 181 | ```python 182 | def downloadSticker(self, path: Path, link: str) -> int: ... 183 | ``` 184 | 185 | ### StickerManager().getPack 186 | 187 | [Show source in manager.py:161](../../../tstickers/manager.py#L161) 188 | 189 | Get a list of File objects. 190 | 191 | #### Arguments 192 | 193 | ---- 194 | - `packName` *str* - name of the pack 195 | 196 | #### Returns 197 | 198 | ------- 199 | - `dict[str,` *Any]* - dictionary containing sticker data 200 | 201 | #### Signature 202 | 203 | ```python 204 | def getPack(self, packName: str) -> dict[str, Any] | None: ... 205 | ``` 206 | 207 | ### StickerManager().getSticker 208 | 209 | [Show source in manager.py:138](../../../tstickers/manager.py#L138) 210 | 211 | Get sticker info from the server. 212 | 213 | #### Arguments 214 | 215 | ---- 216 | fileData (dict[str, Any]): sticker id 217 | 218 | #### Returns 219 | 220 | ------- 221 | - [Sticker](#sticker) - Sticker instance 222 | 223 | #### Signature 224 | 225 | ```python 226 | def getSticker(self, fileData: dict[str, Any]) -> Sticker: ... 227 | ``` 228 | 229 | #### See also 230 | 231 | - [Sticker](#sticker) 232 | 233 | 234 | 235 | ## demojize 236 | 237 | [Show source in manager.py:22](../../../tstickers/manager.py#L22) 238 | 239 | Similar to the emoji.demojize function. 240 | 241 | However, returns a string of unique keywords in alphabetical order seperated by "_" 242 | 243 | #### Arguments 244 | 245 | - `emoji` *str* - emoji unicode char 246 | 247 | #### Returns 248 | 249 | Type: *str* 250 | returns a string of unique keywords in alphabetical order seperated by "_" 251 | 252 | #### Signature 253 | 254 | ```python 255 | def demojize(emoji: str) -> str: ... 256 | ``` -------------------------------------------------------------------------------- /tstickers/manager.py: -------------------------------------------------------------------------------- 1 | """Sticker download functions used by the module entry point.""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | import time 7 | import urllib.parse 8 | from concurrent.futures import ThreadPoolExecutor, as_completed 9 | from dataclasses import dataclass 10 | from json.decoder import JSONDecodeError 11 | from pathlib import Path 12 | from sys import exit as sysexit 13 | from typing import Any 14 | 15 | from emoji import EMOJI_DATA 16 | from loguru import logger 17 | 18 | from tstickers import caching 19 | from tstickers.convert import Backend, convertAnimated, convertStatic 20 | 21 | 22 | def demojize(emoji: str) -> str: 23 | """Similar to the emoji.demojize function. 24 | 25 | However, returns a string of unique keywords in alphabetical order seperated by "_" 26 | 27 | :param str emoji: emoji unicode char 28 | :return str: returns a string of unique keywords in alphabetical order seperated by "_" 29 | """ 30 | 31 | def c14n_part(part: str) -> str: 32 | return re.sub(r"_!@#$%^&*'", "_", part).replace("-", "_").lower() 33 | 34 | def merge_parts(parts: set[str]) -> str: 35 | unique_set = set() 36 | for part in parts: 37 | unique_set.update(part.split("_")) 38 | 39 | unique_set.discard("") 40 | unique_set.discard("with") 41 | 42 | result_list = sorted(unique_set) 43 | return "_".join(result_list) 44 | 45 | emoji_data = EMOJI_DATA.get(emoji) 46 | if emoji_data is None: 47 | return "unknown" 48 | 49 | parts = {c14n_part(emoji_data.get("en", "").strip(":"))} 50 | parts.update(c14n_part(x.strip(":")) for x in emoji_data.get("alias", [])) 51 | 52 | return merge_parts(parts) 53 | 54 | 55 | @dataclass 56 | class Sticker: 57 | """Sticker instance attributes.""" 58 | 59 | name: str = "None" 60 | link: str = "None" 61 | emoji: str = "😀" 62 | fileType: str = "webp" 63 | 64 | def __repr__(self) -> str: 65 | """Get Sticker representation in the form . 66 | 67 | :return str: representation 68 | """ 69 | return f"" 70 | 71 | def emojiName(self) -> str: 72 | """Get the emoji as a string.""" 73 | return demojize(self.emoji) 74 | 75 | 76 | class StickerManager: 77 | """The StickerManager sets up the api and makes requests.""" 78 | 79 | def __init__( 80 | self, 81 | token: str, 82 | session: caching.CachedSession | None = None, 83 | threads: int = 4, 84 | ) -> None: 85 | """Telegram Sticker API and provides functions to simplify downloading 86 | new packs. 87 | 88 | :param str token: bot token obtained from @BotFather 89 | :param caching.CachedSession | None session: the requests session to use, defaults to None 90 | :param int threads: number of threads to download over, defaults to 4 91 | """ 92 | self.threads = threads 93 | self.token = token 94 | self.cwd = Path("downloads") 95 | if session is None: 96 | self.session = caching.cachedSession 97 | else: 98 | self.session = session 99 | self.api = f"https://api.telegram.org/bot{self.token}/" 100 | verify = self.doAPIReq("getMe", {}) 101 | if verify is not None and verify["ok"]: 102 | pass 103 | else: 104 | logger.info("Invalid token.") 105 | sysexit(1) 106 | 107 | def doAPIReq(self, function: str, params: dict[Any, Any]) -> dict[Any, Any] | None: 108 | """Use the telegram api. 109 | 110 | Args: 111 | ---- 112 | function (str): function to execute 113 | params (dict[Any, Any]): function parameters 114 | 115 | Raises: 116 | ------ 117 | RuntimeError: In the event of a failure 118 | 119 | Returns: 120 | ------- 121 | Optional[dict[Any, Any]]: api response 122 | 123 | """ 124 | urlParams = "?" + urllib.parse.urlencode(params) 125 | res = self.session.get(f"{self.api}{function}{urlParams}") 126 | try: 127 | res = res.json() 128 | except JSONDecodeError: 129 | res = {"ok": False, "raw": res} 130 | if res["ok"]: 131 | return res 132 | 133 | logger.info( 134 | f'API method {function} with params {params} failed. Error: "{res["description"]}"' 135 | ) 136 | return None 137 | 138 | def getSticker(self, fileData: dict[str, Any]) -> Sticker: 139 | """Get sticker info from the server. 140 | 141 | Args: 142 | ---- 143 | fileData (dict[str, Any]): sticker id 144 | 145 | Returns: 146 | ------- 147 | Sticker: Sticker instance 148 | 149 | """ 150 | info = self.doAPIReq("getFile", {"file_id": fileData["file_id"]}) 151 | if info is not None: 152 | filePath = info["result"]["file_path"] 153 | return Sticker( 154 | name=filePath.split("/")[-1], 155 | link=f"https://api.telegram.org/file/bot{self.token}/{filePath}", 156 | emoji=fileData["emoji"], 157 | fileType=filePath.split(".")[-1], 158 | ) 159 | return Sticker() 160 | 161 | def getPack(self, packName: str) -> dict[str, Any] | None: 162 | """Get a list of File objects. 163 | 164 | Args: 165 | ---- 166 | packName (str): name of the pack 167 | 168 | Returns: 169 | ------- 170 | dict[str, Any]: dictionary containing sticker data 171 | 172 | """ 173 | params = {"name": packName} 174 | res = self.doAPIReq("getStickerSet", params) 175 | if res is None: 176 | return None 177 | stickers = res["result"]["stickers"] 178 | files = [] 179 | 180 | logger.info(f'Starting to scrape "{packName}" ..') 181 | start = time.time() 182 | with ThreadPoolExecutor(max_workers=self.threads) as executor: 183 | futures = [executor.submit(self.getSticker, i) for i in stickers] 184 | files = [i.result() for i in as_completed(futures)] 185 | end = time.time() 186 | logger.info(f"Time taken to scrape {len(files)} stickers - {end - start:.3f}s") 187 | logger.info("") 188 | 189 | return { 190 | "name": res["result"]["name"].lower(), 191 | "title": res["result"]["title"], 192 | "files": files, 193 | } 194 | 195 | def downloadSticker(self, path: Path, link: str) -> int: 196 | """Download a sticker from the server. 197 | 198 | :param Path path: the path to write to 199 | :param str link: the url to the file on the server 200 | 201 | :return int: path.write_bytes(res.content) 202 | 203 | """ 204 | path.parent.mkdir(parents=True, exist_ok=True) 205 | return path.write_bytes(self.session.get(link).content) 206 | 207 | def downloadPack(self, packName: str) -> bool: 208 | """Download a sticker pack. 209 | 210 | :param str packName: name of the pack 211 | :return bool: success 212 | 213 | """ 214 | 215 | stickerPack = self.getPack(packName) 216 | if stickerPack is None: 217 | return False 218 | 219 | swd: Path = self.cwd / packName 220 | swd.mkdir(parents=True, exist_ok=True) 221 | 222 | downloads = 0 223 | logger.info(f'Starting download of "{packName}" into {swd}') 224 | start = time.time() 225 | with ThreadPoolExecutor(max_workers=self.threads) as executor: 226 | futures = [ 227 | executor.submit( 228 | self.downloadSticker, 229 | swd 230 | / sticker.fileType 231 | / ( 232 | f"{sticker.name.split('_')[-1].split('.')[0]}+{sticker.emojiName()}" 233 | f".{sticker.fileType}" 234 | ), 235 | link=sticker.link, 236 | ) 237 | for sticker in stickerPack["files"] 238 | ] 239 | for i in as_completed(futures): 240 | downloads += 1 if i.result() > 0 else 0 241 | self.session.close() 242 | 243 | end = time.time() 244 | logger.info(f"Time taken to download {downloads} stickers - {end - start:.3f}s") 245 | logger.info("") 246 | return downloads == stickerPack["files"] 247 | 248 | def convertPack( 249 | self, 250 | packName: str, 251 | fps: int = 20, 252 | scale: float = 1, 253 | *, 254 | noCache: bool = False, 255 | backend: Backend = Backend.UNDEFINED, 256 | formats: set[str] | None = None, 257 | ) -> None: 258 | """Convert a downloaded sticker pack given by packName to other formats specified. 259 | 260 | :param str packName: name of the pack to convert 261 | :param int fps: framerate of animated stickers, affecting optimization and 262 | quality (default: 20) 263 | :param float scale: Scale factor of animated stickers, for up/downscaling images, 264 | affecting optimization and quality (default: 1). 265 | :param bool noCache: set to true to disable cache. Defaults to False. 266 | :param Backend backend: select the backend to use to convert animated stickers 267 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to 268 | (default: {"gif", "webp", "apng"}) 269 | 270 | """ 271 | if formats is None: 272 | formats = {"gif", "png", "webp", "apng"} 273 | if not noCache and caching.verify_converted(packName): 274 | return 275 | swd = self.cwd / packName 276 | 277 | start = time.time() 278 | total = len([x for x in swd.glob("**/*") if x.is_file()]) 279 | 280 | logger.info(f'Converting stickers "{packName}"...') 281 | 282 | for fmt in formats: 283 | (swd / fmt).mkdir(parents=True, exist_ok=True) 284 | 285 | animatedFormats = formats.copy() 286 | animatedFormats.discard("png") 287 | 288 | converted = convertedTgs = convertAnimated( 289 | swd, self.threads, fps=fps, scale=scale, backend=backend, formats=animatedFormats 290 | ) 291 | 292 | convertedWebp = convertStatic(swd, self.threads, formats=formats) 293 | converted += convertedWebp 294 | 295 | end = time.time() 296 | logger.info(f"Time taken to convert {converted} stickers (total) - {end - start:.3f}s") 297 | logger.info("") 298 | 299 | caching.create_converted( 300 | packName, 301 | data={ 302 | "version": 2, 303 | "info": { 304 | "packName": packName, 305 | "fps": fps, 306 | "scale": scale, 307 | "swd": swd.as_posix(), 308 | }, 309 | "converted": { 310 | "static": convertedWebp, 311 | "animated": convertedTgs, 312 | "total": total, 313 | }, 314 | }, 315 | ) 316 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub top language](https://img.shields.io/github/languages/top/FHPythonUtils/TStickers.svg?style=for-the-badge&cacheSeconds=28800)](../../) 2 | [![Issues](https://img.shields.io/github/issues/FHPythonUtils/TStickers.svg?style=for-the-badge&cacheSeconds=28800)](../../issues) 3 | [![License](https://img.shields.io/github/license/FHPythonUtils/TStickers.svg?style=for-the-badge&cacheSeconds=28800)](/LICENSE.md) 4 | [![Commit activity](https://img.shields.io/github/commit-activity/m/FHPythonUtils/TStickers.svg?style=for-the-badge&cacheSeconds=28800)](../../commits/master) 5 | [![Last commit](https://img.shields.io/github/last-commit/FHPythonUtils/TStickers.svg?style=for-the-badge&cacheSeconds=28800)](../../commits/master) 6 | [![PyPI Downloads](https://img.shields.io/pypi/dm/tstickers.svg?style=for-the-badge&cacheSeconds=28800)](https://pypistats.org/packages/tstickers) 7 | [![PyPI Total Downloads](https://img.shields.io/badge/dynamic/json?style=for-the-badge&label=total%20downloads&query=%24.total_downloads&url=https%3A%2F%2Fapi%2Epepy%2Etech%2Fapi%2Fv2%2Fprojects%2Ftstickers)](https://pepy.tech/project/tstickers) 8 | [![PyPI Version](https://img.shields.io/pypi/v/tstickers.svg?style=for-the-badge&cacheSeconds=28800)](https://pypi.org/project/tstickers) 9 | 10 | 11 | # TStickers - Telegram Sticker Downloader 12 | 13 | Project Icon 14 | 15 | The `tstickers` package provides functionality for downloading and converting sticker packs from https://t.me/addstickers. Download stickers, and convert them to multiple formats, with caching the converted stickers for faster retrieval. 16 | 17 | - [Basic Use](#basic-use) 18 | - [Using](#using) 19 | - [Help](#help) 20 | - [Documentation](#documentation) 21 | - [Formats](#formats) 22 | - [Install With PIP](#install-with-pip) 23 | - [Language information](#language-information) 24 | - [Built for](#built-for) 25 | - [Install Python on Windows](#install-python-on-windows) 26 | - [Chocolatey](#chocolatey) 27 | - [Windows - Python.org](#windows---pythonorg) 28 | - [Install Python on Linux](#install-python-on-linux) 29 | - [Apt](#apt) 30 | - [Dnf](#dnf) 31 | - [Install Python on MacOS](#install-python-on-macos) 32 | - [Homebrew](#homebrew) 33 | - [MacOS - Python.org](#macos---pythonorg) 34 | - [How to run](#how-to-run) 35 | - [Windows](#windows) 36 | - [Linux/ MacOS](#linux-macos) 37 | - [Building](#building) 38 | - [Testing](#testing) 39 | - [Download Project](#download-project) 40 | - [Clone](#clone) 41 | - [Using The Command Line](#using-the-command-line) 42 | - [Using GitHub Desktop](#using-github-desktop) 43 | - [Download Zip File](#download-zip-file) 44 | - [Community Files](#community-files) 45 | - [Licence](#licence) 46 | - [Changelog](#changelog) 47 | - [Code of Conduct](#code-of-conduct) 48 | - [Contributing](#contributing) 49 | - [Security](#security) 50 | - [Support](#support) 51 | - [Rationale](#rationale) 52 | 53 | ## Basic Use 54 | 55 | https://t.me/addstickers/DonutTheDog 56 | 57 | - NOTE: You need a telegram bot token to make use of the script. Generate a bot 58 | token and paste in a file called 'env'. Send a message to @BotFather to get started. 59 | - Create a file called 'env' (or env.txt) and paste your token 60 | - Get the URL of the telegram sticker pack 61 | - Run the program `python -m tstickers` 62 | - Enter the URL of the sticker pack 63 | - Get the output in the `downloads` folder. 64 | 65 | More info at [Tutorials](/documentation/tutorials) 66 | 67 | ## Using 68 | 69 | 1. Get the URL of the Signal sticker pack. In the form https://t.me/addstickers 70 | 71 | 2. Pass in multiple packs from the commandline with `-p/--pack` 72 | 73 | ```bash 74 | $ tstickers --pack https://t.me/addstickers/DonutTheDog 75 | INFO | ============================================================ 76 | INFO | Starting to scrape "DonutTheDog" .. 77 | INFO | Time taken to scrape 31 stickers - 0.044s 78 | INFO | 79 | INFO | ------------------------------------------------------------ 80 | INFO | Starting download of "donutthedog" into downloads\donutthedog 81 | INFO | Time taken to download 31 stickers - 0.157s 82 | INFO | 83 | INFO | ------------------------------------------------------------ 84 | INFO | -> Cache miss for DonutTheDog! 85 | INFO | Converting stickers "DonutTheDog"... 86 | INFO | Time taken to convert 31 stickers (tgs) - 60.970s 87 | INFO | 88 | INFO | Time taken to convert 31 stickers (webp) - 0.447s 89 | INFO | 90 | INFO | Time taken to convert 62 stickers (total) - 61.434s 91 | INFO | 92 | 93 | ``` 94 | 95 | 3. OR. Enter the URL of the sticker pack when prompted 96 | 97 | ```bash 98 | $ python -m tstickers 99 | Enter sticker_set URL (leave blank to stop): https://t.me/addstickers/DonutTheDog 100 | Enter sticker_set URL (leave blank to stop): 101 | INFO | ============================================================ 102 | INFO | Starting to scrape "DonutTheDog" .. 103 | INFO | Time taken to scrape 31 stickers - 0.044s 104 | INFO | 105 | INFO | ------------------------------------------------------------ 106 | INFO | Starting download of "donutthedog" into downloads\donutthedog 107 | INFO | Time taken to download 31 stickers - 0.157s 108 | INFO | 109 | INFO | ------------------------------------------------------------ 110 | ... 111 | ``` 112 | 113 | 4. Get the output in the `downloads` folder. 114 | 115 | ```powershell 116 | $ ls .\downloads\donutthedog\ 117 | 118 | Mode LastWriteTime Length Name 119 | ---- ------------- ------ ---- 120 | d----- 17/03/2024 17꞉00 apng 121 | d----- 17/03/2024 17꞉01 gif 122 | d----- 17/03/2024 17꞉06 png 123 | d----- 17/03/2024 17꞉00 tgs 124 | d----- 17/03/2024 17꞉02 webp 125 | ``` 126 | 127 | ## Help 128 | 129 | ```bash 130 | $ python -m tstickers --help 131 | usage: Welcome to TStickers, providing all of your sticker needs [-h] [-t TOKEN] [-p PACK [PACK ...]] 132 | [--frameskip FRAMESKIP] [--scale SCALE] 133 | [-b {rlottie-python,pyrlottie}] 134 | 135 | options: 136 | -h, --help show this help message and exit 137 | -t TOKEN, --token TOKEN 138 | Pass in a bot token inline 139 | -p PACK [PACK ...], --pack PACK [PACK ...] 140 | Pass in a pack url inline 141 | --frameskip FRAMESKIP 142 | Set frameskip. default=1 143 | --scale SCALE Set scale. default=1.0 144 | -b {rlottie-python,pyrlottie}, --backend {rlottie-python,pyrlottie} 145 | Specify the convert backend 146 | ``` 147 | 148 | ## Documentation 149 | 150 | A high-level overview of how the documentation is organized organized will help you know 151 | where to look for certain things: 152 | 153 | - [Tutorials](/documentation/tutorials) take you by the hand through a series of steps to get 154 | started using the software. Start here if you’re new. 155 | - The [Technical Reference](/documentation/reference) documents APIs and other aspects of the 156 | machinery. This documentation describes how to use the classes and functions at a lower level 157 | and assume that you have a good high-level understanding of the software. 158 | 162 | 163 | ## Formats 164 | 165 | | Format | Static | Animated | Animated (webm) | 166 | | ------ | ------ | -------- | --------------- | 167 | | .gif | ✔ | ✔ | ❌ | 168 | | .png | ✔ | ✔+ | ❌ | 169 | | .tgs | ❌ | ✔ | ❌ | 170 | | .webp | ✔ | ✔ | ❌ | 171 | | .webm | ❌ | ❌ | ✔ | 172 | 173 | ```txt 174 | + First frame of animated image only 175 | ``` 176 | 177 | Note that static images can fail to save as .gif occasionally in testing 178 | 179 | ## Install With PIP 180 | 181 | ```python 182 | pip install tstickers 183 | ``` 184 | 185 | Head to https://pypi.org/project/tstickers/ for more info 186 | 187 | ## Language information 188 | 189 | ### Built for 190 | 191 | This program has been written for Python versions 3.8 - 3.11 and has been tested with both 3.8 and 192 | 3.11 193 | 194 | ## Install Python on Windows 195 | 196 | ### Chocolatey 197 | 198 | ```powershell 199 | choco install python 200 | ``` 201 | 202 | ### Windows - Python.org 203 | 204 | To install Python, go to https://www.python.org/downloads/windows/ and download the latest 205 | version. 206 | 207 | ## Install Python on Linux 208 | 209 | ### Apt 210 | 211 | ```bash 212 | sudo apt install python3.x 213 | ``` 214 | 215 | ### Dnf 216 | 217 | ```bash 218 | sudo dnf install python3.x 219 | ``` 220 | 221 | ## Install Python on MacOS 222 | 223 | ### Homebrew 224 | 225 | ```bash 226 | brew install python@3.x 227 | ``` 228 | 229 | ### MacOS - Python.org 230 | 231 | To install Python, go to https://www.python.org/downloads/macos/ and download the latest 232 | version. 233 | 234 | ## How to run 235 | 236 | ### Windows 237 | 238 | - Module 239 | `py -3.x -m [module]` or `[module]` (if module installs a script) 240 | 241 | - File 242 | `py -3.x [file]` or `./[file]` 243 | 244 | ### Linux/ MacOS 245 | 246 | - Module 247 | `python3.x -m [module]` or `[module]` (if module installs a script) 248 | 249 | - File 250 | `python3.x [file]` or `./[file]` 251 | 252 | ## Building 253 | 254 | This project uses https://github.com/FHPythonUtils/FHMake to automate most of the building. This 255 | command generates the documentation, updates the requirements.txt and builds the library artefacts 256 | 257 | Note the functionality provided by fhmake can be approximated by the following 258 | 259 | ```sh 260 | handsdown --cleanup -o documentation/reference 261 | poetry export -f requirements.txt --output requirements.txt 262 | poetry export -f requirements.txt --with dev --output requirements_optional.txt 263 | poetry build 264 | ``` 265 | 266 | `fhmake audit` can be run to perform additional checks 267 | 268 | ## Testing 269 | 270 | For testing with the version of python used by poetry use 271 | 272 | ```sh 273 | poetry run pytest 274 | ``` 275 | 276 | Alternatively use `tox` to run tests over python 3.8 - 3.11 277 | 278 | ```sh 279 | tox 280 | ``` 281 | 282 | ## Download Project 283 | 284 | ### Clone 285 | 286 | #### Using The Command Line 287 | 288 | 1. Press the Clone or download button in the top right 289 | 2. Copy the URL (link) 290 | 3. Open the command line and change directory to where you wish to 291 | clone to 292 | 4. Type 'git clone' followed by URL in step 2 293 | 294 | ```bash 295 | git clone https://github.com/FHPythonUtils/TStickers 296 | ``` 297 | 298 | More information can be found at 299 | https://help.github.com/en/articles/cloning-a-repository 300 | 301 | #### Using GitHub Desktop 302 | 303 | 1. Press the Clone or download button in the top right 304 | 2. Click open in desktop 305 | 3. Choose the path for where you want and click Clone 306 | 307 | More information can be found at 308 | https://help.github.com/en/desktop/contributing-to-projects/cloning-a-repository-from-github-to-github-desktop 309 | 310 | ### Download Zip File 311 | 312 | 1. Download this GitHub repository 313 | 2. Extract the zip archive 314 | 3. Copy/ move to the desired location 315 | 316 | ## Community Files 317 | 318 | ### Licence 319 | 320 | MIT License 321 | Copyright (c) FredHappyface 322 | (See the [LICENSE](/LICENSE.md) for more information.) 323 | 324 | ### Changelog 325 | 326 | See the [Changelog](/CHANGELOG.md) for more information. 327 | 328 | ### Code of Conduct 329 | 330 | Online communities include people from many backgrounds. The *Project* 331 | contributors are committed to providing a friendly, safe and welcoming 332 | environment for all. Please see the 333 | [Code of Conduct](https://github.com/FHPythonUtils/.github/blob/master/CODE_OF_CONDUCT.md) 334 | for more information. 335 | 336 | ### Contributing 337 | 338 | Contributions are welcome, please see the 339 | [Contributing Guidelines](https://github.com/FHPythonUtils/.github/blob/master/CONTRIBUTING.md) 340 | for more information. 341 | 342 | ### Security 343 | 344 | Thank you for improving the security of the project, please see the 345 | [Security Policy](https://github.com/FHPythonUtils/.github/blob/master/SECURITY.md) 346 | for more information. 347 | 348 | ### Support 349 | 350 | Thank you for using this project, I hope it is of use to you. Please be aware that 351 | those involved with the project often do so for fun along with other commitments 352 | (such as work, family, etc). Please see the 353 | [Support Policy](https://github.com/FHPythonUtils/.github/blob/master/SUPPORT.md) 354 | for more information. 355 | 356 | ### Rationale 357 | 358 | The rationale acts as a guide to various processes regarding projects such as 359 | the versioning scheme and the programming styles used. Please see the 360 | [Rationale](https://github.com/FHPythonUtils/.github/blob/master/RATIONALE.md) 361 | for more information. 362 | --------------------------------------------------------------------------------