├── .github ├── dependabot.yml └── workflows │ └── rebase.yml ├── .gitignore ├── .idea ├── FileConvertBot.iml ├── codeStyles │ └── codeStyleConfig.xml ├── inspectionProfiles │ ├── Custom.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── runConfigurations │ ├── fabric.xml │ └── main.xml ├── scopes │ └── Custom.xml └── vcs.xml ├── FileConvertBot.sublime-project ├── LICENSE ├── README.md ├── fabfile.py ├── fabfile_sample.cfg ├── images └── logo.png ├── invoke_patch.py ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── setup.cfg └── src ├── analytics.py ├── config_sample.cfg ├── constants.py ├── custom_logger.py ├── database.py ├── main.py ├── migrations ├── 001_nullable_telegram_username.py └── 002_dates_without_milliseconds.py ├── setup.cfg ├── telegram_utils.py └── utils.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: pip 6 | directory: '/' 7 | schedule: 8 | interval: daily 9 | open-pull-requests-limit: 1 10 | reviewers: 11 | - revolter 12 | assignees: 13 | - revolter 14 | ignore: 15 | - dependency-name: '*' 16 | update-types: ['version-update:semver-patch'] 17 | 18 | - package-ecosystem: github-actions 19 | directory: '/' 20 | schedule: 21 | interval: daily 22 | open-pull-requests-limit: 1 23 | reviewers: 24 | - revolter 25 | assignees: 26 | - revolter 27 | ignore: 28 | - dependency-name: '*' 29 | update-types: ['version-update:semver-patch'] 30 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | name: rebase 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | rebase: 9 | if: > 10 | github.event.issue.pull_request != '' && 11 | contains(github.event.comment.body, '/rebase') && ( 12 | github.event.comment.author_association == 'OWNER' || 13 | github.event.comment.author_association == 'COLLABORATOR' 14 | ) 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 🛎 20 | uses: actions/checkout@v2.4.0 21 | with: 22 | token: ${{ secrets.PAT_TOKEN }} 23 | fetch-depth: 0 24 | 25 | - name: Rebase ⤴️ 26 | uses: cirrus-actions/rebase@1.5 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/vim,macos,python,sublimetext,pycharm 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=vim,macos,python,sublimetext,pycharm 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Start of Icon[\r] pattern 11 | Icon[ 12 | ] 13 | # End of Icon[\r] pattern 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### PyCharm ### 35 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 36 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 37 | 38 | # User-specific stuff 39 | .idea/**/workspace.xml 40 | .idea/**/tasks.xml 41 | .idea/**/usage.statistics.xml 42 | .idea/**/dictionaries 43 | .idea/**/shelf 44 | 45 | # Generated files 46 | .idea/**/contentModel.xml 47 | 48 | # Sensitive or high-churn files 49 | .idea/**/dataSources/ 50 | .idea/**/dataSources.ids 51 | .idea/**/dataSources.local.xml 52 | .idea/**/sqlDataSources.xml 53 | .idea/**/dynamic.xml 54 | .idea/**/uiDesigner.xml 55 | .idea/**/dbnavigator.xml 56 | 57 | # Gradle 58 | .idea/**/gradle.xml 59 | .idea/**/libraries 60 | 61 | # Gradle and Maven with auto-import 62 | # When using Gradle or Maven with auto-import, you should exclude module files, 63 | # since they will be recreated, and may cause churn. Uncomment if using 64 | # auto-import. 65 | # .idea/artifacts 66 | # .idea/compiler.xml 67 | # .idea/jarRepositories.xml 68 | # .idea/modules.xml 69 | # .idea/*.iml 70 | # .idea/modules 71 | # *.iml 72 | # *.ipr 73 | 74 | # CMake 75 | cmake-build-*/ 76 | 77 | # Mongo Explorer plugin 78 | .idea/**/mongoSettings.xml 79 | 80 | # File-based project format 81 | *.iws 82 | 83 | # IntelliJ 84 | out/ 85 | 86 | # mpeltonen/sbt-idea plugin 87 | .idea_modules/ 88 | 89 | # JIRA plugin 90 | atlassian-ide-plugin.xml 91 | 92 | # Cursive Clojure plugin 93 | .idea/replstate.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | fabric.properties 100 | 101 | # Editor-based Rest Client 102 | .idea/httpRequests 103 | 104 | # Android studio 3.1+ serialized cache file 105 | .idea/caches/build_file_checksums.ser 106 | 107 | ### PyCharm Patch ### 108 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 109 | 110 | # *.iml 111 | # modules.xml 112 | # .idea/misc.xml 113 | # *.ipr 114 | 115 | # Sonarlint plugin 116 | .idea/**/sonarlint/ 117 | 118 | # SonarQube Plugin 119 | .idea/**/sonarIssues.xml 120 | 121 | # Markdown Navigator plugin 122 | .idea/**/markdown-navigator.xml 123 | .idea/**/markdown-navigator-enh.xml 124 | .idea/**/markdown-navigator/ 125 | 126 | # Cache file creation bug 127 | # See https://youtrack.jetbrains.com/issue/JBR-2257 128 | .idea/$CACHE_FILE$ 129 | 130 | ### Python ### 131 | # Byte-compiled / optimized / DLL files 132 | __pycache__/ 133 | *.py[cod] 134 | *$py.class 135 | 136 | # C extensions 137 | *.so 138 | 139 | # Distribution / packaging 140 | .Python 141 | build/ 142 | develop-eggs/ 143 | dist/ 144 | downloads/ 145 | eggs/ 146 | .eggs/ 147 | lib/ 148 | lib64/ 149 | parts/ 150 | sdist/ 151 | var/ 152 | wheels/ 153 | pip-wheel-metadata/ 154 | share/python-wheels/ 155 | *.egg-info/ 156 | .installed.cfg 157 | *.egg 158 | MANIFEST 159 | 160 | # PyInstaller 161 | # Usually these files are written by a python script from a template 162 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 163 | *.manifest 164 | *.spec 165 | 166 | # Installer logs 167 | pip-log.txt 168 | pip-delete-this-directory.txt 169 | 170 | # Unit test / coverage reports 171 | htmlcov/ 172 | .tox/ 173 | .nox/ 174 | .coverage 175 | .coverage.* 176 | .cache 177 | nosetests.xml 178 | coverage.xml 179 | *.cover 180 | *.py,cover 181 | .hypothesis/ 182 | .pytest_cache/ 183 | 184 | # Translations 185 | *.mo 186 | *.pot 187 | 188 | # Django stuff: 189 | *.log 190 | local_settings.py 191 | db.sqlite3 192 | db.sqlite3-journal 193 | 194 | # Flask stuff: 195 | instance/ 196 | .webassets-cache 197 | 198 | # Scrapy stuff: 199 | .scrapy 200 | 201 | # Sphinx documentation 202 | docs/_build/ 203 | 204 | # PyBuilder 205 | target/ 206 | 207 | # Jupyter Notebook 208 | .ipynb_checkpoints 209 | 210 | # IPython 211 | profile_default/ 212 | ipython_config.py 213 | 214 | # pyenv 215 | .python-version 216 | 217 | # pipenv 218 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 219 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 220 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 221 | # install all needed dependencies. 222 | #Pipfile.lock 223 | 224 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 225 | __pypackages__/ 226 | 227 | # Celery stuff 228 | celerybeat-schedule 229 | celerybeat.pid 230 | 231 | # SageMath parsed files 232 | *.sage.py 233 | 234 | # Environments 235 | .env 236 | .venv 237 | env/ 238 | venv/ 239 | ENV/ 240 | env.bak/ 241 | venv.bak/ 242 | 243 | # Spyder project settings 244 | .spyderproject 245 | .spyproject 246 | 247 | # Rope project settings 248 | .ropeproject 249 | 250 | # mkdocs documentation 251 | /site 252 | 253 | # mypy 254 | .mypy_cache/ 255 | .dmypy.json 256 | dmypy.json 257 | 258 | # Pyre type checker 259 | .pyre/ 260 | 261 | # pytype static type analyzer 262 | .pytype/ 263 | 264 | ### SublimeText ### 265 | # Cache files for Sublime Text 266 | *.tmlanguage.cache 267 | *.tmPreferences.cache 268 | *.stTheme.cache 269 | 270 | # Workspace files are user-specific 271 | *.sublime-workspace 272 | 273 | # Project files should be checked into the repository, unless a significant 274 | # proportion of contributors will probably not be using Sublime Text 275 | # *.sublime-project 276 | 277 | # SFTP configuration file 278 | sftp-config.json 279 | 280 | # Package control specific files 281 | Package Control.last-run 282 | Package Control.ca-list 283 | Package Control.ca-bundle 284 | Package Control.system-ca-bundle 285 | Package Control.cache/ 286 | Package Control.ca-certs/ 287 | Package Control.merged-ca-bundle 288 | Package Control.user-ca-bundle 289 | oscrypto-ca-bundle.crt 290 | bh_unicode_properties.cache 291 | 292 | # Sublime-github package stores a github token in this file 293 | # https://packagecontrol.io/packages/sublime-github 294 | GitHub.sublime-settings 295 | 296 | ### Vim ### 297 | # Swap 298 | [._]*.s[a-v][a-z] 299 | !*.svg # comment out if you don't need vector files 300 | [._]*.sw[a-p] 301 | [._]s[a-rt-v][a-z] 302 | [._]ss[a-gi-z] 303 | [._]sw[a-p] 304 | 305 | # Session 306 | Session.vim 307 | Sessionx.vim 308 | 309 | # Temporary 310 | .netrwhist 311 | *~ 312 | # Auto-generated tag files 313 | tags 314 | # Persistent undo 315 | [._]*.un~ 316 | 317 | # End of https://www.toptal.com/developers/gitignore/api/vim,macos,python,sublimetext,pycharm 318 | 319 | ### Custom ### 320 | # env 321 | env-dev/ 322 | 323 | # config 324 | config.cfg 325 | fabfile.cfg 326 | 327 | # backups 328 | backup_* 329 | 330 | # cache 331 | cache.sqlite 332 | 333 | # databases 334 | file_convert.sqlite 335 | -------------------------------------------------------------------------------- /.idea/FileConvertBot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Custom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/fabric.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /.idea/runConfigurations/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | -------------------------------------------------------------------------------- /.idea/scopes/Custom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /FileConvertBot.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File Convert Bot 2 | 3 | ## Introduction 4 | 5 | Telegram Bot that converts _(for now)_ AAC, OPUS, MP3 and WebM files to voice 6 | messages, HEVC and MP4 (MPEG4, VP6 and VP8) files to video messages or video 7 | notes (rounded ones), video messages to video notes (rounded ones), videos from 8 | some websites to video messages, PDF files to photo messages _(currently only 9 | the first page)_, image files to stickers. It also converts voice messages to 10 | MP3 files and stickers to photo messages. It works in groups too! 11 | 12 | The bot currently runs as [@FileConvertBot](https://t.me/FileConvertBot). 13 | 14 | **All the processing is done in-memory, so no file is ever saved on the disk, 15 | not even temporary!** 16 | 17 | ## Getting Started 18 | 19 | These instructions will get you a copy of the project up and running on your 20 | local machine for development and testing purposes. 21 | 22 | ### Prerequisites 23 | 24 | You need to install [Homebrew](https://brew.sh) by running: 25 | 26 | ```sh 27 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 28 | ``` 29 | 30 | ### Installing 31 | 32 | Install the global dependencies by running: 33 | 34 | ```sh 35 | sudo apt install ffmpeg poppler-utils 36 | ``` 37 | 38 | on Linux or 39 | 40 | ```sh 41 | brew install ffmpeg poppler 42 | ``` 43 | 44 | on macOS. 45 | 46 | Then clone the project and install the dependencies by running: 47 | 48 | ```sh 49 | cd /desired/location/path 50 | git clone https://github.com/revolter/FileConvertBot.git 51 | cd FileConvertBot 52 | 53 | curl https://pyenv.run | bash 54 | export PATH="$HOME/.pyenv/bin:$PATH" 55 | sudo apt update 56 | sudo apt install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev 57 | pyenv install 3.9.0 58 | pyenv global 3.9.0 59 | 60 | curl -sSL https://install.python-poetry.org | python - 61 | poetry shell 62 | poetry install 63 | 64 | cd src 65 | cp config_sample.cfg config.cfg 66 | ``` 67 | 68 | Then, edit the file named `config.cfg` inside the `src` folder with the correct 69 | values and run it using `./main.py --debug`. 70 | 71 | Use `exit` to close the virtual environment. 72 | 73 | ## Deploy 74 | 75 | You can easily deploy this to a cloud machine using 76 | [Fabric](http://fabfile.org): 77 | 78 | ``` 79 | cd /project/location/path 80 | 81 | poetry shell 82 | 83 | cp fabfile_sample.cfg fabfile.cfg 84 | ``` 85 | 86 | Then, edit the file named `fabfile.cfg` inside the root folder with the correct 87 | values and run Fabric using: 88 | 89 | ``` 90 | fab setup 91 | fab deploy 92 | ``` 93 | 94 | You can also deploy a single file using `fab deploy --filename=main.py` or `fab 95 | deploy --filename=pyproject.toml`. 96 | 97 | ## Dependencies 98 | 99 | Currently, you have to manually install `poppler` in order for `PDF` to `PNG` 100 | conversion to work: 101 | 102 | - macOS: `brew install poppler` 103 | - Ubuntu: `sudo apt-get install poppler-utils` 104 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import configparser 4 | import datetime 5 | import os 6 | import typing 7 | 8 | import fabric 9 | import invocations.console 10 | import invoke 11 | 12 | import invoke_patch 13 | import src.constants 14 | 15 | 16 | class GlobalConfig: 17 | host: str 18 | user: str 19 | key_filename: str 20 | project_name: str 21 | project_path: str 22 | 23 | source_filenames = [ 24 | 'main.py', 25 | 'database.py', 26 | 'utils.py', 27 | 'telegram_utils.py', 28 | 'analytics.py', 29 | 'constants.py', 30 | 31 | 'custom_logger.py', 32 | 33 | 'config.cfg' 34 | ] 35 | meta_filenames = [ 36 | 'pyproject.toml', 37 | 'poetry.lock' 38 | ] 39 | source_directories = [ 40 | 'migrations' 41 | ] 42 | 43 | @classmethod 44 | def load(cls) -> None: 45 | try: 46 | fabfile_config = configparser.ConfigParser() 47 | 48 | fabfile_config.read('fabfile.cfg') 49 | 50 | cls.host = fabfile_config.get('Fabric', 'Host') 51 | cls.user = fabfile_config.get('Fabric', 'User') 52 | cls.key_filename = os.path.expanduser(fabfile_config.get('Fabric', 'KeyFilename')) 53 | cls.project_name = fabfile_config.get('Fabric', 'ProjectName') 54 | cls.project_path = fabfile_config.get('Fabric', 'ProjectPath') 55 | except configparser.Error as error: 56 | raise invoke.Exit( 57 | message=f'Config error: {error}', 58 | code=1 59 | ) 60 | 61 | 62 | invoke_patch.fix_annotations() 63 | GlobalConfig.load() 64 | 65 | 66 | @fabric.task 67 | def configure(connection: fabric.Connection) -> None: 68 | connection.user = GlobalConfig.user 69 | connection.inline_ssh_env = True 70 | connection.connect_kwargs.key_filename = GlobalConfig.key_filename 71 | 72 | 73 | @fabric.task(pre=[configure], hosts=[GlobalConfig.host], help={'command': 'The shell command to execute on the server', 'env': 'An optional dictionary with environment variables'}) 74 | def execute(connection: fabric.Connection, command: str, env: typing.Dict[str, str] = None) -> None: 75 | if not command: 76 | return 77 | 78 | connection.run(command, env=env) 79 | 80 | 81 | @fabric.task(pre=[configure], hosts=[GlobalConfig.host]) 82 | def cleanup(connection: fabric.Connection) -> None: 83 | question = f'Are you sure you want to completely delete the project "{GlobalConfig.project_name}" from "{GlobalConfig.host}"?' 84 | 85 | if invocations.console.confirm( 86 | question=question, 87 | assume_yes=False 88 | ): 89 | execute(connection, f'rm -rf {GlobalConfig.project_name}') 90 | execute(connection, f'rm -rf {GlobalConfig.project_path}/{GlobalConfig.project_name}') 91 | 92 | 93 | @fabric.task(pre=[configure, cleanup], hosts=[GlobalConfig.host]) 94 | def setup(connection: fabric.Connection) -> None: 95 | execute(connection, f'mkdir -p {GlobalConfig.project_path}/{GlobalConfig.project_name}') 96 | execute(connection, f'ln -s {GlobalConfig.project_path}/{GlobalConfig.project_name} {GlobalConfig.project_name}') 97 | 98 | execute(connection, 'curl -sSL https://install.python-poetry.org | python -') 99 | 100 | 101 | @fabric.task(pre=[configure], hosts=[GlobalConfig.host], help={'filename': 'An optional filename to deploy to the server'}) 102 | def upload(connection: fabric.Connection, filename: typing.Optional[str] = None) -> None: 103 | def upload_file(file_format: str, file_name: str, destination_path_format='{.project_name}/{}') -> None: 104 | connection.put(file_format.format(file_name), destination_path_format.format(GlobalConfig, file_name)) 105 | 106 | def upload_directory(directory_name: str) -> None: 107 | execute(connection, f'mkdir -p {GlobalConfig.project_name}/{directory_name}') 108 | 109 | for _root, _directories, files in os.walk(f'src/{directory_name}'): 110 | for file in files: 111 | upload_file(f'src/{directory_name}/{{}}', file, f'{{.project_name}}/{directory_name}/{{}}') 112 | 113 | if filename: 114 | if filename in GlobalConfig.source_directories: 115 | upload_directory(filename) 116 | else: 117 | if filename in GlobalConfig.source_filenames: 118 | file_path_format = 'src/{}' 119 | elif filename in GlobalConfig.meta_filenames: 120 | file_path_format = '{}' 121 | else: 122 | raise invoke.ParseError(f'Filename "{filename}" is not registered') 123 | 124 | upload_file(file_path_format, filename) 125 | else: 126 | for name in GlobalConfig.source_filenames: 127 | upload_file('src/{}', name) 128 | 129 | for name in GlobalConfig.meta_filenames: 130 | upload_file('{}', name) 131 | 132 | for directory in GlobalConfig.source_directories: 133 | upload_directory(directory) 134 | 135 | 136 | @fabric.task(pre=[configure], hosts=[GlobalConfig.host], help={'filename': 'An optional filename to deploy to the server'}) 137 | def deploy(connection: fabric.Connection, filename: typing.Optional[str] = None) -> None: 138 | upload(connection, filename) 139 | 140 | with connection.cd(GlobalConfig.project_name): 141 | execute(connection, 'eval "$(pyenv init --path)" && poetry install --no-dev', { 142 | 'PATH': '$HOME/.pyenv/bin:$HOME/.poetry/bin:$PATH' 143 | }) 144 | 145 | 146 | @fabric.task(pre=[configure], hosts=[GlobalConfig.host], help={'filename': 'The filename to backup locally from the server'}) 147 | def backup(connection: fabric.Connection, filename: str) -> None: 148 | current_date = datetime.datetime.now().strftime(src.constants.GENERIC_DATE_FORMAT) 149 | name, extension = os.path.splitext(filename) 150 | 151 | with connection.cd(GlobalConfig.project_name): 152 | connection.get(f'{GlobalConfig.project_name}/{filename}', f'backup_{name}_{current_date}{extension}') 153 | 154 | 155 | @fabric.task(pre=[configure], hosts=[GlobalConfig.host]) 156 | def backup_db(context: fabric.Connection) -> None: 157 | backup(context, 'file_convert.sqlite') 158 | -------------------------------------------------------------------------------- /fabfile_sample.cfg: -------------------------------------------------------------------------------- 1 | [Fabric] 2 | Host: 1.2.3.4 3 | User: root 4 | KeyFilename = ~/.ssh/server.pem 5 | ProjectName: BotUserName 6 | ProjectPath: ~/Public/Telegram 7 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revolter/FileConvertBot/7bcc2c7bf06542674f1f5c5c922208f31ccf18bd/images/logo.png -------------------------------------------------------------------------------- /invoke_patch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import inspect 4 | import types 5 | import typing 6 | import unittest.mock 7 | 8 | import invoke 9 | 10 | 11 | def fix_annotations() -> None: 12 | """ 13 | Copied from https://github.com/pyinvoke/invoke/issues/357#issuecomment-583851322. 14 | """ 15 | 16 | def patched_inspect_getargspec(function: types.FunctionType) -> inspect.ArgSpec: 17 | spec = inspect.getfullargspec(function) 18 | 19 | return inspect.ArgSpec( 20 | args=spec.args, 21 | varargs=spec.varargs, 22 | keywords=spec.varkw, 23 | defaults=spec.defaults or () 24 | ) 25 | 26 | original_task_argspec = invoke.tasks.Task.argspec 27 | 28 | def patched_task_argspec(*args: typing.Any, **kwargs: typing.Any) -> None: 29 | with unittest.mock.patch(target="inspect.getargspec", new=patched_inspect_getargspec): 30 | return original_task_argspec(*args, **kwargs) 31 | 32 | invoke.tasks.Task.argspec = patched_task_argspec 33 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | show_error_codes = True 4 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alabaster" 3 | version = "0.7.12" 4 | description = "A configurable sidebar-enabled Sphinx theme" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "apscheduler" 11 | version = "3.6.3" 12 | description = "In-process task scheduler with Cron-like capabilities" 13 | category = "main" 14 | optional = false 15 | python-versions = "*" 16 | 17 | [package.dependencies] 18 | pytz = "*" 19 | six = ">=1.4.0" 20 | tzlocal = ">=1.2" 21 | 22 | [package.extras] 23 | asyncio = ["trollius"] 24 | doc = ["sphinx", "sphinx-rtd-theme"] 25 | gevent = ["gevent"] 26 | mongodb = ["pymongo (>=2.8)"] 27 | redis = ["redis (>=3.0)"] 28 | rethinkdb = ["rethinkdb (>=2.4.0)"] 29 | sqlalchemy = ["sqlalchemy (>=0.8)"] 30 | testing = ["pytest", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"] 31 | tornado = ["tornado (>=4.3)"] 32 | twisted = ["twisted"] 33 | zookeeper = ["kazoo"] 34 | 35 | [[package]] 36 | name = "babel" 37 | version = "2.9.1" 38 | description = "Internationalization utilities" 39 | category = "dev" 40 | optional = false 41 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 42 | 43 | [package.dependencies] 44 | pytz = ">=2015.7" 45 | 46 | [[package]] 47 | name = "bcrypt" 48 | version = "3.2.0" 49 | description = "Modern password hashing for your software and your servers" 50 | category = "dev" 51 | optional = false 52 | python-versions = ">=3.6" 53 | 54 | [package.dependencies] 55 | cffi = ">=1.1" 56 | six = ">=1.4.1" 57 | 58 | [package.extras] 59 | tests = ["pytest (>=3.2.1,!=3.3.0)"] 60 | typecheck = ["mypy"] 61 | 62 | [[package]] 63 | name = "bleach" 64 | version = "4.1.0" 65 | description = "An easy safelist-based HTML-sanitizing tool." 66 | category = "dev" 67 | optional = false 68 | python-versions = ">=3.6" 69 | 70 | [package.dependencies] 71 | packaging = "*" 72 | six = ">=1.9.0" 73 | webencodings = "*" 74 | 75 | [[package]] 76 | name = "blessings" 77 | version = "1.7" 78 | description = "A thin, practical wrapper around terminal coloring, styling, and positioning" 79 | category = "dev" 80 | optional = false 81 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 82 | 83 | [package.dependencies] 84 | six = "*" 85 | 86 | [[package]] 87 | name = "cachetools" 88 | version = "4.2.2" 89 | description = "Extensible memoizing collections and decorators" 90 | category = "main" 91 | optional = false 92 | python-versions = "~=3.5" 93 | 94 | [[package]] 95 | name = "certifi" 96 | version = "2020.11.8" 97 | description = "Python package for providing Mozilla's CA Bundle." 98 | category = "main" 99 | optional = false 100 | python-versions = "*" 101 | 102 | [[package]] 103 | name = "cffi" 104 | version = "1.14.4" 105 | description = "Foreign Function Interface for Python calling C code." 106 | category = "dev" 107 | optional = false 108 | python-versions = "*" 109 | 110 | [package.dependencies] 111 | pycparser = "*" 112 | 113 | [[package]] 114 | name = "chardet" 115 | version = "3.0.4" 116 | description = "Universal encoding detector for Python 2 and 3" 117 | category = "main" 118 | optional = false 119 | python-versions = "*" 120 | 121 | [[package]] 122 | name = "click" 123 | version = "7.1.2" 124 | description = "Composable command line interface toolkit" 125 | category = "main" 126 | optional = false 127 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 128 | 129 | [[package]] 130 | name = "colorama" 131 | version = "0.4.4" 132 | description = "Cross-platform colored terminal text." 133 | category = "dev" 134 | optional = false 135 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 136 | 137 | [[package]] 138 | name = "cryptography" 139 | version = "3.3.2" 140 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 141 | category = "dev" 142 | optional = false 143 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" 144 | 145 | [package.dependencies] 146 | cffi = ">=1.12" 147 | six = ">=1.4.1" 148 | 149 | [package.extras] 150 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 151 | docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] 152 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 153 | ssh = ["bcrypt (>=3.1.5)"] 154 | test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] 155 | 156 | [[package]] 157 | name = "docutils" 158 | version = "0.16" 159 | description = "Docutils -- Python Documentation Utilities" 160 | category = "dev" 161 | optional = false 162 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 163 | 164 | [[package]] 165 | name = "fabric" 166 | version = "2.5.0" 167 | description = "High level SSH command execution" 168 | category = "dev" 169 | optional = false 170 | python-versions = "*" 171 | 172 | [package.dependencies] 173 | invoke = ">=1.3,<2.0" 174 | paramiko = ">=2.4" 175 | 176 | [package.extras] 177 | pytest = ["mock (>=2.0.0,<3.0)", "pytest (>=3.2.5,<4.0)"] 178 | testing = ["mock (>=2.0.0,<3.0)"] 179 | 180 | [[package]] 181 | name = "ffmpeg-python" 182 | version = "0.2.0" 183 | description = "Python bindings for FFmpeg - with complex filtering support" 184 | category = "main" 185 | optional = false 186 | python-versions = "*" 187 | 188 | [package.dependencies] 189 | future = "*" 190 | 191 | [package.extras] 192 | dev = ["future (==0.17.1)", "numpy (==1.16.4)", "pytest-mock (==1.10.4)", "pytest (==4.6.1)", "Sphinx (==2.1.0)", "tox (==3.12.1)"] 193 | 194 | [[package]] 195 | name = "future" 196 | version = "0.18.2" 197 | description = "Clean single-source support for Python 3 and 2" 198 | category = "main" 199 | optional = false 200 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 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 = "imagesize" 212 | version = "1.2.0" 213 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 214 | category = "dev" 215 | optional = false 216 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 217 | 218 | [[package]] 219 | name = "importlib-metadata" 220 | version = "4.8.2" 221 | description = "Read metadata from Python packages" 222 | category = "dev" 223 | optional = false 224 | python-versions = ">=3.6" 225 | 226 | [package.dependencies] 227 | zipp = ">=0.5" 228 | 229 | [package.extras] 230 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 231 | perf = ["ipython"] 232 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 233 | 234 | [[package]] 235 | name = "invocations" 236 | version = "2.3.0" 237 | description = "Common/best-practice Invoke tasks and collections" 238 | category = "dev" 239 | optional = false 240 | python-versions = "*" 241 | 242 | [package.dependencies] 243 | blessings = ">=1.6,<2" 244 | invoke = ">=1.6,<2.0" 245 | releases = ">=1.6,<2" 246 | semantic-version = ">=2.4,<2.7" 247 | tabulate = "0.7.5" 248 | tqdm = ">=4.8.1" 249 | twine = ">=1.15" 250 | 251 | [[package]] 252 | name = "invoke" 253 | version = "1.6.0" 254 | description = "Pythonic task execution" 255 | category = "dev" 256 | optional = false 257 | python-versions = "*" 258 | 259 | [[package]] 260 | name = "jeepney" 261 | version = "0.7.1" 262 | description = "Low-level, pure Python DBus protocol wrapper." 263 | category = "dev" 264 | optional = false 265 | python-versions = ">=3.6" 266 | 267 | [package.extras] 268 | test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] 269 | trio = ["trio", "async-generator"] 270 | 271 | [[package]] 272 | name = "jinja2" 273 | version = "2.11.3" 274 | description = "A very fast and expressive template engine." 275 | category = "dev" 276 | optional = false 277 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 278 | 279 | [package.dependencies] 280 | MarkupSafe = ">=0.23" 281 | 282 | [package.extras] 283 | i18n = ["Babel (>=0.8)"] 284 | 285 | [[package]] 286 | name = "keyring" 287 | version = "23.2.1" 288 | description = "Store and access your passwords safely." 289 | category = "dev" 290 | optional = false 291 | python-versions = ">=3.6" 292 | 293 | [package.dependencies] 294 | importlib-metadata = ">=3.6" 295 | jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} 296 | pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} 297 | SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} 298 | 299 | [package.extras] 300 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 301 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] 302 | 303 | [[package]] 304 | name = "markupsafe" 305 | version = "1.1.1" 306 | description = "Safely add untrusted strings to HTML/XML markup." 307 | category = "dev" 308 | optional = false 309 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 310 | 311 | [[package]] 312 | name = "mutagen" 313 | version = "1.45.1" 314 | description = "read and write audio tags for many formats" 315 | category = "main" 316 | optional = false 317 | python-versions = ">=3.5, <4" 318 | 319 | [[package]] 320 | name = "mypy" 321 | version = "0.790" 322 | description = "Optional static typing for Python" 323 | category = "dev" 324 | optional = false 325 | python-versions = ">=3.5" 326 | 327 | [package.dependencies] 328 | mypy-extensions = ">=0.4.3,<0.5.0" 329 | typed-ast = ">=1.4.0,<1.5.0" 330 | typing-extensions = ">=3.7.4" 331 | 332 | [package.extras] 333 | dmypy = ["psutil (>=4.0)"] 334 | 335 | [[package]] 336 | name = "mypy-extensions" 337 | version = "0.4.3" 338 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 339 | category = "dev" 340 | optional = false 341 | python-versions = "*" 342 | 343 | [[package]] 344 | name = "packaging" 345 | version = "20.7" 346 | description = "Core utilities for Python packages" 347 | category = "dev" 348 | optional = false 349 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 350 | 351 | [package.dependencies] 352 | pyparsing = ">=2.0.2" 353 | 354 | [[package]] 355 | name = "paramiko" 356 | version = "2.7.2" 357 | description = "SSH2 protocol library" 358 | category = "dev" 359 | optional = false 360 | python-versions = "*" 361 | 362 | [package.dependencies] 363 | bcrypt = ">=3.1.3" 364 | cryptography = ">=2.5" 365 | pynacl = ">=1.0.1" 366 | 367 | [package.extras] 368 | all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] 369 | ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] 370 | gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] 371 | invoke = ["invoke (>=1.3)"] 372 | 373 | [[package]] 374 | name = "pdf2image" 375 | version = "1.15.1" 376 | description = "A wrapper around the pdftoppm and pdftocairo command line tools to convert PDF to a PIL Image list." 377 | category = "main" 378 | optional = false 379 | python-versions = "*" 380 | 381 | [package.dependencies] 382 | pillow = "*" 383 | 384 | [[package]] 385 | name = "peewee" 386 | version = "3.14.0" 387 | description = "a little orm" 388 | category = "main" 389 | optional = false 390 | python-versions = "*" 391 | 392 | [[package]] 393 | name = "peewee-migrate" 394 | version = "1.4.7" 395 | description = "A simple migration engine for Peewee ORM" 396 | category = "main" 397 | optional = false 398 | python-versions = ">=3.7" 399 | 400 | [package.dependencies] 401 | click = ">=6.7" 402 | peewee = ">=3.3.3" 403 | 404 | [package.extras] 405 | build = ["bump2version", "wheel"] 406 | tests = ["pytest", "pytest-mypy", "psycopg2-binary"] 407 | 408 | [[package]] 409 | name = "pillow" 410 | version = "8.3.2" 411 | description = "Python Imaging Library (Fork)" 412 | category = "main" 413 | optional = false 414 | python-versions = ">=3.6" 415 | 416 | [[package]] 417 | name = "pkginfo" 418 | version = "1.8.1" 419 | description = "Query metadatdata from sdists / bdists / installed packages." 420 | category = "dev" 421 | optional = false 422 | python-versions = "*" 423 | 424 | [package.extras] 425 | testing = ["nose", "coverage"] 426 | 427 | [[package]] 428 | name = "pycparser" 429 | version = "2.20" 430 | description = "C parser in Python" 431 | category = "dev" 432 | optional = false 433 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 434 | 435 | [[package]] 436 | name = "pycryptodomex" 437 | version = "3.11.0" 438 | description = "Cryptographic library for Python" 439 | category = "main" 440 | optional = false 441 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 442 | 443 | [[package]] 444 | name = "pygments" 445 | version = "2.7.4" 446 | description = "Pygments is a syntax highlighting package written in Python." 447 | category = "dev" 448 | optional = false 449 | python-versions = ">=3.5" 450 | 451 | [[package]] 452 | name = "pynacl" 453 | version = "1.4.0" 454 | description = "Python binding to the Networking and Cryptography (NaCl) library" 455 | category = "dev" 456 | optional = false 457 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 458 | 459 | [package.dependencies] 460 | cffi = ">=1.4.1" 461 | six = "*" 462 | 463 | [package.extras] 464 | docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] 465 | tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] 466 | 467 | [[package]] 468 | name = "pyparsing" 469 | version = "2.4.7" 470 | description = "Python parsing module" 471 | category = "dev" 472 | optional = false 473 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 474 | 475 | [[package]] 476 | name = "python-telegram-bot" 477 | version = "13.6" 478 | description = "We have made you a wrapper you can't refuse" 479 | category = "main" 480 | optional = false 481 | python-versions = ">=3.6" 482 | 483 | [package.dependencies] 484 | APScheduler = "3.6.3" 485 | cachetools = "4.2.2" 486 | certifi = "*" 487 | pytz = ">=2018.6" 488 | tornado = ">=6.1" 489 | 490 | [package.extras] 491 | json = ["ujson"] 492 | passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"] 493 | socks = ["pysocks"] 494 | 495 | [[package]] 496 | name = "pytz" 497 | version = "2020.4" 498 | description = "World timezone definitions, modern and historical" 499 | category = "main" 500 | optional = false 501 | python-versions = "*" 502 | 503 | [[package]] 504 | name = "pywin32-ctypes" 505 | version = "0.2.0" 506 | description = "" 507 | category = "dev" 508 | optional = false 509 | python-versions = "*" 510 | 511 | [[package]] 512 | name = "readme-renderer" 513 | version = "30.0" 514 | description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" 515 | category = "dev" 516 | optional = false 517 | python-versions = "*" 518 | 519 | [package.dependencies] 520 | bleach = ">=2.1.0" 521 | docutils = ">=0.13.1" 522 | Pygments = ">=2.5.1" 523 | 524 | [package.extras] 525 | md = ["cmarkgfm (>=0.5.0,<0.7.0)"] 526 | 527 | [[package]] 528 | name = "releases" 529 | version = "1.6.3" 530 | description = "A Sphinx extension for changelog manipulation" 531 | category = "dev" 532 | optional = false 533 | python-versions = "*" 534 | 535 | [package.dependencies] 536 | semantic-version = "<2.7" 537 | sphinx = ">=1.3" 538 | 539 | [[package]] 540 | name = "requests" 541 | version = "2.25.0" 542 | description = "Python HTTP for Humans." 543 | category = "main" 544 | optional = false 545 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 546 | 547 | [package.dependencies] 548 | certifi = ">=2017.4.17" 549 | chardet = ">=3.0.2,<4" 550 | idna = ">=2.5,<3" 551 | urllib3 = ">=1.21.1,<1.27" 552 | 553 | [package.extras] 554 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 555 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 556 | 557 | [[package]] 558 | name = "requests-toolbelt" 559 | version = "0.9.1" 560 | description = "A utility belt for advanced users of python-requests" 561 | category = "dev" 562 | optional = false 563 | python-versions = "*" 564 | 565 | [package.dependencies] 566 | requests = ">=2.0.1,<3.0.0" 567 | 568 | [[package]] 569 | name = "rfc3986" 570 | version = "1.5.0" 571 | description = "Validating URI References per RFC 3986" 572 | category = "dev" 573 | optional = false 574 | python-versions = "*" 575 | 576 | [package.extras] 577 | idna2008 = ["idna"] 578 | 579 | [[package]] 580 | name = "secretstorage" 581 | version = "3.3.1" 582 | description = "Python bindings to FreeDesktop.org Secret Service API" 583 | category = "dev" 584 | optional = false 585 | python-versions = ">=3.6" 586 | 587 | [package.dependencies] 588 | cryptography = ">=2.0" 589 | jeepney = ">=0.6" 590 | 591 | [[package]] 592 | name = "semantic-version" 593 | version = "2.6.0" 594 | description = "A library implementing the 'SemVer' scheme." 595 | category = "dev" 596 | optional = false 597 | python-versions = "*" 598 | 599 | [[package]] 600 | name = "six" 601 | version = "1.15.0" 602 | description = "Python 2 and 3 compatibility utilities" 603 | category = "main" 604 | optional = false 605 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 606 | 607 | [[package]] 608 | name = "snowballstemmer" 609 | version = "2.0.0" 610 | description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." 611 | category = "dev" 612 | optional = false 613 | python-versions = "*" 614 | 615 | [[package]] 616 | name = "sphinx" 617 | version = "3.3.1" 618 | description = "Python documentation generator" 619 | category = "dev" 620 | optional = false 621 | python-versions = ">=3.5" 622 | 623 | [package.dependencies] 624 | alabaster = ">=0.7,<0.8" 625 | babel = ">=1.3" 626 | colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} 627 | docutils = ">=0.12" 628 | imagesize = "*" 629 | Jinja2 = ">=2.3" 630 | packaging = "*" 631 | Pygments = ">=2.0" 632 | requests = ">=2.5.0" 633 | snowballstemmer = ">=1.1" 634 | sphinxcontrib-applehelp = "*" 635 | sphinxcontrib-devhelp = "*" 636 | sphinxcontrib-htmlhelp = "*" 637 | sphinxcontrib-jsmath = "*" 638 | sphinxcontrib-qthelp = "*" 639 | sphinxcontrib-serializinghtml = "*" 640 | 641 | [package.extras] 642 | docs = ["sphinxcontrib-websupport"] 643 | lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.790)", "docutils-stubs"] 644 | test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] 645 | 646 | [[package]] 647 | name = "sphinxcontrib-applehelp" 648 | version = "1.0.2" 649 | description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" 650 | category = "dev" 651 | optional = false 652 | python-versions = ">=3.5" 653 | 654 | [package.extras] 655 | lint = ["flake8", "mypy", "docutils-stubs"] 656 | test = ["pytest"] 657 | 658 | [[package]] 659 | name = "sphinxcontrib-devhelp" 660 | version = "1.0.2" 661 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." 662 | category = "dev" 663 | optional = false 664 | python-versions = ">=3.5" 665 | 666 | [package.extras] 667 | lint = ["flake8", "mypy", "docutils-stubs"] 668 | test = ["pytest"] 669 | 670 | [[package]] 671 | name = "sphinxcontrib-htmlhelp" 672 | version = "1.0.3" 673 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 674 | category = "dev" 675 | optional = false 676 | python-versions = ">=3.5" 677 | 678 | [package.extras] 679 | lint = ["flake8", "mypy", "docutils-stubs"] 680 | test = ["pytest", "html5lib"] 681 | 682 | [[package]] 683 | name = "sphinxcontrib-jsmath" 684 | version = "1.0.1" 685 | description = "A sphinx extension which renders display math in HTML via JavaScript" 686 | category = "dev" 687 | optional = false 688 | python-versions = ">=3.5" 689 | 690 | [package.extras] 691 | test = ["pytest", "flake8", "mypy"] 692 | 693 | [[package]] 694 | name = "sphinxcontrib-qthelp" 695 | version = "1.0.3" 696 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." 697 | category = "dev" 698 | optional = false 699 | python-versions = ">=3.5" 700 | 701 | [package.extras] 702 | lint = ["flake8", "mypy", "docutils-stubs"] 703 | test = ["pytest"] 704 | 705 | [[package]] 706 | name = "sphinxcontrib-serializinghtml" 707 | version = "1.1.4" 708 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." 709 | category = "dev" 710 | optional = false 711 | python-versions = ">=3.5" 712 | 713 | [package.extras] 714 | lint = ["flake8", "mypy", "docutils-stubs"] 715 | test = ["pytest"] 716 | 717 | [[package]] 718 | name = "tabulate" 719 | version = "0.7.5" 720 | description = "Pretty-print tabular data" 721 | category = "dev" 722 | optional = false 723 | python-versions = "*" 724 | 725 | [[package]] 726 | name = "tornado" 727 | version = "6.1" 728 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 729 | category = "main" 730 | optional = false 731 | python-versions = ">= 3.5" 732 | 733 | [[package]] 734 | name = "tqdm" 735 | version = "4.54.0" 736 | description = "Fast, Extensible Progress Meter" 737 | category = "dev" 738 | optional = false 739 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 740 | 741 | [package.extras] 742 | dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown", "wheel"] 743 | 744 | [[package]] 745 | name = "twine" 746 | version = "3.6.0" 747 | description = "Collection of utilities for publishing packages on PyPI" 748 | category = "dev" 749 | optional = false 750 | python-versions = ">=3.6" 751 | 752 | [package.dependencies] 753 | colorama = ">=0.4.3" 754 | importlib-metadata = ">=3.6" 755 | keyring = ">=15.1" 756 | pkginfo = ">=1.4.2" 757 | readme-renderer = ">=21.0" 758 | requests = ">=2.20" 759 | requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" 760 | rfc3986 = ">=1.4.0" 761 | tqdm = ">=4.14" 762 | 763 | [[package]] 764 | name = "typed-ast" 765 | version = "1.4.1" 766 | description = "a fork of Python 2 and 3 ast modules with type comment support" 767 | category = "dev" 768 | optional = false 769 | python-versions = "*" 770 | 771 | [[package]] 772 | name = "typing-extensions" 773 | version = "3.7.4.3" 774 | description = "Backported and Experimental Type Hints for Python 3.5+" 775 | category = "dev" 776 | optional = false 777 | python-versions = "*" 778 | 779 | [[package]] 780 | name = "tzlocal" 781 | version = "2.1" 782 | description = "tzinfo object for the local timezone" 783 | category = "main" 784 | optional = false 785 | python-versions = "*" 786 | 787 | [package.dependencies] 788 | pytz = "*" 789 | 790 | [[package]] 791 | name = "urllib3" 792 | version = "1.26.5" 793 | description = "HTTP library with thread-safe connection pooling, file post, and more." 794 | category = "main" 795 | optional = false 796 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 797 | 798 | [package.extras] 799 | brotli = ["brotlipy (>=0.6.0)"] 800 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 801 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 802 | 803 | [[package]] 804 | name = "webencodings" 805 | version = "0.5.1" 806 | description = "Character encoding aliases for legacy web content" 807 | category = "dev" 808 | optional = false 809 | python-versions = "*" 810 | 811 | [[package]] 812 | name = "websockets" 813 | version = "10.1" 814 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 815 | category = "main" 816 | optional = false 817 | python-versions = ">=3.7" 818 | 819 | [[package]] 820 | name = "yt-dlp" 821 | version = "2021.11.10.1" 822 | description = "A youtube-dl fork with additional features and patches" 823 | category = "main" 824 | optional = false 825 | python-versions = ">=3.6" 826 | 827 | [package.dependencies] 828 | mutagen = "*" 829 | pycryptodomex = "*" 830 | websockets = "*" 831 | 832 | [[package]] 833 | name = "zipp" 834 | version = "3.6.0" 835 | description = "Backport of pathlib-compatible object wrapper for zip files" 836 | category = "dev" 837 | optional = false 838 | python-versions = ">=3.6" 839 | 840 | [package.extras] 841 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 842 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 843 | 844 | [metadata] 845 | lock-version = "1.1" 846 | python-versions = "^3.8" 847 | content-hash = "17ff5c13e782117f5ea9922ef6077ddfe25b3323139f0675a8af32187a0658e5" 848 | 849 | [metadata.files] 850 | alabaster = [ 851 | {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, 852 | {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, 853 | ] 854 | apscheduler = [ 855 | {file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"}, 856 | {file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"}, 857 | ] 858 | babel = [ 859 | {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, 860 | {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, 861 | ] 862 | bcrypt = [ 863 | {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, 864 | {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, 865 | {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, 866 | {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, 867 | {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, 868 | {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, 869 | {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, 870 | ] 871 | bleach = [ 872 | {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, 873 | {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, 874 | ] 875 | blessings = [ 876 | {file = "blessings-1.7-py2-none-any.whl", hash = "sha256:caad5211e7ba5afe04367cdd4cfc68fa886e2e08f6f35e76b7387d2109ccea6e"}, 877 | {file = "blessings-1.7-py3-none-any.whl", hash = "sha256:b1fdd7e7a675295630f9ae71527a8ebc10bfefa236b3d6aa4932ee4462c17ba3"}, 878 | {file = "blessings-1.7.tar.gz", hash = "sha256:98e5854d805f50a5b58ac2333411b0482516a8210f23f43308baeb58d77c157d"}, 879 | ] 880 | cachetools = [ 881 | {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, 882 | {file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"}, 883 | ] 884 | certifi = [ 885 | {file = "certifi-2020.11.8-py2.py3-none-any.whl", hash = "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd"}, 886 | {file = "certifi-2020.11.8.tar.gz", hash = "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"}, 887 | ] 888 | cffi = [ 889 | {file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"}, 890 | {file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"}, 891 | {file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"}, 892 | {file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"}, 893 | {file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"}, 894 | {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"}, 895 | {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"}, 896 | {file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"}, 897 | {file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"}, 898 | {file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"}, 899 | {file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"}, 900 | {file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"}, 901 | {file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"}, 902 | {file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"}, 903 | {file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"}, 904 | {file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"}, 905 | {file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"}, 906 | {file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"}, 907 | {file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"}, 908 | {file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"}, 909 | {file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"}, 910 | {file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"}, 911 | {file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"}, 912 | {file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"}, 913 | {file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"}, 914 | {file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"}, 915 | {file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"}, 916 | {file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"}, 917 | {file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"}, 918 | {file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"}, 919 | {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, 920 | {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, 921 | {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, 922 | {file = "cffi-1.14.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e"}, 923 | {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, 924 | {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, 925 | {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, 926 | ] 927 | chardet = [ 928 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 929 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 930 | ] 931 | click = [ 932 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 933 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 934 | ] 935 | colorama = [ 936 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 937 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 938 | ] 939 | cryptography = [ 940 | {file = "cryptography-3.3.2-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:541dd758ad49b45920dda3b5b48c968f8b2533d8981bcdb43002798d8f7a89ed"}, 941 | {file = "cryptography-3.3.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:49570438e60f19243e7e0d504527dd5fe9b4b967b5a1ff21cc12b57602dd85d3"}, 942 | {file = "cryptography-3.3.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a4ac9648d39ce71c2f63fe7dc6db144b9fa567ddfc48b9fde1b54483d26042"}, 943 | {file = "cryptography-3.3.2-cp27-cp27m-win32.whl", hash = "sha256:aa4969f24d536ae2268c902b2c3d62ab464b5a66bcb247630d208a79a8098e9b"}, 944 | {file = "cryptography-3.3.2-cp27-cp27m-win_amd64.whl", hash = "sha256:1bd0ccb0a1ed775cd7e2144fe46df9dc03eefd722bbcf587b3e0616ea4a81eff"}, 945 | {file = "cryptography-3.3.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e18e6ab84dfb0ab997faf8cca25a86ff15dfea4027b986322026cc99e0a892da"}, 946 | {file = "cryptography-3.3.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c7390f9b2119b2b43160abb34f63277a638504ef8df99f11cb52c1fda66a2e6f"}, 947 | {file = "cryptography-3.3.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0d7b69674b738068fa6ffade5c962ecd14969690585aaca0a1b1fc9058938a72"}, 948 | {file = "cryptography-3.3.2-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:922f9602d67c15ade470c11d616f2b2364950602e370c76f0c94c94ae672742e"}, 949 | {file = "cryptography-3.3.2-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:a0f0b96c572fc9f25c3f4ddbf4688b9b38c69836713fb255f4a2715d93cbaf44"}, 950 | {file = "cryptography-3.3.2-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:a777c096a49d80f9d2979695b835b0f9c9edab73b59e4ceb51f19724dda887ed"}, 951 | {file = "cryptography-3.3.2-cp36-abi3-win32.whl", hash = "sha256:3c284fc1e504e88e51c428db9c9274f2da9f73fdf5d7e13a36b8ecb039af6e6c"}, 952 | {file = "cryptography-3.3.2-cp36-abi3-win_amd64.whl", hash = "sha256:7951a966613c4211b6612b0352f5bf29989955ee592c4a885d8c7d0f830d0433"}, 953 | {file = "cryptography-3.3.2.tar.gz", hash = "sha256:5a60d3780149e13b7a6ff7ad6526b38846354d11a15e21068e57073e29e19bed"}, 954 | ] 955 | docutils = [ 956 | {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, 957 | {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, 958 | ] 959 | fabric = [ 960 | {file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"}, 961 | {file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"}, 962 | ] 963 | ffmpeg-python = [ 964 | {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"}, 965 | {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"}, 966 | ] 967 | future = [ 968 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 969 | ] 970 | idna = [ 971 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 972 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 973 | ] 974 | imagesize = [ 975 | {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, 976 | {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, 977 | ] 978 | importlib-metadata = [ 979 | {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, 980 | {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, 981 | ] 982 | invocations = [ 983 | {file = "invocations-2.3.0-py2.py3-none-any.whl", hash = "sha256:4f6d414702735fadacb8057d287fde7496175def8bab388d08e123e8a78dd098"}, 984 | {file = "invocations-2.3.0.tar.gz", hash = "sha256:6e8b52574e3273397f500dadc048c69891a3eb73bec85213ba3107fb6fcadb8b"}, 985 | ] 986 | invoke = [ 987 | {file = "invoke-1.6.0-py2-none-any.whl", hash = "sha256:e6c9917a1e3e73e7ea91fdf82d5f151ccfe85bf30cc65cdb892444c02dbb5f74"}, 988 | {file = "invoke-1.6.0-py3-none-any.whl", hash = "sha256:769e90caeb1bd07d484821732f931f1ad8916a38e3f3e618644687fc09cb6317"}, 989 | {file = "invoke-1.6.0.tar.gz", hash = "sha256:374d1e2ecf78981da94bfaf95366216aaec27c2d6a7b7d5818d92da55aa258d3"}, 990 | ] 991 | jeepney = [ 992 | {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, 993 | {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, 994 | ] 995 | jinja2 = [ 996 | {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, 997 | {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, 998 | ] 999 | keyring = [ 1000 | {file = "keyring-23.2.1-py3-none-any.whl", hash = "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e"}, 1001 | {file = "keyring-23.2.1.tar.gz", hash = "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe"}, 1002 | ] 1003 | markupsafe = [ 1004 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 1005 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 1006 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 1007 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 1008 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 1009 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 1010 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 1011 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 1012 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 1013 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 1014 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 1015 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 1016 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 1017 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 1018 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 1019 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 1020 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 1021 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 1022 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, 1023 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 1024 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 1025 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, 1026 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, 1027 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, 1028 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 1029 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 1030 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 1031 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, 1032 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 1033 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 1034 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, 1035 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, 1036 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, 1037 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 1038 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 1039 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 1040 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 1041 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 1042 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, 1043 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, 1044 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, 1045 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 1046 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 1047 | {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, 1048 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, 1049 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, 1050 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, 1051 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, 1052 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, 1053 | {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, 1054 | {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, 1055 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 1056 | ] 1057 | mutagen = [ 1058 | {file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"}, 1059 | {file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"}, 1060 | ] 1061 | mypy = [ 1062 | {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"}, 1063 | {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"}, 1064 | {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"}, 1065 | {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"}, 1066 | {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"}, 1067 | {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"}, 1068 | {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"}, 1069 | {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"}, 1070 | {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"}, 1071 | {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"}, 1072 | {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"}, 1073 | {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"}, 1074 | {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"}, 1075 | {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"}, 1076 | ] 1077 | mypy-extensions = [ 1078 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 1079 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 1080 | ] 1081 | packaging = [ 1082 | {file = "packaging-20.7-py2.py3-none-any.whl", hash = "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"}, 1083 | {file = "packaging-20.7.tar.gz", hash = "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236"}, 1084 | ] 1085 | paramiko = [ 1086 | {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, 1087 | {file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, 1088 | ] 1089 | pdf2image = [ 1090 | {file = "pdf2image-1.15.1-py3-none-any.whl", hash = "sha256:36dec8cb7612f067c7c652fcdbb98bca0776fe5882eea688e666309b614bca96"}, 1091 | {file = "pdf2image-1.15.1.tar.gz", hash = "sha256:aa6013c1b5b25ceb90caa34834f1ed343e969cfa532100e1472cfe0e96a639b5"}, 1092 | ] 1093 | peewee = [ 1094 | {file = "peewee-3.14.0.tar.gz", hash = "sha256:59c5ef43877029b9133d87001dcc425525de231d1f983cece8828197fb4b84fa"}, 1095 | ] 1096 | peewee-migrate = [ 1097 | {file = "peewee-migrate-1.4.7.tar.gz", hash = "sha256:45ef69bcec9cb57f773b51aee3aab14c769ddd46cd4662ef9b60da83c744474a"}, 1098 | {file = "peewee_migrate-1.4.7-py3-none-any.whl", hash = "sha256:48f079dab77596806df863da3470f920de26b43a7660faa4a3c19f72209703ee"}, 1099 | ] 1100 | pillow = [ 1101 | {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, 1102 | {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, 1103 | {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, 1104 | {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, 1105 | {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, 1106 | {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, 1107 | {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, 1108 | {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, 1109 | {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, 1110 | {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, 1111 | {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, 1112 | {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, 1113 | {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, 1114 | {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, 1115 | {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, 1116 | {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, 1117 | {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, 1118 | {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, 1119 | {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, 1120 | {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, 1121 | {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, 1122 | {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, 1123 | {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, 1124 | {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, 1125 | {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, 1126 | {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, 1127 | {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, 1128 | {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, 1129 | {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, 1130 | {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, 1131 | {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, 1132 | {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, 1133 | {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, 1134 | {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, 1135 | {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, 1136 | {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, 1137 | {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, 1138 | {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, 1139 | {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, 1140 | {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, 1141 | {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, 1142 | {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, 1143 | {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, 1144 | {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, 1145 | {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, 1146 | {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, 1147 | {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, 1148 | {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, 1149 | {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, 1150 | {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, 1151 | {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, 1152 | {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, 1153 | {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, 1154 | ] 1155 | pkginfo = [ 1156 | {file = "pkginfo-1.8.1-py2.py3-none-any.whl", hash = "sha256:bb55a6c017d50f2faea5153abc7b05a750e7ea7ae2cbb7fb3ad6f1dcf8d40988"}, 1157 | {file = "pkginfo-1.8.1.tar.gz", hash = "sha256:65175ffa2c807220673a41c371573ac9a1ea1b19ffd5eef916278f428319934f"}, 1158 | ] 1159 | pycparser = [ 1160 | {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, 1161 | {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, 1162 | ] 1163 | pycryptodomex = [ 1164 | {file = "pycryptodomex-3.11.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:7abfd84a362e4411f7c5f5758c18cbf377a2a2be64b9232e78544d75640c677e"}, 1165 | {file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6a76d7821ae43df8a0e814cca32114875916b9fc2158603b364853de37eb9002"}, 1166 | {file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:1580db5878b1d16a233550829f7c189c43005f7aa818f2f95c7dddbd6a7163cc"}, 1167 | {file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c825611a951baad63faeb9ef1517ef96a20202d6029ae2485b729152cc703fab"}, 1168 | {file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7cc5ee80b2d5ee8f59a761741cfb916a068c97cac5e700c8ce01e1927616aa2f"}, 1169 | {file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:fbe09e3ae95f47c7551a24781d2e348974cde4a0b33bc3b1566f6216479db2b1"}, 1170 | {file = "pycryptodomex-3.11.0-cp27-cp27m-win32.whl", hash = "sha256:9eace1e5420abc4f9e76de01e49caca349b7c80bda9c1643193e23a06c2a332c"}, 1171 | {file = "pycryptodomex-3.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:adc25aa8cfc537373dd46ae97863f16fd955edee14bf54d3eb52bde4e4ac8c7b"}, 1172 | {file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cf30b5e03d974874185b989839c396d799f6e2d4b4d5b2d8bd3ba464eb3cc33f"}, 1173 | {file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c91772cf6808cc2d80279e80b491c48cb688797b6d914ff624ca95d855c24ee5"}, 1174 | {file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c391ec5c423a374a36b90f7c8805fdf51a0410a2b5be9cebd8990e0021cb6da4"}, 1175 | {file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:64a83ab6f54496ab968a6f21a41a620afe0a742573d609fd03dcab7210645153"}, 1176 | {file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:252ac9c1e1ae1c256a75539e234be3096f2d100b9f4bae42ef88067787b9b249"}, 1177 | {file = "pycryptodomex-3.11.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bf2ea67eaa1fff0aecef6da881144f0f91e314b4123491f9a4fa8df0598e48fe"}, 1178 | {file = "pycryptodomex-3.11.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:fe2b8c464ba335e71aed74f830bf2b2881913f8905d166f9c0fe06ca44a1cb5e"}, 1179 | {file = "pycryptodomex-3.11.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:ff0826f3886e85708a0e8ef7ec47020723b998cfed6ae47962d915fcb89ec780"}, 1180 | {file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:1d4d13c59d2cfbc0863c725f5812d66ff0d6836ba738ef26a52e1291056a1c7c"}, 1181 | {file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:2b586d13ef07fa6197b6348a48dbbe9525f4f496205de14edfa4e91d99e69672"}, 1182 | {file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:f35ccfa44a1dd267e392cd76d8525cfcfabee61dd070e15ad2119c54c0c31ddf"}, 1183 | {file = "pycryptodomex-3.11.0-cp35-abi3-win32.whl", hash = "sha256:5baf690d27f39f2ba22f06e8e32c5f1972573ca65db6bdbb8b2c7177a0112dab"}, 1184 | {file = "pycryptodomex-3.11.0-cp35-abi3-win_amd64.whl", hash = "sha256:919cadcedad552e78349d1626115cfd246fc03ad469a4a62c91a12204f0f0d85"}, 1185 | {file = "pycryptodomex-3.11.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:c10b2f6bcbaa9aa51fe08207654100074786d423b03482c0cbe44406ca92d146"}, 1186 | {file = "pycryptodomex-3.11.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:91662b27f5aa8a6d2ad63be9a7d1a403e07bf3c2c5b265a7cc5cbadf6f988e06"}, 1187 | {file = "pycryptodomex-3.11.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:207e53bdbf3a26de6e9dcf3ebaf67ba70a61f733f84c464eca55d278211c1b71"}, 1188 | {file = "pycryptodomex-3.11.0-pp27-pypy_73-win32.whl", hash = "sha256:1dd4271d8d022216533c3547f071662b44d703fd5dbb632c4b5e77b3ee47567f"}, 1189 | {file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c43ddcff251e8b427b3e414b026636617276e008a9d78a44a9195d4bdfcaa0fe"}, 1190 | {file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:ef25d682d0d9ab25c5022a298b5cba9084c7b148a3e71846df2c67ea664eacc7"}, 1191 | {file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:4c7c6418a3c08b2ebfc2cf50ce52de267618063b533083a2c73b40ec54a1b6f5"}, 1192 | {file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:15d25c532de744648f0976c56bd10d07b2a44b7eb2a6261ffe2497980b1102d8"}, 1193 | {file = "pycryptodomex-3.11.0.tar.gz", hash = "sha256:0398366656bb55ebdb1d1d493a7175fc48ade449283086db254ac44c7d318d6d"}, 1194 | ] 1195 | pygments = [ 1196 | {file = "Pygments-2.7.4-py3-none-any.whl", hash = "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435"}, 1197 | {file = "Pygments-2.7.4.tar.gz", hash = "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"}, 1198 | ] 1199 | pynacl = [ 1200 | {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, 1201 | {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, 1202 | {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, 1203 | {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, 1204 | {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, 1205 | {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, 1206 | {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, 1207 | {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, 1208 | {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, 1209 | {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, 1210 | {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, 1211 | {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, 1212 | {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, 1213 | {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, 1214 | {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, 1215 | {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, 1216 | {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, 1217 | {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, 1218 | ] 1219 | pyparsing = [ 1220 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 1221 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 1222 | ] 1223 | python-telegram-bot = [ 1224 | {file = "python-telegram-bot-13.6.tar.gz", hash = "sha256:37cfe8faba16fb68a8b5ab41a10e787c385f6296200c84256cc54d7c16334643"}, 1225 | {file = "python_telegram_bot-13.6-py3-none-any.whl", hash = "sha256:d4b3a8fd6a927bc6dc498fa00e8d6388570d089f5c015418c3b2b954e0719a7a"}, 1226 | ] 1227 | pytz = [ 1228 | {file = "pytz-2020.4-py2.py3-none-any.whl", hash = "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"}, 1229 | {file = "pytz-2020.4.tar.gz", hash = "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268"}, 1230 | ] 1231 | pywin32-ctypes = [ 1232 | {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, 1233 | {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, 1234 | ] 1235 | readme-renderer = [ 1236 | {file = "readme_renderer-30.0-py2.py3-none-any.whl", hash = "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc"}, 1237 | {file = "readme_renderer-30.0.tar.gz", hash = "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8"}, 1238 | ] 1239 | releases = [ 1240 | {file = "releases-1.6.3-py2.py3-none-any.whl", hash = "sha256:cb3435ba372a6807433800fbe473760cfa781171513f670f3c4b76983ac80f18"}, 1241 | {file = "releases-1.6.3.tar.gz", hash = "sha256:555ae4c97a671a420281c1c782e9236be25157b449fdf20b4c4b293fe93db2f1"}, 1242 | ] 1243 | requests = [ 1244 | {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, 1245 | {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, 1246 | ] 1247 | requests-toolbelt = [ 1248 | {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, 1249 | {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, 1250 | ] 1251 | rfc3986 = [ 1252 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 1253 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 1254 | ] 1255 | secretstorage = [ 1256 | {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"}, 1257 | {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, 1258 | ] 1259 | semantic-version = [ 1260 | {file = "semantic_version-2.6.0-py3-none-any.whl", hash = "sha256:2d06ab7372034bcb8b54f2205370f4aa0643c133b7e6dbd129c5200b83ab394b"}, 1261 | {file = "semantic_version-2.6.0.tar.gz", hash = "sha256:2a4328680073e9b243667b201119772aefc5fc63ae32398d6afafff07c4f54c0"}, 1262 | ] 1263 | six = [ 1264 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 1265 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 1266 | ] 1267 | snowballstemmer = [ 1268 | {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, 1269 | {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, 1270 | ] 1271 | sphinx = [ 1272 | {file = "Sphinx-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"}, 1273 | {file = "Sphinx-3.3.1.tar.gz", hash = "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300"}, 1274 | ] 1275 | sphinxcontrib-applehelp = [ 1276 | {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, 1277 | {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, 1278 | ] 1279 | sphinxcontrib-devhelp = [ 1280 | {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, 1281 | {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, 1282 | ] 1283 | sphinxcontrib-htmlhelp = [ 1284 | {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, 1285 | {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, 1286 | ] 1287 | sphinxcontrib-jsmath = [ 1288 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 1289 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 1290 | ] 1291 | sphinxcontrib-qthelp = [ 1292 | {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, 1293 | {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, 1294 | ] 1295 | sphinxcontrib-serializinghtml = [ 1296 | {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, 1297 | {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, 1298 | ] 1299 | tabulate = [ 1300 | {file = "tabulate-0.7.5.tar.gz", hash = "sha256:9071aacbd97a9a915096c1aaf0dc684ac2672904cd876db5904085d6dac9810e"}, 1301 | ] 1302 | tornado = [ 1303 | {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, 1304 | {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, 1305 | {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, 1306 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, 1307 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, 1308 | {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, 1309 | {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, 1310 | {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, 1311 | {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, 1312 | {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, 1313 | {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, 1314 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, 1315 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, 1316 | {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, 1317 | {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, 1318 | {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, 1319 | {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, 1320 | {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, 1321 | {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, 1322 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, 1323 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, 1324 | {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, 1325 | {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, 1326 | {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, 1327 | {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, 1328 | {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, 1329 | {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, 1330 | {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, 1331 | {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, 1332 | {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, 1333 | {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, 1334 | {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, 1335 | {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, 1336 | {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, 1337 | {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, 1338 | {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, 1339 | {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, 1340 | {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, 1341 | {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, 1342 | {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, 1343 | {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, 1344 | ] 1345 | tqdm = [ 1346 | {file = "tqdm-4.54.0-py2.py3-none-any.whl", hash = "sha256:9e7b8ab0ecbdbf0595adadd5f0ebbb9e69010e0bd48bbb0c15e550bf2a5292df"}, 1347 | {file = "tqdm-4.54.0.tar.gz", hash = "sha256:5c0d04e06ccc0da1bd3fa5ae4550effcce42fcad947b4a6cafa77bdc9b09ff22"}, 1348 | ] 1349 | twine = [ 1350 | {file = "twine-3.6.0-py3-none-any.whl", hash = "sha256:916070f8ecbd1985ebed5dbb02b9bda9a092882a96d7069d542d4fc0bb5c673c"}, 1351 | {file = "twine-3.6.0.tar.gz", hash = "sha256:4caad5ef4722e127b3749052fcbffaaf71719b19d4fd4973b29c469957adeba2"}, 1352 | ] 1353 | typed-ast = [ 1354 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 1355 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 1356 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 1357 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 1358 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 1359 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 1360 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 1361 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, 1362 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 1363 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 1364 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 1365 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 1366 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 1367 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, 1368 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 1369 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 1370 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 1371 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 1372 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 1373 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, 1374 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 1375 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 1376 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 1377 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, 1378 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, 1379 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, 1380 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, 1381 | {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, 1382 | {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, 1383 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 1384 | ] 1385 | typing-extensions = [ 1386 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 1387 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 1388 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 1389 | ] 1390 | tzlocal = [ 1391 | {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, 1392 | {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, 1393 | ] 1394 | urllib3 = [ 1395 | {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, 1396 | {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, 1397 | ] 1398 | webencodings = [ 1399 | {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, 1400 | {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, 1401 | ] 1402 | websockets = [ 1403 | {file = "websockets-10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:38db6e2163b021642d0a43200ee2dec8f4980bdbda96db54fde72b283b54cbfc"}, 1404 | {file = "websockets-10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1b60fd297adb9fc78375778a5220da7f07bf54d2a33ac781319650413fc6a60"}, 1405 | {file = "websockets-10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3477146d1f87ead8df0f27e8960249f5248dceb7c2741e8bbec9aa5338d0c053"}, 1406 | {file = "websockets-10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb01ea7b5f52e7125bdc3c5807aeaa2d08a0553979cf2d96a8b7803ea33e15e7"}, 1407 | {file = "websockets-10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9fd62c6dc83d5d35fb6a84ff82ec69df8f4657fff05f9cd6c7d9bec0dd57f0f6"}, 1408 | {file = "websockets-10.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbf080f3892ba1dc8838786ec02899516a9d227abe14a80ef6fd17d4fb57127"}, 1409 | {file = "websockets-10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5560558b0dace8312c46aa8915da977db02738ac8ecffbc61acfbfe103e10155"}, 1410 | {file = "websockets-10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:667c41351a6d8a34b53857ceb8343a45c85d438ee4fd835c279591db8aeb85be"}, 1411 | {file = "websockets-10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:468f0031fdbf4d643f89403a66383247eb82803430b14fa27ce2d44d2662ca37"}, 1412 | {file = "websockets-10.1-cp310-cp310-win32.whl", hash = "sha256:d0d81b46a5c87d443e40ce2272436da8e6092aa91f5fbeb60d1be9f11eff5b4c"}, 1413 | {file = "websockets-10.1-cp310-cp310-win_amd64.whl", hash = "sha256:b68b6caecb9a0c6db537aa79750d1b592a841e4f1a380c6196091e65b2ad35f9"}, 1414 | {file = "websockets-10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a249139abc62ef333e9e85064c27fefb113b16ffc5686cefc315bdaef3eefbc8"}, 1415 | {file = "websockets-10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8877861e3dee38c8d302eee0d5dbefa6663de3b46dc6a888f70cd7e82562d1f7"}, 1416 | {file = "websockets-10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e3872ae57acd4306ecf937d36177854e218e999af410a05c17168cd99676c512"}, 1417 | {file = "websockets-10.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b66e6d514f12c28d7a2d80bb2a48ef223342e99c449782d9831b0d29a9e88a17"}, 1418 | {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9f304a22ece735a3da8a51309bc2c010e23961a8f675fae46fdf62541ed62123"}, 1419 | {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:189ed478395967d6a98bb293abf04e8815349e17456a0a15511f1088b6cb26e4"}, 1420 | {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:08a42856158307e231b199671c4fce52df5786dd3d703f36b5d8ac76b206c485"}, 1421 | {file = "websockets-10.1-cp37-cp37m-win32.whl", hash = "sha256:3ef6f73854cded34e78390dbdf40dfdcf0b89b55c0e282468ef92646fce8d13a"}, 1422 | {file = "websockets-10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:89e985d40d407545d5f5e2e58e1fdf19a22bd2d8cd54d20a882e29f97e930a0a"}, 1423 | {file = "websockets-10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:002071169d2e44ce8eb9e5ebac9fbce142ba4b5146eef1cfb16b177a27662657"}, 1424 | {file = "websockets-10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfae282c2aa7f0c4be45df65c248481f3509f8c40ca8b15ed96c35668ae0ff69"}, 1425 | {file = "websockets-10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97b4b68a2ddaf5c4707ae79c110bfd874c5be3c6ac49261160fb243fa45d8bbb"}, 1426 | {file = "websockets-10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c9407719f42cb77049975410490c58a705da6af541adb64716573e550e5c9db"}, 1427 | {file = "websockets-10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d858fb31e5ac992a2cdf17e874c95f8a5b1e917e1fb6b45ad85da30734b223f"}, 1428 | {file = "websockets-10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7bdd3d26315db0a9cf8a0af30ca95e0aa342eda9c1377b722e71ccd86bc5d1dd"}, 1429 | {file = "websockets-10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e259be0863770cb91b1a6ccf6907f1ac2f07eff0b7f01c249ed751865a70cb0d"}, 1430 | {file = "websockets-10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6b014875fae19577a392372075e937ebfebf53fd57f613df07b35ab210f31534"}, 1431 | {file = "websockets-10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:98de71f86bdb29430fd7ba9997f47a6b10866800e3ea577598a786a785701bb0"}, 1432 | {file = "websockets-10.1-cp38-cp38-win32.whl", hash = "sha256:3a02ab91d84d9056a9ee833c254895421a6333d7ae7fff94b5c68e4fa8095519"}, 1433 | {file = "websockets-10.1-cp38-cp38-win_amd64.whl", hash = "sha256:7d6673b2753f9c5377868a53445d0c321ef41ff3c8e3b6d57868e72054bfce5f"}, 1434 | {file = "websockets-10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ddab2dc69ee5ae27c74dbfe9d7bb6fee260826c136dca257faa1a41d1db61a89"}, 1435 | {file = "websockets-10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14e9cf68a08d1a5d42109549201aefba473b1d925d233ae19035c876dd845da9"}, 1436 | {file = "websockets-10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4819c6fb4f336fd5388372cb556b1f3a165f3f68e66913d1a2fc1de55dc6f58"}, 1437 | {file = "websockets-10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05e7f098c76b0a4743716590bb8f9706de19f1ef5148d61d0cf76495ec3edb9c"}, 1438 | {file = "websockets-10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bb6256de5a4fb1d42b3747b4e2268706c92965d75d0425be97186615bf2f24f"}, 1439 | {file = "websockets-10.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:888a5fa2a677e0c2b944f9826c756475980f1b276b6302e606f5c4ff5635be9e"}, 1440 | {file = "websockets-10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6fdec1a0b3e5630c58e3d8704d2011c678929fce90b40908c97dfc47de8dca72"}, 1441 | {file = "websockets-10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:531d8eb013a9bc6b3ad101588182aa9b6dd994b190c56df07f0d84a02b85d530"}, 1442 | {file = "websockets-10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0d93b7cadc761347d98da12ec1930b5c71b2096f1ceed213973e3cda23fead9c"}, 1443 | {file = "websockets-10.1-cp39-cp39-win32.whl", hash = "sha256:d9b245db5a7e64c95816e27d72830e51411c4609c05673d1ae81eb5d23b0be54"}, 1444 | {file = "websockets-10.1-cp39-cp39-win_amd64.whl", hash = "sha256:882c0b8bdff3bf1bd7f024ce17c6b8006042ec4cceba95cf15df57e57efa471c"}, 1445 | {file = "websockets-10.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:10edd9d7d3581cfb9ff544ac09fc98cab7ee8f26778a5a8b2d5fd4b0684c5ba5"}, 1446 | {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa83174390c0ff4fc1304fbe24393843ac7a08fdd59295759c4b439e06b1536"}, 1447 | {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:483edee5abed738a0b6a908025be47f33634c2ad8e737edd03ffa895bd600909"}, 1448 | {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:816ae7dac2c6522cfa620947ead0ca95ac654916eebf515c94d7c28de5601a6e"}, 1449 | {file = "websockets-10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1dafe98698ece09b8ccba81b910643ff37198e43521d977be76caf37709cf62b"}, 1450 | {file = "websockets-10.1.tar.gz", hash = "sha256:181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"}, 1451 | ] 1452 | yt-dlp = [ 1453 | {file = "yt-dlp-2021.11.10.1.tar.gz", hash = "sha256:f0ad6ae2e2838b608df2fd125f2a777a7ad832d3e757ee6d4583b84b21e44388"}, 1454 | {file = "yt_dlp-2021.11.10.1-py2.py3-none-any.whl", hash = "sha256:9c3d85fdeeac3d61cfc85fd72d55fe6b63fcf1d19d05e2841cf2e544922ed157"}, 1455 | ] 1456 | zipp = [ 1457 | {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, 1458 | {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, 1459 | ] 1460 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "FileConvertBot" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Iulian Onofrei "] 6 | license = "GNU" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8" 10 | ffmpeg-python = "*" 11 | pdf2image = "*" 12 | peewee = "*" 13 | peewee-migrate = "*" 14 | python-telegram-bot = "*" 15 | requests = "*" 16 | yt-dlp = "*" 17 | 18 | [tool.poetry.dev-dependencies] 19 | fabric = "*" 20 | invocations = "*" 21 | mypy = "*" 22 | 23 | [build-system] 24 | requires = ["poetry-core>=1.0.0"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pep8] 2 | ignore = E501 3 | -------------------------------------------------------------------------------- /src/analytics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import enum 4 | import logging 5 | import typing 6 | 7 | import requests 8 | import telegram.ext 9 | 10 | import constants 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class AnalyticsType(enum.Enum): 16 | COMMAND = 'command' 17 | MESSAGE = 'message' 18 | 19 | 20 | class AnalyticsHandler: 21 | def __init__(self) -> None: 22 | self.googleToken: typing.Optional[str] = None 23 | self.userAgent: typing.Optional[str] = None 24 | 25 | def __google_track(self, analytics_type: AnalyticsType, user: telegram.User, data: str) -> None: 26 | if not self.googleToken: 27 | return 28 | 29 | url = constants.GOOGLE_ANALYTICS_BASE_URL.format(self.googleToken, user.id, analytics_type.value, data) 30 | 31 | response = requests.get(url, headers={'User-Agent': self.userAgent or 'TelegramBot'}) 32 | 33 | if response.status_code != 200: 34 | logger.error(f'Google analytics error: {response.status_code}') 35 | 36 | def track(self, context: telegram.ext.CallbackContext, analytics_type: AnalyticsType, user: telegram.User, data='') -> None: 37 | if data is None: 38 | data = '' 39 | 40 | context.dispatcher.run_async(self.__google_track, analytics_type, user, data) 41 | -------------------------------------------------------------------------------- /src/config_sample.cfg: -------------------------------------------------------------------------------- 1 | [Telegram] 2 | Name: BotUserName 3 | TestName: TestBotUserName 4 | Key: 123456:ABCDEF 5 | TestKey: 789012:GHIJKL 6 | Admin: 987654 7 | 8 | [Webhook] 9 | Port: 8443 10 | SSH: /home/root/.ssh 11 | 12 | Key: %(SSH)s/telegram.key 13 | Cert: %(SSH)s/telegram.pem 14 | Url: https://1.2.3.4:%(Port)s/ 15 | 16 | [Google] 17 | Key: AB-123456-1 18 | -------------------------------------------------------------------------------- /src/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | 5 | # See also: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters 6 | GOOGLE_ANALYTICS_BASE_URL = 'https://www.google-analytics.com/collect?v=1&t=event&tid={}&cid={}&ec={}&ea={}' 7 | 8 | LOGS_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 9 | 10 | GENERIC_DATE_FORMAT = '%Y-%m-%d' 11 | GENERIC_DATE_TIME_FORMAT = f'{GENERIC_DATE_FORMAT} %H:%M:%S' 12 | 13 | EPOCH_DATE = datetime.datetime(1970, 1, 1) 14 | 15 | MAX_VIDEO_NOTE_LENGTH = 60 16 | MAX_VIDEO_NOTE_SIZE = 638 17 | 18 | AUDIO_CODEC_NAMES = ['aac', 'mp3'] 19 | 20 | VIDEO_CODEC_NAMES = ['h264', 'hevc', 'mpeg4', 'vp6', 'vp8'] 21 | VIDEO_NOTE_CROP_OFFSET_PARAMS = 'abs(in_w - in_h) / 2' 22 | VIDEO_NOTE_CROP_SIZE_PARAMS = 'min(in_w, in_h)' 23 | VIDEO_NOTE_SCALE_SIZE_PARAMS = 'min(min(in_w, in_h), {})'.format(MAX_VIDEO_NOTE_SIZE) 24 | 25 | 26 | class OutputType: 27 | NONE = 'none' 28 | AUDIO = 'audio' 29 | VIDEO = 'video' 30 | VIDEO_NOTE = 'video_note' 31 | PHOTO = 'photo' 32 | STICKER = 'sticker' 33 | FILE = 'file' 34 | -------------------------------------------------------------------------------- /src/custom_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import constants 4 | 5 | 6 | class LoggerFilter(logging.Filter): 7 | def __init__(self, level: int, name='') -> None: 8 | super().__init__(name=name) 9 | 10 | self.level = level 11 | 12 | def filter(self, log_record: logging.LogRecord) -> bool: 13 | return log_record.levelno <= self.level 14 | 15 | 16 | def configure_root_logger() -> None: 17 | logger = logging.getLogger() 18 | 19 | logging.basicConfig(format=constants.LOGS_FORMAT, level=logging.INFO) 20 | 21 | error_logging_handler = logging.FileHandler('errors.log') 22 | error_logging_handler.setFormatter(logging.Formatter(constants.LOGS_FORMAT)) 23 | error_logging_handler.setLevel(logging.ERROR) 24 | error_logging_handler.addFilter(LoggerFilter(logging.ERROR)) 25 | 26 | logger.addHandler(error_logging_handler) 27 | 28 | warning_logging_handler = logging.FileHandler('warnings.log') 29 | warning_logging_handler.setFormatter(logging.Formatter(constants.LOGS_FORMAT)) 30 | warning_logging_handler.setLevel(logging.WARNING) 31 | warning_logging_handler.addFilter(LoggerFilter(logging.WARNING)) 32 | 33 | logger.addHandler(warning_logging_handler) 34 | -------------------------------------------------------------------------------- /src/database.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import annotations 4 | 5 | import datetime 6 | import logging 7 | import typing 8 | import uuid 9 | 10 | import peewee 11 | import peewee_migrate 12 | import playhouse.sqlite_ext 13 | import telegram 14 | 15 | import constants 16 | import telegram_utils 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | database = peewee.SqliteDatabase('file_convert.sqlite') 21 | 22 | database.connect() 23 | 24 | router = peewee_migrate.Router(database, migrate_table='migration', logger=logger) 25 | 26 | 27 | def get_current_datetime() -> str: 28 | return datetime.datetime.now().strftime(constants.GENERIC_DATE_TIME_FORMAT) 29 | 30 | 31 | class BaseModel(peewee.Model): 32 | rowid = playhouse.sqlite_ext.RowIDField() 33 | 34 | created_at = peewee.DateTimeField(default=get_current_datetime) 35 | updated_at = peewee.DateTimeField() 36 | 37 | class Meta: 38 | database = database 39 | 40 | 41 | class User(BaseModel): 42 | id = peewee.TextField(unique=True, default=uuid.uuid4) 43 | telegram_id = peewee.BigIntegerField(unique=True) 44 | telegram_username = peewee.TextField(null=True) 45 | 46 | def get_markdown_description(self) -> str: 47 | if self.telegram_username is None: 48 | username = telegram_utils.escape_v2_markdown_text('-') 49 | else: 50 | escaped_username = telegram_utils.escape_v2_markdown_text( 51 | text=f'@{self.telegram_username}', 52 | entity_type=telegram.MessageEntity.CODE 53 | ) 54 | username = f'`{escaped_username}`' 55 | 56 | user_id = telegram_utils.escape_v2_markdown_text_link( 57 | text=str(self.telegram_id), 58 | url=f'tg://user?id={self.telegram_id}' 59 | ) 60 | 61 | return ( 62 | f'{self.rowid}{telegram_utils.ESCAPED_FULL_STOP} {telegram_utils.ESCAPED_VERTICAL_LINE} ' 63 | f'{user_id} {telegram_utils.ESCAPED_VERTICAL_LINE} ' 64 | f'{username}' 65 | ) 66 | 67 | def get_created_at(self) -> str: 68 | date = typing.cast(datetime.datetime, self.created_at) 69 | 70 | return date.strftime(constants.GENERIC_DATE_TIME_FORMAT) 71 | 72 | def get_updated_ago(self) -> str: 73 | if self.updated_at == self.created_at: 74 | return '-' 75 | 76 | delta_seconds = round((datetime.datetime.now() - self.updated_at).total_seconds()) 77 | time_ago = str(datetime.datetime.fromtimestamp(delta_seconds) - constants.EPOCH_DATE) 78 | 79 | return f'{time_ago} ago' 80 | 81 | @classmethod 82 | def create_or_update_user(cls, id: int, username: typing.Optional[str]) -> typing.Optional[User]: 83 | current_date_time = get_current_datetime() 84 | 85 | try: 86 | defaults = { 87 | 'telegram_username': username, 88 | 89 | 'updated_at': current_date_time 90 | } 91 | 92 | (db_user, is_created) = cls.get_or_create(telegram_id=id, defaults=defaults) 93 | 94 | db_user.telegram_username = username 95 | db_user.updated_at = current_date_time 96 | 97 | db_user.save() 98 | 99 | if is_created: 100 | return db_user 101 | except peewee.PeeweeException as error: 102 | logger.error(f'Database error: "{error}" for id: {id} and username: {username}') 103 | 104 | return None 105 | 106 | @classmethod 107 | def get_users_table(cls, sorted_by_updated_at=False) -> str: 108 | users_table = '' 109 | 110 | try: 111 | sort_field = cls.updated_at if sorted_by_updated_at else cls.created_at 112 | 113 | query = cls.select() 114 | 115 | if sorted_by_updated_at: 116 | query = query.where(cls.created_at != cls.updated_at) 117 | 118 | query = query.order_by(sort_field.desc()).limit(10) 119 | 120 | for user in reversed(query): 121 | users_table += ( 122 | f'\n{user.get_markdown_description()} {telegram_utils.ESCAPED_VERTICAL_LINE} ' 123 | f'{telegram_utils.escape_v2_markdown_text(user.get_created_at())} {telegram_utils.ESCAPED_VERTICAL_LINE} ' 124 | f'{telegram_utils.escape_v2_markdown_text(user.get_updated_ago())}' 125 | ) 126 | except peewee.PeeweeException: 127 | pass 128 | 129 | if not users_table: 130 | users_table = 'No users' 131 | 132 | return users_table 133 | 134 | 135 | migrator = router.migrator 136 | 137 | migrator.create_table(User) 138 | 139 | router.run() 140 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import configparser 6 | import io 7 | import json 8 | import logging 9 | import os 10 | import sys 11 | import threading 12 | 13 | import ffmpeg 14 | import pdf2image 15 | import PIL 16 | import telegram.ext 17 | import telegram.utils.helpers 18 | import telegram_utils 19 | import yt_dlp as youtube_dl 20 | 21 | import analytics 22 | import constants 23 | import custom_logger 24 | import database 25 | import utils 26 | 27 | custom_logger.configure_root_logger() 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | BOT_NAME: str 32 | BOT_TOKEN: str 33 | 34 | ADMIN_USER_ID: int 35 | 36 | updater: telegram.ext.Updater 37 | analytics_handler: analytics.AnalyticsHandler 38 | 39 | 40 | def stop_and_restart() -> None: 41 | updater.stop() 42 | os.execl(sys.executable, sys.executable, *sys.argv) 43 | 44 | 45 | def create_or_update_user(bot: telegram.Bot, user: telegram.User) -> None: 46 | db_user = database.User.create_or_update_user(user.id, user.username) 47 | 48 | if db_user: 49 | prefix = 'New user:' 50 | 51 | bot.send_message( 52 | chat_id=ADMIN_USER_ID, 53 | text=( 54 | f'{telegram_utils.escape_v2_markdown_text(prefix)} ' 55 | f'{db_user.get_markdown_description()}' 56 | ), 57 | parse_mode=telegram.ParseMode.MARKDOWN_V2, 58 | disable_notification=True 59 | ) 60 | 61 | 62 | def start_command_handler(update: telegram.Update, context: telegram.ext.CallbackContext) -> None: 63 | message = update.message 64 | 65 | if message is None: 66 | return 67 | 68 | bot = context.bot 69 | 70 | chat_id = message.chat_id 71 | user = message.from_user 72 | 73 | if user is None: 74 | return 75 | 76 | create_or_update_user(bot, user) 77 | 78 | analytics_handler.track(context, analytics.AnalyticsType.COMMAND, user, '/start') 79 | 80 | bot.send_message(chat_id, 'Send me a file to try to convert it to something better.') 81 | 82 | 83 | def restart_command_handler(update: telegram.Update, context: telegram.ext.CallbackContext) -> None: 84 | message = update.message 85 | 86 | if message is None: 87 | return 88 | 89 | bot = context.bot 90 | 91 | if not utils.check_admin(bot, context, message, analytics_handler, ADMIN_USER_ID): 92 | return 93 | 94 | bot.send_message(message.chat_id, 'Restarting...') 95 | 96 | threading.Thread(target=stop_and_restart).start() 97 | 98 | 99 | def logs_command_handler(update: telegram.Update, context: telegram.ext.CallbackContext) -> None: 100 | message = update.message 101 | 102 | if message is None: 103 | return 104 | 105 | bot = context.bot 106 | 107 | chat_id = message.chat_id 108 | 109 | if not utils.check_admin(bot, context, message, analytics_handler, ADMIN_USER_ID): 110 | return 111 | 112 | try: 113 | bot.send_document(chat_id, open('errors.log', 'rb')) 114 | except telegram.TelegramError: 115 | bot.send_message(chat_id, 'Log is empty') 116 | 117 | 118 | def users_command_handler(update: telegram.Update, context: telegram.ext.CallbackContext) -> None: 119 | message = update.message 120 | 121 | if message is None: 122 | return 123 | 124 | bot = context.bot 125 | 126 | chat_id = message.chat_id 127 | 128 | if not utils.check_admin(bot, context, message, analytics_handler, ADMIN_USER_ID): 129 | return 130 | 131 | args = context.args or [] 132 | 133 | bot.send_message( 134 | chat_id=chat_id, 135 | text=database.User.get_users_table('updated' in args), 136 | parse_mode=telegram.ParseMode.MARKDOWN_V2 137 | ) 138 | 139 | 140 | def message_file_handler(update: telegram.Update, context: telegram.ext.CallbackContext) -> None: 141 | message = update.effective_message 142 | chat = update.effective_chat 143 | 144 | if chat is None: 145 | return 146 | 147 | chat_type = chat.type 148 | bot = context.bot 149 | 150 | if message is None: 151 | return 152 | 153 | if cli_args.debug and not utils.check_admin(bot, context, message, analytics_handler, ADMIN_USER_ID): 154 | return 155 | 156 | message_id = message.message_id 157 | chat_id = message.chat.id 158 | attachment = message.effective_attachment 159 | 160 | if attachment is None: 161 | return 162 | 163 | if type(attachment) is list: 164 | if chat_type == telegram.Chat.PRIVATE: 165 | bot.send_message( 166 | chat_id, 167 | 'You need to send the image as a file to convert it to a sticker.', 168 | reply_to_message_id=message_id 169 | ) 170 | 171 | return 172 | 173 | if not isinstance(attachment, ( 174 | telegram.Audio, 175 | telegram.Document, 176 | telegram.Voice, 177 | telegram.Sticker 178 | )): 179 | return 180 | 181 | message_type = telegram.utils.helpers.effective_message_type(message) 182 | 183 | file_size = attachment.file_size 184 | 185 | if file_size is None: 186 | return 187 | 188 | if not utils.ensure_size_under_limit(file_size, telegram.constants.MAX_FILESIZE_DOWNLOAD, update, context): 189 | return 190 | 191 | user = message.from_user 192 | 193 | input_file_id = attachment.file_id 194 | input_file_name = None 195 | 196 | if isinstance(attachment, (telegram.Audio, telegram.Document)): 197 | input_file_name = attachment.file_name 198 | 199 | if input_file_name is None and isinstance(attachment, telegram.Audio): 200 | input_file_name = attachment.title 201 | 202 | if user is not None: 203 | create_or_update_user(bot, user) 204 | 205 | analytics_handler.track(context, analytics.AnalyticsType.MESSAGE, user) 206 | 207 | if chat_type == telegram.Chat.PRIVATE: 208 | bot.send_chat_action(chat_id, telegram.ChatAction.TYPING) 209 | 210 | input_file = bot.get_file(input_file_id) 211 | input_file_url = input_file.file_path 212 | 213 | probe = None 214 | 215 | try: 216 | probe = ffmpeg.probe(input_file_url) 217 | except ffmpeg.Error: 218 | pass 219 | 220 | with io.BytesIO() as output_bytes: 221 | output_type = constants.OutputType.NONE 222 | caption = None 223 | invalid_format = None 224 | 225 | if message_type == 'voice': 226 | output_type = constants.OutputType.FILE 227 | 228 | mp3_bytes = utils.convert(output_type, input_audio_url=input_file_url) 229 | 230 | if not utils.ensure_valid_converted_file( 231 | file_bytes=mp3_bytes, 232 | update=update, 233 | context=context 234 | ): 235 | return 236 | 237 | if mp3_bytes is not None: 238 | output_bytes.write(mp3_bytes) 239 | 240 | output_bytes.name = 'voice.mp3' 241 | elif message_type == 'sticker': 242 | with io.BytesIO() as input_bytes: 243 | input_file.download(out=input_bytes) 244 | 245 | try: 246 | image = PIL.Image.open(input_bytes) 247 | 248 | with io.BytesIO() as image_bytes: 249 | image.save(image_bytes, format='PNG') 250 | 251 | image_bytes.seek(0) 252 | 253 | output_bytes.write(image_bytes.read()) 254 | 255 | output_type = constants.OutputType.PHOTO 256 | 257 | sticker = message['sticker'] 258 | emoji = sticker['emoji'] 259 | set_name = sticker['set_name'] 260 | 261 | caption = f'Sticker for the emoji "{emoji}" from the set "{set_name}"' 262 | except Exception as error: 263 | logger.error(f'PIL error: {error}') 264 | else: 265 | if probe: 266 | for stream in probe['streams']: 267 | codec_name = stream.get('codec_name') 268 | 269 | if codec_name is not None: 270 | invalid_format = codec_name 271 | 272 | if codec_name in constants.VIDEO_CODEC_NAMES: 273 | output_type = constants.OutputType.VIDEO 274 | 275 | mp4_bytes = utils.convert(output_type, input_video_url=input_file_url) 276 | 277 | if not utils.ensure_valid_converted_file( 278 | file_bytes=mp4_bytes, 279 | update=update, 280 | context=context 281 | ): 282 | return 283 | 284 | if mp4_bytes is not None: 285 | output_bytes.write(mp4_bytes) 286 | 287 | break 288 | 289 | continue 290 | 291 | if output_type == constants.OutputType.NONE: 292 | for stream in probe['streams']: 293 | codec_name = stream.get('codec_name') 294 | 295 | if codec_name is not None: 296 | invalid_format = codec_name 297 | 298 | if codec_name in constants.AUDIO_CODEC_NAMES: 299 | output_type = constants.OutputType.AUDIO 300 | 301 | opus_bytes = utils.convert(output_type, input_audio_url=input_file_url) 302 | 303 | if not utils.ensure_valid_converted_file( 304 | file_bytes=opus_bytes, 305 | update=update, 306 | context=context 307 | ): 308 | return 309 | 310 | if opus_bytes is not None: 311 | output_bytes.write(opus_bytes) 312 | 313 | break 314 | elif codec_name == 'opus': 315 | input_file.download(out=output_bytes) 316 | 317 | output_type = constants.OutputType.AUDIO 318 | 319 | break 320 | 321 | continue 322 | 323 | if output_type == constants.OutputType.NONE: 324 | with io.BytesIO() as input_bytes: 325 | input_file.download(out=input_bytes) 326 | 327 | input_bytes.seek(0) 328 | 329 | try: 330 | images = pdf2image.convert_from_bytes(input_bytes.read()) 331 | image = images[0] 332 | 333 | with io.BytesIO() as image_bytes: 334 | image.save(image_bytes, format='PNG') 335 | 336 | image_bytes.seek(0) 337 | 338 | output_bytes.write(image_bytes.read()) 339 | 340 | output_type = constants.OutputType.PHOTO 341 | except Exception as error: 342 | logger.error(f'pdf2image error: {error}') 343 | 344 | if output_type == constants.OutputType.NONE: 345 | try: 346 | image = PIL.Image.open(input_bytes) 347 | 348 | with io.BytesIO() as image_bytes: 349 | image.save(image_bytes, format='WEBP') 350 | 351 | image_bytes.seek(0) 352 | 353 | output_bytes.write(image_bytes.read()) 354 | 355 | output_type = constants.OutputType.STICKER 356 | except Exception as error: 357 | logger.error(f'PIL error: {error}') 358 | 359 | if output_type == constants.OutputType.NONE: 360 | if chat_type == telegram.Chat.PRIVATE: 361 | if invalid_format is None and input_file_url is not None: 362 | parts = os.path.splitext(input_file_url) 363 | 364 | if parts is not None and len(parts) >= 2: 365 | extension = parts[1] 366 | 367 | if extension is not None: 368 | invalid_format = extension[1:] 369 | 370 | bot.send_message( 371 | chat_id=chat_id, 372 | text=f'File type "{invalid_format}" is not yet supported.', 373 | reply_to_message_id=message_id 374 | ) 375 | 376 | return 377 | 378 | output_bytes.seek(0) 379 | 380 | output_file_size = output_bytes.getbuffer().nbytes 381 | 382 | if caption is None and input_file_name is not None: 383 | caption = input_file_name[:telegram.constants.MAX_CAPTION_LENGTH] 384 | 385 | if output_type == constants.OutputType.AUDIO: 386 | if not utils.ensure_size_under_limit(output_file_size, telegram.constants.MAX_FILESIZE_UPLOAD, update, context, file_reference_text='Converted file'): 387 | return 388 | 389 | bot.send_chat_action(chat_id, telegram.ChatAction.UPLOAD_VOICE) 390 | 391 | bot.send_voice( 392 | chat_id, 393 | output_bytes, 394 | caption=caption, 395 | reply_to_message_id=message_id 396 | ) 397 | 398 | return 399 | elif output_type == constants.OutputType.VIDEO: 400 | if not utils.ensure_size_under_limit(output_file_size, telegram.constants.MAX_FILESIZE_UPLOAD, update, context, file_reference_text='Converted file'): 401 | return 402 | 403 | bot.send_chat_action(chat_id, telegram.ChatAction.UPLOAD_VIDEO) 404 | 405 | utils.send_video(bot, chat_id, message_id, output_bytes, caption, chat_type) 406 | 407 | return 408 | elif output_type == constants.OutputType.PHOTO: 409 | if not utils.ensure_size_under_limit(output_file_size, telegram.constants.MAX_PHOTOSIZE_UPLOAD, update, context, file_reference_text='Converted file'): 410 | return 411 | 412 | bot.send_photo( 413 | chat_id, 414 | output_bytes, 415 | caption=caption, 416 | reply_to_message_id=message_id 417 | ) 418 | 419 | return 420 | elif output_type == constants.OutputType.STICKER: 421 | bot.send_sticker( 422 | chat_id, 423 | output_bytes, 424 | reply_to_message_id=message_id 425 | ) 426 | 427 | return 428 | elif output_type == constants.OutputType.FILE: 429 | if not utils.ensure_size_under_limit(output_file_size, telegram.constants.MAX_FILESIZE_UPLOAD, update, context, file_reference_text='Converted file'): 430 | return 431 | 432 | bot.send_chat_action(chat_id, telegram.ChatAction.UPLOAD_DOCUMENT) 433 | 434 | bot.send_document( 435 | chat_id, 436 | output_bytes, 437 | reply_to_message_id=message_id 438 | ) 439 | 440 | return 441 | 442 | if chat_type == telegram.Chat.PRIVATE: 443 | bot.send_message( 444 | chat_id, 445 | 'File type is not yet supported.', 446 | reply_to_message_id=message_id 447 | ) 448 | 449 | 450 | def message_video_handler(update: telegram.Update, context: telegram.ext.CallbackContext) -> None: 451 | message = update.effective_message 452 | 453 | if message is None: 454 | return 455 | 456 | chat = update.effective_chat 457 | 458 | if chat is None: 459 | return 460 | 461 | chat_type = chat.type 462 | bot = context.bot 463 | 464 | if chat_type != telegram.Chat.PRIVATE: 465 | return 466 | 467 | if cli_args.debug and not utils.check_admin(bot, context, message, analytics_handler, ADMIN_USER_ID): 468 | return 469 | 470 | message_id = message.message_id 471 | chat_id = message.chat.id 472 | attachment = message.video 473 | 474 | if attachment is None: 475 | return 476 | 477 | file_size = attachment.file_size 478 | 479 | if file_size is not None and not utils.ensure_size_under_limit(file_size, telegram.constants.MAX_FILESIZE_DOWNLOAD, update, context): 480 | return 481 | 482 | user = update.effective_user 483 | 484 | input_file_id = attachment.file_id 485 | 486 | if user is not None: 487 | create_or_update_user(bot, user) 488 | 489 | analytics_handler.track(context, analytics.AnalyticsType.MESSAGE, user) 490 | 491 | bot.send_chat_action(chat_id, telegram.ChatAction.TYPING) 492 | 493 | input_file = bot.get_file(input_file_id) 494 | input_file_url = input_file.file_path 495 | 496 | probe = None 497 | 498 | try: 499 | probe = ffmpeg.probe(input_file_url) 500 | except ffmpeg.Error: 501 | pass 502 | 503 | with io.BytesIO() as output_bytes: 504 | output_type = constants.OutputType.NONE 505 | 506 | invalid_format = None 507 | 508 | if probe: 509 | for stream in probe['streams']: 510 | codec_name = stream.get('codec_name') 511 | 512 | if codec_name is not None: 513 | invalid_format = codec_name 514 | 515 | if codec_name in constants.VIDEO_CODEC_NAMES: 516 | output_type = constants.OutputType.VIDEO_NOTE 517 | 518 | mp4_bytes = utils.convert(output_type, input_video_url=input_file_url) 519 | 520 | if not utils.ensure_valid_converted_file( 521 | file_bytes=mp4_bytes, 522 | update=update, 523 | context=context 524 | ): 525 | return 526 | 527 | if mp4_bytes is not None: 528 | output_bytes.write(mp4_bytes) 529 | 530 | break 531 | 532 | continue 533 | 534 | if output_type == constants.OutputType.NONE: 535 | if invalid_format is None and input_file_url is not None: 536 | parts = os.path.splitext(input_file_url) 537 | 538 | if parts is not None and len(parts) >= 2: 539 | extension = parts[1] 540 | 541 | if extension is not None: 542 | invalid_format = extension[1:] 543 | 544 | bot.send_message( 545 | chat_id=chat_id, 546 | text=f'File type "{invalid_format}" is not yet supported.', 547 | reply_to_message_id=message_id 548 | ) 549 | 550 | return 551 | 552 | output_bytes.seek(0) 553 | 554 | output_file_size = output_bytes.getbuffer().nbytes 555 | 556 | if output_type == constants.OutputType.VIDEO_NOTE: 557 | if not utils.ensure_size_under_limit(output_file_size, telegram.constants.MAX_FILESIZE_UPLOAD, update, context, file_reference_text='Converted file'): 558 | return 559 | 560 | bot.send_chat_action(chat_id, telegram.ChatAction.UPLOAD_VIDEO) 561 | 562 | utils.send_video_note(bot, chat_id, message_id, output_bytes) 563 | 564 | return 565 | 566 | bot.send_message( 567 | chat_id, 568 | 'File type is not yet supported.', 569 | reply_to_message_id=message_id 570 | ) 571 | 572 | 573 | def message_text_handler(update: telegram.Update, context: telegram.ext.CallbackContext) -> None: 574 | message = update.effective_message 575 | 576 | if message is None: 577 | return 578 | 579 | chat = update.effective_chat 580 | 581 | if chat is None: 582 | return 583 | 584 | chat_type = chat.type 585 | bot = context.bot 586 | 587 | if cli_args.debug and not utils.check_admin(bot, context, message, analytics_handler, ADMIN_USER_ID): 588 | return 589 | 590 | message_id = message.message_id 591 | chat_id = message.chat.id 592 | user = message.from_user 593 | entities = message.parse_entities() 594 | 595 | if user is not None: 596 | create_or_update_user(bot, user) 597 | 598 | analytics_handler.track(context, analytics.AnalyticsType.MESSAGE, user) 599 | 600 | valid_entities = { 601 | entity: text for entity, text in entities.items() if entity.type in [telegram.MessageEntity.URL, telegram.MessageEntity.TEXT_LINK] 602 | } 603 | entity, text = next(iter(valid_entities.items())) 604 | 605 | if entity is None: 606 | return 607 | 608 | input_link = entity.url 609 | 610 | if input_link is None: 611 | input_link = text 612 | 613 | with io.BytesIO() as output_bytes: 614 | caption = None 615 | video_url = None 616 | audio_url = None 617 | 618 | try: 619 | yt_dl_options = { 620 | 'logger': logger, 621 | 'no_color': True 622 | } 623 | 624 | with youtube_dl.YoutubeDL(yt_dl_options) as yt_dl: 625 | video_info = yt_dl.extract_info(input_link, download=False) 626 | 627 | if 'entries' in video_info: 628 | video = video_info['entries'][0] 629 | else: 630 | video = video_info 631 | 632 | if 'title' in video: 633 | caption = video['title'] 634 | else: 635 | caption = input_link 636 | 637 | file_size = None 638 | 639 | if 'requested_formats' in video: 640 | requested_formats = video['requested_formats'] 641 | 642 | video_data = list(filter(lambda requested_format: requested_format['vcodec'] != 'none', requested_formats))[0] 643 | audio_data = list(filter(lambda requested_format: requested_format['acodec'] != 'none', requested_formats))[0] 644 | 645 | if 'filesize' in video_data: 646 | file_size = video_data['filesize'] 647 | 648 | video_url = video_data['url'] 649 | 650 | if file_size is None: 651 | file_size = utils.get_file_size(video_url) 652 | 653 | audio_url = audio_data['url'] 654 | elif 'url' in video: 655 | video_url = video['url'] 656 | file_size = utils.get_file_size(video_url) 657 | 658 | if file_size is not None: 659 | if not utils.ensure_size_under_limit(file_size, telegram.constants.MAX_FILESIZE_UPLOAD, update, context): 660 | return 661 | 662 | except Exception as error: 663 | logger.error(f'youtube-dl error: {error}') 664 | 665 | if chat_type == telegram.Chat.PRIVATE and (caption is None or video_url is None): 666 | bot.send_message( 667 | chat_id, 668 | 'No video found on this link.', 669 | disable_web_page_preview=True, 670 | reply_to_message_id=message_id 671 | ) 672 | 673 | return 674 | 675 | mp4_bytes = utils.convert(constants.OutputType.VIDEO, input_video_url=video_url, input_audio_url=audio_url) 676 | 677 | if not utils.ensure_valid_converted_file( 678 | file_bytes=mp4_bytes, 679 | update=update, 680 | context=context 681 | ): 682 | return 683 | 684 | if mp4_bytes is not None: 685 | output_bytes.write(mp4_bytes) 686 | 687 | output_bytes.seek(0) 688 | 689 | if caption is not None: 690 | caption = caption[:telegram.constants.MAX_CAPTION_LENGTH] 691 | 692 | utils.send_video(bot, chat_id, message_id, output_bytes, caption, chat_type) 693 | 694 | 695 | def message_answer_handler(update: telegram.Update, context: telegram.ext.CallbackContext) -> None: 696 | callback_query = update.callback_query 697 | 698 | if callback_query is None: 699 | return 700 | 701 | raw_callback_data = callback_query.data 702 | 703 | if raw_callback_data is None: 704 | callback_query.answer() 705 | 706 | return 707 | 708 | callback_data = json.loads(raw_callback_data) 709 | 710 | if callback_data is None: 711 | callback_query.answer() 712 | 713 | return 714 | 715 | message = update.effective_message 716 | 717 | if message is None: 718 | return 719 | 720 | chat = update.effective_chat 721 | 722 | if chat is None: 723 | return 724 | 725 | chat_type = chat.type 726 | bot = context.bot 727 | 728 | attachment = message.effective_attachment 729 | 730 | if attachment is None: 731 | return 732 | 733 | if not isinstance(attachment, telegram.Video): 734 | return 735 | 736 | file_size = attachment.file_size 737 | 738 | if file_size is not None and not utils.ensure_size_under_limit(file_size, telegram.constants.MAX_FILESIZE_DOWNLOAD, update, context): 739 | return 740 | 741 | attachment_file_id = attachment.file_id 742 | 743 | message_id = message.message_id 744 | chat_id = message.chat.id 745 | 746 | user = update.effective_user 747 | 748 | if user is not None: 749 | create_or_update_user(bot, user) 750 | 751 | analytics_handler.track(context, analytics.AnalyticsType.MESSAGE, user) 752 | 753 | if chat_type == telegram.Chat.PRIVATE: 754 | bot.send_chat_action(chat_id, telegram.ChatAction.TYPING) 755 | 756 | input_file = bot.get_file(attachment_file_id) 757 | input_file_url = input_file.file_path 758 | 759 | probe = None 760 | 761 | try: 762 | probe = ffmpeg.probe(input_file_url) 763 | except ffmpeg.Error: 764 | pass 765 | 766 | with io.BytesIO() as output_bytes: 767 | output_type = constants.OutputType.NONE 768 | 769 | invalid_format = None 770 | 771 | if probe: 772 | for stream in probe['streams']: 773 | codec_name = stream.get('codec_name') 774 | 775 | if codec_name is not None: 776 | invalid_format = codec_name 777 | 778 | if codec_name in constants.VIDEO_CODEC_NAMES: 779 | output_type = constants.OutputType.VIDEO_NOTE 780 | 781 | mp4_bytes = utils.convert(output_type, input_video_url=input_file_url) 782 | 783 | if not utils.ensure_valid_converted_file( 784 | file_bytes=mp4_bytes, 785 | update=update, 786 | context=context 787 | ): 788 | callback_query.answer() 789 | 790 | return 791 | 792 | if mp4_bytes is not None: 793 | output_bytes.write(mp4_bytes) 794 | 795 | break 796 | 797 | continue 798 | 799 | if output_type == constants.OutputType.NONE: 800 | if chat_type == telegram.Chat.PRIVATE: 801 | if invalid_format is None and input_file_url is not None: 802 | parts = os.path.splitext(input_file_url) 803 | 804 | if parts is not None and len(parts) >= 2: 805 | extension = parts[1] 806 | 807 | if extension is not None: 808 | invalid_format = extension[1:] 809 | 810 | bot.send_message( 811 | chat_id=chat_id, 812 | text=f'File type "{invalid_format}" is not yet supported.', 813 | reply_to_message_id=message_id 814 | ) 815 | 816 | callback_query.answer() 817 | 818 | return 819 | 820 | output_bytes.seek(0) 821 | 822 | output_file_size = output_bytes.getbuffer().nbytes 823 | 824 | if output_type == constants.OutputType.VIDEO_NOTE: 825 | if not utils.ensure_size_under_limit(output_file_size, telegram.constants.MAX_FILESIZE_UPLOAD, update, context, file_reference_text='Converted file'): 826 | callback_query.answer() 827 | 828 | return 829 | 830 | bot.send_chat_action(chat_id, telegram.ChatAction.UPLOAD_VIDEO) 831 | 832 | utils.send_video_note(bot, chat_id, message_id, output_bytes) 833 | 834 | callback_query.answer() 835 | 836 | return 837 | 838 | if chat_type == telegram.Chat.PRIVATE: 839 | bot.send_message( 840 | chat_id, 841 | 'File type is not yet supported.', 842 | reply_to_message_id=message_id 843 | ) 844 | 845 | callback_query.answer() 846 | 847 | 848 | def error_handler(update: object, context: telegram.ext.CallbackContext) -> None: 849 | update_str = update.to_dict() if isinstance(update, telegram.Update) else str(update) 850 | 851 | logger.error(f'Update "{json.dumps(update_str, indent=4, ensure_ascii=False)}" caused error "{context.error}"') 852 | 853 | 854 | def main() -> None: 855 | message_file_filters = ( 856 | ( 857 | telegram.ext.Filters.audio | 858 | telegram.ext.Filters.document | 859 | telegram.ext.Filters.photo 860 | ) & ( 861 | ~ telegram.ext.Filters.animation 862 | ) 863 | ) | ( 864 | telegram.ext.Filters.chat_type.private & ( 865 | telegram.ext.Filters.voice | 866 | telegram.ext.Filters.sticker 867 | ) 868 | ) 869 | 870 | message_text_filters = ( 871 | telegram.ext.Filters.chat_type.private & ( 872 | telegram.ext.Filters.text & ( 873 | telegram.ext.Filters.entity(telegram.MessageEntity.URL) | 874 | telegram.ext.Filters.entity(telegram.MessageEntity.TEXT_LINK) 875 | ) 876 | ) 877 | ) 878 | 879 | video_filter = telegram.ext.Filters.video 880 | 881 | dispatcher = updater.dispatcher 882 | 883 | dispatcher.add_handler(telegram.ext.CommandHandler('start', start_command_handler)) 884 | 885 | dispatcher.add_handler(telegram.ext.CommandHandler('restart', restart_command_handler)) 886 | dispatcher.add_handler(telegram.ext.CommandHandler('logs', logs_command_handler)) 887 | dispatcher.add_handler(telegram.ext.CommandHandler('users', users_command_handler, pass_args=True)) 888 | 889 | dispatcher.add_handler(telegram.ext.MessageHandler(message_file_filters, message_file_handler, run_async=True)) 890 | dispatcher.add_handler(telegram.ext.MessageHandler(video_filter, message_video_handler, run_async=True)) 891 | dispatcher.add_handler(telegram.ext.MessageHandler(message_text_filters, message_text_handler, run_async=True)) 892 | dispatcher.add_handler(telegram.ext.CallbackQueryHandler(message_answer_handler, run_async=True)) 893 | 894 | if cli_args.debug: 895 | logger.info('Started polling') 896 | 897 | updater.start_polling(timeout=0.01) 898 | else: 899 | dispatcher.add_error_handler(error_handler) 900 | 901 | if cli_args.server and not cli_args.polling: 902 | logger.info('Started webhook') 903 | 904 | if config: 905 | webhook = config['Webhook'] 906 | 907 | port = int(webhook['Port']) 908 | key = webhook['Key'] 909 | cert = webhook['Cert'] 910 | url = webhook['Url'] + BOT_TOKEN 911 | 912 | if cli_args.set_webhook: 913 | logger.info('Updated webhook') 914 | else: 915 | setattr(updater.bot, 'set_webhook', (lambda *args, **kwargs: False)) 916 | 917 | updater.start_webhook( 918 | listen='0.0.0.0', 919 | port=port, 920 | url_path=BOT_TOKEN, 921 | key=key, 922 | cert=cert, 923 | webhook_url=url 924 | ) 925 | else: 926 | logger.error('Missing bot webhook config') 927 | 928 | return 929 | else: 930 | logger.info('Started polling') 931 | 932 | updater.start_polling() 933 | 934 | logger.info('Bot started. Press Ctrl-C to stop.') 935 | 936 | updater.bot.send_message(ADMIN_USER_ID, 'Bot has been restarted') 937 | updater.idle() 938 | 939 | 940 | if __name__ == '__main__': 941 | parser = argparse.ArgumentParser() 942 | 943 | parser.add_argument('-d', '--debug', action='store_true') 944 | 945 | parser.add_argument('-p', '--polling', action='store_true') 946 | parser.add_argument('-sw', '--set-webhook', action='store_true') 947 | parser.add_argument('-s', '--server', action='store_true') 948 | 949 | cli_args = parser.parse_args() 950 | 951 | if cli_args.debug: 952 | logger.info('Debug') 953 | 954 | config = None 955 | 956 | try: 957 | config = configparser.ConfigParser() 958 | 959 | config.read('config.cfg') 960 | 961 | BOT_NAME = config.get('Telegram', 'Name' if cli_args.server else 'TestName') 962 | BOT_TOKEN = config.get('Telegram', 'Key' if cli_args.server else 'TestKey') 963 | except configparser.Error as config_error: 964 | logger.error(f'Config error: {config_error}') 965 | 966 | sys.exit(1) 967 | 968 | if not BOT_TOKEN: 969 | logger.error('Missing bot token') 970 | 971 | sys.exit(2) 972 | 973 | updater = telegram.ext.Updater(BOT_TOKEN) 974 | analytics_handler = analytics.AnalyticsHandler() 975 | 976 | try: 977 | ADMIN_USER_ID = config.getint('Telegram', 'Admin') 978 | 979 | if not cli_args.debug: 980 | analytics_handler.googleToken = config.get('Google', 'Key') 981 | except configparser.Error as config_error: 982 | logger.warning(f'Config error: {config_error}') 983 | 984 | analytics_handler.userAgent = BOT_NAME 985 | 986 | main() 987 | -------------------------------------------------------------------------------- /src/migrations/001_nullable_telegram_username.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import peewee 4 | import peewee_migrate 5 | 6 | 7 | def migrate(migrator: peewee_migrate.Migrator, _database: peewee.Database, fake=False, **_kwargs: typing.Any) -> None: 8 | if fake is True: 9 | return 10 | 11 | migrator.drop_not_null('user', 'telegram_username') 12 | -------------------------------------------------------------------------------- /src/migrations/002_dates_without_milliseconds.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import peewee 4 | import peewee_migrate 5 | 6 | GENERIC_DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' 7 | 8 | 9 | def migrate(migrator: peewee_migrate.Migrator, _database: peewee.Database, fake=False, **_kwargs: typing.Any) -> None: 10 | if fake is True: 11 | return 12 | 13 | user_class = migrator.orm['user'] 14 | 15 | for user in user_class.select(): 16 | user.created_at = user.created_at.strftime(GENERIC_DATE_TIME_FORMAT) 17 | user.updated_at = user.updated_at.strftime(GENERIC_DATE_TIME_FORMAT) 18 | 19 | user.save() 20 | -------------------------------------------------------------------------------- /src/setup.cfg: -------------------------------------------------------------------------------- 1 | [pep8] 2 | ignore = E126,E501 3 | -------------------------------------------------------------------------------- /src/telegram_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import typing 4 | 5 | import telegram.utils.helpers 6 | 7 | 8 | def escape_v2_markdown_text(text: str, entity_type: typing.Optional[str] = None) -> str: 9 | return telegram.utils.helpers.escape_markdown( 10 | text=text, 11 | version=2, 12 | entity_type=entity_type 13 | ) 14 | 15 | 16 | def escape_v2_markdown_text_link(text: str, url: str) -> str: 17 | escaped_text = escape_v2_markdown_text(text) 18 | escaped_url = escape_v2_markdown_text( 19 | text=url, 20 | entity_type=telegram.MessageEntity.TEXT_LINK 21 | ) 22 | 23 | return f'[{escaped_text}]({escaped_url})' 24 | 25 | 26 | ESCAPED_FULL_STOP = escape_v2_markdown_text('.') 27 | ESCAPED_VERTICAL_LINE = escape_v2_markdown_text('|') 28 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import io 4 | import json 5 | import logging 6 | import typing 7 | 8 | import ffmpeg 9 | import telegram.ext 10 | 11 | import analytics 12 | import constants 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def check_admin(bot: telegram.Bot, context: telegram.ext.CallbackContext, message: telegram.Message, analytics_handler: analytics.AnalyticsHandler, admin_user_id: int) -> bool: 18 | user = message.from_user 19 | 20 | if user is None: 21 | return False 22 | 23 | analytics_handler.track(context, analytics.AnalyticsType.COMMAND, user, message.text) 24 | 25 | if user.id != admin_user_id: 26 | bot.send_message(message.chat_id, 'You are not allowed to use this command') 27 | 28 | return False 29 | 30 | return True 31 | 32 | 33 | def ensure_size_under_limit(size: int, limit: int, update: telegram.Update, context: telegram.ext.CallbackContext, file_reference_text='File') -> bool: 34 | if size <= limit: 35 | return True 36 | 37 | chat = update.effective_chat 38 | 39 | if chat is None: 40 | return False 41 | 42 | chat_type = chat.type 43 | 44 | if chat_type == telegram.Chat.PRIVATE: 45 | message = update.effective_message 46 | 47 | if message is None: 48 | return False 49 | 50 | message_id = message.message_id 51 | chat_id = chat.id 52 | 53 | context.bot.send_message( 54 | chat_id=chat_id, 55 | text=( 56 | f'{file_reference_text} size {get_size_string_from_bytes(size)} ' 57 | f'exceeds the maximum limit of {get_size_string_from_bytes(limit)} ' 58 | '(limit imposed by Telegram, not by this bot).' 59 | ), 60 | reply_to_message_id=message_id 61 | ) 62 | 63 | return False 64 | 65 | 66 | def ensure_valid_converted_file(file_bytes: typing.Optional[bytes], update: telegram.Update, context: telegram.ext.CallbackContext) -> bool: 67 | if file_bytes is not None: 68 | return True 69 | 70 | chat = update.effective_chat 71 | 72 | if chat is None: 73 | return False 74 | 75 | chat_type = chat.type 76 | 77 | if chat_type == telegram.Chat.PRIVATE: 78 | message = update.effective_message 79 | 80 | if message is None: 81 | return False 82 | 83 | message_id = message.message_id 84 | chat_id = chat.id 85 | 86 | context.bot.send_message( 87 | chat_id=chat_id, 88 | text='File could not be converted.', 89 | reply_to_message_id=message_id 90 | ) 91 | 92 | return False 93 | 94 | 95 | def send_video(bot: telegram.Bot, chat_id: int, message_id: int, output_bytes: io.BytesIO, caption: typing.Optional[str], chat_type: str) -> None: 96 | reply_markup: typing.Optional[telegram.ReplyMarkup] = None 97 | 98 | if chat_type == telegram.Chat.PRIVATE: 99 | button = telegram.InlineKeyboardButton('Rounded', callback_data=json.dumps({})) 100 | reply_markup = telegram.InlineKeyboardMarkup([[button]]) 101 | 102 | bot.send_video( 103 | chat_id, 104 | output_bytes, 105 | caption=caption, 106 | supports_streaming=True, 107 | reply_to_message_id=message_id, 108 | reply_markup=reply_markup 109 | ) 110 | 111 | 112 | def send_video_note(bot: telegram.Bot, chat_id: int, message_id: int, output_bytes: io.BytesIO) -> None: 113 | bot.send_video_note( 114 | chat_id, 115 | output_bytes, 116 | reply_to_message_id=message_id 117 | ) 118 | 119 | 120 | def get_file_size(video_url: str) -> int: 121 | info = ffmpeg.probe(video_url, show_entries='format=size') 122 | size = info.get('format', {}).get('size') 123 | 124 | return int(size) 125 | 126 | 127 | def has_audio_stream(video_url: typing.Optional[str]) -> bool: 128 | if not video_url: 129 | return False 130 | 131 | info = ffmpeg.probe(video_url, select_streams='a', show_entries='format=:streams=index') 132 | streams = info.get('streams', []) 133 | 134 | return len(streams) > 0 135 | 136 | 137 | def convert(output_type: str, input_video_url: typing.Optional[str] = None, input_audio_url: typing.Optional[str] = None) -> typing.Optional[bytes]: 138 | try: 139 | if output_type == constants.OutputType.AUDIO: 140 | return ( 141 | ffmpeg 142 | .input(input_audio_url) 143 | .output('pipe:', format='opus', strict='-2') 144 | .run(capture_stdout=True) 145 | )[0] 146 | elif output_type == constants.OutputType.VIDEO: 147 | if input_audio_url is None: 148 | return ( 149 | ffmpeg 150 | .input(input_video_url) 151 | .output('pipe:', format='mp4', movflags='frag_keyframe+empty_moov', strict='-2') 152 | .run(capture_stdout=True) 153 | )[0] 154 | else: 155 | input_video = ffmpeg.input(input_video_url) 156 | input_audio = ffmpeg.input(input_audio_url) 157 | 158 | return ( 159 | ffmpeg 160 | .output(input_video, input_audio, 'pipe:', format='mp4', movflags='frag_keyframe+empty_moov', strict='-2') 161 | .run(capture_stdout=True) 162 | )[0] 163 | elif output_type == constants.OutputType.VIDEO_NOTE: 164 | # Copied from https://github.com/kkroening/ffmpeg-python/issues/184#issuecomment-504390452. 165 | 166 | ffmpeg_input = ( 167 | ffmpeg 168 | .input(input_video_url, t=constants.MAX_VIDEO_NOTE_LENGTH) 169 | ) 170 | ffmpeg_input_video = ( 171 | ffmpeg_input 172 | .video 173 | .crop( 174 | constants.VIDEO_NOTE_CROP_OFFSET_PARAMS, 175 | constants.VIDEO_NOTE_CROP_OFFSET_PARAMS, 176 | constants.VIDEO_NOTE_CROP_SIZE_PARAMS, 177 | constants.VIDEO_NOTE_CROP_SIZE_PARAMS 178 | ) 179 | .filter( 180 | 'scale', 181 | constants.VIDEO_NOTE_SCALE_SIZE_PARAMS, 182 | constants.VIDEO_NOTE_SCALE_SIZE_PARAMS 183 | ) 184 | ) 185 | 186 | ffmpeg_output: ffmpeg.nodes.OutputStream 187 | 188 | if has_audio_stream(input_video_url): 189 | ffmpeg_input_audio = ffmpeg_input.audio 190 | ffmpeg_joined = ffmpeg.concat(ffmpeg_input_video, ffmpeg_input_audio, v=1, a=1).node 191 | ffmpeg_output = ffmpeg.output(ffmpeg_joined[0], ffmpeg_joined[1], 'pipe:', format='mp4', movflags='frag_keyframe+empty_moov', strict='-2') 192 | else: 193 | ffmpeg_joined = ffmpeg.concat(ffmpeg_input_video, v=1).node 194 | ffmpeg_output = ffmpeg.output(ffmpeg_joined[0], 'pipe:', format='mp4', movflags='frag_keyframe+empty_moov', strict='-2') 195 | 196 | return ffmpeg_output.run(capture_stdout=True)[0] 197 | elif output_type == constants.OutputType.FILE: 198 | return ( 199 | ffmpeg 200 | .input(input_audio_url) 201 | .output('pipe:', format='mp3', strict='-2') 202 | .run(capture_stdout=True) 203 | )[0] 204 | except ffmpeg.Error as error: 205 | logger.error(f'ffmpeg error: {error}') 206 | 207 | return None 208 | 209 | 210 | def get_size_string_from_bytes(bytes_count: int, suffix='B') -> str: 211 | """ 212 | Partially copied from https://stackoverflow.com/a/1094933/865175. 213 | """ 214 | 215 | converted_bytes_count = float(bytes_count) 216 | 217 | for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: 218 | if abs(converted_bytes_count) < 1000.0: 219 | return '%3.1f %s%s' % (converted_bytes_count, unit, suffix) 220 | 221 | converted_bytes_count /= 1000.0 222 | 223 | return '%.1f %s%s' % (converted_bytes_count, 'Y', suffix) 224 | --------------------------------------------------------------------------------