├── tests ├── __init__.py ├── data │ └── png │ │ └── .dir ├── test_cli.py ├── test_downloader.py ├── test_user.py └── test_caching.py ├── readme-assets └── icons │ ├── name.png │ ├── proj-icon.png │ └── SigStickers.xcf ├── sigstickers ├── __main__.py ├── __init__.py ├── cli.py ├── caching.py └── downloader.py ├── documentation └── reference │ ├── sigstickers │ ├── module.md │ ├── index.md │ ├── cli.md │ ├── downloader.md │ └── caching.md │ └── README.md ├── .github └── workflows │ ├── mirror-to-codeberg.yaml │ └── test-lint.yaml ├── requirements.txt ├── LICENSE.md ├── CHANGELOG.md ├── .pre-commit-config.yaml ├── pyproject.toml ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/png/.dir: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /readme-assets/icons/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SigStickers/HEAD/readme-assets/icons/name.png -------------------------------------------------------------------------------- /readme-assets/icons/proj-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SigStickers/HEAD/readme-assets/icons/proj-icon.png -------------------------------------------------------------------------------- /sigstickers/__main__.py: -------------------------------------------------------------------------------- 1 | """entry point for python -m sigstickers.""" 2 | 3 | from sigstickers.cli import cli 4 | 5 | cli() 6 | -------------------------------------------------------------------------------- /readme-assets/icons/SigStickers.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SigStickers/HEAD/readme-assets/icons/SigStickers.xcf -------------------------------------------------------------------------------- /sigstickers/__init__.py: -------------------------------------------------------------------------------- 1 | """Entry point for python -m sigstickers.""" 2 | 3 | from sigstickers.cli import cli, main 4 | 5 | _ = (cli, main) 6 | -------------------------------------------------------------------------------- /documentation/reference/sigstickers/module.md: -------------------------------------------------------------------------------- 1 | # Module 2 | 3 | [Sigstickers Index](../README.md#sigstickers-index) / [Sigstickers](./index.md#sigstickers) / Module 4 | 5 | > Auto-generated documentation for [sigstickers.__main__](../../../sigstickers/__main__.py) module. 6 | - [Module](#module) 7 | -------------------------------------------------------------------------------- /documentation/reference/README.md: -------------------------------------------------------------------------------- 1 | # Sigstickers Index 2 | 3 | > Auto-generated documentation index. 4 | 5 | A full list of `Sigstickers` project modules. 6 | 7 | - [Sigstickers](sigstickers/index.md#sigstickers) 8 | - [Module](sigstickers/module.md#module) 9 | - [Caching](sigstickers/caching.md#caching) 10 | - [Cli](sigstickers/cli.md#cli) 11 | - [Downloader](sigstickers/downloader.md#downloader) 12 | -------------------------------------------------------------------------------- /documentation/reference/sigstickers/index.md: -------------------------------------------------------------------------------- 1 | # Sigstickers 2 | 3 | [Sigstickers Index](../README.md#sigstickers-index) / Sigstickers 4 | 5 | > Auto-generated documentation for [sigstickers](../../../sigstickers/__init__.py) module. 6 | 7 | - [Sigstickers](#sigstickers) 8 | - [Modules](#modules) 9 | 10 | ## Modules 11 | 12 | - [Module](./module.md) 13 | - [Caching](./caching.md) 14 | - [Cli](./cli.md) 15 | - [Downloader](./downloader.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/SigStickers.git" 19 | GIT_USERNAME: FredHappyface 20 | GIT_PASSWORD: ${{ secrets.CODEBERG_PASSWORD }} 21 | -------------------------------------------------------------------------------- /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 | anyio==3.7.1 5 | certifi==2025.1.31 6 | cffi==1.17.1 7 | colorama==0.4.6 ; sys_platform == 'win32' 8 | cryptography==3.4.8 9 | emoji==2.14.1 10 | exceptiongroup==1.2.2 ; python_full_version < '3.11' 11 | h11==0.14.0 12 | httpcore==0.17.3 13 | httpx==0.24.1 14 | idna==3.10 15 | loguru==0.7.3 16 | pillow==11.1.0 17 | protobuf==3.20.3 18 | pycparser==2.22 19 | signalstickers-client==3.3.0 20 | sniffio==1.3.1 21 | win32-setctime==1.2.0 ; sys_platform == 'win32' 22 | -------------------------------------------------------------------------------- /documentation/reference/sigstickers/cli.md: -------------------------------------------------------------------------------- 1 | # Cli 2 | 3 | [Sigstickers Index](../README.md#sigstickers-index) / [Sigstickers](./index.md#sigstickers) / Cli 4 | 5 | > Auto-generated documentation for [sigstickers.cli](../../../sigstickers/cli.py) module. 6 | 7 | - [Cli](#cli) 8 | - [cli](#cli) 9 | - [main](#main) 10 | 11 | ## cli 12 | 13 | [Show source in cli.py:18](../../../sigstickers/cli.py#L18) 14 | 15 | CLI entry point. 16 | 17 | #### Signature 18 | 19 | ```python 20 | def cli() -> None: ... 21 | ``` 22 | 23 | 24 | 25 | ## main 26 | 27 | [Show source in cli.py:41](../../../sigstickers/cli.py#L41) 28 | 29 | Download, and convert sticker packs. 30 | 31 | #### Signature 32 | 33 | ```python 34 | def main(packs: list[str], cwd: Path = DEFAULT_CWD) -> int: ... 35 | ``` 36 | 37 | #### See also 38 | 39 | - [DEFAULT_CWD](./downloader.md#default_cwd) -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | THISDIR = Path(__file__).resolve().parent 7 | PROJECT_DIR = THISDIR.parent 8 | sys.path.insert(0, str(PROJECT_DIR)) 9 | 10 | from sigstickers.cli import main 11 | 12 | cwd = THISDIR / "data" 13 | 14 | 15 | def test_cli() -> None: 16 | assert ( 17 | main( 18 | [ 19 | "https://signal.art/addstickers/#pack_id=b676ec334ee2f771cadff5d095971e8c&pack_key=c957a57000626a2dc3cb69bf0e79c91c6b196b74d4d6ca1cbb830d3ad0ad4e36" 20 | ], 21 | cwd=cwd, 22 | ) 23 | == 0 24 | ) 25 | assert len(list(Path(f"{cwd}/downloads/DonutTheDog/png").iterdir())) == 28 26 | 27 | 28 | def test_cli_invalid_url() -> None: 29 | assert ( 30 | main( 31 | [ 32 | "https://signal.art/addstickers/#pack_key=c957a57000626a2dc3cb69bf0e79c91c6b196b74d4d6ca1cbb830d3ad0ad4e36" 33 | ], 34 | cwd=cwd, 35 | ) 36 | == 1 37 | ) 38 | -------------------------------------------------------------------------------- /tests/test_downloader.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | from typing import Generator 4 | 5 | import pytest 6 | 7 | from sigstickers.downloader import download_pack 8 | 9 | 10 | @pytest.fixture 11 | def test_data() -> Generator[Path, None, None]: 12 | test_dir = Path("test_data") 13 | yield test_dir 14 | shutil.rmtree(test_dir) 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_download_pack(test_data: Path) -> None: 19 | pack_id = "b676ec334ee2f771cadff5d095971e8c" 20 | pack_key = "c957a57000626a2dc3cb69bf0e79c91c6b196b74d4d6ca1cbb830d3ad0ad4e36" 21 | sticker_dir, _pack_name = await download_pack(pack_id, pack_key, cwd=test_data) 22 | assert sticker_dir.exists() 23 | assert sticker_dir.is_dir() 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_download_pack_bad_name(test_data: Path) -> None: 28 | pack_id = "4d92b5e3e92d1ac099830b17ac10793d" 29 | pack_key = "c8526aa2e25b911a405d39c1d4ee3977586e945550fddc33e1316626116da512" 30 | sticker_dir, _pack_name = await download_pack(pack_id, pack_key, cwd=test_data) 31 | assert sticker_dir.exists() 32 | assert sticker_dir.is_dir() 33 | -------------------------------------------------------------------------------- /.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) 2021 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 | -------------------------------------------------------------------------------- /tests/test_user.py: -------------------------------------------------------------------------------- 1 | """tests""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | import sys 7 | from pathlib import Path 8 | 9 | THISDIR = Path(__file__).resolve().parent 10 | PROJECT_DIR = THISDIR.parent 11 | sys.path.insert(0, str(PROJECT_DIR)) 12 | 13 | from sigstickers import downloader 14 | 15 | cwd = THISDIR / "data" 16 | 17 | packs = [ 18 | { 19 | "packId": "b676ec334ee2f771cadff5d095971e8c", 20 | "packKey": "c957a57000626a2dc3cb69bf0e79c91c6b196b74d4d6ca1cbb830d3ad0ad4e36", 21 | "len": 28, 22 | } 23 | ] 24 | 25 | 26 | def test_downloadPack() -> None: 27 | swd, packName = asyncio.run( 28 | downloader.download_pack(packs[0]["packId"], packs[0]["packKey"], cwd) 29 | ) 30 | assert swd.as_posix(), packName == ( 31 | f"{cwd}/downloads/DonutTheDog", 32 | "DonutTheDog", 33 | ) 34 | assert len(list(Path(f"{cwd}/downloads/DonutTheDog/webp").iterdir())) == packs[0]["len"] 35 | 36 | 37 | def test_convertPack() -> None: 38 | swd, packName = asyncio.run( 39 | downloader.download_pack(packs[0]["packId"], packs[0]["packKey"], cwd) 40 | ) 41 | asyncio.run(downloader.convert_pack(swd, packName, no_cache=True)) 42 | assert len(list(Path(f"{cwd}/downloads/DonutTheDog/png").iterdir())) == packs[0]["len"] 43 | 44 | 45 | def test_convertPack_cache() -> None: 46 | swd, packName = asyncio.run( 47 | downloader.download_pack(packs[0]["packId"], packs[0]["packKey"], cwd) 48 | ) 49 | asyncio.run(downloader.convert_pack(swd, packName, no_cache=False)) 50 | assert len(list(Path(f"{cwd}/downloads/DonutTheDog/png").iterdir())) == packs[0]["len"] 51 | -------------------------------------------------------------------------------- /sigstickers/cli.py: -------------------------------------------------------------------------------- 1 | """Download sticker packs from Signal.""" 2 | 3 | from __future__ import annotations 4 | 5 | import argparse 6 | import asyncio 7 | import functools 8 | import operator 9 | from pathlib import Path 10 | from sys import exit as sysexit 11 | from urllib import parse 12 | 13 | from loguru import logger 14 | 15 | from sigstickers.downloader import DEFAULT_CWD, convert_pack, download_pack 16 | 17 | 18 | def cli() -> None: # pragma: no cover 19 | """CLI entry point.""" 20 | parser = argparse.ArgumentParser("Welcome to SigSticker, providing all of your sticker needs") 21 | parser.add_argument( 22 | "-p", 23 | "--pack", 24 | help="Pass in a pack URL inline", 25 | nargs="+", 26 | action="append", 27 | ) 28 | args = parser.parse_args() 29 | # Get the packs 30 | packs: list[str] = functools.reduce(operator.iadd, args.pack or [[]], []) 31 | if not packs: 32 | packs = [] 33 | while True: 34 | name = input("Enter sticker_set URL (leave blank to stop): ").strip() 35 | if not name: 36 | break 37 | packs.append(name) 38 | sysexit(main(packs)) 39 | 40 | 41 | def main(packs: list[str], cwd: Path = DEFAULT_CWD) -> int: 42 | """Download, and convert sticker packs.""" 43 | for pack in packs: 44 | pack_attrs: dict[str, list[str]] = parse.parse_qs(parse.urlparse(pack).fragment) 45 | if {"pack_id", "pack_key"} > pack_attrs.keys(): 46 | logger.error( 47 | "Sticker URLs need a pack_id and pack_key. Eg. https://signal.art/" 48 | "addstickers/#pack_id=9acc9e8aba563d26a4994e69263e3b25&" 49 | "pack_key=5a6dff3948c28efb9b7aaf93ecc375c69fc316e78077ed26867a14d10a0f6a12" 50 | ) 51 | return 1 52 | 53 | downloaded = asyncio.run( 54 | download_pack("".join(pack_attrs["pack_id"]), "".join(pack_attrs["pack_key"]), cwd=cwd) 55 | ) 56 | 57 | asyncio.run(convert_pack(*downloaded)) 58 | return 0 59 | -------------------------------------------------------------------------------- /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 | - drop support for python 3.8 9 | 10 | ## 2024.1 - 2024/03/17 11 | 12 | - update deps 13 | - use ruff 14 | - code quality improvements 15 | - fix 'Can't download packs with ":" (Colon) in it' - https://github.com/FHPythonUtils/SigStickers/issues/3 16 | - 98% test coverage 17 | 18 | ## 2024 - 2024/01/07 19 | 20 | - update dependencies 21 | 22 | ## 2023 - 2023/08/31 23 | 24 | - Update deps 25 | 26 | ## 2022.1.1 - 2022/06/25 27 | 28 | - Fix: use `os.makedirs` instead of `os.mkdir` 29 | - Update pre-commit 30 | 31 | ## 2022.1 - 2022/04/11 32 | 33 | - Tests and code improvements 34 | - Update pre-commit 35 | 36 | ## 2022 - 2022/01/23 37 | 38 | - Improvements to saving gifs 39 | - Bump pillow version (CVE-2022-22815, CVE-2022-22816, CVE-2022-22817) 40 | - Use urllib.parse to parse urls in place of string splitting 41 | 42 | ## 2021.2.3 - 2021/12/10 43 | 44 | - Fix https://github.com/FHPythonUtils/SigStickers/issues/1 45 | - More meaningful error messages 46 | 47 | ## 2021.2.1 - 2021/10/08 48 | 49 | - Implement action='extend' for pre 3.7 eg. `python3 -m sigstickers -p pack1 pack2 -p pack3` 50 | 51 | ## 2021.2 - 2021/10/04 52 | 53 | - Added caching functionality - output cache hit/miss to stdout for converter 54 | - refactored code 55 | 56 | ## 2021.1 - 2021/01/19 57 | 58 | - File names are now the emoji as text followed by the emoji glyph e.g. 59 | "0+smiling_face_with_3_hearts+🥰" followed by the file extension 60 | (requires `emoji` for this) 61 | - Strings double-quoted 62 | 63 | ## 2021.0.1 - 2021/01/13 64 | 65 | - Improvements to gifs 66 | - Animations supported in gifs 67 | - Bugfixes 68 | 69 | ## 2021 - 2021/01/13 70 | 71 | - First release 72 | - Similar UI to `tstickers` 73 | - Gifs seem to look horrible 74 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tests/test_caching.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from sigstickers.caching import CACHE_DIR, create_converted, verify_converted 5 | 6 | THISDIR = Path(__file__).resolve().parent 7 | 8 | 9 | # just want files that exist here 10 | file_exists = "pyproject.toml" 11 | file_exists_2 = ".gitignore" 12 | 13 | file_not_exists = "file/not/exists" 14 | 15 | 16 | def test_verify_converted_v1() -> None: 17 | pack_name = "Test_Pack_v1" 18 | cache_file = CACHE_DIR / pack_name 19 | cache_data = { 20 | "info": {"swd": f"{THISDIR}/data"}, 21 | "converted": {"converted": 10, "total": 10}, 22 | } 23 | cache_file.write_text(json.dumps(cache_data)) 24 | 25 | assert verify_converted(Path(pack_name)) 26 | 27 | 28 | def test_verify_converted_v1_not_exists() -> None: 29 | pack_name = "Test_Pack_v1_not_exists" 30 | cache_file = CACHE_DIR / pack_name 31 | cache_data = { 32 | "info": {"swd": file_not_exists}, 33 | "converted": {"converted": 10, "total": 10}, 34 | } 35 | cache_file.write_text(json.dumps(cache_data)) 36 | 37 | assert not verify_converted(Path(pack_name)) 38 | 39 | 40 | def test_verify_converted_v2() -> None: 41 | pack_name = "Test_Pack_v2" 42 | cache_file = CACHE_DIR / pack_name 43 | cache_data = { 44 | "version": 2, 45 | "webp_files": [file_exists, file_exists_2], 46 | "converted_files": [[file_exists], [file_exists_2]], 47 | } 48 | cache_file.write_text(json.dumps(cache_data)) 49 | 50 | assert verify_converted(Path(pack_name)) 51 | 52 | 53 | def test_verify_converted_v2_partial_convert() -> None: 54 | pack_name = "Test_Pack_v2_partial_convert" 55 | cache_file = CACHE_DIR / pack_name 56 | cache_data = { 57 | "version": 2, 58 | "webp_files": [file_exists, file_exists_2], 59 | "converted_files": [[file_exists]], 60 | } 61 | cache_file.write_text(json.dumps(cache_data)) 62 | 63 | assert not verify_converted(Path(pack_name)) 64 | 65 | 66 | def test_verify_converted_v2_not_exists() -> None: 67 | pack_name = "Test_Pack_v2_not_exists" 68 | cache_file = CACHE_DIR / pack_name 69 | cache_data = { 70 | "version": 2, 71 | "webp_files": [file_exists, file_not_exists], 72 | "converted_files": [[file_exists], [file_exists_2]], 73 | } 74 | cache_file.write_text(json.dumps(cache_data)) 75 | 76 | assert not verify_converted(Path(pack_name)) 77 | 78 | 79 | def test_create_converted() -> None: 80 | pack_name = "Test_Pack" 81 | cache_data = {"example_key": "example_value"} 82 | 83 | create_converted(Path(pack_name), cache_data) 84 | 85 | cache_file = CACHE_DIR / pack_name 86 | assert cache_file.exists() 87 | assert json.loads(cache_file.read_text()) == cache_data 88 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sigstickers" 3 | version = "2025" 4 | description = "Download sticker packs from Signal" 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>=11.1.0", 24 | "signalstickers-client>=3.3.0", 25 | ] 26 | 27 | [project.urls] 28 | Homepage = "https://github.com/FHPythonUtils/SigStickers" 29 | Repository = "https://github.com/FHPythonUtils/SigStickers" 30 | Documentation = "https://github.com/FHPythonUtils/SigStickers/blob/master/README.md" 31 | 32 | [project.scripts] 33 | sigstickers = "sigstickers:cli" 34 | 35 | [dependency-groups] 36 | dev = [ 37 | "coverage>=7.6.12", 38 | "handsdown>=2.1.0", 39 | "pyright>=1.1.394", 40 | "pytest>=8.3.4", 41 | "pytest-asyncio>=0.25.3", 42 | "ruff>=0.9.6", 43 | "safety>=3.3.0", 44 | ] 45 | 46 | [tool.ruff] 47 | line-length = 100 48 | indent-width = 4 49 | target-version = "py38" 50 | 51 | [tool.ruff.lint] 52 | select = ["ALL"] 53 | ignore = [ 54 | "COM812", # enforce trailing comma 55 | "D2", # pydocstyle formatting 56 | "ISC001", 57 | "N", # pep8 naming 58 | "PLR09", # pylint refactor too many 59 | "TCH", # type check blocks 60 | "W191", # ignore this to allow tabs 61 | ] 62 | fixable = ["ALL"] 63 | 64 | [tool.ruff.lint.per-file-ignores] 65 | "**/{tests,docs,tools}/*" = ["D", "S101", "E402", "PLR2004"] 66 | 67 | [tool.ruff.lint.flake8-tidy-imports] 68 | ban-relative-imports = "all" # Disallow all relative imports. 69 | 70 | [tool.ruff.format] 71 | indent-style = "tab" 72 | docstring-code-format = true 73 | line-ending = "lf" 74 | 75 | [tool.pyright] 76 | venvPath = "." 77 | venv = ".venv" 78 | 79 | [tool.coverage.run] 80 | branch = true 81 | 82 | [tool.tox] 83 | legacy_tox_ini = """ 84 | [tox] 85 | env_list = 86 | py313 87 | py312 88 | py311 89 | py310 90 | py39 91 | 92 | [testenv] 93 | deps = 94 | pytest 95 | commands = pytest tests 96 | """ 97 | 98 | [build-system] 99 | requires = ["hatchling"] 100 | build-backend = "hatchling.build" 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | downloads/* 3 | uv.lock 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | -------------------------------------------------------------------------------- /documentation/reference/sigstickers/downloader.md: -------------------------------------------------------------------------------- 1 | # Downloader 2 | 3 | [Sigstickers Index](../README.md#sigstickers-index) / [Sigstickers](./index.md#sigstickers) / Downloader 4 | 5 | > Auto-generated documentation for [sigstickers.downloader](../../../sigstickers/downloader.py) module. 6 | 7 | - [Downloader](#downloader) 8 | - [convertWithPIL](#convertwithpil) 9 | - [convert_pack](#convert_pack) 10 | - [download_pack](#download_pack) 11 | - [save_sticker](#save_sticker) 12 | 13 | ## convertWithPIL 14 | 15 | [Show source in downloader.py:83](../../../sigstickers/downloader.py#L83) 16 | 17 | Convert a webp file to png and gif. 18 | 19 | #### Arguments 20 | 21 | - `input_path` *Path* - path to the input image/ sticker 22 | :param set[str] formats: set of formats 23 | 24 | #### Returns 25 | 26 | Type: *list[str]* 27 | paths (as strings) of converted files 28 | 29 | #### Signature 30 | 31 | ```python 32 | def convertWithPIL(input_path: Path) -> list[str]: ... 33 | ``` 34 | 35 | 36 | 37 | ## convert_pack 38 | 39 | [Show source in downloader.py:112](../../../sigstickers/downloader.py#L112) 40 | 41 | Convert the webp images into png and gif images. 42 | 43 | #### Arguments 44 | 45 | - `swd` *Path* - name of the directory to convert 46 | - `pack_name` *Path* - name of the sticker pack (for cache + logging) 47 | :param bool, optional no_cache: set to true to disable cache. Defaults to False. 48 | 49 | #### Signature 50 | 51 | ```python 52 | async def convert_pack(swd: Path, pack_name: Path, no_cache: bool = False) -> None: ... 53 | ``` 54 | 55 | 56 | 57 | ## download_pack 58 | 59 | [Show source in downloader.py:45](../../../sigstickers/downloader.py#L45) 60 | 61 | Download a sticker pack. 62 | 63 | #### Arguments 64 | 65 | - `pack_id` *Path* - pack_id from url param. eg b676ec334ee2f771cadff5d095971e8c 66 | - `pack_key` *Path* - pack_key from url param. eg 67 | c957a57000626a2dc3cb69bf0e79c91c6b196b74d4d6ca1cbb830d3ad0ad4e36 68 | :param Path, optional cwd: set the current working directory 69 | 70 | #### Returns 71 | 72 | Type: *tuple[Path, Path]* 73 | sticker working directory and pack title 74 | 75 | #### Signature 76 | 77 | ```python 78 | async def download_pack( 79 | pack_id: str, pack_key: str, cwd: Path = DEFAULT_CWD 80 | ) -> tuple[Path, Path]: ... 81 | ``` 82 | 83 | #### See also 84 | 85 | - [DEFAULT_CWD](#default_cwd) 86 | 87 | 88 | 89 | ## save_sticker 90 | 91 | [Show source in downloader.py:23](../../../sigstickers/downloader.py#L23) 92 | 93 | Save a sticker. 94 | 95 | #### Arguments 96 | 97 | - `sticker` *Sticker* - the sticker object 98 | - `path` *Path* - the path to write to 99 | 100 | #### Returns 101 | 102 | Type: *Path* 103 | the filepath the file was written to 104 | 105 | #### Signature 106 | 107 | ```python 108 | def save_sticker(sticker: Sticker, path: Path) -> Path: ... 109 | ``` -------------------------------------------------------------------------------- /sigstickers/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 | 11 | CACHE_DIR = Path(".cache") 12 | if not CACHE_DIR.exists(): 13 | CACHE_DIR.mkdir() 14 | 15 | 16 | def verify_converted(pack_name: Path) -> bool: 17 | """Verify the cache for a packName eg. "DonutTheDog". Uses the cache "version" 18 | to call the verify function for that version. 19 | 20 | :param Path pack_name: name of the sticker pack eg. "DonutTheDog" 21 | :return bool: if the converted cache has been verified 22 | 23 | """ 24 | cache = CACHE_DIR / pack_name 25 | if cache.exists(): 26 | data = json.loads(cache.read_text(encoding="utf-8")) 27 | verify_func = _get_verify_function(data.get("version", 1)) 28 | if verify_func(data): 29 | logger.info(f"-> Cache hit for {pack_name}!") 30 | return True 31 | logger.info(f"-> Cache miss for {pack_name}!") 32 | return False 33 | 34 | 35 | def _verify_converted_v1(data: dict[str, Any]) -> bool: 36 | """Verify the cache for a packName using cache data. 37 | 38 | :param dict[Path, Any] data: packName cache data to verify 39 | :return bool: if the converted cache has been verified 40 | 41 | """ 42 | swd = data.get("info", {}).get("swd") 43 | if swd: 44 | png_dir = Path(swd) / "png" 45 | return png_dir.exists() and data["converted"]["converted"] == data["converted"]["total"] 46 | return False 47 | 48 | 49 | def _verify_converted_v2(data: dict[str, Any]) -> bool: 50 | """Verify the cache for a packName using cache data. 51 | 52 | :param dict[Path, Any] data: packName cache data to verify 53 | :return bool: if the converted cache has been verified 54 | 55 | """ 56 | webp_files: list[str] = data.get("webp_files", []) 57 | converted_files: list[list[str]] = data.get("converted_files", []) 58 | 59 | if len(webp_files) != len(converted_files): 60 | return False 61 | 62 | for x in webp_files + [item for sublist in converted_files for item in sublist]: 63 | if not Path(x).is_file(): 64 | return False 65 | return True 66 | 67 | 68 | def create_converted(pack_name: Path, data: dict) -> None: 69 | """Write cache data to a file identified by packName. 70 | 71 | :param Path pack_name: name of the sticker pack eg. "DonutTheDog" 72 | :param dict data: packName cache data to write to cache 73 | 74 | """ 75 | cache = CACHE_DIR / pack_name 76 | cache.write_text(json.dumps(data), encoding="utf-8") 77 | 78 | 79 | def _get_verify_function(version: int) -> Callable[[dict[str, Any]], bool]: 80 | """Get the appropriate cache verification function based on version. 81 | 82 | :param int version: Cache version 83 | :return Callable[[dict[str, Any]], bool]: Cache verification function 84 | 85 | """ 86 | return { 87 | 1: _verify_converted_v1, 88 | 2: _verify_converted_v2, 89 | }.get(version, _verify_converted_v1) 90 | -------------------------------------------------------------------------------- /documentation/reference/sigstickers/caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | [Sigstickers Index](../README.md#sigstickers-index) / [Sigstickers](./index.md#sigstickers) / Caching 4 | 5 | > Auto-generated documentation for [sigstickers.caching](../../../sigstickers/caching.py) module. 6 | 7 | - [Caching](#caching) 8 | - [_get_verify_function](#_get_verify_function) 9 | - [_verify_converted_v1](#_verify_converted_v1) 10 | - [_verify_converted_v2](#_verify_converted_v2) 11 | - [create_converted](#create_converted) 12 | - [verify_converted](#verify_converted) 13 | 14 | ## _get_verify_function 15 | 16 | [Show source in caching.py:79](../../../sigstickers/caching.py#L79) 17 | 18 | Get the appropriate cache verification function based on version. 19 | 20 | #### Arguments 21 | 22 | - `version` *int* - Cache version 23 | 24 | #### Returns 25 | 26 | Type: *Callable[[dict[str, Any]], bool]* 27 | Cache verification function 28 | 29 | #### Signature 30 | 31 | ```python 32 | def _get_verify_function(version: int) -> Callable[[dict[str, Any]], bool]: ... 33 | ``` 34 | 35 | 36 | 37 | ## _verify_converted_v1 38 | 39 | [Show source in caching.py:35](../../../sigstickers/caching.py#L35) 40 | 41 | Verify the cache for a packName using cache data. 42 | 43 | :param dict[Path, Any] data: packName cache data to verify 44 | 45 | #### Returns 46 | 47 | Type: *bool* 48 | if the converted cache has been verified 49 | 50 | #### Signature 51 | 52 | ```python 53 | def _verify_converted_v1(data: dict[str, Any]) -> bool: ... 54 | ``` 55 | 56 | 57 | 58 | ## _verify_converted_v2 59 | 60 | [Show source in caching.py:49](../../../sigstickers/caching.py#L49) 61 | 62 | Verify the cache for a packName using cache data. 63 | 64 | :param dict[Path, Any] data: packName cache data to verify 65 | 66 | #### Returns 67 | 68 | Type: *bool* 69 | if the converted cache has been verified 70 | 71 | #### Signature 72 | 73 | ```python 74 | def _verify_converted_v2(data: dict[str, Any]) -> bool: ... 75 | ``` 76 | 77 | 78 | 79 | ## create_converted 80 | 81 | [Show source in caching.py:68](../../../sigstickers/caching.py#L68) 82 | 83 | Write cache data to a file identified by packName. 84 | 85 | #### Arguments 86 | 87 | - `pack_name` *Path* - name of the sticker pack eg. "DonutTheDog" 88 | - `data` *dict* - packName cache data to write to cache 89 | 90 | #### Signature 91 | 92 | ```python 93 | def create_converted(pack_name: Path, data: dict) -> None: ... 94 | ``` 95 | 96 | 97 | 98 | ## verify_converted 99 | 100 | [Show source in caching.py:16](../../../sigstickers/caching.py#L16) 101 | 102 | Verify the cache for a packName eg. "DonutTheDog". Uses the cache "version" 103 | to call the verify function for that version. 104 | 105 | #### Arguments 106 | 107 | - `pack_name` *Path* - name of the sticker pack eg. "DonutTheDog" 108 | 109 | #### Returns 110 | 111 | Type: *bool* 112 | if the converted cache has been verified 113 | 114 | #### Signature 115 | 116 | ```python 117 | def verify_converted(pack_name: Path) -> bool: ... 118 | ``` -------------------------------------------------------------------------------- /sigstickers/downloader.py: -------------------------------------------------------------------------------- 1 | """Sticker download and convert functions used by the module entry point.""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | import time 7 | import unicodedata 8 | from concurrent.futures import ThreadPoolExecutor, as_completed 9 | from pathlib import Path 10 | 11 | from emoji import demojize 12 | from loguru import logger 13 | from PIL import Image 14 | from signalstickers_client import StickersClient 15 | from signalstickers_client.models.sticker import Sticker 16 | 17 | from sigstickers.caching import create_converted, verify_converted 18 | 19 | UNKNOWN = "🤷‍♂️" 20 | DEFAULT_CWD = Path.cwd() 21 | 22 | 23 | def save_sticker(sticker: Sticker, path: Path) -> Path: 24 | """Save a sticker. 25 | 26 | :param Sticker sticker: the sticker object 27 | :param Path path: the path to write to 28 | 29 | :return Path: the filepath the file was written to 30 | 31 | """ 32 | sticker_emoji = sticker.emoji or UNKNOWN 33 | file_path = path / f"{sticker.id}+{demojize(sticker_emoji)[1:-1]}+{sticker_emoji}.webp" 34 | file_path.write_bytes(sticker.image_data or b"") 35 | return file_path 36 | 37 | 38 | def _sanitize_filename(filename: str) -> str: 39 | sanitized_filename = re.sub(r"[^\w\s.-]", "_", filename) 40 | sanitized_filename = re.sub(r"\s+", "_", sanitized_filename) 41 | sanitized_filename = sanitized_filename.strip(" .") 42 | return unicodedata.normalize("NFKD", sanitized_filename).encode("ascii", "ignore").decode() 43 | 44 | 45 | async def download_pack(pack_id: str, pack_key: str, cwd: Path = DEFAULT_CWD) -> tuple[Path, Path]: 46 | """Download a sticker pack. 47 | 48 | :param Path pack_id: pack_id from url param. eg b676ec334ee2f771cadff5d095971e8c 49 | :param Path pack_key: pack_key from url param. eg 50 | c957a57000626a2dc3cb69bf0e79c91c6b196b74d4d6ca1cbb830d3ad0ad4e36 51 | :param Path, optional cwd: set the current working directory 52 | 53 | :return tuple[Path, Path]: sticker working directory and pack title 54 | 55 | """ 56 | logger.info("=" * 60) 57 | start = time.time() 58 | async with StickersClient() as client: 59 | pack = await client.get_pack(pack_id, pack_key) 60 | pack_name = Path(_sanitize_filename(pack.title or UNKNOWN)) 61 | end = time.time() 62 | logger.info(f'Starting to scrape "{pack_name}" ...') 63 | logger.info(f"Time taken to scrape {pack.nb_stickers} stickers - {end - start:.3f}s") 64 | logger.info("") 65 | 66 | swd = cwd / "downloads" / pack_name 67 | swd.mkdir(parents=True, exist_ok=True) 68 | webp_dir = swd / "webp" 69 | webp_dir.mkdir(parents=True, exist_ok=True) 70 | 71 | # Save the stickers 72 | logger.info("-" * 60) 73 | logger.info(f'Starting download of "{pack_name}" into {swd}') 74 | with ThreadPoolExecutor(max_workers=4) as executor: 75 | for i in as_completed( 76 | [executor.submit(save_sticker, sticker, webp_dir) for sticker in pack.stickers] 77 | ): 78 | i.result() 79 | 80 | return swd, pack_name 81 | 82 | 83 | def convertWithPIL(input_path: Path) -> list[str]: 84 | """Convert a webp file to png and gif. 85 | 86 | :param Path input_path: path to the input image/ sticker 87 | :param set[str] formats: set of formats 88 | :return list[str]: paths (as strings) of converted files 89 | """ 90 | 91 | input_file = input_path.as_posix() 92 | 93 | img = Image.open(input_file) 94 | png_file = input_file.replace("webp", "png") 95 | gif_file = input_file.replace("webp", "gif") 96 | img.save(png_file) 97 | 98 | try: 99 | img.save( 100 | gif_file, 101 | version="GIF89a", 102 | disposal=2, 103 | save_all=True, 104 | loop=0, 105 | ) 106 | except (ValueError, TypeError): 107 | logger.error(f"Failed to save {input_file} as gif") 108 | return [png_file] 109 | return [png_file, gif_file] 110 | 111 | 112 | async def convert_pack( 113 | swd: Path, 114 | pack_name: Path, 115 | *, 116 | no_cache: bool = False, 117 | ) -> None: 118 | """Convert the webp images into png and gif images. 119 | 120 | :param Path swd: name of the directory to convert 121 | :param Path pack_name: name of the sticker pack (for cache + logging) 122 | :param bool, optional no_cache: set to true to disable cache. Defaults to False. 123 | """ 124 | logger.info("-" * 60) 125 | if not no_cache and verify_converted(pack_name): 126 | return 127 | 128 | (swd / "png").mkdir(parents=True, exist_ok=True) 129 | (swd / "gif").mkdir(parents=True, exist_ok=True) 130 | 131 | webp_files = [file for file in (swd / "webp").iterdir() if file.is_file()] 132 | 133 | # Convert stickers 134 | start = time.time() 135 | logger.info(f'Converting stickers for "{pack_name}"...') 136 | converted_files = [] 137 | with ThreadPoolExecutor(max_workers=4) as executor: 138 | for converted in as_completed( 139 | [executor.submit(convertWithPIL, sticker) for sticker in webp_files] 140 | ): 141 | converted_files.extend(converted.result()) 142 | end = time.time() 143 | logger.info( 144 | f"Time taken to convert {len(converted_files)}/{len(webp_files)} " 145 | f"stickers - {end - start:.3f}s" 146 | ) 147 | 148 | logger.info("") 149 | create_converted( 150 | pack_name, 151 | data={ 152 | "version": 2, 153 | "converted_files": converted_files, 154 | "webp_files": _files_to_str(webp_files), 155 | }, 156 | ) 157 | 158 | 159 | def _files_to_str(files: list[Path]) -> list[str]: 160 | return [x.absolute().as_posix() for x in files] 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub top language](https://img.shields.io/github/languages/top/FHPythonUtils/SigStickers.svg?style=for-the-badge&cacheSeconds=28800)](../../) 2 | [![Issues](https://img.shields.io/github/issues/FHPythonUtils/SigStickers.svg?style=for-the-badge&cacheSeconds=28800)](../../issues) 3 | [![License](https://img.shields.io/github/license/FHPythonUtils/SigStickers.svg?style=for-the-badge&cacheSeconds=28800)](/LICENSE.md) 4 | [![Commit activity](https://img.shields.io/github/commit-activity/m/FHPythonUtils/SigStickers.svg?style=for-the-badge&cacheSeconds=28800)](../../commits/master) 5 | [![Last commit](https://img.shields.io/github/last-commit/FHPythonUtils/SigStickers.svg?style=for-the-badge&cacheSeconds=28800)](../../commits/master) 6 | [![PyPI Downloads](https://img.shields.io/pypi/dm/sigstickers.svg?style=for-the-badge&cacheSeconds=28800)](https://pypistats.org/packages/sigstickers) 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%2Fsigstickers)](https://pepy.tech/project/sigstickers) 8 | [![PyPI Version](https://img.shields.io/pypi/v/sigstickers.svg?style=for-the-badge&cacheSeconds=28800)](https://pypi.org/project/sigstickers) 9 | 10 | 11 | # SigStickers - Signal Sticker Downloader 12 | 13 | Project Icon 14 | 15 | The `sigstickers` package provides functionality for downloading and converting sticker packs from https://signal.art/addstickers (find packs at https://www.sigstick.com/). Download stickers in WebP format, and convert them to PNG and GIF formats, with caching the converted stickers for faster retrieval. 16 | 17 | - [Key Features](#key-features) 18 | - [Using](#using) 19 | - [Formats](#formats) 20 | - [Documentation](#documentation) 21 | - [Install With PIP](#install-with-pip) 22 | - [Language information](#language-information) 23 | - [Built for](#built-for) 24 | - [Install Python on Windows](#install-python-on-windows) 25 | - [Chocolatey](#chocolatey) 26 | - [Windows - Python.org](#windows---pythonorg) 27 | - [Install Python on Linux](#install-python-on-linux) 28 | - [Apt](#apt) 29 | - [Dnf](#dnf) 30 | - [Install Python on MacOS](#install-python-on-macos) 31 | - [Homebrew](#homebrew) 32 | - [MacOS - Python.org](#macos---pythonorg) 33 | - [How to run](#how-to-run) 34 | - [Windows](#windows) 35 | - [Linux/ MacOS](#linux-macos) 36 | - [Building](#building) 37 | - [Testing](#testing) 38 | - [Download Project](#download-project) 39 | - [Clone](#clone) 40 | - [Using The Command Line](#using-the-command-line) 41 | - [Using GitHub Desktop](#using-github-desktop) 42 | - [Download Zip File](#download-zip-file) 43 | - [Community Files](#community-files) 44 | - [Licence](#licence) 45 | - [Changelog](#changelog) 46 | - [Code of Conduct](#code-of-conduct) 47 | - [Contributing](#contributing) 48 | - [Security](#security) 49 | - [Support](#support) 50 | - [Rationale](#rationale) 51 | 52 | ## Key Features 53 | 54 | 1. **Sticker Pack Downloading** from Signal from their https://signal.art/addstickers url 55 | 2. **Sticker Pack Conversion** from the WebP format to PNG and GIF formats, making them compatible with various platforms and applications. 56 | 3. **Caching Functionality** to store converted sticker images locally, reducing the need to re-convert them 57 | 4. **Asynchronous Processing** for downloading and converting sticker packs 58 | 59 | ## Using 60 | 61 | 1. Get the URL of the Signal sticker pack. In the form https://signal.art/addstickers (find packs at https://www.sigstick.com/) 62 | 63 | 2. Pass in multiple packs from the commandline with `-p/--pack` 64 | 65 | ```bash 66 | $ python -m sigstickers --help 67 | usage: Welcome to SigSticker, providing all of your sticker needs [-h] [-p PACK [PACK ...]] 68 | 69 | options: 70 | -h, --help show this help message and exit 71 | -p PACK [PACK ...], --pack PACK [PACK ...] 72 | Pass in a pack URL inline 73 | 74 | $ python -m sigstickers --pack 'https://signal.art/addstickers/#pack_id=b676ec334ee2f771cadff5d095971e8c&pack_key=c957a57000626a2dc3cb69bf0e79c91c6b196b74d4d6ca1cbb830d3ad0ad4e36' 75 | 76 | 2024-03-17 00:14:16.354 | INFO | sigstickers.downloader:download_pack:82 - ============================================================ 77 | 2024-03-17 00:14:16.805 | INFO | sigstickers.downloader:download_pack:88 - Starting to scrape "DonutTheDog" ... 78 | 2024-03-17 00:14:16.812 | INFO | sigstickers.downloader:download_pack:89 - Time taken to scrape 28 stickers - 0.999s 79 | 2024-03-17 00:14:16.813 | INFO | sigstickers.downloader:download_pack:90 - 80 | 2024-03-17 00:14:16.816 | INFO | sigstickers.downloader:download_pack:96 - ------------------------------------------------------------ 81 | 2024-03-17 00:14:16.820 | INFO | sigstickers.downloader:download_pack:97 - Starting download of "DonutTheDog" into ...\downloads\DonutTheDog 82 | 2024-03-17 00:14:16.894 | INFO | sigstickers.downloader:convert_pack:151 - ------------------------------------------------------------ 83 | 2024-03-17 00:14:16.897 | INFO | sigstickers.caching:verify_converted:35 - -> Cache miss for DonutTheDog! 84 | 2024-03-17 00:14:16.905 | INFO | sigstickers.downloader:convert_pack:163 - Converting stickers "DonutTheDog"... 85 | 2024-03-17 00:14:29.655 | INFO | sigstickers.downloader:convert_pack:171 - Time taken to convert 28/28 stickers - 12.749s 86 | 2024-03-17 00:14:29.656 | INFO | sigstickers.downloader:convert_pack:175 - 87 | ``` 88 | 89 | 3. OR. Enter the URL of the sticker pack 90 | 91 | ```bash 92 | $ python -m sigstickers 93 | Enter sticker_set URL (leave blank to stop): https://signal.art/addstickers/#pack_id=b676ec334ee2f771cadff5d095971e8c&pack_key=c957a57000626a2dc3cb69bf0e79c91c6b196b74d4d6ca1cbb830d3ad0ad4e36 94 | Enter sticker_set URL (leave blank to stop): 95 | 2024-03-17 00:18:25.528 | INFO | sigstickers.downloader:download_pack:82 - ============================================================ 96 | 2024-03-17 00:18:26.415 | INFO | sigstickers.downloader:download_pack:88 - Starting to scrape "DonutTheDog" ... 97 | 2024-03-17 00:18:26.417 | INFO | sigstickers.downloader:download_pack:89 - Time taken to scrape 28 stickers - 0.885s 98 | 2024-03-17 00:18:26.420 | INFO | sigstickers.downloader:download_pack:90 - 99 | 2024-03-17 00:18:26.426 | INFO | sigstickers.downloader:download_pack:96 - ------------------------------------------------------------ 100 | 2024-03-17 00:18:26.428 | INFO | sigstickers.downloader:download_pack:97 - Starting download of "DonutTheDog" into ...\downloads\DonutTheDog 101 | 2024-03-17 00:18:26.497 | INFO | sigstickers.downloader:convert_pack:151 - ------------------------------------------------------------ 102 | 2024-03-17 00:18:26.524 | INFO | sigstickers.caching:verify_converted:33 - -> Cache hit for DonutTheDog! 103 | ``` 104 | 105 | 4. Get the output in the `downloads` folder. 106 | 107 | ```powershell 108 | $ ls .\downloads\DonutTheDog\ 109 | 110 | Mode LastWriteTime Length Name 111 | ---- ------------- ------ ---- 112 | d----- 17/03/2024 00꞉14 gif 113 | d----- 17/03/2024 00꞉14 png 114 | d----- 17/03/2024 00꞉08 webp 115 | 116 | $ ls .\downloads\DonutTheDog\webp 117 | 118 | Mode LastWriteTime Length Name 119 | ---- ------------- ------ ---- 120 | -a---- 17/03/2024 00꞉18 285292 0+face_with_tears_of_joy+😂.webp 121 | -a---- 17/03/2024 00꞉18 271726 1+face_blowing_a_kiss+😘.webp 122 | -a---- 17/03/2024 00꞉18 306995 10+smiling_face_with_horns+😈.webp 123 | -a---- 17/03/2024 00꞉18 293578 11+partying_face+🥳.webp 124 | -a---- 17/03/2024 00꞉18 266627 12+angry_face+😠.webp 125 | ``` 126 | 127 | ## Formats 128 | 129 | | Format | Static | Animated | 130 | | ------ | ------ | -------- | 131 | | .gif | ✔$ | ✔$ | 132 | | .png | ✔ | + | 133 | | .webp | ✔ | ✔ | 134 | 135 | ```txt 136 | + The first frame of an animated image is saved as png 137 | $ Some images saved as gif do not render as expected 138 | ``` 139 | 140 | ## Documentation 141 | 142 | A high-level overview of how the documentation is organized organized will help you know 143 | where to look for certain things: 144 | 145 | 149 | - The [Technical Reference](/documentation/reference) documents APIs and other aspects of the 150 | machinery. This documentation describes how to use the classes and functions at a lower level 151 | and assume that you have a good high-level understanding of the software. 152 | 156 | 157 | ## Install With PIP 158 | 159 | ```python 160 | pip install sigstickers 161 | ``` 162 | 163 | Head to https://pypi.org/project/sigstickers/ for more info 164 | 165 | ## Language information 166 | 167 | ### Built for 168 | 169 | This program has been written for Python versions 3.8 - 3.11 and has been tested with both 3.8 and 170 | 3.11 171 | 172 | ## Install Python on Windows 173 | 174 | ### Chocolatey 175 | 176 | ```powershell 177 | choco install python 178 | ``` 179 | 180 | ### Windows - Python.org 181 | 182 | To install Python, go to https://www.python.org/downloads/windows/ and download the latest 183 | version. 184 | 185 | ## Install Python on Linux 186 | 187 | ### Apt 188 | 189 | ```bash 190 | sudo apt install python3.x 191 | ``` 192 | 193 | ### Dnf 194 | 195 | ```bash 196 | sudo dnf install python3.x 197 | ``` 198 | 199 | ## Install Python on MacOS 200 | 201 | ### Homebrew 202 | 203 | ```bash 204 | brew install python@3.x 205 | ``` 206 | 207 | ### MacOS - Python.org 208 | 209 | To install Python, go to https://www.python.org/downloads/macos/ and download the latest 210 | version. 211 | 212 | ## How to run 213 | 214 | ### Windows 215 | 216 | - Module 217 | `py -3.x -m [module]` or `[module]` (if module installs a script) 218 | 219 | - File 220 | `py -3.x [file]` or `./[file]` 221 | 222 | ### Linux/ MacOS 223 | 224 | - Module 225 | `python3.x -m [module]` or `[module]` (if module installs a script) 226 | 227 | - File 228 | `python3.x [file]` or `./[file]` 229 | 230 | ## Building 231 | 232 | This project uses https://github.com/FHPythonUtils/FHMake to automate most of the building. This 233 | command generates the documentation, updates the requirements.txt and builds the library artefacts 234 | 235 | Note the functionality provided by fhmake can be approximated by the following 236 | 237 | ```sh 238 | handsdown --cleanup -o documentation/reference 239 | poetry export -f requirements.txt --output requirements.txt 240 | poetry export -f requirements.txt --with dev --output requirements_optional.txt 241 | poetry build 242 | ``` 243 | 244 | `fhmake audit` can be run to perform additional checks 245 | 246 | ## Testing 247 | 248 | For testing with the version of python used by poetry use 249 | 250 | ```sh 251 | poetry run pytest 252 | ``` 253 | 254 | Alternatively use `tox` to run tests over python 3.8 - 3.11 255 | 256 | ```sh 257 | tox 258 | ``` 259 | 260 | ## Download Project 261 | 262 | ### Clone 263 | 264 | #### Using The Command Line 265 | 266 | 1. Press the Clone or download button in the top right 267 | 2. Copy the URL (link) 268 | 3. Open the command line and change directory to where you wish to 269 | clone to 270 | 4. Type 'git clone' followed by URL in step 2 271 | 272 | ```bash 273 | git clone https://github.com/FHPythonUtils/SigStickers 274 | ``` 275 | 276 | More information can be found at 277 | https://help.github.com/en/articles/cloning-a-repository 278 | 279 | #### Using GitHub Desktop 280 | 281 | 1. Press the Clone or download button in the top right 282 | 2. Click open in desktop 283 | 3. Choose the path for where you want and click Clone 284 | 285 | More information can be found at 286 | https://help.github.com/en/desktop/contributing-to-projects/cloning-a-repository-from-github-to-github-desktop 287 | 288 | ### Download Zip File 289 | 290 | 1. Download this GitHub repository 291 | 2. Extract the zip archive 292 | 3. Copy/ move to the desired location 293 | 294 | ## Community Files 295 | 296 | ### Licence 297 | 298 | MIT License 299 | Copyright (c) FredHappyface 300 | (See the [LICENSE](/LICENSE.md) for more information.) 301 | 302 | ### Changelog 303 | 304 | See the [Changelog](/CHANGELOG.md) for more information. 305 | 306 | ### Code of Conduct 307 | 308 | Online communities include people from many backgrounds. The *Project* 309 | contributors are committed to providing a friendly, safe and welcoming 310 | environment for all. Please see the 311 | [Code of Conduct](https://github.com/FHPythonUtils/.github/blob/master/CODE_OF_CONDUCT.md) 312 | for more information. 313 | 314 | ### Contributing 315 | 316 | Contributions are welcome, please see the 317 | [Contributing Guidelines](https://github.com/FHPythonUtils/.github/blob/master/CONTRIBUTING.md) 318 | for more information. 319 | 320 | ### Security 321 | 322 | Thank you for improving the security of the project, please see the 323 | [Security Policy](https://github.com/FHPythonUtils/.github/blob/master/SECURITY.md) 324 | for more information. 325 | 326 | ### Support 327 | 328 | Thank you for using this project, I hope it is of use to you. Please be aware that 329 | those involved with the project often do so for fun along with other commitments 330 | (such as work, family, etc). Please see the 331 | [Support Policy](https://github.com/FHPythonUtils/.github/blob/master/SUPPORT.md) 332 | for more information. 333 | 334 | ### Rationale 335 | 336 | The rationale acts as a guide to various processes regarding projects such as 337 | the versioning scheme and the programming styles used. Please see the 338 | [Rationale](https://github.com/FHPythonUtils/.github/blob/master/RATIONALE.md) 339 | for more information. 340 | --------------------------------------------------------------------------------