├── .dockerignore ├── .flake8 ├── .github ├── FUNDING.yml └── workflows │ ├── release-to-dockerhub.yml │ └── release-to-pypi.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── poetry.lock ├── pyproject.toml ├── scripts ├── format-imports.sh ├── format.sh ├── lint.sh └── start_api.py └── signal_cli_rest_api ├── __init__.py ├── api ├── __init__.py ├── block.py ├── groups.py ├── messages.py ├── profile.py └── register.py ├── config.py ├── main.py ├── schemas.py └── utils.py /.dockerignore: -------------------------------------------------------------------------------- 1 | signal-cli-config 2 | dist 3 | .vscode 4 | scripts 5 | .gitignore 6 | docker-compose.yml 7 | Dockerfile 8 | __pycache__ 9 | .pytest_cache -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache 4 | 5 | # compatibility with black 6 | ignore = E203 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://flattr.com/@SebastianNoelLuebke"] 2 | -------------------------------------------------------------------------------- /.github/workflows/release-to-dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | 11 | jobs: 12 | docker: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v2 18 | - 19 | name: Docker meta 20 | id: docker_meta 21 | uses: crazy-max/ghaction-docker-meta@v1 22 | with: 23 | images: sebastiannoelluebke/signal-cli-rest-api 24 | tag-semver: | 25 | {{version}} 26 | {{major}}.{{minor}} 27 | - 28 | name: Set up QEMU 29 | uses: docker/setup-qemu-action@v1 30 | - 31 | name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v1 33 | - 34 | name: Login to DockerHub 35 | if: github.event_name != 'pull_request' 36 | uses: docker/login-action@v1 37 | with: 38 | username: ${{ secrets.DOCKER_USERNAME }} 39 | password: ${{ secrets.DOCKER_PASSWORD }} 40 | - 41 | name: Build and push 42 | uses: docker/build-push-action@v2 43 | with: 44 | context: . 45 | file: ./Dockerfile 46 | platforms: linux/amd64 47 | push: ${{ github.event_name != 'pull_request' }} 48 | tags: ${{ steps.docker_meta.outputs.tags }} 49 | labels: ${{ steps.docker_meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/release-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Build and publish to pypi 12 | uses: JRubics/poetry-publish@v1 13 | with: 14 | python_version: '3.8' 15 | pypi_token: ${{ secrets.PYPI_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | signal-cli-config 2 | dist 3 | 4 | # ide 5 | .vscode 6 | 7 | # python 8 | __pycache__ 9 | .python-version 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | # Install java 4 | RUN set -eux; \ 5 | mkdir -p /usr/share/man/man1; \ 6 | apt-get update; \ 7 | apt-get install --no-install-recommends -y \ 8 | openjdk-11-jre-headless \ 9 | wget \ 10 | ; \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | # Download & Install signal-cli 14 | ENV SIGNAL_CLI_VERSION=0.8.1 15 | RUN cd /tmp/ \ 16 | && wget https://github.com/AsamK/signal-cli/releases/download/v"${SIGNAL_CLI_VERSION}"/signal-cli-"${SIGNAL_CLI_VERSION}".tar.gz \ 17 | && tar xf signal-cli-"${SIGNAL_CLI_VERSION}".tar.gz -C /opt \ 18 | && ln -s /opt/signal-cli-"${SIGNAL_CLI_VERSION}"/bin/signal-cli /usr/bin/si\ 19 | gnal-cli 20 | 21 | # Copy poetry.lock* in case it doesn't exist in the repo 22 | COPY ./pyproject.toml ./poetry.lock* ./ 23 | 24 | # Install Poetry & disable virtualenv creation 25 | RUN pip install --no-cache poetry && \ 26 | poetry config virtualenvs.create false 27 | 28 | RUN poetry install --no-root --no-dev && \ 29 | rm -rf ~/.cache/{pip,pypoetry} 30 | 31 | # Copy app 32 | COPY ./signal_cli_rest_api/ signal_cli_rest_api/ 33 | 34 | # Prepare mount point for signal-cli 35 | RUN mkdir -p $HOME/.config/signal-cli 36 | 37 | EXPOSE 8000 38 | 39 | CMD ["uvicorn", "signal_cli_rest_api.main:app", "--host", "0.0.0.0"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sebastian Noel Lübke 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | As I switched from Signal to Matrix this project isnt maintained anymore. If you still need it i recomment using this fork https://github.com/kahrpatrick/signal-cli-rest-api 2 | 3 | 4 | 5 | 6 | # signal-cli-rest-api 7 | signal-cli-rest-api is a wrapper around [signal-cli](https://github.com/AsamK/signal-cli) and allows you to interact with it through http requests. 8 | 9 | ## Features 10 | * register/verify/unregister a number 11 | * send messages to multiple users/a group with one or multiple attachments 12 | * receive messages (with attachments) 13 | * block/unblock users and groups 14 | * link to existing device 15 | * list/create/update/leave groups 16 | * update profile (name/avatar) 17 | 18 | ## To-Do 19 | * integrate dbus daemon for faster sending 20 | * authentication 21 | 22 | ## Installation 23 | 24 | ### pip 25 | 26 | If you install signal-cli-rest-api through pip you need to manually install [signal-cli](https://github.com/AsamK/signal-cli) on your system. 27 | 28 | ```console 29 | # by default the app will look for the signal config files in ~/.local/share/signal-cli 30 | # you can change the directory by setting the SIGNAL_CONFIG_PATH env var to the desired path 31 | # e.g. export SIGNAL_CONFIG_PATH=/opt/signal 32 | pip install signal-cli-rest-api 33 | uvicorn signal_cli_rest_api.main:app --host 0.0.0.0 --port 8000 34 | ``` 35 | 36 | ### Docker 37 | 38 | ```console 39 | export SIGNAL_DATA_DIR=~/signal/ 40 | docker run --name signal --restart unless-stopped -p 8000:8000 -v $SIGNAL_DATA_DIR:/root/.local/share/signal-cli sebastiannoelluebke/signal-cli-rest-api 41 | ``` 42 | 43 | ### docker-compose 44 | ```console 45 | git clone https://github.com/SebastianLuebke/signal-cli-rest-api.git 46 | cd signal-cli-rest-api 47 | # docker-compose build 48 | docker-compose up -d 49 | ``` 50 | 51 | ## Security Notice 52 | signal-cli-rest-api doesn't have any authentication for now. Everyone who knows the service address+port and the number is able to get your messages and send messages. So only use it a trusted environment and block external access. 53 | 54 | ## Interactive Documentation 55 | 56 | After installing signal-cli-rest-api start it and open the following page [http://localhost:8000/docs](http://localhost:8000/docs) 57 | 58 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | signal: 4 | image: sebastiannoelluebke/signal-cli-rest-api 5 | container_name: signal-cli-rest-api 6 | build: . 7 | restart: unless-stopped 8 | ports: 9 | - "127.0.0.1:8000:8000" 10 | volumes: 11 | - "$HOME/.local/share/signal-cli/:/root/.local/share/signal-cli/" 12 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiofiles" 3 | version = "0.6.0" 4 | description = "File support for asyncio." 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "appdirs" 11 | version = "1.4.4" 12 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 13 | category = "dev" 14 | optional = false 15 | python-versions = "*" 16 | 17 | [[package]] 18 | name = "astroid" 19 | version = "2.5.2" 20 | description = "An abstract syntax tree for Python with inference support." 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=3.6" 24 | 25 | [package.dependencies] 26 | lazy-object-proxy = ">=1.4.0" 27 | wrapt = ">=1.11,<1.13" 28 | 29 | [[package]] 30 | name = "atomicwrites" 31 | version = "1.4.0" 32 | description = "Atomic file writes." 33 | category = "dev" 34 | optional = false 35 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 36 | 37 | [[package]] 38 | name = "attrs" 39 | version = "20.3.0" 40 | description = "Classes Without Boilerplate" 41 | category = "dev" 42 | optional = false 43 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 44 | 45 | [package.extras] 46 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 47 | docs = ["furo", "sphinx", "zope.interface"] 48 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 49 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 50 | 51 | [[package]] 52 | name = "autoflake" 53 | version = "1.4" 54 | description = "Removes unused imports and unused variables" 55 | category = "dev" 56 | optional = false 57 | python-versions = "*" 58 | 59 | [package.dependencies] 60 | pyflakes = ">=1.1.0" 61 | 62 | [[package]] 63 | name = "black" 64 | version = "20.8b1" 65 | description = "The uncompromising code formatter." 66 | category = "dev" 67 | optional = false 68 | python-versions = ">=3.6" 69 | 70 | [package.dependencies] 71 | appdirs = "*" 72 | click = ">=7.1.2" 73 | mypy-extensions = ">=0.4.3" 74 | pathspec = ">=0.6,<1" 75 | regex = ">=2020.1.8" 76 | toml = ">=0.10.1" 77 | typed-ast = ">=1.4.0" 78 | typing-extensions = ">=3.7.4" 79 | 80 | [package.extras] 81 | colorama = ["colorama (>=0.4.3)"] 82 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 83 | 84 | [[package]] 85 | name = "certifi" 86 | version = "2020.12.5" 87 | description = "Python package for providing Mozilla's CA Bundle." 88 | category = "main" 89 | optional = false 90 | python-versions = "*" 91 | 92 | [[package]] 93 | name = "click" 94 | version = "7.1.2" 95 | description = "Composable command line interface toolkit" 96 | category = "main" 97 | optional = false 98 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 99 | 100 | [[package]] 101 | name = "colorama" 102 | version = "0.4.4" 103 | description = "Cross-platform colored terminal text." 104 | category = "dev" 105 | optional = false 106 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 107 | 108 | [[package]] 109 | name = "ecdsa" 110 | version = "0.14.1" 111 | description = "ECDSA cryptographic signature library (pure python)" 112 | category = "main" 113 | optional = false 114 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 115 | 116 | [package.dependencies] 117 | six = "*" 118 | 119 | [[package]] 120 | name = "fastapi" 121 | version = "0.58.1" 122 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 123 | category = "main" 124 | optional = false 125 | python-versions = ">=3.6" 126 | 127 | [package.dependencies] 128 | pydantic = ">=0.32.2,<2.0.0" 129 | starlette = "0.13.4" 130 | 131 | [package.extras] 132 | all = ["requests", "aiofiles", "jinja2", "python-multipart", "itsdangerous", "pyyaml", "graphene", "ujson", "orjson", "email-validator", "uvicorn", "async-exit-stack", "async-generator"] 133 | dev = ["pyjwt", "passlib", "autoflake", "flake8", "uvicorn", "graphene"] 134 | doc = ["mkdocs", "mkdocs-material", "markdown-include", "typer", "typer-cli", "pyyaml"] 135 | test = ["pytest (5.4.3)", "pytest-cov (2.10.0)", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "peewee", "databases", "orjson", "async-exit-stack", "async-generator", "python-multipart", "aiofiles", "flask"] 136 | 137 | [[package]] 138 | name = "flake8" 139 | version = "3.9.0" 140 | description = "the modular source code checker: pep8 pyflakes and co" 141 | category = "dev" 142 | optional = false 143 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 144 | 145 | [package.dependencies] 146 | mccabe = ">=0.6.0,<0.7.0" 147 | pycodestyle = ">=2.7.0,<2.8.0" 148 | pyflakes = ">=2.3.0,<2.4.0" 149 | 150 | [[package]] 151 | name = "h11" 152 | version = "0.9.0" 153 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 154 | category = "main" 155 | optional = false 156 | python-versions = "*" 157 | 158 | [[package]] 159 | name = "httpcore" 160 | version = "0.12.3" 161 | description = "A minimal low-level HTTP client." 162 | category = "main" 163 | optional = false 164 | python-versions = ">=3.6" 165 | 166 | [package.dependencies] 167 | h11 = "<1.0.0" 168 | sniffio = ">=1.0.0,<2.0.0" 169 | 170 | [package.extras] 171 | http2 = ["h2 (>=3,<5)"] 172 | 173 | [[package]] 174 | name = "httptools" 175 | version = "0.1.1" 176 | description = "A collection of framework independent HTTP protocol utils." 177 | category = "main" 178 | optional = false 179 | python-versions = "*" 180 | 181 | [package.extras] 182 | test = ["Cython (0.29.14)"] 183 | 184 | [[package]] 185 | name = "httpx" 186 | version = "0.16.1" 187 | description = "The next generation HTTP client." 188 | category = "main" 189 | optional = false 190 | python-versions = ">=3.6" 191 | 192 | [package.dependencies] 193 | certifi = "*" 194 | httpcore = ">=0.12.0,<0.13.0" 195 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 196 | sniffio = "*" 197 | 198 | [package.extras] 199 | brotli = ["brotlipy (>=0.7.0,<0.8.0)"] 200 | http2 = ["h2 (>=3.0.0,<4.0.0)"] 201 | 202 | [[package]] 203 | name = "idna" 204 | version = "2.10" 205 | description = "Internationalized Domain Names in Applications (IDNA)" 206 | category = "main" 207 | optional = false 208 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 209 | 210 | [[package]] 211 | name = "isort" 212 | version = "5.8.0" 213 | description = "A Python utility / library to sort Python imports." 214 | category = "dev" 215 | optional = false 216 | python-versions = ">=3.6,<4.0" 217 | 218 | [package.extras] 219 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 220 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 221 | colors = ["colorama (>=0.4.3,<0.5.0)"] 222 | 223 | [[package]] 224 | name = "lazy-object-proxy" 225 | version = "1.6.0" 226 | description = "A fast and thorough lazy object proxy." 227 | category = "dev" 228 | optional = false 229 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 230 | 231 | [[package]] 232 | name = "mccabe" 233 | version = "0.6.1" 234 | description = "McCabe checker, plugin for flake8" 235 | category = "dev" 236 | optional = false 237 | python-versions = "*" 238 | 239 | [[package]] 240 | name = "more-itertools" 241 | version = "8.7.0" 242 | description = "More routines for operating on iterables, beyond itertools" 243 | category = "dev" 244 | optional = false 245 | python-versions = ">=3.5" 246 | 247 | [[package]] 248 | name = "mypy-extensions" 249 | version = "0.4.3" 250 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 251 | category = "dev" 252 | optional = false 253 | python-versions = "*" 254 | 255 | [[package]] 256 | name = "packaging" 257 | version = "20.9" 258 | description = "Core utilities for Python packages" 259 | category = "dev" 260 | optional = false 261 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 262 | 263 | [package.dependencies] 264 | pyparsing = ">=2.0.2" 265 | 266 | [[package]] 267 | name = "pathspec" 268 | version = "0.8.1" 269 | description = "Utility library for gitignore style pattern matching of file paths." 270 | category = "dev" 271 | optional = false 272 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 273 | 274 | [[package]] 275 | name = "pluggy" 276 | version = "0.13.1" 277 | description = "plugin and hook calling mechanisms for python" 278 | category = "dev" 279 | optional = false 280 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 281 | 282 | [package.extras] 283 | dev = ["pre-commit", "tox"] 284 | 285 | [[package]] 286 | name = "py" 287 | version = "1.10.0" 288 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 289 | category = "dev" 290 | optional = false 291 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 292 | 293 | [[package]] 294 | name = "pyasn1" 295 | version = "0.4.8" 296 | description = "ASN.1 types and codecs" 297 | category = "main" 298 | optional = false 299 | python-versions = "*" 300 | 301 | [[package]] 302 | name = "pycodestyle" 303 | version = "2.7.0" 304 | description = "Python style guide checker" 305 | category = "dev" 306 | optional = false 307 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 308 | 309 | [[package]] 310 | name = "pydantic" 311 | version = "1.8.1" 312 | description = "Data validation and settings management using python 3.6 type hinting" 313 | category = "main" 314 | optional = false 315 | python-versions = ">=3.6.1" 316 | 317 | [package.dependencies] 318 | typing-extensions = ">=3.7.4.3" 319 | 320 | [package.extras] 321 | dotenv = ["python-dotenv (>=0.10.4)"] 322 | email = ["email-validator (>=1.0.3)"] 323 | 324 | [[package]] 325 | name = "pyflakes" 326 | version = "2.3.1" 327 | description = "passive checker of Python programs" 328 | category = "dev" 329 | optional = false 330 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 331 | 332 | [[package]] 333 | name = "pylint" 334 | version = "2.7.4" 335 | description = "python code static checker" 336 | category = "dev" 337 | optional = false 338 | python-versions = "~=3.6" 339 | 340 | [package.dependencies] 341 | astroid = ">=2.5.2,<2.7" 342 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 343 | isort = ">=4.2.5,<6" 344 | mccabe = ">=0.6,<0.7" 345 | toml = ">=0.7.1" 346 | 347 | [package.extras] 348 | docs = ["sphinx (3.5.1)", "python-docs-theme (2020.12)"] 349 | 350 | [[package]] 351 | name = "pyparsing" 352 | version = "2.4.7" 353 | description = "Python parsing module" 354 | category = "dev" 355 | optional = false 356 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 357 | 358 | [[package]] 359 | name = "pypng" 360 | version = "0.0.20" 361 | description = "Pure Python PNG image encoder/decoder" 362 | category = "main" 363 | optional = false 364 | python-versions = "*" 365 | 366 | [[package]] 367 | name = "pyqrcode" 368 | version = "1.2.1" 369 | description = "A QR code generator written purely in Python with SVG, EPS, PNG and terminal output." 370 | category = "main" 371 | optional = false 372 | python-versions = "*" 373 | 374 | [package.extras] 375 | PNG = ["pypng (>=0.0.13)"] 376 | 377 | [[package]] 378 | name = "pytest" 379 | version = "5.4.3" 380 | description = "pytest: simple powerful testing with Python" 381 | category = "dev" 382 | optional = false 383 | python-versions = ">=3.5" 384 | 385 | [package.dependencies] 386 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 387 | attrs = ">=17.4.0" 388 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 389 | more-itertools = ">=4.0.0" 390 | packaging = "*" 391 | pluggy = ">=0.12,<1.0" 392 | py = ">=1.5.0" 393 | wcwidth = "*" 394 | 395 | [package.extras] 396 | checkqa-mypy = ["mypy (v0.761)"] 397 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 398 | 399 | [[package]] 400 | name = "python-jose" 401 | version = "3.2.0" 402 | description = "JOSE implementation in Python" 403 | category = "main" 404 | optional = false 405 | python-versions = "*" 406 | 407 | [package.dependencies] 408 | ecdsa = "<0.15" 409 | pyasn1 = "*" 410 | rsa = "*" 411 | six = "<2.0" 412 | 413 | [package.extras] 414 | cryptography = ["cryptography"] 415 | pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] 416 | pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] 417 | 418 | [[package]] 419 | name = "regex" 420 | version = "2021.3.17" 421 | description = "Alternative regular expression module, to replace re." 422 | category = "dev" 423 | optional = false 424 | python-versions = "*" 425 | 426 | [[package]] 427 | name = "rfc3986" 428 | version = "1.4.0" 429 | description = "Validating URI References per RFC 3986" 430 | category = "main" 431 | optional = false 432 | python-versions = "*" 433 | 434 | [package.dependencies] 435 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 436 | 437 | [package.extras] 438 | idna2008 = ["idna"] 439 | 440 | [[package]] 441 | name = "rsa" 442 | version = "4.7.2" 443 | description = "Pure-Python RSA implementation" 444 | category = "main" 445 | optional = false 446 | python-versions = ">=3.5, <4" 447 | 448 | [package.dependencies] 449 | pyasn1 = ">=0.1.3" 450 | 451 | [[package]] 452 | name = "six" 453 | version = "1.15.0" 454 | description = "Python 2 and 3 compatibility utilities" 455 | category = "main" 456 | optional = false 457 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 458 | 459 | [[package]] 460 | name = "sniffio" 461 | version = "1.2.0" 462 | description = "Sniff out which async library your code is running under" 463 | category = "main" 464 | optional = false 465 | python-versions = ">=3.5" 466 | 467 | [[package]] 468 | name = "starlette" 469 | version = "0.13.4" 470 | description = "The little ASGI library that shines." 471 | category = "main" 472 | optional = false 473 | python-versions = ">=3.6" 474 | 475 | [package.extras] 476 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 477 | 478 | [[package]] 479 | name = "toml" 480 | version = "0.10.2" 481 | description = "Python Library for Tom's Obvious, Minimal Language" 482 | category = "dev" 483 | optional = false 484 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 485 | 486 | [[package]] 487 | name = "typed-ast" 488 | version = "1.4.2" 489 | description = "a fork of Python 2 and 3 ast modules with type comment support" 490 | category = "dev" 491 | optional = false 492 | python-versions = "*" 493 | 494 | [[package]] 495 | name = "typing-extensions" 496 | version = "3.7.4.3" 497 | description = "Backported and Experimental Type Hints for Python 3.5+" 498 | category = "main" 499 | optional = false 500 | python-versions = "*" 501 | 502 | [[package]] 503 | name = "uvicorn" 504 | version = "0.11.8" 505 | description = "The lightning-fast ASGI server." 506 | category = "main" 507 | optional = false 508 | python-versions = "*" 509 | 510 | [package.dependencies] 511 | click = ">=7.0.0,<8.0.0" 512 | h11 = ">=0.8,<0.10" 513 | httptools = {version = ">=0.1.0,<0.2.0", markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""} 514 | uvloop = {version = ">=0.14.0", markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""} 515 | websockets = ">=8.0.0,<9.0.0" 516 | 517 | [package.extras] 518 | watchgodreload = ["watchgod (>=0.6,<0.7)"] 519 | 520 | [[package]] 521 | name = "uvloop" 522 | version = "0.15.2" 523 | description = "Fast implementation of asyncio event loop on top of libuv" 524 | category = "main" 525 | optional = false 526 | python-versions = ">=3.7" 527 | 528 | [package.extras] 529 | dev = ["Cython (>=0.29.20,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)", "aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] 530 | docs = ["Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)"] 531 | test = ["aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] 532 | 533 | [[package]] 534 | name = "wcwidth" 535 | version = "0.2.5" 536 | description = "Measures the displayed width of unicode strings in a terminal" 537 | category = "dev" 538 | optional = false 539 | python-versions = "*" 540 | 541 | [[package]] 542 | name = "websockets" 543 | version = "8.1" 544 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 545 | category = "main" 546 | optional = false 547 | python-versions = ">=3.6.1" 548 | 549 | [[package]] 550 | name = "wrapt" 551 | version = "1.12.1" 552 | description = "Module for decorators, wrappers and monkey patching." 553 | category = "dev" 554 | optional = false 555 | python-versions = "*" 556 | 557 | [metadata] 558 | lock-version = "1.1" 559 | python-versions = "~3.8" 560 | content-hash = "3b163ba492f2e9ea8d1536381cf913b9d8f2318c3babdcaebb90852b0bd18fd5" 561 | 562 | [metadata.files] 563 | aiofiles = [ 564 | {file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"}, 565 | {file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"}, 566 | ] 567 | appdirs = [ 568 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 569 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 570 | ] 571 | astroid = [ 572 | {file = "astroid-2.5.2-py3-none-any.whl", hash = "sha256:cd80bf957c49765dce6d92c43163ff9d2abc43132ce64d4b1b47717c6d2522df"}, 573 | {file = "astroid-2.5.2.tar.gz", hash = "sha256:6b0ed1af831570e500e2437625979eaa3b36011f66ddfc4ce930128610258ca9"}, 574 | ] 575 | atomicwrites = [ 576 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 577 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 578 | ] 579 | attrs = [ 580 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 581 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 582 | ] 583 | autoflake = [ 584 | {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, 585 | ] 586 | black = [ 587 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 588 | ] 589 | certifi = [ 590 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 591 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 592 | ] 593 | click = [ 594 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 595 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 596 | ] 597 | colorama = [ 598 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 599 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 600 | ] 601 | ecdsa = [ 602 | {file = "ecdsa-0.14.1-py2.py3-none-any.whl", hash = "sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe"}, 603 | {file = "ecdsa-0.14.1.tar.gz", hash = "sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e"}, 604 | ] 605 | fastapi = [ 606 | {file = "fastapi-0.58.1-py3-none-any.whl", hash = "sha256:d7499761d5ca901cdf5b6b73018d14729593f8ab1ea22d241f82fa574fc406ad"}, 607 | {file = "fastapi-0.58.1.tar.gz", hash = "sha256:92e59b77eef7d6eaa80b16d275adda06b5f33b12d777e3fc5521b2f7f4718e13"}, 608 | ] 609 | flake8 = [ 610 | {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"}, 611 | {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"}, 612 | ] 613 | h11 = [ 614 | {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, 615 | {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, 616 | ] 617 | httpcore = [ 618 | {file = "httpcore-0.12.3-py3-none-any.whl", hash = "sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"}, 619 | {file = "httpcore-0.12.3.tar.gz", hash = "sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9"}, 620 | ] 621 | httptools = [ 622 | {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, 623 | {file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"}, 624 | {file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"}, 625 | {file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"}, 626 | {file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"}, 627 | {file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"}, 628 | {file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"}, 629 | {file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"}, 630 | {file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"}, 631 | {file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"}, 632 | {file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"}, 633 | {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, 634 | ] 635 | httpx = [ 636 | {file = "httpx-0.16.1-py3-none-any.whl", hash = "sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"}, 637 | {file = "httpx-0.16.1.tar.gz", hash = "sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537"}, 638 | ] 639 | idna = [ 640 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 641 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 642 | ] 643 | isort = [ 644 | {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, 645 | {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, 646 | ] 647 | lazy-object-proxy = [ 648 | {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, 649 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, 650 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, 651 | {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, 652 | {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, 653 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, 654 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, 655 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, 656 | {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, 657 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, 658 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, 659 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, 660 | {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, 661 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, 662 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, 663 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, 664 | {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, 665 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, 666 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, 667 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, 668 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, 669 | {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, 670 | ] 671 | mccabe = [ 672 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 673 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 674 | ] 675 | more-itertools = [ 676 | {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, 677 | {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, 678 | ] 679 | mypy-extensions = [ 680 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 681 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 682 | ] 683 | packaging = [ 684 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 685 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 686 | ] 687 | pathspec = [ 688 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 689 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 690 | ] 691 | pluggy = [ 692 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 693 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 694 | ] 695 | py = [ 696 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 697 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 698 | ] 699 | pyasn1 = [ 700 | {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, 701 | {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, 702 | {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, 703 | {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, 704 | {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, 705 | {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, 706 | {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, 707 | {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, 708 | {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, 709 | {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, 710 | {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, 711 | {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, 712 | {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, 713 | ] 714 | pycodestyle = [ 715 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 716 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 717 | ] 718 | pydantic = [ 719 | {file = "pydantic-1.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0c40162796fc8d0aa744875b60e4dc36834db9f2a25dbf9ba9664b1915a23850"}, 720 | {file = "pydantic-1.8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fff29fe54ec419338c522b908154a2efabeee4f483e48990f87e189661f31ce3"}, 721 | {file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:fbfb608febde1afd4743c6822c19060a8dbdd3eb30f98e36061ba4973308059e"}, 722 | {file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:eb8ccf12295113ce0de38f80b25f736d62f0a8d87c6b88aca645f168f9c78771"}, 723 | {file = "pydantic-1.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:20d42f1be7c7acc352b3d09b0cf505a9fab9deb93125061b376fbe1f06a5459f"}, 724 | {file = "pydantic-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dde4ca368e82791de97c2ec019681ffb437728090c0ff0c3852708cf923e0c7d"}, 725 | {file = "pydantic-1.8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3bbd023c981cbe26e6e21c8d2ce78485f85c2e77f7bab5ec15b7d2a1f491918f"}, 726 | {file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:830ef1a148012b640186bf4d9789a206c56071ff38f2460a32ae67ca21880eb8"}, 727 | {file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:fb77f7a7e111db1832ae3f8f44203691e15b1fa7e5a1cb9691d4e2659aee41c4"}, 728 | {file = "pydantic-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3bcb9d7e1f9849a6bdbd027aabb3a06414abd6068cb3b21c49427956cce5038a"}, 729 | {file = "pydantic-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2287ebff0018eec3cc69b1d09d4b7cebf277726fa1bd96b45806283c1d808683"}, 730 | {file = "pydantic-1.8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4bbc47cf7925c86a345d03b07086696ed916c7663cb76aa409edaa54546e53e2"}, 731 | {file = "pydantic-1.8.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6388ef4ef1435364c8cc9a8192238aed030595e873d8462447ccef2e17387125"}, 732 | {file = "pydantic-1.8.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:dd4888b300769ecec194ca8f2699415f5f7760365ddbe243d4fd6581485fa5f0"}, 733 | {file = "pydantic-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:8fbb677e4e89c8ab3d450df7b1d9caed23f254072e8597c33279460eeae59b99"}, 734 | {file = "pydantic-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2f2736d9a996b976cfdfe52455ad27462308c9d3d0ae21a2aa8b4cd1a78f47b9"}, 735 | {file = "pydantic-1.8.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3114d74329873af0a0e8004627f5389f3bb27f956b965ddd3e355fe984a1789c"}, 736 | {file = "pydantic-1.8.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:258576f2d997ee4573469633592e8b99aa13bda182fcc28e875f866016c8e07e"}, 737 | {file = "pydantic-1.8.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c17a0b35c854049e67c68b48d55e026c84f35593c66d69b278b8b49e2484346f"}, 738 | {file = "pydantic-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8bc082afef97c5fd3903d05c6f7bb3a6af9fc18631b4cc9fedeb4720efb0c58"}, 739 | {file = "pydantic-1.8.1-py3-none-any.whl", hash = "sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520"}, 740 | {file = "pydantic-1.8.1.tar.gz", hash = "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3"}, 741 | ] 742 | pyflakes = [ 743 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 744 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 745 | ] 746 | pylint = [ 747 | {file = "pylint-2.7.4-py3-none-any.whl", hash = "sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a"}, 748 | {file = "pylint-2.7.4.tar.gz", hash = "sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee"}, 749 | ] 750 | pyparsing = [ 751 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 752 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 753 | ] 754 | pypng = [ 755 | {file = "pypng-0.0.20.tar.gz", hash = "sha256:1032833440c91bafee38a42c38c02d00431b24c42927feb3e63b104d8550170b"}, 756 | ] 757 | pyqrcode = [ 758 | {file = "PyQRCode-1.2.1.tar.gz", hash = "sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5"}, 759 | {file = "PyQRCode-1.2.1.zip", hash = "sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6"}, 760 | ] 761 | pytest = [ 762 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 763 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 764 | ] 765 | python-jose = [ 766 | {file = "python-jose-3.2.0.tar.gz", hash = "sha256:4e4192402e100b5fb09de5a8ea6bcc39c36ad4526341c123d401e2561720335b"}, 767 | {file = "python_jose-3.2.0-py2.py3-none-any.whl", hash = "sha256:67d7dfff599df676b04a996520d9be90d6cdb7e6dd10b4c7cacc0c3e2e92f2be"}, 768 | ] 769 | regex = [ 770 | {file = "regex-2021.3.17-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b97ec5d299c10d96617cc851b2e0f81ba5d9d6248413cd374ef7f3a8871ee4a6"}, 771 | {file = "regex-2021.3.17-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cb4ee827857a5ad9b8ae34d3c8cc51151cb4a3fe082c12ec20ec73e63cc7c6f0"}, 772 | {file = "regex-2021.3.17-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:633497504e2a485a70a3268d4fc403fe3063a50a50eed1039083e9471ad0101c"}, 773 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a59a2ee329b3de764b21495d78c92ab00b4ea79acef0f7ae8c1067f773570afa"}, 774 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14"}, 775 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4651f839dbde0816798e698626af6a2469eee6d9964824bb5386091255a1694f"}, 776 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:39c44532d0e4f1639a89e52355b949573e1e2c5116106a395642cbbae0ff9bcd"}, 777 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3d9a7e215e02bd7646a91fb8bcba30bc55fd42a719d6b35cf80e5bae31d9134e"}, 778 | {file = "regex-2021.3.17-cp36-cp36m-win32.whl", hash = "sha256:159fac1a4731409c830d32913f13f68346d6b8e39650ed5d704a9ce2f9ef9cb3"}, 779 | {file = "regex-2021.3.17-cp36-cp36m-win_amd64.whl", hash = "sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5"}, 780 | {file = "regex-2021.3.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9d8d286c53fe0cbc6d20bf3d583cabcd1499d89034524e3b94c93a5ab85ca90"}, 781 | {file = "regex-2021.3.17-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:201e2619a77b21a7780580ab7b5ce43835e242d3e20fef50f66a8df0542e437f"}, 782 | {file = "regex-2021.3.17-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d47d359545b0ccad29d572ecd52c9da945de7cd6cf9c0cfcb0269f76d3555689"}, 783 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ea2f41445852c660ba7c3ebf7d70b3779b20d9ca8ba54485a17740db49f46932"}, 784 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:486a5f8e11e1f5bbfcad87f7c7745eb14796642323e7e1829a331f87a713daaa"}, 785 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:18e25e0afe1cf0f62781a150c1454b2113785401ba285c745acf10c8ca8917df"}, 786 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a2ee026f4156789df8644d23ef423e6194fad0bc53575534101bb1de5d67e8ce"}, 787 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c0788010a93ace8a174d73e7c6c9d3e6e3b7ad99a453c8ee8c975ddd9965643"}, 788 | {file = "regex-2021.3.17-cp37-cp37m-win32.whl", hash = "sha256:575a832e09d237ae5fedb825a7a5bc6a116090dd57d6417d4f3b75121c73e3be"}, 789 | {file = "regex-2021.3.17-cp37-cp37m-win_amd64.whl", hash = "sha256:8e65e3e4c6feadf6770e2ad89ad3deb524bcb03d8dc679f381d0568c024e0deb"}, 790 | {file = "regex-2021.3.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0df9a0ad2aad49ea3c7f65edd2ffb3d5c59589b85992a6006354f6fb109bb18"}, 791 | {file = "regex-2021.3.17-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b98bc9db003f1079caf07b610377ed1ac2e2c11acc2bea4892e28cc5b509d8d5"}, 792 | {file = "regex-2021.3.17-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:808404898e9a765e4058bf3d7607d0629000e0a14a6782ccbb089296b76fa8fe"}, 793 | {file = "regex-2021.3.17-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:5770a51180d85ea468234bc7987f5597803a4c3d7463e7323322fe4a1b181578"}, 794 | {file = "regex-2021.3.17-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:976a54d44fd043d958a69b18705a910a8376196c6b6ee5f2596ffc11bff4420d"}, 795 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:63f3ca8451e5ff7133ffbec9eda641aeab2001be1a01878990f6c87e3c44b9d5"}, 796 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bcd945175c29a672f13fce13a11893556cd440e37c1b643d6eeab1988c8b209c"}, 797 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:3d9356add82cff75413bec360c1eca3e58db4a9f5dafa1f19650958a81e3249d"}, 798 | {file = "regex-2021.3.17-cp38-cp38-win32.whl", hash = "sha256:f5d0c921c99297354cecc5a416ee4280bd3f20fd81b9fb671ca6be71499c3fdf"}, 799 | {file = "regex-2021.3.17-cp38-cp38-win_amd64.whl", hash = "sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa"}, 800 | {file = "regex-2021.3.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c2e364491406b7888c2ad4428245fc56c327e34a5dfe58fd40df272b3c3dab3"}, 801 | {file = "regex-2021.3.17-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8bd4f91f3fb1c9b1380d6894bd5b4a519409135bec14c0c80151e58394a4e88a"}, 802 | {file = "regex-2021.3.17-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:882f53afe31ef0425b405a3f601c0009b44206ea7f55ee1c606aad3cc213a52c"}, 803 | {file = "regex-2021.3.17-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139"}, 804 | {file = "regex-2021.3.17-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:360a01b5fa2ad35b3113ae0c07fb544ad180603fa3b1f074f52d98c1096fa15e"}, 805 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:709f65bb2fa9825f09892617d01246002097f8f9b6dde8d1bb4083cf554701ba"}, 806 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:c66221e947d7207457f8b6f42b12f613b09efa9669f65a587a2a71f6a0e4d106"}, 807 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c782da0e45aff131f0bed6e66fbcfa589ff2862fc719b83a88640daa01a5aff7"}, 808 | {file = "regex-2021.3.17-cp39-cp39-win32.whl", hash = "sha256:dc9963aacb7da5177e40874585d7407c0f93fb9d7518ec58b86e562f633f36cd"}, 809 | {file = "regex-2021.3.17-cp39-cp39-win_amd64.whl", hash = "sha256:a0d04128e005142260de3733591ddf476e4902c0c23c1af237d9acf3c96e1b38"}, 810 | {file = "regex-2021.3.17.tar.gz", hash = "sha256:4b8a1fb724904139149a43e172850f35aa6ea97fb0545244dc0b805e0154ed68"}, 811 | ] 812 | rfc3986 = [ 813 | {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, 814 | {file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"}, 815 | ] 816 | rsa = [ 817 | {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, 818 | {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"}, 819 | ] 820 | six = [ 821 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 822 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 823 | ] 824 | sniffio = [ 825 | {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, 826 | {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, 827 | ] 828 | starlette = [ 829 | {file = "starlette-0.13.4-py3-none-any.whl", hash = "sha256:0fb4b38d22945b46acb880fedee7ee143fd6c0542992501be8c45c0ed737dd1a"}, 830 | {file = "starlette-0.13.4.tar.gz", hash = "sha256:04fe51d86fd9a594d9b71356ed322ccde5c9b448fc716ac74155e5821a922f8d"}, 831 | ] 832 | toml = [ 833 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 834 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 835 | ] 836 | typed-ast = [ 837 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, 838 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, 839 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, 840 | {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, 841 | {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, 842 | {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, 843 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, 844 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, 845 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, 846 | {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, 847 | {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, 848 | {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, 849 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, 850 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, 851 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, 852 | {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, 853 | {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, 854 | {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, 855 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, 856 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, 857 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, 858 | {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, 859 | {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, 860 | {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, 861 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, 862 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, 863 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, 864 | {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, 865 | {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, 866 | {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, 867 | ] 868 | typing-extensions = [ 869 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 870 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 871 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 872 | ] 873 | uvicorn = [ 874 | {file = "uvicorn-0.11.8-py3-none-any.whl", hash = "sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef"}, 875 | {file = "uvicorn-0.11.8.tar.gz", hash = "sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26"}, 876 | ] 877 | uvloop = [ 878 | {file = "uvloop-0.15.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69"}, 879 | {file = "uvloop-0.15.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7"}, 880 | {file = "uvloop-0.15.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d"}, 881 | {file = "uvloop-0.15.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c"}, 882 | {file = "uvloop-0.15.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47"}, 883 | {file = "uvloop-0.15.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c"}, 884 | {file = "uvloop-0.15.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc"}, 885 | {file = "uvloop-0.15.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760"}, 886 | {file = "uvloop-0.15.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c"}, 887 | {file = "uvloop-0.15.2.tar.gz", hash = "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01"}, 888 | ] 889 | wcwidth = [ 890 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 891 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 892 | ] 893 | websockets = [ 894 | {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"}, 895 | {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"}, 896 | {file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"}, 897 | {file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"}, 898 | {file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"}, 899 | {file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"}, 900 | {file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"}, 901 | {file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"}, 902 | {file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"}, 903 | {file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"}, 904 | {file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"}, 905 | {file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"}, 906 | {file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"}, 907 | {file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"}, 908 | {file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"}, 909 | {file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"}, 910 | {file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"}, 911 | {file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"}, 912 | {file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"}, 913 | {file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"}, 914 | {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, 915 | {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, 916 | ] 917 | wrapt = [ 918 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 919 | ] 920 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "signal-cli-rest-api" 3 | version = "0.2.0" 4 | description = "" 5 | readme = "README.md" 6 | repository = "https://github.com/SebastianLuebke/signal-cli-rest-api" 7 | authors = ["Sebastian Noel Lübke "] 8 | 9 | [tool.poetry.dependencies] 10 | python = "~3.8" 11 | fastapi = "^0.58.0" 12 | uvicorn = "^0.11.5" 13 | python-jose = "^3.1.0" 14 | pyqrcode = "^1.2.1" 15 | pypng = "^0.0.20" 16 | aiofiles = "^0.6.0" 17 | httpx = "^0.16.1" 18 | 19 | [tool.poetry.dev-dependencies] 20 | pytest = "^5.2" 21 | flake8 = "^3.8.3" 22 | pylint = "^2.5.3" 23 | autoflake = "^1.4" 24 | black = "^20.8b1" 25 | 26 | [tool.isort] 27 | multi_line_output = 3 28 | include_trailing_comma = true 29 | force_grid_wrap = 0 30 | line_length = 88 31 | 32 | [build-system] 33 | requires = ["poetry>=0.12"] 34 | build-backend = "poetry.masonry.api" 35 | 36 | -------------------------------------------------------------------------------- /scripts/format-imports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | set -x 3 | 4 | # Sort imports one per line, so autoflake can remove unused imports 5 | isort --recursive --force-single-line-imports --apply signal_cli_rest_api 6 | sh ./scripts/format.sh -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | set -x 3 | 4 | autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place signal_cli_rest_api --exclude=__init__.py 5 | black signal_cli_rest_api 6 | isort --recursive --apply signal_cli_rest_api 7 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | black signal_cli_rest_api --check 6 | isort --recursive --check-only signal_cli_rest_api 7 | flake8 signal_cli_rest_api -------------------------------------------------------------------------------- /scripts/start_api.py: -------------------------------------------------------------------------------- 1 | """Start API without Docker, e.g. for interactive debugging.""" 2 | 3 | import uvicorn 4 | 5 | from signal_cli_rest_api.main import app 6 | 7 | uvicorn.run(app, host="127.0.0.1", port=8000) 8 | -------------------------------------------------------------------------------- /signal_cli_rest_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luebke-dev/signal-cli-rest-api/fe08db6c89693b4ec468740c87ddae3daf0cdf31/signal_cli_rest_api/__init__.py -------------------------------------------------------------------------------- /signal_cli_rest_api/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luebke-dev/signal-cli-rest-api/fe08db6c89693b4ec468740c87ddae3daf0cdf31/signal_cli_rest_api/api/__init__.py -------------------------------------------------------------------------------- /signal_cli_rest_api/api/block.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from fastapi import APIRouter 4 | 5 | from signal_cli_rest_api.schemas import Block 6 | from signal_cli_rest_api.utils import run_signal_cli_command 7 | 8 | router = APIRouter() 9 | 10 | 11 | @router.post("/{number}", response_model=Block) 12 | async def block_numbers_or_groups(block: Block, number: str) -> Any: 13 | """ 14 | block one or multiple numbers or group id's 15 | """ 16 | cmd = ["-u", number, "block"] 17 | 18 | if block.group: 19 | cmd.append("-g") 20 | 21 | cmd += block.numbers 22 | 23 | await run_signal_cli_command(cmd) 24 | 25 | return block 26 | 27 | 28 | @router.delete("/{number}", response_model=Block) 29 | async def unblock_numbers_or_groups(unblock: Block, number: str) -> Any: 30 | """ 31 | unblock one or multiple numbers or group id's 32 | """ 33 | cmd = ["-u", number, "unblock"] 34 | 35 | if unblock.group: 36 | cmd.append("-g") 37 | 38 | cmd += unblock.numbers 39 | 40 | await run_signal_cli_command(cmd) 41 | 42 | return unblock 43 | -------------------------------------------------------------------------------- /signal_cli_rest_api/api/groups.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from fastapi import APIRouter 4 | 5 | from signal_cli_rest_api.config import settings 6 | from signal_cli_rest_api.schemas import GroupCreate, GroupOut, GroupUpdate 7 | from signal_cli_rest_api.utils import ( 8 | read_groups, 9 | run_signal_cli_command, 10 | save_attachment, 11 | ) 12 | 13 | router = APIRouter() 14 | 15 | 16 | @router.get("/{number}", response_model=List[GroupOut]) 17 | async def get_groups(number: str, detailed: bool = False) -> Any: 18 | """ 19 | get groups 20 | """ 21 | 22 | cmd = ["-u", number, "listGroups"] 23 | 24 | if detailed: 25 | cmd.append("-d") 26 | 27 | response = await run_signal_cli_command(cmd) 28 | 29 | groups = read_groups(response) 30 | 31 | return groups 32 | 33 | 34 | @router.post("/{number}", status_code=201, response_model=GroupOut) 35 | async def create_group(group: GroupCreate, number: str) -> Any: 36 | """ 37 | Create Group 38 | """ 39 | 40 | cmd = ["updateGroup", "-n", group.name] 41 | 42 | if group.avatar: 43 | cmd.append("-a") 44 | await save_attachment(group.avatar) 45 | cmd.append(f"{settings.signal_upload_path}{group.avatar.filename}") 46 | 47 | cmd += ["-m"] 48 | cmd += group.members 49 | 50 | response = await run_signal_cli_command(cmd) 51 | 52 | return GroupOut(**group.dict(), id=response.split('"')[1]) 53 | 54 | 55 | @router.put("/{number}/{id}", response_model=GroupOut) 56 | async def edit_group(id: str, group: GroupUpdate, number: str) -> Any: 57 | """ 58 | Edit a group. You can't remove a member from a group 59 | """ 60 | 61 | cmd = ["-u", number, "updateGroup", "-g", id] 62 | 63 | if group.name: 64 | cmd += ["-n", group.name] 65 | 66 | if group.avatar: 67 | cmd.append("-a") 68 | await save_attachment(group.avatar) 69 | cmd.append(f"{settings.signal_upload_path}{group.avatar.filename}") 70 | 71 | if len(group.members) > 0: 72 | cmd += ["-m"] 73 | cmd += group.members 74 | 75 | await run_signal_cli_command(cmd) 76 | 77 | return GroupOut(**group.dict(), id=id) 78 | 79 | 80 | @router.delete("/{number}/{id}") 81 | async def leave_group_by_id(id: str, number: str) -> Any: 82 | """ 83 | leave a group by id 84 | """ 85 | 86 | cmd = ["-u", number, "quitGroup", "-g", id] 87 | 88 | await run_signal_cli_command(cmd) 89 | 90 | return id 91 | -------------------------------------------------------------------------------- /signal_cli_rest_api/api/messages.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Any, List 4 | 5 | from fastapi import APIRouter, BackgroundTasks 6 | 7 | from signal_cli_rest_api.config import settings 8 | from signal_cli_rest_api.schemas import ( 9 | MessageIncoming, 10 | MessageOutgoing, 11 | MessageSent, 12 | ReactionOut, 13 | ) 14 | from signal_cli_rest_api.utils import run_signal_cli_command, save_attachment 15 | 16 | router = APIRouter() 17 | 18 | 19 | @router.get("/{number}", response_model=List[MessageIncoming]) 20 | async def get_messages(number: str) -> Any: 21 | """ 22 | get messages 23 | """ 24 | 25 | response = await run_signal_cli_command(["-u", number, "--output=json", "receive"]) 26 | return [json.loads(m) for m in response.split("\n") if m != ""] 27 | 28 | 29 | @router.post("/{number}", response_model=MessageSent, status_code=201) 30 | async def send_message( 31 | message: MessageOutgoing, number: str, background_tasks: BackgroundTasks 32 | ) -> Any: 33 | """ 34 | send message 35 | """ 36 | 37 | cmd = ["-u", number, "send", "-m", message.text] 38 | 39 | if message.group: 40 | cmd.append("-g") 41 | cmd.append(message.groupId) 42 | else: 43 | cmd += message.receivers 44 | 45 | if len(message.attachments) > 0: 46 | cmd.append("-a") 47 | for attachment in message.attachments: 48 | await save_attachment(attachment) 49 | attachment_path = f"{settings.signal_upload_path}{attachment.filename}" 50 | cmd.append(attachment_path) 51 | background_tasks.add_task(os.remove, attachment_path) 52 | 53 | response = await run_signal_cli_command(cmd) 54 | 55 | return MessageSent(**message.dict(), timestamp=response.split("\n")[0]) 56 | 57 | 58 | @router.post("/{number}/reaction") 59 | async def send_reaction(number: str, reaction: ReactionOut) -> Any: 60 | """ 61 | send a reaction 62 | 63 | https://emojipedia.org/ 64 | """ 65 | cmd = ["-u", number, "sendReaction"] 66 | 67 | if reaction.group: 68 | cmd += ["-g", reaction.receiver] 69 | else: 70 | cmd.append(reaction.receiver) 71 | 72 | cmd += [ 73 | "-a", 74 | reaction.target_number, 75 | "-t", 76 | reaction.target_timestamp, 77 | "-e", 78 | reaction.emoji, 79 | ] 80 | 81 | await run_signal_cli_command(cmd) 82 | 83 | 84 | @router.delete("/{number}/reaction") 85 | async def delete_reaction(number: str, reaction: ReactionOut) -> Any: 86 | """ 87 | remove a reaction 88 | """ 89 | cmd = ["-u", number, "sendReaction"] 90 | 91 | if reaction.group: 92 | cmd += ["-g", reaction.receiver] 93 | else: 94 | cmd.append(reaction.receiver) 95 | 96 | cmd += [ 97 | "-a", 98 | reaction.target_number, 99 | "-t", 100 | reaction.target_timestamp, 101 | "-e", 102 | reaction.emoji, 103 | "-r", 104 | ] 105 | 106 | await run_signal_cli_command(cmd) 107 | -------------------------------------------------------------------------------- /signal_cli_rest_api/api/profile.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any 3 | 4 | from fastapi import APIRouter, BackgroundTasks 5 | 6 | from signal_cli_rest_api.config import settings 7 | from signal_cli_rest_api.schemas import ProfileUpdate 8 | from signal_cli_rest_api.utils import run_signal_cli_command, save_attachment 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.put("/{number}", response_model=ProfileUpdate) 14 | async def update_profile( 15 | profile: ProfileUpdate, number: str, background_tasks: BackgroundTasks 16 | ) -> Any: 17 | """ 18 | updates your profile 19 | """ 20 | 21 | cmd = ["-u", number, "updateProfile"] 22 | 23 | if profile.name: 24 | cmd += ["--n", profile.name] 25 | 26 | if profile.remove_avatar: 27 | cmd.append("--remove-avatar") 28 | elif profile.avatar: 29 | cmd.append("--avatar") 30 | await save_attachment(profile.avatar) 31 | attachment_path = f"{settings.signal_upload_path}{profile.avatar.filename}" 32 | cmd.append(attachment_path) 33 | background_tasks.add_task(os.remove, attachment_path) 34 | 35 | await run_signal_cli_command(cmd) 36 | 37 | return profile 38 | -------------------------------------------------------------------------------- /signal_cli_rest_api/api/register.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import Any 3 | 4 | import pyqrcode 5 | from fastapi import APIRouter 6 | from starlette.responses import StreamingResponse 7 | 8 | from signal_cli_rest_api.schemas import Registration, Verification 9 | from signal_cli_rest_api.utils import run_signal_cli_command 10 | 11 | router = APIRouter() 12 | 13 | 14 | @router.post("/{number}/link") 15 | async def link_device(number: str) -> Any: 16 | response = await run_signal_cli_command(["link"], False) 17 | response = response.rstrip(b"\n") 18 | buf = BytesIO() 19 | qr = pyqrcode.create(response, error="L") 20 | qr.png(buf, scale=3) 21 | buf.seek(0) # important here! 22 | return StreamingResponse(buf, media_type="image/png") 23 | 24 | 25 | @router.post("/{number}/update-account") 26 | async def update_account(number: str) -> Any: 27 | response = await run_signal_cli_command(["-u", number, "updateAccount"]) 28 | return response 29 | 30 | 31 | @router.post("/{number}", response_model=Registration) 32 | async def register_number(registration: Registration, number: str) -> Any: 33 | """ 34 | register a new number 35 | """ 36 | 37 | cmd = ["-u", number, "register"] 38 | 39 | if registration.voice_verification: 40 | cmd.append("--voice") 41 | 42 | if registration.captcha: 43 | cmd.extend(["--captcha", registration.captcha]) 44 | 45 | await run_signal_cli_command(cmd) 46 | return registration 47 | 48 | 49 | @router.post("/{number}/verify", response_model=Verification) 50 | async def verify_registration(verification: Verification, number: str) -> Any: 51 | """Verify a registration. 52 | 53 | Using the installation pin is currently not supported by signal-cli. 54 | """ 55 | 56 | cmd = ["-u", number, "verify", verification.verification_code] 57 | 58 | if verification.pin: 59 | cmd.extend(["-p", verification.pin]) 60 | 61 | await run_signal_cli_command(cmd) 62 | return verification 63 | -------------------------------------------------------------------------------- /signal_cli_rest_api/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pydantic import BaseSettings 4 | 5 | 6 | class Settings(BaseSettings): 7 | signal_config_path: str = os.environ.get( 8 | "SIGNAL_CONFIG_PATH", os.path.expanduser("~/.local/share/signal-cli") 9 | ) 10 | signal_upload_path: str = f"{signal_config_path}/uploads/" 11 | signal_attachments_path: str = f"{signal_config_path}/attachments/" 12 | 13 | 14 | settings = Settings() 15 | -------------------------------------------------------------------------------- /signal_cli_rest_api/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from signal_cli_rest_api.api import block, groups, messages, profile, register 4 | 5 | app = FastAPI(title="signal-cli-rest-api", version="0.1.97") 6 | 7 | app.include_router(block.router, prefix="/block", tags=["block"]) 8 | app.include_router(groups.router, prefix="/groups", tags=["groups"]) 9 | app.include_router(messages.router, prefix="/messages", tags=["messages"]) 10 | app.include_router(profile.router, prefix="/profile", tags=["profile"]) 11 | app.include_router(register.router, prefix="/register", tags=["register"]) 12 | -------------------------------------------------------------------------------- /signal_cli_rest_api/schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, List, Optional 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | class AttachmentIn(BaseModel): 8 | url: Optional[str] = None 9 | filename: str 10 | content: Optional[Any] = None 11 | 12 | 13 | class MessageOutgoing(BaseModel): 14 | text: str 15 | receivers: List[str] 16 | group: bool = False 17 | groupId: str 18 | attachments: List[AttachmentIn] = [] 19 | 20 | 21 | class MessageSent(MessageOutgoing): 22 | timestamp: datetime 23 | 24 | 25 | class AttachmentOut(BaseModel): 26 | contentType: str 27 | filename: Optional[str] = None 28 | id: str 29 | size: int 30 | 31 | 32 | class GroupInfo(BaseModel): 33 | groupId: str 34 | members: Optional[List[str]] = None 35 | name: Optional[str] = None 36 | 37 | 38 | class DataMessage(BaseModel): 39 | timestamp: str 40 | message: Optional[str] = None 41 | expiresInSeconds: int 42 | attachments: Optional[List[AttachmentOut]] = None 43 | groupInfo: Optional[GroupInfo] = None 44 | 45 | 46 | class Envelope(BaseModel): 47 | source: str 48 | sourceDevice: int 49 | relay: Any 50 | timestamp: str 51 | dataMessage: Optional[DataMessage] = None 52 | 53 | 54 | class MessageIncoming(BaseModel): 55 | envelope: Envelope 56 | syncMessage: Any 57 | callMessage: Any 58 | receiptMessage: Any 59 | 60 | 61 | class ReactionOut(BaseModel): 62 | receiver: str 63 | group: bool = False 64 | target_number: str 65 | target_timestamp: str 66 | emoji: str 67 | 68 | 69 | class Block(BaseModel): 70 | numbers: List[str] 71 | group: Optional[bool] = False 72 | 73 | 74 | class GroupCreate(BaseModel): 75 | name: str 76 | members: List[str] = [] 77 | avatar: Optional[AttachmentIn] = None 78 | 79 | 80 | class GroupUpdate(BaseModel): 81 | name: Optional[str] 82 | members: List[str] = [] 83 | avatar: Optional[AttachmentIn] = None 84 | 85 | 86 | class GroupOut(BaseModel): 87 | id: Optional[str] = None 88 | name: Optional[str] = None 89 | members: List[str] = [] 90 | blocked: bool = False 91 | active: bool = True 92 | 93 | 94 | class ProfileUpdate(BaseModel): 95 | name: Optional[str] = None 96 | avatar: Optional[AttachmentIn] = None 97 | remove_avatar: Optional[bool] = False 98 | 99 | 100 | class Verification(BaseModel): 101 | verification_code: str 102 | pin: Optional[str] = None 103 | 104 | class Config: 105 | schema_extra = {"example": {"verification_code": "123456", "pin": None}} 106 | 107 | 108 | class Registration(BaseModel): 109 | voice_verification: bool = False 110 | captcha: Optional[str] = None 111 | 112 | class Config: 113 | schema_extra = {"example": {"voice_verification": True, "captcha": ""}} 114 | -------------------------------------------------------------------------------- /signal_cli_rest_api/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | from typing import Any, List 4 | 5 | import aiofiles 6 | import httpx 7 | from fastapi import HTTPException 8 | 9 | from signal_cli_rest_api.config import settings 10 | from signal_cli_rest_api.schemas import AttachmentIn 11 | 12 | 13 | def read_groups(groups_string: str): 14 | groups = [] 15 | for group in groups_string.split("\n"): 16 | if group == "": 17 | continue 18 | 19 | # remove unwanted characters 20 | chars_to_remove = ["[", "]", ","] 21 | 22 | for char in chars_to_remove: 23 | group = group.replace(char, "") 24 | 25 | splitted = group.split(" ") 26 | active_index = splitted.index("Active:") 27 | 28 | id = splitted[1] 29 | name = " ".join(splitted[3 : active_index - 1]) 30 | active = True if splitted[active_index + 1] == "true" else False 31 | blocked = True if splitted[active_index + 3] == "true" else False 32 | members = [] 33 | 34 | try: 35 | members_index = splitted.index("Members:") 36 | members = splitted[members_index + 1 :] 37 | except ValueError: 38 | pass 39 | 40 | groups.append( 41 | { 42 | "id": id, 43 | "name": name, 44 | "active": active, 45 | "blocked": blocked, 46 | "members": members, 47 | } 48 | ) 49 | 50 | return groups 51 | 52 | 53 | async def save_attachment(attachment: AttachmentIn): 54 | if attachment.url is None and attachment.content is None: 55 | raise HTTPException(status_code=422) 56 | async with aiofiles.open( 57 | f"{settings.signal_upload_path}{attachment.filename}", "wb" 58 | ) as file: 59 | content = b"" 60 | if attachment.url: 61 | async with httpx.AsyncClient() as client: 62 | r = await client.get(attachment.url, allow_redirects=True) 63 | if r.status_code != 200: 64 | raise HTTPException( 65 | status_code=400, detail="Downloading image failed" 66 | ) 67 | content = r.content 68 | elif attachment.content: 69 | content = base64.b64decode(attachment.content) 70 | 71 | await file.write(content) 72 | 73 | 74 | async def run_signal_cli_command(cmd: List[str], wait: bool = True) -> Any: 75 | base_cmd = ["signal-cli", "--config", settings.signal_config_path] 76 | 77 | full_cmd = " ".join(base_cmd + cmd) 78 | 79 | process = await asyncio.subprocess.create_subprocess_shell( 80 | full_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE 81 | ) 82 | 83 | if wait: 84 | stdout, stderr = await process.communicate() 85 | if stderr: 86 | raise HTTPException( 87 | status_code=500, 88 | detail=f"Starting signal-cli process failed: {stderr.decode()}", 89 | ) 90 | return stdout.decode() 91 | 92 | return await process.stdout.readline() 93 | --------------------------------------------------------------------------------