├── .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 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Custom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/fabric.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
--------------------------------------------------------------------------------