├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── fastapi_security_telegram_webhook ├── __init__.py ├── py.typed └── security.py ├── poetry.lock └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # Distribution / packaging 8 | .Python 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | .eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | pip-wheel-metadata/ 22 | share/python-wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # Environments 29 | .env 30 | .venv 31 | env/ 32 | venv/ 33 | 34 | # mypy 35 | .mypy_cache/ 36 | .dmypy.json 37 | dmypy.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.7 3 | 4 | install: 5 | - pip install poetry>=1.0.0 6 | - poetry install 7 | 8 | script: 9 | - poetry run mypy fastapi_security_telegram_webhook 10 | - poetry run black --check fastapi_security_telegram_webhook 11 | - poetry build 12 | 13 | deploy: 14 | - provider: script 15 | script: poetry publish --username $PYPI_USERNAME --password $PYPI_PASSWORD --build 16 | on: 17 | tags: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dima Boger 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 | # fastapi-security-telegram-webhook 2 | 3 | Plugin for [FastAPI](https://github.com/tiangolo/fastapi) which allows you to secure your Telegram Bot API webhook 4 | endpoint with IP restriction and an optional secret token. 5 | 6 | Telegram provides two ways of getting updates: long polling and webhook. When you use webhook you just register 7 | endpoint address and telegram sends JSON to this address. If the bad guy finds out the address of your webhook, then 8 | he can send fake "telegram updates" to your bot. 9 | 10 | Telegram doesn't provide any security features like signing or authentication mechanisms, so securing webhook is a task 11 | for a bot developer. 12 | 13 | Thence, for securing your webhook you have only two option: 14 | - Allow requests only from Telegram subnets. 15 | [The are fixed in documentation](https://core.telegram.org/bots/webhooks#the-short-version), but may change in future. 16 | - Use secret value in endpoint address, e.g. `/telegram-webhook/468e95826f224a60a4e9355ab76e0875`. It will 17 | complicate the brute force attack and you can easily change it if the value was compromised. 18 | 19 | This little plugin allows you to use both ways to secure. 20 | 21 | ## How to use 22 | 23 | Use pip or another package management util: 24 | ```bash 25 | pip install fastapi-security-telegram-webhook 26 | ``` 27 | 28 | or 29 | 30 | ```bash 31 | poetry add fastapi-security-telegram-webhook 32 | ``` 33 | 34 | or 35 | 36 | ```bash 37 | pipenv install fastapi-security-telegram-webhook 38 | ``` 39 | 40 | Package contains two Security objects: 41 | - `OnlyTelegramNetwork` allows request only from telegram subnets 42 | - `OnlyTelegramNetworkWithSecret` additionally checks secret in path 43 | 44 | Example with `OnlyTelegramNetworkWithSecret`. Pay attention to `{secret}` in path operation, it's required 45 | 46 | ```python 47 | from fastapi import FastAPI, Body, Depends 48 | from fastapi_security_telegram_webhook import OnlyTelegramNetworkWithSecret 49 | 50 | app = FastAPI() 51 | webhook_security = OnlyTelegramNetworkWithSecret(real_secret="your-secret-from-config-or-env") 52 | 53 | # {secret} in path and OnlyTelegramNetworkWithSecret as dependency: 54 | @app.post('/webhook/{secret}', dependencies=[Depends(webhook_security)]) 55 | def process_telegram_update(update_raw = Body(...)): 56 | ... 57 | 58 | ``` 59 | 60 | ## Use behind proxy 61 | 62 | The plugin uses `starlette.Request.client.host` for extracting IP address of the request, so if your web-app is 63 | behind proxy you should pass the real IP to the app. 64 | 65 | For `uvicorn` you can use `--proxy-headers` as it describes in 66 | [documentation](https://www.uvicorn.org/deployment/#running-behind-nginx). 67 | -------------------------------------------------------------------------------- /fastapi_security_telegram_webhook/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_security_telegram_webhook.security import ( 2 | OnlyTelegramNetwork, 3 | OnlyTelegramNetworkWithSecret, 4 | ) 5 | 6 | __all__ = [ 7 | "OnlyTelegramNetwork", 8 | "OnlyTelegramNetworkWithSecret", 9 | ] 10 | -------------------------------------------------------------------------------- /fastapi_security_telegram_webhook/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b0g3r/fastapi-security-telegram-webhook/13fad9c7017b18cf30830a67862fd8f5e48a97eb/fastapi_security_telegram_webhook/py.typed -------------------------------------------------------------------------------- /fastapi_security_telegram_webhook/security.py: -------------------------------------------------------------------------------- 1 | from secrets import compare_digest 2 | from ipaddress import ( 3 | IPv4Network, 4 | IPv4Address, 5 | ) 6 | from typing import ( 7 | Optional, 8 | Sequence, 9 | ) 10 | 11 | from fastapi import ( 12 | Path, 13 | HTTPException, 14 | ) 15 | from fastapi.openapi.models import HTTPBase 16 | from fastapi.security.base import SecurityBase 17 | from starlette.requests import Request 18 | 19 | # Source: https://core.telegram.org/bots/webhooks#the-short-version 20 | DEFAULT_NETWORKS = [IPv4Network("149.154.160.0/20"), IPv4Network("91.108.4.0/22")] 21 | SECRET_PATH_PARAM = Path( 22 | ..., 23 | alias="secret", 24 | description="Secret token, should be placed as `{secret}` in path operation function", 25 | ) 26 | 27 | 28 | def check_secret(request_secret: str, real_secret: str) -> None: 29 | """ 30 | Compare secrets with safe method. 31 | 32 | :raises: HTTPException 33 | """ 34 | if not compare_digest(request_secret, real_secret): 35 | raise HTTPException(status_code=403) 36 | 37 | 38 | def check_ip(request_ip: IPv4Address, telegram_networks: Sequence[IPv4Network]) -> None: 39 | """ 40 | Check that request was from telegram networks. 41 | 42 | :raises: HTTPException 43 | """ 44 | if not any(request_ip in network for network in telegram_networks): 45 | raise HTTPException(status_code=403, detail="Bad IP address") 46 | 47 | 48 | def convert_to_ip(request_host: Optional[str]) -> IPv4Address: 49 | """ 50 | Convert request host name to IP object. 51 | 52 | :raises: HTTPException 53 | """ 54 | if request_host is None: 55 | raise HTTPException(status_code=500, detail="IP address cannot be empty") 56 | return IPv4Address(request_host) 57 | 58 | 59 | class OnlyTelegramNetwork(SecurityBase): 60 | """ 61 | Secure telegram webhook, validate that request was made from one of telegram subnets. 62 | """ 63 | 64 | scheme_name = "only_telegram_network" 65 | model = HTTPBase( 66 | scheme=scheme_name, 67 | description="Your request should be permitted from Telegram subnet", 68 | ) 69 | 70 | def __init__( 71 | self, *, telegram_networks: Optional[Sequence[IPv4Network]] = None, 72 | ): 73 | if telegram_networks is None: 74 | telegram_networks = DEFAULT_NETWORKS 75 | self.telegram_networks = telegram_networks 76 | 77 | def __call__(self, request: Request) -> None: 78 | """ 79 | :raises: HTTPException 80 | """ 81 | request_host: Optional[str] = request.client.host 82 | request_ip = convert_to_ip(request_host) 83 | check_ip(request_ip, self.telegram_networks) 84 | 85 | 86 | class OnlyTelegramNetworkWithSecret(SecurityBase): 87 | """ 88 | Secure telegram webhook, validate that request was made from one of telegram subnets and contains 89 | correct `secret` in path. 90 | 91 | If you use this type of security, please, add correspond `{secret}` in path operation function. 92 | 93 | Check example: 94 | >>> from fastapi import FastAPI, Depends, Body 95 | ... app = FastAPI() 96 | ... webhook_security = OnlyTelegramNetworkWithSecret(real_secret="your-secret-from-config-or-env") 97 | ... 98 | ... # {secret} in path and OnlyTelegramNetworkWithSecret as dependency: 99 | ... @app.post('/webhook/{secret}', dependencies=[Depends(webhook_security)]) 100 | ... def process_telegram_update(update_raw = Body(...)): 101 | ... ... 102 | """ 103 | 104 | scheme_name = "only_telegram_network_with_secret" 105 | model = HTTPBase( 106 | scheme=scheme_name, 107 | description="You should pass the 'secret' value in the path and request " 108 | "should be permitted from Telegram subnet", 109 | ) 110 | 111 | def __init__( 112 | self, 113 | *, 114 | real_secret: str, 115 | telegram_networks: Optional[Sequence[IPv4Network]] = None, 116 | ): 117 | self.real_secret = real_secret 118 | if telegram_networks is None: 119 | telegram_networks = DEFAULT_NETWORKS 120 | self.telegram_networks = telegram_networks 121 | 122 | def __call__( 123 | self, request: Request, request_secret: str = SECRET_PATH_PARAM 124 | ) -> None: 125 | """ 126 | :raises: HTTPException 127 | """ 128 | request_host: Optional[str] = request.client.host 129 | request_ip = convert_to_ip(request_host) 130 | check_ip(request_ip, self.telegram_networks) 131 | check_secret(request_secret, self.real_secret) 132 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 4 | name = "appdirs" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.4.3" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "Classes Without Boilerplate" 12 | name = "attrs" 13 | optional = false 14 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 15 | version = "19.3.0" 16 | 17 | [package.extras] 18 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 19 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 20 | docs = ["sphinx", "zope.interface"] 21 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 22 | 23 | [[package]] 24 | category = "dev" 25 | description = "The uncompromising code formatter." 26 | name = "black" 27 | optional = false 28 | python-versions = ">=3.6" 29 | version = "19.10b0" 30 | 31 | [package.dependencies] 32 | appdirs = "*" 33 | attrs = ">=18.1.0" 34 | click = ">=6.5" 35 | pathspec = ">=0.6,<1" 36 | regex = "*" 37 | toml = ">=0.9.4" 38 | typed-ast = ">=1.4.0" 39 | 40 | [package.extras] 41 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 42 | 43 | [[package]] 44 | category = "dev" 45 | description = "Composable command line interface toolkit" 46 | name = "click" 47 | optional = false 48 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 49 | version = "7.1.1" 50 | 51 | [[package]] 52 | category = "main" 53 | description = "A backport of the dataclasses module for Python 3.6" 54 | marker = "python_version < \"3.7\"" 55 | name = "dataclasses" 56 | optional = false 57 | python-versions = "*" 58 | version = "0.6" 59 | 60 | [[package]] 61 | category = "main" 62 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 63 | name = "fastapi" 64 | optional = false 65 | python-versions = ">=3.6" 66 | version = "0.52.0" 67 | 68 | [package.dependencies] 69 | pydantic = ">=0.32.2,<2.0.0" 70 | starlette = "0.13.2" 71 | 72 | [package.extras] 73 | all = ["requests", "aiofiles", "jinja2", "python-multipart", "itsdangerous", "pyyaml", "graphene", "ujson", "email-validator", "uvicorn", "async-exit-stack", "async-generator"] 74 | dev = ["pyjwt", "passlib", "autoflake", "flake8", "uvicorn", "graphene"] 75 | doc = ["mkdocs", "mkdocs-material", "markdown-include"] 76 | test = ["pytest (>=4.0.0)", "pytest-cov", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "peewee", "databases", "orjson", "async-exit-stack", "async-generator", "python-multipart", "aiofiles", "ujson", "flask"] 77 | 78 | [[package]] 79 | category = "dev" 80 | description = "Optional static typing for Python" 81 | name = "mypy" 82 | optional = false 83 | python-versions = ">=3.5" 84 | version = "0.761" 85 | 86 | [package.dependencies] 87 | mypy-extensions = ">=0.4.3,<0.5.0" 88 | typed-ast = ">=1.4.0,<1.5.0" 89 | typing-extensions = ">=3.7.4" 90 | 91 | [package.extras] 92 | dmypy = ["psutil (>=4.0)"] 93 | 94 | [[package]] 95 | category = "dev" 96 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 97 | name = "mypy-extensions" 98 | optional = false 99 | python-versions = "*" 100 | version = "0.4.3" 101 | 102 | [[package]] 103 | category = "dev" 104 | description = "Utility library for gitignore style pattern matching of file paths." 105 | name = "pathspec" 106 | optional = false 107 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 108 | version = "0.7.0" 109 | 110 | [[package]] 111 | category = "main" 112 | description = "Data validation and settings management using python 3.6 type hinting" 113 | name = "pydantic" 114 | optional = false 115 | python-versions = ">=3.6" 116 | version = "1.4" 117 | 118 | [package.dependencies] 119 | [package.dependencies.dataclasses] 120 | python = "<3.7" 121 | version = ">=0.6" 122 | 123 | [package.extras] 124 | dotenv = ["python-dotenv (>=0.10.4)"] 125 | email = ["email-validator (>=1.0.3)"] 126 | typing_extensions = ["typing-extensions (>=3.7.2)"] 127 | 128 | [[package]] 129 | category = "dev" 130 | description = "Alternative regular expression module, to replace re." 131 | name = "regex" 132 | optional = false 133 | python-versions = "*" 134 | version = "2020.2.20" 135 | 136 | [[package]] 137 | category = "main" 138 | description = "The little ASGI library that shines." 139 | name = "starlette" 140 | optional = false 141 | python-versions = ">=3.6" 142 | version = "0.13.2" 143 | 144 | [package.extras] 145 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 146 | 147 | [[package]] 148 | category = "dev" 149 | description = "Python Library for Tom's Obvious, Minimal Language" 150 | name = "toml" 151 | optional = false 152 | python-versions = "*" 153 | version = "0.10.0" 154 | 155 | [[package]] 156 | category = "dev" 157 | description = "a fork of Python 2 and 3 ast modules with type comment support" 158 | name = "typed-ast" 159 | optional = false 160 | python-versions = "*" 161 | version = "1.4.1" 162 | 163 | [[package]] 164 | category = "dev" 165 | description = "Backported and Experimental Type Hints for Python 3.5+" 166 | name = "typing-extensions" 167 | optional = false 168 | python-versions = "*" 169 | version = "3.7.4.1" 170 | 171 | [metadata] 172 | content-hash = "336aabefbf5b3abc5a1336c46edcf7973f10ce0ef69f21c4b36f35b30c063c4c" 173 | python-versions = ">=3.6" 174 | 175 | [metadata.files] 176 | appdirs = [ 177 | {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, 178 | {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, 179 | ] 180 | attrs = [ 181 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 182 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 183 | ] 184 | black = [ 185 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 186 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 187 | ] 188 | click = [ 189 | {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, 190 | {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, 191 | ] 192 | dataclasses = [ 193 | {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, 194 | {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, 195 | ] 196 | fastapi = [ 197 | {file = "fastapi-0.52.0-py3-none-any.whl", hash = "sha256:532648b4e16dd33673d71dc0b35dff1b4d20c709d04078010e258b9f3a79771a"}, 198 | {file = "fastapi-0.52.0.tar.gz", hash = "sha256:721b11d8ffde52c669f52741b6d9d761fe2e98778586f4cfd6f5e47254ba5016"}, 199 | ] 200 | mypy = [ 201 | {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, 202 | {file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, 203 | {file = "mypy-0.761-cp35-cp35m-win_amd64.whl", hash = "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72"}, 204 | {file = "mypy-0.761-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2"}, 205 | {file = "mypy-0.761-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0"}, 206 | {file = "mypy-0.761-cp36-cp36m-win_amd64.whl", hash = "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474"}, 207 | {file = "mypy-0.761-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a"}, 208 | {file = "mypy-0.761-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749"}, 209 | {file = "mypy-0.761-cp37-cp37m-win_amd64.whl", hash = "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1"}, 210 | {file = "mypy-0.761-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7"}, 211 | {file = "mypy-0.761-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"}, 212 | {file = "mypy-0.761-cp38-cp38-win_amd64.whl", hash = "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b"}, 213 | {file = "mypy-0.761-py3-none-any.whl", hash = "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217"}, 214 | {file = "mypy-0.761.tar.gz", hash = "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf"}, 215 | ] 216 | mypy-extensions = [ 217 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 218 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 219 | ] 220 | pathspec = [ 221 | {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, 222 | {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"}, 223 | ] 224 | pydantic = [ 225 | {file = "pydantic-1.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:07911aab70f3bc52bb845ce1748569c5e70478ac977e106a150dd9d0465ebf04"}, 226 | {file = "pydantic-1.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752"}, 227 | {file = "pydantic-1.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:61d22d36808087d3184ed6ac0d91dd71c533b66addb02e4a9930e1e30833202f"}, 228 | {file = "pydantic-1.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f863456d3d4bf817f2e5248553dee3974c5dc796f48e6ddb599383570f4215ac"}, 229 | {file = "pydantic-1.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bbbed364376f4a0aebb9ea452ff7968b306499a9e74f4db69b28ff2cd4043a11"}, 230 | {file = "pydantic-1.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e27559cedbd7f59d2375bfd6eea29a330ea1a5b0589c34d6b4e0d7bec6027bbf"}, 231 | {file = "pydantic-1.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:50e4e948892a6815649ad5a9a9379ad1e5f090f17842ac206535dfaed75c6f2f"}, 232 | {file = "pydantic-1.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8848b4eb458469739126e4c1a202d723dd092e087f8dbe3104371335f87ba5df"}, 233 | {file = "pydantic-1.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:831a0265a9e3933b3d0f04d1a81bba543bafbe4119c183ff2771871db70524ab"}, 234 | {file = "pydantic-1.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:47b8db7024ba3d46c3d4768535e1cf87b6c8cf92ccd81e76f4e1cb8ee47688b3"}, 235 | {file = "pydantic-1.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:51f11c8bbf794a68086540da099aae4a9107447c7a9d63151edbb7d50110cf21"}, 236 | {file = "pydantic-1.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6100d7862371115c40be55cc4b8d766a74b1d0dbaf99dbfe72bb4bac0faf89ed"}, 237 | {file = "pydantic-1.4-py36.py37.py38-none-any.whl", hash = "sha256:72184c1421103cca128300120f8f1185fb42a9ea73a1c9845b1c53db8c026a7d"}, 238 | {file = "pydantic-1.4.tar.gz", hash = "sha256:f17ec336e64d4583311249fb179528e9a2c27c8a2eaf590ec6ec2c6dece7cb3f"}, 239 | ] 240 | regex = [ 241 | {file = "regex-2020.2.20-cp27-cp27m-win32.whl", hash = "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb"}, 242 | {file = "regex-2020.2.20-cp27-cp27m-win_amd64.whl", hash = "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74"}, 243 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400"}, 244 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0"}, 245 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc"}, 246 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0"}, 247 | {file = "regex-2020.2.20-cp36-cp36m-win32.whl", hash = "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69"}, 248 | {file = "regex-2020.2.20-cp36-cp36m-win_amd64.whl", hash = "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b"}, 249 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e"}, 250 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242"}, 251 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce"}, 252 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab"}, 253 | {file = "regex-2020.2.20-cp37-cp37m-win32.whl", hash = "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431"}, 254 | {file = "regex-2020.2.20-cp37-cp37m-win_amd64.whl", hash = "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1"}, 255 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_i686.whl", hash = "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045"}, 256 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26"}, 257 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2"}, 258 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70"}, 259 | {file = "regex-2020.2.20-cp38-cp38-win32.whl", hash = "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d"}, 260 | {file = "regex-2020.2.20-cp38-cp38-win_amd64.whl", hash = "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa"}, 261 | {file = "regex-2020.2.20.tar.gz", hash = "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5"}, 262 | ] 263 | starlette = [ 264 | {file = "starlette-0.13.2-py3-none-any.whl", hash = "sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b"}, 265 | {file = "starlette-0.13.2.tar.gz", hash = "sha256:a9bb130fa7aa736eda8a814b6ceb85ccf7a209ed53843d0d61e246b380afa10f"}, 266 | ] 267 | toml = [ 268 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 269 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 270 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 271 | ] 272 | typed-ast = [ 273 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 274 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 275 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 276 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 277 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 278 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 279 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 280 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 281 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 282 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 283 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 284 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 285 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 286 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 287 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 288 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 289 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 290 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 291 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 292 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 293 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 294 | ] 295 | typing-extensions = [ 296 | {file = "typing_extensions-3.7.4.1-py2-none-any.whl", hash = "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d"}, 297 | {file = "typing_extensions-3.7.4.1-py3-none-any.whl", hash = "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"}, 298 | {file = "typing_extensions-3.7.4.1.tar.gz", hash = "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2"}, 299 | ] 300 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi-security-telegram-webhook" 3 | version = "0.2.0" 4 | description = "" 5 | authors = ["Dima Boger "] 6 | license = "MIT" 7 | readme = 'README.md' 8 | repository = "https://github.com/piterpy-meetup/fastapi-security-telegram-webhook" 9 | homepage = "https://github.com/piterpy-meetup/fastapi-security-telegram-webhook" 10 | keywords = ['fastapi', 'fastapi security', 'telegram', 'telegram webhook', 'bot webhook'] 11 | include = ["fastapi_security_telegram_webhook/py.typed"] 12 | 13 | [tool.poetry.dependencies] 14 | python = ">=3.6" 15 | fastapi = "*" 16 | 17 | [tool.poetry.dev-dependencies] 18 | black = "^19.10b0" 19 | mypy = "^0.761" 20 | 21 | [build-system] 22 | requires = ["poetry>=0.12"] 23 | build-backend = "poetry.masonry.api" 24 | --------------------------------------------------------------------------------