├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py ├── examples.rst ├── images │ ├── keys_hierarchy.png │ ├── pattrs.png │ ├── rbfm_schema.png │ └── tgbox_ppart_id.png ├── index.rst ├── installation.rst ├── localbox.rst ├── make.bat ├── protocol.rst ├── remotebox.rst └── tgbox.rst ├── setup.py └── tgbox ├── __init__.py ├── api ├── __init__.py ├── abstract.py ├── db.py ├── local.py ├── remote.py ├── sync.py └── utils.py ├── crypto.py ├── defaults.py ├── errors.py ├── fastelethon.py ├── keys.py ├── other ├── tgbox_logo.png └── words.txt ├── tools.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # VIM 132 | *.sw* 133 | 134 | # User defined 135 | *.hidden 136 | *.sql 137 | *.sqlite 138 | *test.py 139 | BoxDownloads/ 140 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | python: 15 | install: 16 | - method: pip 17 | path: . 18 | extra_requirements: 19 | - doc 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include tgbox/other * 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | TGBOX: encrypted cloud storage based on Telegram 2 | ================================================ 3 | .. epigraph:: 4 | 5 | | ❕ This repository contains a set of classes and functions used to manage TGBOX. 6 | | Try the `tgbox-cli `__ if you're interested in working implementation! 7 | 8 | .. code-block:: python 9 | 10 | import tgbox, tgbox.api.sync 11 | 12 | # This two will not work. Get your own at https://my.telegram.org 13 | API_ID, API_HASH = 1234567, '00000000000000000000000000000000' 14 | 15 | tc = tgbox.api.TelegramClient(api_id=API_ID, api_hash=API_HASH) 16 | tc.start() # This method will prompt you for Phone, Code & Password 17 | 18 | print(phrase := tgbox.keys.Phrase.generate()) # Your secret Box Phrase 19 | basekey = tgbox.keys.make_basekey(phrase) # Will Require 1GB of RAM 20 | box = tgbox.api.make_box(tc, basekey) # Will make Encrypted File Storage 21 | 22 | # Will upload selected file to the RemoteBox, cache information 23 | # in LocalBox and return the tgbox.api.abstract.BoxFile object 24 | abbf = box.push(input('File to upload (path): ')) 25 | 26 | # Retrieving some info from the BoxFile 27 | print('File size:', abbf.size, 'bytes') 28 | print('File name:', abbf.file_name) 29 | 30 | downloaded = abbf.download() # Downloading your file from Remote. 31 | print(downloaded) # Will print path to downloaded file object 32 | 33 | box.done() # Work is done. Close all connections! 34 | 35 | .. epigraph:: 36 | 37 | | ❔ This code block heavily utilize the magic ``tgbox.api.sync`` module and high-level functions 38 | | from the ``tgbox.api.abstract`` module for showcase. For actual *Async* code, see `Examples `__. 39 | 40 | Motivation 41 | ---------- 42 | 43 | The Telegram is beautiful app. Not only by mean of features and Client API, but it's also used to be good in cryptography and secure messaging. In the last years, core and client devs of Telegram mostly work for "social-network features", i.e video chats and message reactions which is OK (until stories, wtf?), but there also can be plenty of "crypto-related" things implemented. 44 | 45 | Target 46 | ------ 47 | 48 | This *[unofficial]* library targets to be a PoC of **encrypted file storage** inside the Telegram, and should be used as standalone *Python library*. 49 | 50 | Abstract 51 | -------- 52 | 53 | We name *"encrypted cloud storage"* as **Box** and the API to it as ``tgbox``. The *Box* splits into the **RemoteBox** and the **LocalBox**. They define a basic primitives. You can **share** your *Box* and separate *Files* with other people absolutely **secure** - only You and someone you want will have decryption key, even through insecure communication canals (`e2e `__). You can make **unlimited** amount of Boxes, Upload & Download **speed is faster** than in official Telegram clients and maximum filesize is around **2GB** and around **4GB** for Premium users. 54 | 55 | Documentation 56 | ------------- 57 | 58 | See `ReadTheDocs `__ for main information and help. 59 | 60 | You can also build docs from the source 61 | 62 | .. code-block:: console 63 | 64 | git clone https://github.com/NonProject/tgbox --branch=indev 65 | cd tgbox && python3 -m pip install .[doc] # Install with doc 66 | cd docs && make html && firefox _build/html/index.html 67 | 68 | Third party & thanks to 69 | ----------------------- 70 | - `⭐️ `__ **Stargazers!** 71 | - `Sphinx_book_theme `__ (`BSD 3-Clause `__) 72 | - `Aiosqlite `__ (`MIT `__) 73 | - `Telethon `__ (`MIT `__) 74 | - `Ecdsa `__ (`LICENSE `__) 75 | - `Filetype `__ (`MIT `__) 76 | - `Cryptg `__ (`LICENSE `__) 77 | - `Cryptography `__ (`LICENSE `__) 78 | 79 | Resources 80 | --------- 81 | - Official **developer channel**: `@nontgbox `__ 82 | - **Example** TGBOX **container**: `@nontgbox_non `__ 83 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # To show inherited methods on all classes we need to specify some env args: 8 | # export SPHINX_APIDOC_OPTIONS="members,undoc-members,show-inheritance,inherited-members" 9 | # sphinx-apidoc -o _docs {project_path} 10 | 11 | import os 12 | import sys 13 | 14 | # -- Path setup -------------------------------------------------------------- 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | 20 | sys.path.insert(0, os.path.abspath('..')) 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | from tgbox.version import VERSION 25 | 26 | project = 'tgbox' 27 | copyright = '2024, NonProjects' 28 | author = 'NotStatilko' 29 | 30 | # The full version, including alpha/beta/rc tags 31 | release = VERSION 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 38 | extensions = [ 39 | 'sphinx.ext.autodoc', 40 | 'sphinx.ext.autosectionlabel', 41 | 'sphinx.ext.viewcode', 42 | 'sphinx_togglebutton' 43 | ] 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 51 | 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | html_theme = "sphinx_book_theme" 58 | -------------------------------------------------------------------------------- /docs/images/keys_hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NonProjects/tgbox/c55abb8b731af4c9b46795cde73c9bff436049ae/docs/images/keys_hierarchy.png -------------------------------------------------------------------------------- /docs/images/pattrs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NonProjects/tgbox/c55abb8b731af4c9b46795cde73c9bff436049ae/docs/images/pattrs.png -------------------------------------------------------------------------------- /docs/images/rbfm_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NonProjects/tgbox/c55abb8b731af4c9b46795cde73c9bff436049ae/docs/images/rbfm_schema.png -------------------------------------------------------------------------------- /docs/images/tgbox_ppart_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NonProjects/tgbox/c55abb8b731af4c9b46795cde73c9bff436049ae/docs/images/tgbox_ppart_id.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | TGBOX: encrypted cloud storage based on Telegram 2 | ================================================ 3 | 4 | Motivation 5 | ---------- 6 | 7 | The Telegram is beautiful app. Not only by mean of features and Client API, but it's also good in cryptography and secure messaging. In the last years, core and client devs of Telegram mostly work for "social-network features", i.e video chats and message reactions which is OK, but there also can be plenty of "crypto-related" things. 8 | 9 | Target 10 | ------ 11 | 12 | This *[unofficial]* library targets to be a PoC of **encrypted file storage** inside the Telegram, but can be used as standalone API. 13 | 14 | Abstract 15 | -------- 16 | 17 | We name *"encrypted cloud storage"* as **Box** and the API to it as **Tgbox**. There is **two** of boxes: the **RemoteBox** and the **LocalBox**. They define a basic primitives. You can share your Box and separate Files with other people absolutely secure - only You and someone you want will have decryption key, even through insecure communication canals (`e2e `_). You can make unlimited amount of Boxes, Upload & Download speed is **faster** than in official Telegram clients and maximum filesize is around **2GB** and around **4GB** for Premium users. 18 | 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | :caption: Core: 23 | 24 | installation 25 | protocol 26 | remotebox 27 | localbox 28 | examples 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | :caption: Modules: 33 | 34 | tgbox 35 | 36 | Indices and tables 37 | ================== 38 | 39 | * :ref:`genindex` 40 | * :ref:`modindex` 41 | * :ref:`search` 42 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | .. note:: 5 | TGBOX library **require** `Python `_ version **3.8 or above**. 6 | 7 | PyPI (pip) 8 | ---------- 9 | 10 | .. code-block:: console 11 | 12 | python3 -m pip install tgbox # Pure Python (very slow) 13 | python3 -m pip install tgbox[fast] # With C libraries 14 | 15 | Clone from GitHub 16 | ----------------- 17 | 18 | .. code-block:: console 19 | 20 | git clone https://github.com/NonProjects/tgbox 21 | python3 -m pip install ./tgbox/[fast] 22 | 23 | Optional dependencies 24 | --------------------- 25 | 26 | - Library can work in a **Pure Python** way, without `cryptography `_, by using `pyaes `_ and `ecdsa `_ only, but this will be **much slower** and **not so secure**. Pure Python is **not recommended** for use, but testing only is OK! 27 | 28 | .. note:: 29 | The `cryptography `_ project has `wheels `_ for many systems. Big chance that you **will not need to compile a C code**, so always try to install ``tgbox[fast]``. 30 | 31 | - Library will use the **Regex** python *package* (`PyPI `_) if it's installed in environment. 32 | 33 | - With `FFmpeg `_, library can **make previews** for media files and **extract duration** to attach it to the *RemoteBox File*. To work, it should be in your System ``PATH`` (`see more about PATH `_). We will call it as ``ffmpeg`` (:const:`tgbox.defaults.FFMPEG`) shell command via `subprocess `_. 34 | 35 | -------------------------------------------------------------------------------- /docs/localbox.rst: -------------------------------------------------------------------------------- 1 | LocalBox 2 | ======== 3 | 4 | The *LocalBox* is a `SQLite Database `_ and a place where we store *Metadata* of pushed to :doc:`remotebox` *Files*. 5 | 6 | .. note:: 7 | :class:`~tgbox.api.local.DecryptedLocalBox` can be fully restored from :class:`~tgbox.api.remote.DecryptedRemoteBox`. 8 | 9 | Tables 10 | ------ 11 | 12 | *LocalBox* has four tables: *BOX_DATA*, *FILES*, *PATH_PARTS* and *DEFAULTS*. 13 | 14 | BOX_DATA 15 | ^^^^^^^^ 16 | 17 | *BOX_DATA* store information about *Box*, *Session*, etc. 18 | 19 | ============== =========== ======== ========= ======= ====== ======== ======================= 20 | BOX_CHANNEL_ID BOX_CR_TIME BOX_SALT MAINKEY SESSION API_ID API_HASH FAST_SYNC_LAST_EVENT_ID 21 | ============== =========== ======== ========= ======= ====== ======== ======================= 22 | BLOB BLOB BLOB BLOB|NULL BLOB BLOB BLOB BLOB 23 | ============== =========== ======== ========= ======= ====== ======== ======================= 24 | 25 | - ``BOX_CHANNEL_ID`` -- *Encrypted RemoteBox (Telegram channel) ID* 26 | - ``BOX_CR_TIME`` -- *Encrypted LocalBox creation time* 27 | - ``BOX_SALT`` -- *BoxSalt for MainKey creation* 28 | - ``MAINKEY`` -- *Encrypted by BaseKey MainKey. Used if RemoteBox was cloned* 29 | - ``SESSION`` -- *Encrypted by BaseKey Telethon's StringSession* 30 | - ``API_ID`` -- *Encrypted by MainKey your API_ID from the my.telegram.org site* 31 | - ``API_HASH`` -- *Encrypted by MainKey your API_HASH from the my.telegram.org site* 32 | - ``FAST_SYNC_LAST_EVENT_ID`` -- *Last found on the fast syncing event ID* 33 | 34 | FILES 35 | ^^^^^ 36 | 37 | *FILES* store information about uploaded to the *RemoteBox* files. 38 | 39 | ================ =========== ========== ========= =========== ======== ================ 40 | ID {PRIMARY_KEY} UPLOAD_TIME PPATH_HEAD FILEKEY FINGERPRINT METADATA UPDATED_METADATA 41 | ================ =========== ========== ========= =========== ======== ================ 42 | INT BLOB BLOB BLOB|NULL BLOB BLOB BLOB|NULL 43 | ================ =========== ========== ========= =========== ======== ================ 44 | 45 | .. note:: 46 | - ``ID`` is a Telegram message ID. **Must** be unique as any SQLite *PrimaryKey* 47 | - ``PPATH_HEAD`` is a path PartID of the last part (folder). See a ":ref:`How does we store file paths`" 48 | - ``FILEKEY`` will be not ``NULL`` only when you import *RemoteBox File* from other's :doc:`remotebox`. In this case it will be encrypted by :class:`~tgbox.keys.MainKey` 49 | - ``FINGERPRINT`` is, in short, a *SHA256* over *File path* with *file name* plus *Mainkey* (:func:`~tgbox.tools.make_file_fingerprint`), not a hash of file. We use it to check if some file was already uploaded to *RemoteBox*. 50 | - We take ``METADATA`` plus *File IV* from the *RemoteBox File* and place it to the *LocalBox* without changing anything 51 | - ``UPDATED_METADATA`` is a user changes of ``METADATA``, encrypted and packed with the *PackedAttributes* algorithm (see :doc:`protocol`) 52 | 53 | DEFAULTS 54 | ^^^^^^^^ 55 | 56 | *DEFAULTS* store some of the default TGBOX values 57 | 58 | ============ ============= ============= ============= ============== 59 | METADATA_MAX FILE_PATH_MAX DOWNLOAD_PATH DEF_NO_FOLDER DEF_UNK_FOLDER 60 | ============ ============= ============= ============= ============== 61 | INTEGER INTEGER TEXT TEXT TEXT 62 | ============ ============= ============= ============= ============== 63 | 64 | .. note:: 65 | - ``METADATA_MAX`` is the bytesize limit of the TGBOX file metadata 66 | - ``FILE_PATH_MAX`` is the bytesize limit of the file path 67 | - ``DOWNLOAD_PATH`` is the default download path 68 | - ``DEF_NO_FOLDER`` is the default folder when file path is not specified on uploading/importing 69 | - ``DEF_UNK_FOLDER`` is the default folder to which files will be placed on download if ``hide_folder`` is ``True`` 70 | 71 | PATH_PARTS 72 | ^^^^^^^^^^ 73 | 74 | *PATH_PARTS* store every path part in encrypted form with their IDs. 75 | 76 | ======== ===================== ============== 77 | ENC_PART PART_ID {PRIMARY_KEY} PARENT_PART_ID 78 | ======== ===================== ============== 79 | BLOB BLOB BLOB|NULL 80 | ======== ===================== ============== 81 | 82 | .. note:: 83 | - ``ENC_PART`` is an encrypted by :class:`~tgbox.keys.MainKey` *Path Part* 84 | - ``PART_ID`` is *Path Part ID* (see :ref:`How does we store file paths`) 85 | - ``PARENT_PART_ID`` is a *Part ID* of *Parent path* (see :ref:`How does we store file paths`) 86 | 87 | 88 | How does we store file paths 89 | ---------------------------- 90 | 91 | Every file in TGBOX (as well as in any OS) must have a *file path*. TGBOX *should* accept any path that `pathlib.Path `_ can support: the UNIX-like and Windows-like. So, let's imagine that we have an abstract file called *file.txt*. It's absolute (Unix-like) path will be ``/home/user/Documents``. The *RemoteBox File* will store its path in a *File Metadata* as is. However, in the *LocalBox* we will store it more efficiently, in a Blockchain-like way. 92 | 93 | | 94 | 95 | .. image:: images/tgbox_ppart_id.png 96 | :align: center 97 | :width: 500px 98 | 99 | | 100 | 101 | In this schema we split a *File path* by parts (*/*, *home*, *...*). A path anchor (*/* in UNIX and i.e *C:\\* on Windows) is also considered a *Path Part*. Our goal here is to create a **unique** *ID* for each *Path Part*, and we complete it by **hashing** string concated from :class:`~tgbox.keys.MainKey`, *SHA256* of *Path Part* (i.e *user*) and *Parent Part ID* (in this schema, the *Parent Part ID* is *Px*). For a first *Path Part* we use empty bytestring (``b''`` as *P₀*), as there is currently no parent. 102 | 103 | .. admonition:: *Path Part ID generator* in Python code 104 | :class: dropdown 105 | 106 | .. code-block:: python 107 | 108 | ... # Some code was omitted 109 | 110 | def ppart_id_generator(path: Path, mainkey: MainKey) -> Generator[tuple, None, None]: 111 | """ 112 | This generator will iterate over path parts and 113 | yield their unique IDs. We will use this to better 114 | navigate over *abstract* Folders in the LocalBox. 115 | 116 | The path **shouldn't** contain a file name, 117 | otherwise directory will contain it as folder. 118 | 119 | */home/user/* is **OK** 120 | */home/user/file.txt* is **NOT** 121 | 122 | Will yield a tuple (PART, PARENT_PART_ID, PART_ID) 123 | """ 124 | parent_part_id = b'' # The root (/ anchor) doesn't have parent 125 | for part in path.parts: 126 | part_id = sha256( 127 | mainkey\ 128 | + sha256(part.encode()).digest()\ 129 | + parent_part_id 130 | ) 131 | yield (part, parent_part_id, part_id.digest()) 132 | parent_part_id = part_id.digest() 133 | 134 | .. tip:: 135 | See it in a ``tools`` module: :func:`tgbox.tools.ppart_id_generator`. 136 | 137 | 138 | Additionally, we encrypt the *Path Part* with a :class:`~tgbox.keys.MainKey` and store it with *Part ID (Px)* and *Parent Part ID (Px-1)* in a ``PATH_PARTS`` table of *LocalBox* SQLite *Database*. In the default :doc:`protocol` behaviour this process is initiated after user pushed file to *RemoteBox*, on saving *Metadata* to *LocalBox*. We make a *Path Part IDs* and insert data into ``PATH_PARTS`` table if it's not contains this *Part ID*. When we store data of the *File* in the ``FILES`` table, we insert the **last** *Part ID* (or *Path Part ID Head*) into the ``FILES:PPATH_HEAD`` column (see :ref:`FILES`). 139 | 140 | Why bother? 141 | ^^^^^^^^^^^ 142 | 143 | All files with the same *Directory* will be linked to the unique *Part ID*. This gives us ability to quick-fetch all *LocalBox Files* that have the same *File path*. With this, we can reduce amount of files to search on. Let's imagine that you're a *Linux* user which share *Box* with a *Windows* user, and you want to find some file which was uploaded from the *Windows*. You can make a :class:`~tgbox.tools.SearchFilter` with a ``scope='C:\\'`` keyword argument. This **will not** fetch any of the files uploaded from *Linux*. 144 | 145 | You can work at full with such *Abstract Directories* by using methods of :class:`~tgbox.api.local.DecryptedLocalBoxDirectory`. For example, you can iterate over it with :meth:`~tgbox.api.local.DecryptedLocalBoxDirectory.iterdir`, load parent *Directory* with :meth:`~tgbox.api.local.DecryptedLocalBoxDirectory.lload` and so on. To get a *Directory* object you can use a :meth:`~tgbox.api.local.DecryptedLocalBox.get_directory` (or use :meth:`~tgbox.api.local.DecryptedLocalBox.contents`). Also, every :class:`~tgbox.api.local.DecryptedLocalBoxFile` contains a :attr:`~tgbox.api.local.DecryptedLocalBoxFile.directory` property. 146 | 147 | .. note:: 148 | Searching (or just getting) *LocalBox Files* filtered by :class:`~tgbox.tools.SearchFilter` with ``scope`` (or with :meth:`~tgbox.api.local.DecryptedLocalBox.contents`) is **always better** and typically more faster. We **will not** need to decrypt **each** *File* and compare it with other *filters* (this can be slow if you have a plenty of files, like, thousands). 149 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/remotebox.rst: -------------------------------------------------------------------------------- 1 | RemoteBox 2 | ========= 3 | 4 | The **RemoteBox** is a *Telegram Channel* and a place where we store *Metadata* **plus** *Encrypted Files*. *RemoteBox* stores encoded by `Url Safe Base64 `_ :class:`~tgbox.crypto.BoxSalt` in the description. By default, all created by :doc:`protocol` *Telegram Channels* will have a ``f"TGBOX[{tgbox.defaults.VERBYTE.hex()}]: "`` prefix in the *name*. 5 | 6 | .. versionadded:: v1.5 7 | Now users can **add their description** about *Box* in a format "*My optional Description! @ *". I.e: *My text @ amiBJVgXyJ9akLHSZV7T8hvcTmRpKvr9LhvY9WiTU18=*. **From your code**, you can easily fetch it via :meth:`~tgbox.api.remote.DecryptedRemoteBox.get_box_description` method. 8 | 9 | | Currently, the *RemoteBox* doesn't really have any special things to discuss here. 10 | | We use it only to store a *Files* produced by the :doc:`protocol`. 11 | -------------------------------------------------------------------------------- /docs/tgbox.rst: -------------------------------------------------------------------------------- 1 | tgbox package 2 | ============= 3 | 4 | tgbox.api.local module 5 | ---------------------- 6 | 7 | .. automodule:: tgbox.api.local 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | :inherited-members: 12 | 13 | tgbox.api.remote module 14 | ----------------------- 15 | 16 | .. automodule:: tgbox.api.remote 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | :inherited-members: 21 | 22 | tgbox.api.db module 23 | ------------------- 24 | 25 | .. automodule:: tgbox.api.db 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | :inherited-members: 30 | 31 | tgbox.api.utils module 32 | ---------------------- 33 | 34 | .. automodule:: tgbox.api.utils 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | :inherited-members: 39 | 40 | tgbox.api.abstract module 41 | ------------------------- 42 | 43 | .. automodule:: tgbox.api.abstract 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | :inherited-members: 48 | 49 | tgbox.api.sync module 50 | ---------------------- 51 | 52 | .. automodule:: tgbox.api.sync 53 | :members: 54 | :undoc-members: 55 | :show-inheritance: 56 | :inherited-members: 57 | 58 | tgbox.crypto module 59 | ------------------- 60 | 61 | .. automodule:: tgbox.crypto 62 | :members: 63 | :undoc-members: 64 | :show-inheritance: 65 | :inherited-members: 66 | 67 | tgbox.errors module 68 | ------------------- 69 | 70 | .. automodule:: tgbox.errors 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | :inherited-members: 75 | 76 | tgbox.fastelethon module 77 | ------------------------ 78 | 79 | .. automodule:: tgbox.fastelethon 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | :inherited-members: 84 | 85 | tgbox.keys module 86 | ----------------- 87 | 88 | .. automodule:: tgbox.keys 89 | :members: 90 | :undoc-members: 91 | :show-inheritance: 92 | :inherited-members: 93 | 94 | tgbox.tools module 95 | ------------------ 96 | 97 | .. automodule:: tgbox.tools 98 | :members: 99 | :undoc-members: 100 | :show-inheritance: 101 | :inherited-members: 102 | 103 | tgbox.defaults module 104 | ---------------------- 105 | 106 | .. automodule:: tgbox.defaults 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | :inherited-members: 111 | 112 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from ast import literal_eval 3 | from sys import version_info 4 | 5 | 6 | CURRENT_PYTHON = version_info[:2] 7 | REQUIRED_PYTHON = (3, 8) 8 | 9 | if CURRENT_PYTHON < REQUIRED_PYTHON: 10 | raise RuntimeError('The "tgbox" library require Python v3.8+') 11 | 12 | 13 | with open('tgbox/version.py', encoding='utf-8') as f: 14 | version = literal_eval(f.read().split('=',1)[1].strip()) 15 | 16 | setup( 17 | name = 'tgbox', 18 | packages = ['tgbox', 'tgbox.api'], 19 | version = version, 20 | license = 'LGPL-2.1', 21 | description = 'Encrypted cloud storage Protocol based on a Telegram API', 22 | long_description = open('README.rst', encoding='utf-8').read(), 23 | author = 'NonProjects', 24 | author_email = 'thenonproton@pm.me', 25 | url = 'https://github.com/NonProjects/tgbox', 26 | download_url = f'https://github.com/NonProjects/tgbox/archive/refs/tags/v{version}.tar.gz', 27 | 28 | long_description_content_type='text/x-rst', 29 | 30 | package_data = { 31 | 'tgbox': ['tgbox/other'], 32 | }, 33 | include_package_data = True, 34 | 35 | install_requires = [ 36 | 'aiosqlite==0.20.0', 37 | 'telethon==1.38.1', 38 | 'ecdsa==0.19.0', 39 | 'filetype==1.2.0', 40 | 'pysocks==1.7.1' 41 | ], 42 | keywords = [ 43 | 'Telegram', 'Cloud-Storage', 'Cloud', 44 | 'API', 'Asyncio', 'Non-official' 45 | ], 46 | extras_require = { 47 | 'fast': [ 48 | 'cryptography<45.0.0', 49 | 'cryptg==0.5.0.post0' 50 | ], 51 | 'doc': [ 52 | 'sphinx-book-theme==1.1.3', 53 | 'sphinx-togglebutton==0.3.2' 54 | ] 55 | }, 56 | classifiers = [ 57 | 'Development Status :: 4 - Beta', 58 | 'Intended Audience :: Developers', 59 | 'Topic :: Security :: Cryptography', 60 | 'Topic :: Software Development :: Libraries', 61 | 'Topic :: Software Development :: Libraries :: Python Modules', 62 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 63 | 'Programming Language :: Python :: 3.8', 64 | 'Programming Language :: Python :: 3.9', 65 | 'Programming Language :: Python :: 3.10', 66 | 'Programming Language :: Python :: 3.11', 67 | 'Programming Language :: Python :: 3.12' 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /tgbox/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Encrypted cloud storage API based on Telegram 3 | https://github.com/NonProjects/tgbox 4 | """ 5 | 6 | __author__ = 'https://github.com/NonProjects' 7 | __maintainer__ = 'https://github.com/NotStatilko' 8 | __email__ = 'thenonproton@pm.me' 9 | 10 | __copyright__ = 'Copyright 2023, NonProjects' 11 | __license__ = 'LGPL-2.1' 12 | 13 | __all__ = [ 14 | 'api', 15 | 'defaults', 16 | 'crypto', 17 | 'errors', 18 | 'keys', 19 | 'tools', 20 | 'version', 21 | 'sync', 22 | ] 23 | import logging 24 | 25 | logger = logging.getLogger(__name__) 26 | logger.addHandler(logging.NullHandler()) 27 | 28 | import sys 29 | 30 | # This function will auto-log all unhandled exceptions 31 | def log_excepthook(exc_type, exc_value, exc_traceback): 32 | # I don't think we should log KeyboardInterrupt 33 | if not issubclass(exc_type, KeyboardInterrupt): 34 | logger.critical( 35 | 'Found Critical error! See Traceback below:', 36 | exc_info=(exc_type, exc_value, exc_traceback) 37 | ) 38 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 39 | 40 | sys.excepthook = log_excepthook 41 | 42 | from asyncio import get_event_loop 43 | from typing import Coroutine 44 | 45 | from . import api 46 | from . import defaults 47 | from . import crypto 48 | from . import errors 49 | from . import keys 50 | from . import tools 51 | from . import version 52 | 53 | __version__ = version.VERSION 54 | 55 | 56 | def sync(coroutine: Coroutine): 57 | """ 58 | Will call asynchronous function in 59 | current asyncio loop and return result. 60 | """ 61 | loop = get_event_loop() 62 | return loop.run_until_complete(coroutine) 63 | -------------------------------------------------------------------------------- /tgbox/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .remote import * 2 | from .local import * 3 | from .abstract import * 4 | 5 | from .utils import TelegramClient 6 | -------------------------------------------------------------------------------- /tgbox/api/db.py: -------------------------------------------------------------------------------- 1 | """This module stores wrappers around Tgbox SQL DB.""" 2 | 3 | import logging 4 | 5 | from typing import ( 6 | Optional, Union, 7 | AsyncGenerator 8 | ) 9 | from os import PathLike 10 | from pathlib import Path 11 | 12 | import aiosqlite 13 | 14 | from ..errors import PathIsDirectory 15 | from ..tools import anext 16 | from .. import defaults 17 | 18 | 19 | __all__ = ['SqlTableWrapper', 'TgboxDB', 'TABLES'] 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | TABLES = { 24 | 'BOX_DATA': ( 25 | ('BOX_CHANNEL_ID', 'BLOB NOT NULL'), 26 | ('BOX_CR_TIME', 'BLOB NOT NULL'), 27 | ('BOX_SALT', 'BLOB NOT NULL'), 28 | ('MAINKEY', 'BLOB'), 29 | ('SESSION', 'BLOB NOT NULL'), 30 | ('API_ID', 'BLOB NOT NULL'), 31 | ('API_HASH', 'BLOB NOT NULL'), 32 | ('FAST_SYNC_LAST_EVENT_ID', 'BLOB') 33 | ), 34 | 'FILES': ( 35 | ('ID', 'INTEGER PRIMARY KEY'), 36 | ('UPLOAD_TIME', 'BLOB NOT NULL'), 37 | ('PPATH_HEAD', 'BLOB NOT NULL'), 38 | ('FILEKEY', 'BLOB'), 39 | ('FINGERPRINT', 'BLOB'), 40 | ('METADATA', 'BLOB NOT NULL'), 41 | ('UPDATED_METADATA', 'BLOB') 42 | ), 43 | 'PATH_PARTS': ( 44 | ('ENC_PART', 'BLOB NOT NULL'), 45 | ('PART_ID', 'BLOB NOT NULL PRIMARY KEY'), 46 | ('PARENT_PART_ID', 'BLOB'), 47 | ), 48 | 'DEFAULTS': ( # Default value 49 | ('METADATA_MAX', 'INTEGER NOT NULL', int(defaults.Limits.METADATA_MAX)), 50 | ('FILE_PATH_MAX', 'INTEGER NOT NULL', int(defaults.Limits.FILE_PATH_MAX)), 51 | 52 | ('DOWNLOAD_PATH', 'TEXT NOT NULL', str(defaults.DOWNLOAD_PATH)), 53 | ('DEF_NO_FOLDER', 'TEXT NOT NULL', str(defaults.DEF_NO_FOLDER)), 54 | ('DEF_UNK_FOLDER', 'TEXT NOT NULL', str(defaults.DEF_UNK_FOLDER)) 55 | ) 56 | } 57 | class SqlTableWrapper: 58 | """A low-level wrapper to SQLite Tables.""" 59 | def __init__(self, aiosql_conn, table_name: str): 60 | self._table_name = table_name 61 | self._aiosql_conn = aiosql_conn 62 | 63 | def __repr__(self) -> str: 64 | return f'' 65 | 66 | async def __aiter__(self) -> tuple: 67 | """Will yield rows as self.select without ``sql_statement``""" 68 | async for row in self.select(): 69 | yield row 70 | 71 | @property 72 | def table_name(self) -> str: 73 | """Returns table name""" 74 | return self._table_name 75 | 76 | async def count_rows(self) -> int: 77 | """Execute ``SELECT count(*) from TABLE_NAME``""" 78 | 79 | logger.debug(f'SELECT count(*) FROM {self._table_name}') 80 | 81 | cursor = await self._aiosql_conn.execute( 82 | f'SELECT count(*) FROM {self._table_name}' 83 | ) 84 | return (await cursor.fetchone())[0] 85 | 86 | async def select(self, sql_tuple: Optional[tuple] = None) -> AsyncGenerator: 87 | """ 88 | If ``sql_tuple`` isn't specified, then will be used 89 | ``(SELECT * FROM TABLE_NAME, ())`` statement. 90 | """ 91 | if not sql_tuple: 92 | sql_tuple = (f'SELECT * FROM {self._table_name}',()) 93 | 94 | logger.debug(f'self._aiosql_conn.execute(*{sql_tuple})') 95 | 96 | cursor = await self._aiosql_conn.execute(*sql_tuple) 97 | async for row in cursor: yield row 98 | 99 | async def select_once(self, sql_tuple: Optional[tuple] = None) -> tuple: 100 | """ 101 | Will return first row which match the ``sql_tuple``, 102 | see ``select()`` method for ``sql_tuple`` details. 103 | """ 104 | return await anext(self.select(sql_tuple=sql_tuple)) 105 | 106 | async def insert( 107 | self, *args, sql_statement: Optional[str] = None, 108 | commit: bool=True) -> None: 109 | """ 110 | If ``sql_statement`` isn't specified, then will be used 111 | ``INSERT INTO TABLE_NAME values (...)``. 112 | 113 | This method doesn't check if you insert correct data 114 | or correct amount of it, you should know DB structure. 115 | """ 116 | if not sql_statement: 117 | sql_statement = ( 118 | f'INSERT INTO {self._table_name} values (' 119 | + ('?,' * len(args))[:-1] + ')' 120 | ) 121 | logger.debug(f'self._aiosql_conn.execute({sql_statement}, {args})') 122 | await self._aiosql_conn.execute(sql_statement, args) 123 | 124 | if commit: 125 | logger.debug('self._aiosql_conn.commit()') 126 | await self._aiosql_conn.commit() 127 | 128 | async def execute(self, sql_tuple: tuple, commit: bool=True): 129 | logger.debug(f'self._aiosql_conn.execute(*{sql_tuple})') 130 | result = await self._aiosql_conn.execute(*sql_tuple) 131 | if commit: 132 | logger.debug('self._aiosql_conn.commit()') 133 | await self._aiosql_conn.commit() 134 | return result # Returns Cursor object 135 | 136 | async def commit(self) -> None: 137 | logger.info('SqlTableWrapper._aiosql_conn.commit()') 138 | await self._aiosql_conn.commit() 139 | 140 | class TgboxDB: 141 | def __init__(self, db_path: Union[PathLike, str]): 142 | """ 143 | Arguments: 144 | db_path (``PathLike``, ``str``): 145 | Path to the Tgbox DB. 146 | """ 147 | if isinstance(db_path, PathLike): 148 | self._db_path = db_path 149 | else: 150 | self._db_path = Path(db_path) 151 | 152 | if self._db_path.is_dir(): 153 | raise PathIsDirectory('Path is directory.') 154 | 155 | self._db_path.parent.mkdir(exist_ok=True, parents=True) 156 | 157 | self._aiosql_db = None 158 | self._aiosql_db_is_closed = None 159 | self._initialized = False 160 | 161 | self._name = self._db_path.name 162 | 163 | def __str__(self) -> str: 164 | return f'{self.__class__.__name__}("{str(self._db_path)}") # {self._initialized=}' 165 | 166 | def __repr__(self) -> str: 167 | return f'{self.__class__.__name__}("{str(self._db_path)}")' 168 | 169 | @property 170 | def name(self) -> str: 171 | """Returns TgboxDB name""" 172 | return self._name 173 | 174 | @property 175 | def db_path(self) -> PathLike: 176 | """Returns a path to TgboxDB file""" 177 | return self._db_path 178 | 179 | @property 180 | def initialized(self) -> bool: 181 | """Will return True if TgboxDB is initialized""" 182 | return self._initialized 183 | 184 | @property 185 | def closed(self) -> bool: 186 | """ 187 | This method will return ``None`` if DB wasn't opened, 188 | False if it's still opened, True if it's was closed. 189 | """ 190 | return self._aiosql_db_is_closed 191 | 192 | @staticmethod 193 | async def create(db_path: Union[str, PathLike]) -> 'TgboxDB': 194 | """Will initialize TgboxDB""" 195 | return await TgboxDB(db_path).init() 196 | 197 | async def close(self) -> None: 198 | """Will close TgboxDB""" 199 | logger.info(f'{self._db_path} @ self._aiosql_db.close()') 200 | await self._aiosql_db.close() 201 | self._aiosql_db_is_closed = True 202 | 203 | async def init(self) -> 'TgboxDB': 204 | logger.debug(f'tgbox.api.db.TgboxDB.init("{self._db_path}")') 205 | 206 | logger.info(f'Opening SQLite connection to {self._db_path}') 207 | self._aiosql_db = await aiosqlite.connect(self._db_path) 208 | 209 | for table, data in TABLES.items(): 210 | try: 211 | columns = ', '.join((f'{i[0]} {i[1]}' for i in data)) 212 | await self._aiosql_db.execute( 213 | f'CREATE TABLE {table} ({columns})' 214 | ) 215 | if table == 'DEFAULTS': 216 | sql_statement = ( 217 | f'INSERT INTO {table} VALUES (' 218 | + ('?,' * len(data))[:-1] + ')' 219 | ) 220 | await self._aiosql_db.execute( 221 | sql_statement, [i[2] for i in data] 222 | ) 223 | except aiosqlite.OperationalError: # Table exists 224 | # The code below will update TgboxDB schema if it's outdated 225 | table_columns = await self._aiosql_db.execute( 226 | f'PRAGMA table_info({table})' 227 | ) 228 | table_columns = set((i[1] for i in await table_columns.fetchall())) 229 | required_columns = set((i[0] for i in data)) 230 | 231 | if table_columns != required_columns: 232 | logger.info(f'TgboxDB {self._db_path} seems outdated. Updating...') 233 | table_columns &= required_columns 234 | 235 | logger.debug(f'CREATE TABLE "updated!{table}" ({columns})') 236 | 237 | await self._aiosql_db.execute( 238 | f'CREATE TABLE "updated!{table}" ({columns})' 239 | ) 240 | table_columns_str = ', '.join(table_columns) 241 | 242 | logger.debug( 243 | f"""INSERT INTO "updated!{table}" ({table_columns_str}) """ 244 | f"""SELECT {table_columns_str} FROM {table}""" 245 | ) 246 | await self._aiosql_db.execute( 247 | f"""INSERT INTO "updated!{table}" ({table_columns_str}) """ 248 | f"""SELECT {table_columns_str} FROM {table}""" 249 | ) 250 | logger.debug(f'DROP TABLE {table}') 251 | await self._aiosql_db.execute(f'DROP TABLE {table}') 252 | 253 | logger.debug(f'ALTER TABLE "updated!{table}" RENAME TO {table}') 254 | await self._aiosql_db.execute( 255 | f'ALTER TABLE "updated!{table}" RENAME TO {table}' 256 | ) 257 | logger.info('TgboxDB._aiosql_conn.commit()') 258 | await self._aiosql_db.commit() 259 | self._aiosql_db_is_closed = False 260 | 261 | tables = await self._aiosql_db.execute( 262 | "SELECT name FROM sqlite_master WHERE type='table'" 263 | ) 264 | for table in (await tables.fetchall()): 265 | setattr(self, table[0], SqlTableWrapper(self._aiosql_db, table[0])) 266 | 267 | self._initialized = True 268 | return self 269 | -------------------------------------------------------------------------------- /tgbox/api/sync.py: -------------------------------------------------------------------------------- 1 | """ 2 | This (slightly changed module & its features) was taken from the Telethon 3 | library made by Lonami under MIT License: github.com/LonamiWebs/Telethon 4 | 5 | Parts of this file i moved to the tgbox.api.utils package module, check 6 | the out _syncify_wrap_func() and syncify functions. They are NOT mine. 7 | 8 | Thanks to the Lonami. See part of the original description: 9 | 10 | ---> 11 | This magical module will rewrite all public methods in the public interface 12 | of the library so they can run the loop on their own if it's not already 13 | running. This rewrite may not be desirable if the end user always uses the 14 | methods they way they should be ran, but it's incredibly useful for quick 15 | scripts and the runtime overhead is relatively low.<--- 16 | 17 | All you should do is to firstly import this module, then anything you want. 18 | """ 19 | 20 | import logging 21 | 22 | from typing import AsyncGenerator 23 | 24 | from . import local 25 | from . import remote 26 | from . import abstract 27 | 28 | from .abstract import Box, BoxFile 29 | from .utils import TelegramClient, syncify 30 | 31 | from ..tools import anext 32 | from .. import sync as sync_coro 33 | 34 | __all__ = ['sync_agen', 'sync_coro'] 35 | 36 | logger = logging.getLogger(__name__) 37 | 38 | def sync_agen(async_gen: AsyncGenerator): 39 | """ 40 | This will make async generator to sync 41 | generator, so we can write "for" loop. 42 | 43 | Use this functions on generators that 44 | you want to syncify. For example, if 45 | you want to iterate over LocalBox in 46 | sync code (to load *only* local files): 47 | 48 | .. code-block:: python 49 | 50 | ... # Some code was omited 51 | 52 | async_gen = box.dlb.files(reverse=True) 53 | for dlbf in tgbox.api.sync.sync_agen(async_gen): 54 | print(dlbf.id, dlbf.file_name, dlbf.size) 55 | 56 | .. tip:: 57 | To sync coroutines you can use a ``sync`` func 58 | from the tgbox package (`tgbox.sync`) or use 59 | it from here as `tgbox.api.sync.sync_coro` 60 | """ 61 | try: 62 | while True: 63 | yield sync_coro(anext(async_gen)) 64 | except StopAsyncIteration: 65 | return 66 | 67 | syncify( 68 | Box, BoxFile, TelegramClient, 69 | local, remote, abstract 70 | ) 71 | # We inherit some methods from the parent classes 72 | # on __init__ in the 'abstract' module. We did not 73 | # sync this methods here, so we will need to sync 74 | # it additionally later. This flags will help us. 75 | Box._needs_syncify = True # pylint: disable=W0212 76 | BoxFile._needs_syncify = True # pylint: disable=W0212 77 | 78 | # We import classes and functions from the 'api' package 79 | # in __init__.py (.) so they can be accessed via the 80 | # from 'tgbox.api import Box' (e.g). As this import comes 81 | # before the user imports 'tgbox.api.sync', __init__.py 82 | # caches the Async versions of this functions. So, for 83 | # example, after 'import tgbox.api.sync' the functions 84 | # or classes in __init__.py (get_box, Box, etc) will 85 | # stay the same, but in 'api' they will be synced. 86 | # 87 | # from tgbox.api import get_box <-- Will stay Async 88 | # from tgbox.api.abstract import get_box <-- Will become Sync 89 | # 90 | # This is a strange behaviour and below we fix it by 91 | # updating __dict__ of the __init__.py with synced 92 | # versions of classes/functions. A bit quirky way, 93 | # but will resolve our issue. 94 | from .abstract import ( 95 | __dict__ as abstract__dict__, 96 | __all__ as abstract__all__ 97 | ) 98 | from .local import ( 99 | __dict__ as local__dict__, 100 | __all__ as local__all__ 101 | ) 102 | from .remote import ( 103 | __dict__ as remote__dict__, 104 | __all__ as remote__all__ 105 | ) 106 | from . import __dict__ as root__dict__ 107 | 108 | __dict_to_update = ( 109 | (abstract__dict__, abstract__all__), 110 | (local__dict__, local__all__), 111 | (remote__dict__, remote__all__) 112 | ) 113 | for x__dict__, x__all__ in __dict_to_update: 114 | for k,v in root__dict__.items(): 115 | # We update only things that presented in both 116 | # modules (sync & x) AND in x__all__ 117 | if k in x__dict__ and k in x__all__: 118 | logger.debug(f'__init__.{k} was updated!') 119 | root__dict__[k] = x__dict__[k] 120 | -------------------------------------------------------------------------------- /tgbox/api/utils.py: -------------------------------------------------------------------------------- 1 | """Module with utils for api package.""" 2 | 3 | import logging 4 | 5 | from pathlib import Path 6 | from functools import wraps 7 | from dataclasses import dataclass 8 | from base64 import urlsafe_b64encode 9 | 10 | from asyncio import get_event_loop_policy, get_running_loop 11 | from typing import BinaryIO, Optional, Union, AsyncGenerator 12 | from inspect import iscoroutinefunction, isasyncgenfunction, isasyncgen 13 | try: 14 | # Try to use Third-party Regex if installed 15 | from regex import search as re_search 16 | except ImportError: 17 | from re import search as re_search 18 | 19 | from telethon.tl.custom.file import File 20 | from telethon.sessions import StringSession 21 | 22 | from telethon.tl.types import Photo, Document 23 | from telethon.tl.types.auth import SentCode 24 | 25 | from telethon import TelegramClient as TTelegramClient 26 | from telethon.errors import SessionPasswordNeededError 27 | from telethon.tl.functions.auth import ResendCodeRequest 28 | 29 | from ..fastelethon import download_file 30 | from ..tools import anext, SearchFilter, _TypeList 31 | from .. import defaults 32 | 33 | from .db import TABLES, TgboxDB 34 | 35 | 36 | __all__ = [ 37 | 'search_generator', 38 | 'DirectoryRoot', 39 | 'PreparedFile', 40 | 'TelegramClient', 41 | 'DefaultsTableWrapper', 42 | 'RemoteBoxDefaults' 43 | ] 44 | logger = logging.getLogger(__name__) 45 | 46 | class TelegramClient(TTelegramClient): 47 | """ 48 | A little extension to the ``telethon.TelegramClient``. 49 | 50 | This class inherits Telethon's TelegramClient and support 51 | all features that has ``telethon.TelegramClient``. 52 | 53 | Typical usage: 54 | 55 | .. code-block:: python 56 | 57 | from asyncio import run as asyncio_run 58 | from tgbox.api import TelegramClient, make_remotebox 59 | from getpass import getpass # For hidden input 60 | 61 | PHONE_NUMBER = '+10000000000' # Your phone number 62 | API_ID = 1234567 # Your API_ID: https://my.telegram.org 63 | API_HASH = '00000000000000000000000000000000' # Your API_HASH 64 | 65 | async def main(): 66 | tc = TelegramClient( 67 | phone_number = PHONE_NUMBER, 68 | api_id = API_ID, 69 | api_hash = API_HASH 70 | ) 71 | await tc.connect() 72 | await tc.send_code() 73 | 74 | await tc.log_in( 75 | code = int(input('Code: ')), 76 | password = getpass('Pass: ') 77 | ) 78 | erb = await make_remotebox(tc) 79 | 80 | asyncio_run(main()) 81 | """ 82 | __version__ = defaults.VERSION 83 | 84 | def __init__( 85 | self, api_id: int, api_hash: str, 86 | phone_number: Optional[str] = None, 87 | session: Optional[Union[str, StringSession]] = None, 88 | **kwargs) -> None: 89 | """ 90 | .. note:: 91 | You should specify at least ``session`` or ``phone_number``. 92 | 93 | Arguments: 94 | api_id (``int``): 95 | API_ID from https://my.telegram.org. 96 | 97 | api_hash (``int``): 98 | API_HASH from https://my.telegram.org. 99 | 100 | phone_number (``str``, optional): 101 | Phone number linked to your Telegram 102 | account. You may want to specify it 103 | to recieve log-in code. You should 104 | specify it if ``session`` is ``None``. 105 | 106 | session (``str``, ``StringSession``, optional): 107 | ``StringSession`` that give access to 108 | your Telegram account. You can get it 109 | after connecting and signing in via 110 | ``TelegramClient.session.save()`` method. 111 | 112 | ..note:: 113 | You can use ``.start()`` method on ``TelegramClient`` 114 | without specifying ``phone_number`` or ``session``, 115 | otherwise ``phone_number`` OR ``session`` is required. 116 | 117 | ..tip:: 118 | This ``TelegramClient`` support all keyword 119 | arguments (**kwargs) that support parent 120 | ``telethon.TelegramClient`` object. 121 | """ 122 | super().__init__( 123 | StringSession(session), 124 | api_id, api_hash, **kwargs 125 | ) 126 | self._api_id, self._api_hash = api_id, api_hash 127 | self._phone_number, self._session = phone_number, session 128 | 129 | def __check_session_phone_number(self): 130 | if not self._session and not self._phone_number: 131 | raise ValueError( 132 | 'You should set at least "session" or "phone_number".' 133 | ) 134 | 135 | def set_phone_number(self, phone_number: str) -> None: 136 | """Use this function if you didn't 137 | specified ``phone_number`` on init. 138 | """ 139 | self._phone_number = phone_number 140 | 141 | def set_session(self, session: Union[str, StringSession]) -> None: 142 | """Use this function if you didn't 143 | specified ``session`` on init. 144 | """ 145 | self._session = session 146 | 147 | async def send_code(self, force_sms: Optional[bool]=False) -> SentCode: 148 | """ 149 | Sends the Telegram code needed to login to the given phone number. 150 | 151 | Arguments: 152 | force_sms (``bool``, optional): 153 | Whether to force sending as SMS. 154 | """ 155 | self.__check_session_phone_number() 156 | logger.info(f'Sending login code to {self._phone_number}...') 157 | 158 | return await self.send_code_request( 159 | self._phone_number, force_sms=force_sms 160 | ) 161 | async def log_in( 162 | self, password: Optional[str] = None, 163 | code: Optional[Union[int,str]] = None) -> None: 164 | """ 165 | Logs in to Telegram to an existing user account. 166 | You should only use this if you are not signed in yet. 167 | 168 | Arguments: 169 | password (``str``, optional): 170 | Your 2FA password. You can ignore 171 | this if you don't enabled it yet. 172 | 173 | code (``int``, optional): 174 | The code that Telegram sent you after calling 175 | ``TelegramClient.send_code()`` method. 176 | """ 177 | self.__check_session_phone_number() 178 | 179 | if not await self.is_user_authorized(): 180 | try: 181 | logger.info(f'Trying to sign-in with {self._phone_number} and {code} code..') 182 | await self.sign_in(self._phone_number, code) 183 | except SessionPasswordNeededError: 184 | logger.info( 185 | 'Log-in without 2FA password failed. ' 186 | f'Trying to sign-in with {self._phone_number}, ' 187 | f'password and {code} code..' 188 | ) 189 | await self.sign_in(password=password) 190 | else: 191 | logger.debug(f'User {self._phone_number} is already authorized.') 192 | 193 | async def resend_code(self, sent_code: SentCode) -> SentCode: 194 | """ 195 | Will send you login code again. This can be used to 196 | force Telegram send you SMS or Call to dictate code. 197 | 198 | Arguments: 199 | sent_code (``SentCode``): 200 | Result of the ``tc.send_code`` or 201 | result of the ``tc.resend_code`` method. 202 | 203 | Example: 204 | 205 | .. code-block:: python 206 | 207 | tc = tgbox.api.TelegramClient(...) 208 | sent_code = await tc.send_code() 209 | sent_code = await tc.resend_code(sent_code) 210 | """ 211 | self.__check_session_phone_number() 212 | 213 | logger.info(f'Resending login code to {self._phone_number}...') 214 | return await self(ResendCodeRequest( 215 | self._phone_number, sent_code.phone_code_hash) 216 | ) 217 | 218 | class TelegramVirtualFile: 219 | """ 220 | You can use this class for re-upload to RemoteBox 221 | files that already was uploaded to any other 222 | Telegram chat. Wrap it over ``Document`` and 223 | specify in the ``DecryptedLocalBox.prepare_file`` 224 | """ 225 | def __init__(self, document: Union[Photo, Document], tc: TelegramClient): 226 | self.tc = tc 227 | self.document = document 228 | 229 | file = File(document) 230 | 231 | self.name = file.name 232 | self.size = file.size 233 | self.mime = file.mime_type 234 | 235 | self.duration = file.duration\ 236 | if file.duration else 0 237 | 238 | self._downloader = None 239 | 240 | def __repr__(self) -> str: 241 | return ( 242 | f'' 244 | ) 245 | async def get_preview(self, quality: int=1) -> bytes: 246 | if hasattr(self.document,'sizes')\ 247 | and not self.document.sizes: 248 | return b'' 249 | 250 | if hasattr(self.document,'thumbs')\ 251 | and not self.document.thumbs: 252 | return b'' 253 | 254 | return await self.tc.download_media( 255 | message = self.document, 256 | thumb = quality, file = bytes 257 | ) 258 | async def read(self, size: int=-1) -> bytes: # pylint: disable=unused-argument 259 | """Will return <= 512KiB of data. 'size' ignored""" 260 | if not self._downloader: 261 | self._downloader = download_file( 262 | self.tc, self.document 263 | ) 264 | chunk = await anext(self._downloader) 265 | return chunk 266 | 267 | @dataclass 268 | class PreparedFile: 269 | """ 270 | This dataclass store data needed for upload 271 | by ``DecryptedRemoteBox.push_file`` in future. 272 | 273 | Usually it's only for internal use. 274 | """ 275 | dlb: 'tgbox.api.local.DecryptedLocalBox' 276 | file: BinaryIO 277 | filekey: 'tgbox.keys.FileKey' 278 | filesize: int 279 | filepath: Path 280 | filesalt: 'tgbox.crypto.FileSalt' 281 | hmackey: 'tgbox.keys.HMACKey' 282 | fingerprint: bytes 283 | metadata: bytes 284 | imported: bool 285 | 286 | def set_file_id(self, id: int): 287 | """You should set ID after pushing to remote""" 288 | self.file_id = id # pylint: disable=attribute-defined-outside-init 289 | 290 | def set_upload_time(self, upload_time: int): 291 | """You should set time after pushing to remote""" 292 | # pylint: disable=attribute-defined-outside-init 293 | self.upload_time = upload_time 294 | 295 | def set_updated_enc_metadata(self, ue_metadata: bytes): 296 | """ 297 | If user requested to update some already pushed 298 | to Remote file AND if target file HAS Updated 299 | Encrypted Metadata in caption then we need to 300 | re-encrypt it with a new filekey and attach here. 301 | 302 | This is for internal usage, you can ignore it. 303 | """ 304 | # pylint: disable=attribute-defined-outside-init 305 | self.updated_enc_metadata = ue_metadata 306 | 307 | class DirectoryRoot: 308 | """ 309 | Type used to specify that you want to 310 | access absolute local directory root. 311 | 312 | This class doesn't have any methods, 313 | please use it only for ``lbd.iterdir`` 314 | """ 315 | 316 | async def search_generator( 317 | sf: SearchFilter, it_messages: Optional[AsyncGenerator] = None, 318 | lb: Optional['tgbox.api.local.DecryptedLocalBox'] = None, 319 | cache_preview: bool=True, reverse: bool=False, 320 | fetch_count: Optional[int] = 100, 321 | erase_encrypted_metadata: bool=True) -> AsyncGenerator: 322 | """ 323 | Generator used to search for files in dlb and rb. It's 324 | only for internal use and you shouldn't use it in your 325 | own projects. 326 | 327 | If file is exported from other RemoteBox and was imported to your 328 | LocalBox, then you can specify ``dlb`` as ``lb``. AsyncGenerator 329 | will try to get ``FileKey`` and decrypt ``EncryptedRemoteBoxFile``. 330 | Otherwise imported file will be ignored. 331 | """ 332 | in_func = re_search if sf.in_filters['re'] else lambda p,s: p in s 333 | 334 | if it_messages: 335 | iter_from = it_messages 336 | 337 | elif any((sf.in_filters['scope'], sf.ex_filters['scope'])): 338 | if not sf.in_filters['scope']: 339 | lbf = await anext(lb.files(), None) 340 | if not lbf: return # Local doesn't have files 341 | 342 | async def scope_generator(scope: Union[str, list]): 343 | scope = scope if scope else DirectoryRoot 344 | scope = scope if isinstance(scope, _TypeList) else [scope] 345 | 346 | for current_scope in scope: 347 | if current_scope is DirectoryRoot: 348 | iterdir = lbf.directory.iterdir(ppid=current_scope) 349 | 350 | elif hasattr(current_scope, '_part_id'): 351 | iterdir = current_scope.iterdir() 352 | else: 353 | iterdir = await lb.get_directory(current_scope) 354 | if not iterdir: 355 | return 356 | iterdir = iterdir.iterdir() 357 | 358 | async for content in iterdir: 359 | if hasattr(content, '_part_id'): 360 | # This is DecryptedLocalBoxDirectory 361 | 362 | if sf.ex_filters['scope'] or sf.in_filters['non_recursive_scope']: 363 | await content.lload(full=True) 364 | 365 | if str(content) in sf.ex_filters['scope']\ 366 | or sf.in_filters['non_recursive_scope']: 367 | continue # This directory is excluded 368 | 369 | async for dlbf in scope_generator(content): 370 | yield dlbf # This is DecryptedLocalBoxFile 371 | else: 372 | yield content # This is DecryptedLocalBoxFile 373 | 374 | iter_from = scope_generator(sf.in_filters['scope']) 375 | else: 376 | min_id = sf.in_filters['min_id'][-1]\ 377 | if sf.in_filters['min_id'] else None 378 | 379 | max_id = sf.in_filters['max_id'][-1]\ 380 | if sf.in_filters['max_id'] else None 381 | 382 | iter_from = lb.files( 383 | min_id = min_id, 384 | max_id = max_id, 385 | ids = sf.in_filters['id'], 386 | cache_preview = cache_preview, 387 | reverse = reverse, 388 | fetch_count=fetch_count, 389 | erase_encrypted_metadata=erase_encrypted_metadata 390 | ) 391 | if not isasyncgen(iter_from): 392 | # The .files() generator was syncified, so we can't 393 | # use the "async for" on it. We will make a little wrapper 394 | async def _async_iter_from(_iter_from): 395 | try: 396 | while True: 397 | yield await next(_iter_from) 398 | except StopAsyncIteration: 399 | return 400 | 401 | iter_from = _async_iter_from(iter_from) 402 | 403 | if not iter_from: 404 | raise ValueError('At least it_messages or lb must be specified.') 405 | 406 | async for file in iter_from: 407 | if hasattr(file, '_rb'): # *RemoteBoxFile 408 | file_size = file.file_size 409 | 410 | elif hasattr(file, '_lb'): # *LocalBoxFile 411 | file_size = file.size 412 | else: 413 | continue 414 | 415 | if hasattr(file, 'file_path') and file.file_path: 416 | file_path = str(file.file_path) 417 | else: 418 | file_path = '' 419 | 420 | # We will use it as flags, the first is for 'include', the second is 421 | # for 'exclude'. Both should be True to match SearchFilter filters. 422 | yield_result = [True, True] 423 | 424 | for indx, filter in enumerate((sf.in_filters, sf.ex_filters)): 425 | if filter['imported']: 426 | if bool(file.imported) != bool(filter['imported']): 427 | if indx == 0: # O is Include 428 | yield_result[indx] = False 429 | break 430 | 431 | elif bool(file.imported) == bool(filter['imported']): 432 | if indx == 1: # 1 is Exclude 433 | yield_result[indx] = False 434 | break 435 | 436 | for sender in filter['sender']: 437 | # If sender is int, then it we check only against 438 | # the 'sender_id', if str, we check against the 439 | # 'sender', and if sender isnumeric(), we convert 440 | # to int and also check against 'sender_id' 441 | 442 | # sender and sender_id is presented only in RemoteBox 443 | # files and only in Box channels with Sign Messages 444 | # -> Show Author Profiles enabled. If file doesn't 445 | # have sender, then always skip a file. 446 | 447 | file_sender_id, _check = getattr(file, 'sender_id', None), False 448 | 449 | if isinstance(sender, int): 450 | if sender == file_sender: 451 | _check = True 452 | 453 | if isinstance(sender, str): 454 | file_sender = getattr(file, 'sender') or '' 455 | 456 | if in_func(sender, file_sender): 457 | _check = True 458 | else: 459 | if sender.isnumeric() and int(sender) == file_sender_id: 460 | _check = True 461 | 462 | if _check: 463 | if indx == 1: 464 | yield_result[indx] = False 465 | break 466 | else: 467 | if filter['sender']: 468 | if indx == 0: 469 | yield_result[indx] = False 470 | break 471 | 472 | for minor_version in filter['minor_version']: 473 | if minor_version == file.minor_version: 474 | if indx == 1: 475 | yield_result[indx] = False 476 | break 477 | else: 478 | if filter['minor_version']: 479 | if indx == 0: 480 | yield_result[indx] = False 481 | break 482 | 483 | for mime in filter['mime']: 484 | if in_func(mime, file.mime): 485 | if indx == 1: 486 | yield_result[indx] = False 487 | break 488 | else: 489 | if filter['mime']: 490 | if indx == 0: 491 | yield_result[indx] = False 492 | break 493 | 494 | if filter['min_time']: 495 | if file.upload_time < filter['min_time'][-1]: 496 | if indx == 0: 497 | yield_result[indx] = False 498 | break 499 | 500 | elif file.upload_time >= filter['min_time'][-1]: 501 | if indx == 1: 502 | yield_result[indx] = False 503 | break 504 | 505 | if filter['max_time']: 506 | if file.upload_time > filter['max_time'][-1]: 507 | if indx == 0: 508 | yield_result[indx] = False 509 | break 510 | 511 | elif file.upload_time <= filter['max_time'][-1]: 512 | if indx == 1: 513 | yield_result[indx] = False 514 | break 515 | 516 | if filter['min_size']: 517 | if file_size < filter['min_size'][-1]: 518 | if indx == 0: 519 | yield_result[indx] = False 520 | break 521 | 522 | elif file_size >= filter['min_size'][-1]: 523 | if indx == 1: 524 | yield_result[indx] = False 525 | break 526 | 527 | if filter['max_size']: 528 | if file_size > filter['max_size'][-1]: 529 | if indx == 0: 530 | yield_result[indx] = False 531 | break 532 | 533 | elif file_size <= filter['max_size'][-1]: 534 | if indx == 1: 535 | yield_result[indx] = False 536 | break 537 | 538 | if filter['min_id']: 539 | if file.id < filter['min_id'][-1]: 540 | if indx == 0: 541 | yield_result[indx] = False 542 | break 543 | 544 | elif file.id >= filter['min_id'][-1]: 545 | if indx == 1: 546 | yield_result[indx] = False 547 | break 548 | 549 | if filter['max_id']: 550 | if file.id > filter['max_id'][-1]: 551 | if indx == 0: 552 | yield_result[indx] = False 553 | break 554 | 555 | elif file.id <= filter['max_id'][-1]: 556 | if indx == 1: 557 | yield_result[indx] = False 558 | break 559 | 560 | for id in filter['id']: 561 | if file.id == id: 562 | if indx == 1: 563 | yield_result[indx] = False 564 | break 565 | else: 566 | if filter['id']: 567 | if indx == 0: 568 | yield_result[indx] = False 569 | break 570 | 571 | if hasattr(file, '_cattrs'): 572 | for cattr in filter['cattrs']: 573 | for k,v in cattr.items(): 574 | if k in file.cattrs: 575 | if in_func(v, file.cattrs[k]): 576 | if indx == 1: 577 | yield_result[indx] = False 578 | break 579 | else: 580 | if filter['cattrs']: 581 | if indx == 0: 582 | yield_result[indx] = False 583 | break 584 | 585 | # If it_messages is specified, then we're making search 586 | # on the RemoteBox, thus, we can't use the "scope" filter, 587 | # which is LocalBox-only; so we will treat it as the 588 | # simple "file_path" filter to mimic "scope". 589 | if it_messages: 590 | sf_file_path = [*filter['file_path'], *filter['scope']] 591 | else: 592 | sf_file_path = filter['file_path'] 593 | 594 | for filter_file_path in sf_file_path: 595 | if in_func(str(filter_file_path), file_path): 596 | if indx == 1: 597 | yield_result[indx] = False 598 | break 599 | else: 600 | if sf_file_path: 601 | if indx == 0: 602 | yield_result[indx] = False 603 | break 604 | 605 | for file_name in filter['file_name']: 606 | if in_func(file_name, file.file_name): 607 | if indx == 1: 608 | yield_result[indx] = False 609 | break 610 | else: 611 | if filter['file_name']: 612 | if indx == 0: 613 | yield_result[indx] = False 614 | break 615 | 616 | for file_salt in filter['file_salt']: 617 | if isinstance(file_salt, str): 618 | fsalt = urlsafe_b64encode(file.file_salt.salt).decode() 619 | else: 620 | fsalt = file.file_salt 621 | 622 | if in_func(file_salt, fsalt): 623 | if indx == 1: 624 | yield_result[indx] = False 625 | break 626 | else: 627 | if filter['file_salt']: 628 | if indx == 0: 629 | yield_result[indx] = False 630 | break 631 | 632 | for verbyte in filter['verbyte']: 633 | if verbyte == file.verbyte: 634 | if indx == 1: 635 | yield_result[indx] = False 636 | break 637 | else: 638 | if filter['verbyte']: 639 | if indx == 0: 640 | yield_result[indx] = False 641 | break 642 | 643 | if all(yield_result): 644 | logger.debug(f'SearchFilter matched ID{file.id}') 645 | yield file 646 | else: 647 | logger.debug(f'SearchFilter mismatch ID{file.id} [{yield_result}]') 648 | continue 649 | 650 | class DefaultsTableWrapper: 651 | """ 652 | This little class will wrap around the 653 | DEFAULTS table of TGBOX DB and will 654 | fetch all contents of it. 655 | 656 | You can await the ``change`` coroutine 657 | to change default values to your own. 658 | """ 659 | def __init__(self, tgbox_db: TgboxDB): 660 | """ 661 | Arguments: 662 | tgbox_db (``TgboxDB``): 663 | An initialized ``TgboxDB``. 664 | """ 665 | self._tgbox_db = tgbox_db 666 | self._initialized = False 667 | 668 | def __repr__(self) -> str: 669 | return (f'{self.__class__.__name__}({repr(self._tgbox_db)})') 670 | 671 | def __str__(self) -> str: 672 | return (f'{self.__class__.__name__}({repr(self._tgbox_db)}) # {self._initialized=}') 673 | 674 | @property 675 | def initialized(self) -> bool: 676 | return self._initialized 677 | 678 | async def init(self) -> 'DefaultsTableWrapper': 679 | """Fetch the defaults and initialize""" 680 | logger.debug( 681 | 'Initializing DefaultsTableWrapper for ' 682 | f'{self._tgbox_db._db_path} LocalBox' 683 | ) 684 | if self._tgbox_db.closed: 685 | await self._tgbox_db.init() 686 | 687 | defaults_ = await self._tgbox_db.DEFAULTS.select_once() 688 | for default, value in zip(TABLES['DEFAULTS'], defaults_): 689 | # Some defaults must be Path objects to work correctly 690 | if default[0] in ('DEF_UNK_FOLDER', 'DEF_NO_FOLDER', 'DOWNLOAD_PATH'): 691 | value = Path(value) 692 | 693 | setattr(self, default[0], value) 694 | 695 | self._initialized = True 696 | return self 697 | 698 | async def change(self, key: str, value) -> None: 699 | """ 700 | This method can change the defaults values 701 | 702 | Arguments: 703 | key (``str``): 704 | Key to change, i.e METADATA_MAX. 705 | 706 | value: 707 | Key's new value. 708 | 709 | .. warning:: 710 | We **don't** verify here that value 711 | type corresponds to real type of Key 712 | or that value doesn't overflow the 713 | allowed value maximum. Be sure to 714 | specify the correct Key values. 715 | 716 | Example: 717 | 718 | .. code-block:: python 719 | 720 | from asyncio import run as asyncio_run 721 | 722 | from tgbox.defaults import DEF_TGBOX_NAME 723 | from tgbox.api.db import TgboxDB 724 | from tgbox.api.utils import DefaultsTableWrapper 725 | 726 | async def main(): 727 | # Make a DefaultsTableWrapper object 728 | tdb = await TgboxDB(DEF_TGBOX_NAME).init() 729 | dtw = await DefaultsTableWrapper(tdb).init() 730 | 731 | # Change METADATA_MAX to the max allowed size 732 | dtw.change('METADATA_MAX', 256**3-1) 733 | 734 | # Access DTW from the DecryptedLocalBox 735 | ... # Some code was omited here 736 | # Change the default download path 737 | dlb.defaults.change('DOWNLOAD_PATH', 'Downloads') 738 | 739 | asyncio_run(main()) 740 | """ 741 | getattr(self, key) # Vetrify that Key exist 742 | 743 | logger.info(f'Changing defaults | UPDATE DEFAULTS SET {key}={value}') 744 | await self._tgbox_db.DEFAULTS.execute(( 745 | f'UPDATE DEFAULTS SET {key}=?', (value,) 746 | )) 747 | 748 | @dataclass 749 | class RemoteBoxDefaults: 750 | METADATA_MAX: int 751 | FILE_PATH_MAX: int 752 | DEF_UNK_FOLDER: Path 753 | DEF_NO_FOLDER: Path 754 | DOWNLOAD_PATH: Path 755 | 756 | 757 | def _syncify_wrap_func(t, method_name): 758 | method = getattr(t, method_name) 759 | 760 | @wraps(method) 761 | def syncified(*args, **kwargs): 762 | coro = method(*args, **kwargs) 763 | try: 764 | loop = get_running_loop() 765 | except RuntimeError: 766 | loop = get_event_loop_policy().get_event_loop() 767 | 768 | if loop.is_running(): 769 | return coro 770 | else: 771 | return loop.run_until_complete(coro) 772 | 773 | # Save an accessible reference to the original method 774 | setattr(syncified, '__tb.sync', method) 775 | setattr(t, method_name, syncified) 776 | 777 | def _syncify_wrap_agen(t, method_name): 778 | method = getattr(t, method_name) 779 | 780 | @wraps(method) 781 | def syncified(*args, **kwargs): 782 | coro = method(*args, **kwargs) 783 | try: 784 | loop = get_running_loop() 785 | except RuntimeError: 786 | loop = get_event_loop_policy().get_event_loop() 787 | try: 788 | while True: 789 | if loop.is_running(): 790 | yield anext(coro) 791 | else: 792 | yield loop.run_until_complete(anext(coro)) 793 | except StopAsyncIteration: 794 | return 795 | 796 | # Save an accessible reference to the original method 797 | setattr(syncified, '__tb.sync', method) 798 | setattr(t, method_name, syncified) 799 | 800 | def syncify(*types): 801 | """ 802 | Converts all the methods in the given types (class definitions) 803 | into synchronous, which return either the coroutine or the result 804 | based on whether ``asyncio's`` event loop is running. 805 | """ 806 | do_not_sync = ('search_generator',) 807 | 808 | for t in types: 809 | for name in dir(t): 810 | if name in do_not_sync: 811 | continue 812 | 813 | if not name.startswith('_') or name == '__call__': 814 | if isasyncgenfunction(getattr(t, name)): 815 | _syncify_wrap_agen(t, name) 816 | 817 | elif iscoroutinefunction(getattr(t, name)): 818 | _syncify_wrap_func(t, name) 819 | 820 | -------------------------------------------------------------------------------- /tgbox/crypto.py: -------------------------------------------------------------------------------- 1 | """This module stores all cryptography used in API.""" 2 | 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | from os import urandom 8 | from typing import Union, Optional 9 | 10 | from pyaes.util import ( 11 | append_PKCS7_padding, 12 | strip_PKCS7_padding 13 | ) 14 | from .errors import ModeInvalid 15 | try: 16 | from cryptography.hazmat.primitives.ciphers\ 17 | import Cipher, algorithms, modes 18 | FAST_ENCRYPTION = True 19 | logger.info('Fast cryptography library was found.') 20 | except ModuleNotFoundError: 21 | # We can use PyAES if there is no cryptography library. 22 | # PyAES is much slower. You can use it for quick tests. 23 | from pyaes import AESModeOfOperationCBC 24 | FAST_ENCRYPTION = False 25 | logger.warning('Fast cryptography library was NOT found. ') 26 | try: 27 | # Check if cryptg is installed. 28 | from cryptg import __name__ as _ 29 | del _ 30 | FAST_TELETHON = True 31 | except ModuleNotFoundError: 32 | FAST_TELETHON = False 33 | 34 | __all__ = [ 35 | 'AESwState', 36 | 'get_rnd_bytes', 37 | 'FAST_TELETHON', 38 | 'FAST_ENCRYPTION', 39 | 'Salt', 'BoxSalt', 40 | 'FileSalt', 'IV' 41 | ] 42 | class IV: 43 | """This is a class-wrapper for AES IV""" 44 | def __init__(self, iv: Union[bytes, memoryview]): 45 | self.iv = iv if isinstance(iv, bytes) else bytes(iv) 46 | 47 | def __repr__(self) -> str: 48 | return f'{self.__class__.__name__}({repr(self.iv)})' 49 | 50 | def __str__(self) -> str: 51 | return f'{self.__class__.__name__}({repr(self.iv)}) # at {hex(id(self))}' 52 | 53 | def __add__(self, other): 54 | return self.iv + other 55 | 56 | def __len__(self) -> int: 57 | return len(self.iv) 58 | 59 | def __eq__(self, other) -> bool: 60 | if hasattr(other, 'iv'): 61 | return self.iv == other.iv 62 | return False 63 | 64 | @classmethod 65 | def generate(cls, bytelength: Optional[int] = 16): 66 | """ 67 | Generates AES IV by ``bytelength`` 68 | 69 | Arguments: 70 | bytelength (``int``, optional): 71 | Bytelength of IV. 16 bytes by default. 72 | """ 73 | return cls(get_rnd_bytes(bytelength)) 74 | 75 | def hex(self) -> str: 76 | """Returns IV as hexadecimal""" 77 | return self.iv.hex() 78 | 79 | class Salt: 80 | """This is a class-wrapper for some TGBOX salt""" 81 | def __init__(self, salt: Union[bytes, memoryview]): 82 | self.salt = salt if isinstance(salt, bytes) else bytes(salt) 83 | 84 | def __repr__(self) -> str: 85 | return f'{self.__class__.__name__}({repr(self.salt)})' 86 | 87 | def __str__(self) -> str: 88 | return f'{self.__class__.__name__}({repr(self.salt)}) # at {hex(id(self))}' 89 | 90 | def __add__(self, other): 91 | return self.salt + other 92 | 93 | def __len__(self) -> int: 94 | return len(self.salt) 95 | 96 | def __eq__(self, other) -> bool: 97 | if hasattr(other, 'salt'): 98 | return self.salt == other.salt 99 | return False 100 | 101 | @classmethod 102 | def generate(cls, bytelength: Optional[int] = 32): 103 | """ 104 | Generates Salt by ``bytelength`` 105 | 106 | Arguments: 107 | bytelength (``int``, optional): 108 | Bytelength of Salt. 32 bytes by default. 109 | """ 110 | return cls(get_rnd_bytes(bytelength)) 111 | 112 | def hex(self) -> str: 113 | """Returns Salt as hexadecimal""" 114 | return self.salt.hex() 115 | 116 | class BoxSalt(Salt): 117 | """This is a class-wrapper for BoxSalt""" 118 | 119 | class FileSalt(Salt): 120 | """This is a class-wrapper for FileSalt""" 121 | 122 | 123 | class _PyaesState: 124 | def __init__(self, key: Union[bytes, 'Key'], iv: IV): 125 | """ 126 | Class to wrap ``pyaes.AESModeOfOperationCBC`` 127 | if there is no ``FAST_ENCRYPTION``. 128 | 129 | .. note:: 130 | You should use only ``encrypt()`` or 131 | ``decrypt()`` method per one object. 132 | 133 | Arguments: 134 | key (``bytes``, ``Key``): 135 | AES encryption/decryption Key. 136 | 137 | iv (``IV``): 138 | AES Initialization Vector. 139 | """ 140 | key = key.key if hasattr(key, 'key') else key 141 | self.iv = iv 142 | 143 | self._aes_state = AESModeOfOperationCBC( # pylint: disable=E0601 144 | key = bytes(key), 145 | iv = self.iv.iv 146 | ) 147 | self.__mode = None # encrypt mode is 1 and decrypt is 2 148 | 149 | @staticmethod 150 | def __convert_memoryview(data: Union[bytes, memoryview]) -> bytes: 151 | # PyAES doesn't support memoryview, convert to bytes 152 | if isinstance(data, memoryview) and not FAST_ENCRYPTION: 153 | data = data.tobytes() 154 | return data 155 | 156 | def encrypt(self, data: Union[bytes, memoryview]) -> bytes: 157 | """``data`` length must be divisible by 16.""" 158 | if not self.__mode: 159 | self.__mode = 1 160 | else: 161 | if self.__mode != 1: 162 | raise ModeInvalid('You should use only decrypt function.') 163 | 164 | data = self.__convert_memoryview(data) 165 | assert not len(data) % 16; total = b'' 166 | 167 | for _ in range(len(data) // 16): 168 | total += self._aes_state.encrypt(data[:16]) 169 | data = data[16:] 170 | 171 | return total 172 | 173 | def decrypt(self, data: Union[bytes, memoryview]) -> bytes: 174 | """``data`` length must be divisible by 16.""" 175 | if not self.__mode: 176 | self.__mode = 2 177 | else: 178 | if self.__mode != 2: 179 | raise ModeInvalid('You should use only encrypt function.') 180 | 181 | data = self.__convert_memoryview(data) 182 | assert not len(data) % 16; total = b'' 183 | 184 | for _ in range(len(data) // 16): 185 | total += self._aes_state.decrypt(data[:16]) 186 | data = data[16:] 187 | 188 | return total 189 | 190 | class AESwState: 191 | """ 192 | Wrapper around AES CBC which preserve state. 193 | 194 | .. note:: 195 | You should use only ``encrypt()`` or 196 | ``decrypt()`` method per one object. 197 | """ 198 | def __init__( 199 | self, key: Union[bytes, 'Key'], 200 | iv: Optional[Union[IV, bytes]] = None 201 | ): 202 | """ 203 | Arguments: 204 | key (``bytes``, ``Key``): 205 | AES encryption/decryption Key. 206 | 207 | iv (``IV``, ``bytes``, optional): 208 | AES Initialization Vector. 209 | 210 | If mode is *Encryption*, and 211 | isn't specified, will be used 212 | bytes from `urandom(16)`. 213 | 214 | If mode is *Decryption*, and 215 | isn't specified, will be used 216 | first 16 bytes of ciphertext. 217 | """ 218 | self.key = key.key if hasattr(key, 'key') else key 219 | self.iv, self.__mode, self._aes_cbc = iv, None, None 220 | 221 | if self.iv and not isinstance(self.iv, IV): 222 | self.iv = IV(self.iv) 223 | self.__iv_concated = False 224 | 225 | def __repr__(self) -> str: 226 | return f', {repr(self.iv)})>' 227 | 228 | def __str__(self) -> str: 229 | return f', {repr(self.iv)})> # {self.__mode=}' 230 | 231 | def __init_aes_state(self, mode: int) -> None: 232 | if FAST_ENCRYPTION: 233 | self._aes_cbc = Cipher(algorithms.AES(self.key), modes.CBC(self.iv.iv)) 234 | 235 | if mode == 1: # Encryption 236 | encryptor = self._aes_cbc.encryptor() 237 | setattr(self._aes_cbc, 'encrypt', encryptor.update) 238 | else: # Decryption 239 | decryptor = self._aes_cbc.decryptor() 240 | setattr(self._aes_cbc, 'decrypt', decryptor.update) 241 | else: 242 | self._aes_cbc = _PyaesState(self.key, self.iv) 243 | 244 | @property 245 | def mode(self) -> int: 246 | """ 247 | Returns ``1`` if mode is encryption 248 | and ``2`` if decryption. 249 | """ 250 | return self.__mode 251 | 252 | def encrypt(self, data: bytes, pad: bool=True, concat_iv: bool=True) -> bytes: 253 | """ 254 | Encrypts ``data`` with AES CBC. 255 | 256 | If ``concat_iv`` is ``True``, then 257 | first 16 bytes of result will be IV. 258 | """ 259 | if not self.__mode: 260 | self.__mode = 1 261 | 262 | if not self.iv: self.iv = IV.generate() 263 | self.__init_aes_state(self.__mode) 264 | else: 265 | if self.__mode != 1: 266 | raise ModeInvalid('You should use only decrypt method.') 267 | 268 | if pad: data = append_PKCS7_padding(data) 269 | data = self._aes_cbc.encrypt(data) 270 | 271 | if concat_iv and not self.__iv_concated: 272 | self.__iv_concated = True 273 | return self.iv.iv + data 274 | 275 | return data 276 | 277 | def decrypt(self, data: bytes, unpad: bool=True) -> bytes: 278 | """ 279 | Decrypts ``data`` with AES CBC. 280 | 281 | ``data`` length must be evenly divisible by 16. 282 | """ 283 | if not self.__mode: 284 | self.__mode = 2 285 | 286 | if not self.iv: 287 | self.iv, data = IV(data[:16]), data[16:] 288 | self.__init_aes_state(self.__mode) 289 | else: 290 | if self.__mode != 2: 291 | raise ModeInvalid('You should use only encrypt method.') 292 | 293 | data = self._aes_cbc.decrypt(data) 294 | if unpad: data = strip_PKCS7_padding(data) 295 | return data 296 | 297 | def get_rnd_bytes(length: int=32) -> bytes: 298 | """Returns ``os.urandom(length)``.""" 299 | return urandom(length) 300 | -------------------------------------------------------------------------------- /tgbox/defaults.py: -------------------------------------------------------------------------------- 1 | """This module stores API defaults.""" 2 | 3 | import logging 4 | 5 | from os import getenv 6 | from enum import IntEnum 7 | from pathlib import Path 8 | try: 9 | from sys import _MEIPASS 10 | except ImportError: 11 | _MEIPASS = None 12 | 13 | from .version import VERSION 14 | 15 | # Used to check minor protocol version for 16 | # the available features in TGBOX 17 | MINOR_VERSION = int(VERSION.split('.')[1]) 18 | 19 | __all__ = [ 20 | 'Limits', 21 | 'UploadLimits', 22 | 'Scrypt', 23 | 'VERSION', 24 | 'MINOR_VERSION', 25 | 'VERBYTE', 26 | 'DEF_TGBOX_NAME', 27 | 'REMOTEBOX_PREFIX', 28 | 'DEF_NO_FOLDER', 29 | 'DEF_UNK_FOLDER', 30 | 'PREFIX', 31 | 'BOX_IMAGE_PATH', 32 | 'WORDS_PATH', 33 | 'FFMPEG', 34 | 'ABSPATH', 35 | 'DOWNLOAD_PATH', 36 | 'PYINSTALLER_DATA' 37 | ] 38 | logger = logging.getLogger(__name__) 39 | 40 | # This will be True if we're inside the ReadTheDocs build 41 | READTHEDOCS: bool = bool(getenv('READTHEDOCS')) 42 | 43 | class Limits(IntEnum): 44 | """Default TGBOX API limits""" 45 | # We store metadata size in three bytes, but 46 | # by default it's size is limited to 1MB. You 47 | # can set it up to the 256^3-1, but to share 48 | # your files with other people they should have 49 | # a METADATA_MAX that >= than yours. 50 | METADATA_MAX: int=1000000 51 | # Max FILE_PATH length on Linux 52 | # is 4096 bytes (4KiB). 53 | FILE_PATH_MAX: int=4096 54 | 55 | class UploadLimits(IntEnum): 56 | """Telegram filesize limits""" 57 | DEFAULT = 2000000000 58 | PREMIUM = 4000000000 59 | 60 | class Scrypt(IntEnum): 61 | """Default Scrypt KDF configuration""" 62 | # See https://en.wikipedia.org/wiki/Scrypt for base info about Scrypt 63 | # ------------------------------------------------------------------- 64 | # This salt affects basekeys, you can change it to protect your RemoteBox 65 | # from bruteforcing, but make sure to backup it. If you will lose your custom 66 | # salt, then recovery of your decryption key for TGBOX will be impossible. 67 | # Typically you won't need to change it, because if you use strong password 68 | # (or phrase generated by keys.Phrase) then it's already should be impossible 69 | # to brute force. Think of this as 2FA. Changed salt will not protect you if 70 | # you leaked your mainkey, but will protect you from passphrase leakage. 71 | SALT: int=0x37CE65C834C6EFE05DFAD02413C0950072A1FE3ED48A33368333848D9C782167 72 | # You can change any Scrypt params. Please note that by default resulted 73 | # key will be hashed with sha256, so BaseKey is always 32 bytes long. 74 | # This defaults is balanced to use 1GB of RAM on each Key make. 75 | DKLEN: int=32 76 | N: int=2**20 77 | R: int=8 78 | P: int=1 79 | 80 | # Path that will be used to save downloaded files. 81 | DOWNLOAD_PATH: Path=Path('DownloadsTGBOX') 82 | 83 | VERBYTE: bytes=b'\x01' 84 | 85 | DEF_TGBOX_NAME: str='TGBOX' 86 | REMOTEBOX_PREFIX: str=f'{DEF_TGBOX_NAME}[{VERBYTE.hex()}]: ' 87 | 88 | DEF_NO_FOLDER: Path=Path('NO_FOLDER') 89 | DEF_UNK_FOLDER: Path=Path('UNKNOWN_FOLDER') 90 | 91 | PREFIX: bytes=b'\x00TGBOX' 92 | 93 | ABSPATH: Path = Path(_MEIPASS) if _MEIPASS is not None \ 94 | else Path(__file__).parent 95 | 96 | # Get path to "other" folder where we store 97 | # words.txt and tgbox_logo.png files. 98 | _other: Path = ABSPATH / 'other' 99 | 100 | # We will use it in subprocess.call 101 | FFMPEG = 'ffmpeg' 102 | 103 | # You can add ffmpeg.exe to 'other' folder 104 | # before build with PyInstaller on Windows or 105 | # just if you want TGBOX to make file thumbnails 106 | # or extract duration. It will be added to 107 | # your resulted executable. 108 | # 109 | # https://www.ffmpeg.org/download.html#build-windows 110 | # 111 | for file in _other.iterdir(): 112 | if file.name == 'ffmpeg.exe': 113 | logger.info(f'FFMPEG found in {str(_other)}, we will use it') 114 | FFMPEG = _other / 'ffmpeg.exe' 115 | 116 | # By default, PyInstaller will not grab files 117 | # from 'other' folder. To resolve this error 118 | # you will need to manually add it to .spec file. 119 | # 120 | # See 121 | # '.spec file' example: 122 | # github.com/NotStatilko/tgbox-cli/blob/main/tgbox_cli.spec 123 | # 124 | # 'Installation' in TGBOX docs: 125 | # tgbox.readthedocs.io/en/latest/installation.html 126 | # 127 | PYINSTALLER_DATA: dict = { 128 | str(Path('other', i.name)): str(i) 129 | for i in _other.glob('*') 130 | } 131 | BOX_IMAGE_PATH: Path = _other / 'tgbox_logo.png' 132 | WORDS_PATH: Path = _other / 'words.txt' 133 | -------------------------------------------------------------------------------- /tgbox/errors.py: -------------------------------------------------------------------------------- 1 | """This module stores all Tgbox-unique exceptions.""" 2 | 3 | class TgboxException(Exception): 4 | """Base TGBOX Exception""" 5 | 6 | def __init__(self, message=None): 7 | super().__init__(message or self.__doc__) 8 | 9 | # Base Exceptions 10 | 11 | class NotInitializedError(TgboxException): 12 | """The class you try to use isn't initialized""" 13 | 14 | class PathIsDirectory(TgboxException): 15 | """Specified path is Directory""" 16 | 17 | class LimitExceeded(TgboxException): 18 | """Value is out of allowed range""" 19 | 20 | class NotATgboxFile(TgboxException): 21 | """This Telegram message isn't a TGBOX file""" 22 | 23 | class InvalidFile(TgboxException): 24 | """Specified file is invalid""" 25 | 26 | # Crypto Exceptions 27 | 28 | class IncorrectKey(TgboxException): 29 | """Specified key is invalid.""" 30 | 31 | class ModeInvalid(TgboxException): 32 | """You should use only decryption or encryption per class""" 33 | 34 | class AESError(TgboxException): 35 | """Invalid AES configuration""" 36 | 37 | # Tools Exceptions 38 | 39 | class ConcatError(TgboxException): 40 | """You must concat metadata before using OpenPretender""" 41 | 42 | class PreviewImpossible(TgboxException): 43 | """Can\'t create file preview""" 44 | 45 | class DurationImpossible(TgboxException): 46 | """Can\'t get media duration""" 47 | 48 | # Database Exceptions 49 | 50 | class InUseException(TgboxException): 51 | """The DB already exists and in use""" 52 | 53 | class BrokenDatabase(TgboxException): 54 | """Can\'t parse SQLite DB""" 55 | 56 | # RemoteBox Exceptions 57 | 58 | class RemoteFileNotFound(TgboxException): 59 | """Seems that there is no requested by you file""" 60 | 61 | class SessionUnregistered(TgboxException): 62 | """Session you trying to use was disconnected""" 63 | 64 | class RemoteBoxInaccessible(TgboxException): 65 | """The RemoteBox you try to use is inaccessible""" 66 | 67 | class NotEnoughRights(TgboxException): 68 | """You don't have rights for this action""" 69 | 70 | class NoPlaceLeftForMetadata(TgboxException): 71 | """Your edited metadata overflow Telegram caption limit""" 72 | 73 | # LocalBox Exceptions 74 | 75 | class AlreadyImported(TgboxException): 76 | """LocalBox have file with same ID""" 77 | 78 | class NotImported(TgboxException): 79 | """The file you try to retrieve wasn't imported yet""" 80 | 81 | class FingerprintExists(TgboxException): 82 | """File with the same file path already uploaded to the Box""" 83 | -------------------------------------------------------------------------------- /tgbox/fastelethon.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copied from Mautrix-Telegram 3 | github.com/tulir/mautrix-telegram/ 4 | 5 | Copied again from Painor GitHub 6 | gist.github.com/painor/7e74de80ae0c819d3e9abcf9989a8dd6 7 | 8 | | This file was patched for TGBOX project (github.com/NonProjects/tgbox) 9 | | and may not work as you expect in your code. 10 | 11 | Big thanks to all contributors of this module. 12 | """ 13 | 14 | import os 15 | import math 16 | import asyncio 17 | import hashlib 18 | import inspect 19 | import logging 20 | 21 | from typing import ( 22 | Optional, List, AsyncGenerator, 23 | Union, Awaitable, BinaryIO, 24 | DefaultDict, Tuple 25 | ) 26 | from collections import defaultdict 27 | 28 | from telethon.tl.functions.auth import ( 29 | ExportAuthorizationRequest, 30 | ImportAuthorizationRequest 31 | ) 32 | from telethon.errors import FilePartsInvalidError 33 | from telethon import utils, helpers, TelegramClient 34 | from telethon.tl.custom.file import File 35 | 36 | from telethon.crypto import AuthKey 37 | from telethon.network import MTProtoSender 38 | 39 | from telethon.tl.alltlobjects import LAYER 40 | from telethon.tl.functions import InvokeWithLayerRequest 41 | 42 | from telethon.tl.types import ( 43 | Document, InputFileLocation, 44 | InputDocumentFileLocation, 45 | InputPhotoFileLocation, 46 | InputPeerPhotoFileLocation, 47 | TypeInputFile, Photo, 48 | InputFileBig, InputFile 49 | ) 50 | from telethon.tl.functions.upload import ( 51 | GetFileRequest, 52 | SaveFilePartRequest, 53 | SaveBigFilePartRequest 54 | ) 55 | logger: logging.Logger = logging.getLogger(__name__) 56 | 57 | TypeLocation = Union[ 58 | Document, Photo, InputDocumentFileLocation, 59 | InputPeerPhotoFileLocation, 60 | InputFileLocation, InputPhotoFileLocation 61 | ] 62 | class DownloadSender: 63 | client: TelegramClient 64 | sender: MTProtoSender 65 | request: GetFileRequest 66 | remaining: int 67 | stride: int 68 | 69 | def __init__( 70 | self, client: TelegramClient, 71 | sender: MTProtoSender, 72 | file: TypeLocation, 73 | offset: int, limit: int, 74 | stride: int, count: int) -> None: 75 | 76 | # Offset must be divisible by 4096, and then by 524288, 77 | # otherwise error will be raised. 78 | self.offset = offset 79 | 80 | self.sender = sender 81 | self.client = client 82 | self.stride = stride 83 | self.remaining = count 84 | 85 | self.request = GetFileRequest( 86 | file, offset=self.offset, limit=limit 87 | ) 88 | 89 | async def next(self) -> Optional[bytes]: 90 | if not self.remaining: 91 | return None 92 | 93 | result = await self.client._call(self.sender, self.request) 94 | 95 | self.remaining -= 1 96 | self.request.offset += self.stride 97 | return result.bytes 98 | 99 | def disconnect(self) -> Awaitable[None]: 100 | return self.sender.disconnect() 101 | 102 | 103 | class UploadSender: 104 | client: TelegramClient 105 | sender: MTProtoSender 106 | request: Union[SaveFilePartRequest, SaveBigFilePartRequest] 107 | part_count: int 108 | stride: int 109 | previous: Optional[asyncio.Task] 110 | loop: asyncio.AbstractEventLoop 111 | 112 | def __init__(self, client: TelegramClient, sender: MTProtoSender, file_id: int, part_count: int, big: bool, 113 | index: int, 114 | stride: int, loop: asyncio.AbstractEventLoop) -> None: 115 | self.client = client 116 | self.sender = sender 117 | self.part_count = part_count 118 | if big: 119 | self.request = SaveBigFilePartRequest(file_id, index, part_count, b"") 120 | else: 121 | self.request = SaveFilePartRequest(file_id, index, b"") 122 | self.stride = stride 123 | self.previous = None 124 | self.loop = loop 125 | 126 | async def next(self, data: bytes) -> None: 127 | if self.previous: 128 | await self.previous 129 | self.previous = self.loop.create_task(self._next(data)) 130 | 131 | async def _next(self, data: bytes) -> None: 132 | self.request.bytes = data 133 | logger.debug(f"Sending file part {self.request.file_part}/{self.part_count}" 134 | f" with {len(data)} bytes") 135 | await self.client._call(self.sender, self.request) 136 | self.request.file_part += self.stride 137 | 138 | async def disconnect(self) -> None: 139 | if self.previous: 140 | await self.previous 141 | return await self.sender.disconnect() 142 | 143 | 144 | class ParallelTransferrer: 145 | client: TelegramClient 146 | loop: asyncio.AbstractEventLoop 147 | dc_id: int 148 | senders: Optional[List[Union[DownloadSender, UploadSender]]] 149 | auth_key: AuthKey 150 | upload_ticker: int 151 | 152 | def __init__(self, client: TelegramClient, dc_id: Optional[int] = None) -> None: 153 | self.client = client 154 | self.loop = self.client.loop 155 | self.dc_id = dc_id or self.client.session.dc_id 156 | self.auth_key = (None if dc_id and self.client.session.dc_id != dc_id 157 | else self.client.session.auth_key) 158 | self.senders = None 159 | self.upload_ticker = 0 160 | 161 | async def _cleanup(self) -> None: 162 | try: 163 | await asyncio.gather(*[sender.disconnect() for sender in self.senders]) 164 | except FilePartsInvalidError: 165 | pass 166 | self.senders = None 167 | 168 | @staticmethod 169 | def _get_connection_count(file_size: int, max_count: int = 20, 170 | full_size: int = 100 * 1024 * 1024) -> int: 171 | if file_size > full_size: 172 | return max_count 173 | return math.ceil((file_size / full_size) * max_count) 174 | 175 | async def _init_download( 176 | self, connections: int, 177 | file: TypeLocation, 178 | part_count: int, 179 | part_size: int, 180 | offset: int) -> None: 181 | 182 | minimum, remainder = divmod(part_count, connections) 183 | 184 | def get_part_count() -> int: 185 | nonlocal remainder 186 | if remainder > 0: 187 | remainder -= 1 188 | return minimum + 1 189 | return minimum 190 | 191 | # The first cross-DC sender will export+import the authorization, so 192 | # we always create it before creating any other senders. 193 | self.senders = [ 194 | await self._create_download_sender( 195 | file, 0, part_size, connections * part_size, 196 | get_part_count(), offset=offset) 197 | ] 198 | offset_to_increment = offset 199 | 200 | senders_prepared = [] 201 | 202 | for i in range(1, connections): 203 | offset_to_increment += 524288 204 | 205 | sender = self._create_download_sender( 206 | file, i, part_size, 207 | connections * part_size, 208 | get_part_count(), 209 | offset = offset_to_increment 210 | ) 211 | senders_prepared.append(sender) 212 | 213 | gather_result = await asyncio.gather(*senders_prepared) 214 | self.senders.extend(gather_result) 215 | 216 | async def _create_download_sender( 217 | self, file: TypeLocation, 218 | index: int, 219 | part_size: int, 220 | stride: int, 221 | part_count: int, 222 | offset: int = None) -> DownloadSender: 223 | 224 | offset = offset if offset else index * part_size 225 | 226 | return DownloadSender( 227 | self.client, 228 | await self._create_sender(), 229 | file, offset, part_size, 230 | stride, part_count 231 | ) 232 | 233 | async def _init_upload(self, connections: int, file_id: int, part_count: int, big: bool 234 | ) -> None: 235 | self.senders = [ 236 | await self._create_upload_sender(file_id, part_count, big, 0, connections), 237 | *await asyncio.gather( 238 | *[self._create_upload_sender(file_id, part_count, big, i, connections) 239 | for i in range(1, connections)]) 240 | ] 241 | 242 | async def _create_upload_sender(self, file_id: int, part_count: int, big: bool, index: int, 243 | stride: int) -> UploadSender: 244 | return UploadSender(self.client, await self._create_sender(), file_id, part_count, big, index, stride, 245 | loop=self.loop) 246 | 247 | async def _create_sender(self) -> MTProtoSender: 248 | dc = await self.client._get_dc(self.dc_id) 249 | sender = MTProtoSender(self.auth_key, loggers=self.client._log) 250 | await sender.connect(self.client._connection(dc.ip_address, dc.port, dc.id, 251 | loggers=self.client._log, 252 | proxy=self.client._proxy)) 253 | if not self.auth_key: 254 | logger.debug(f"Exporting auth to DC {self.dc_id}") 255 | auth = await self.client(ExportAuthorizationRequest(self.dc_id)) 256 | self.client._init_request.query = ImportAuthorizationRequest(id=auth.id, 257 | bytes=auth.bytes) 258 | req = InvokeWithLayerRequest(LAYER, self.client._init_request) 259 | await sender.send(req) 260 | self.auth_key = sender.auth_key 261 | return sender 262 | 263 | async def init_upload(self, file_id: int, file_size: int, part_size_kb: Optional[float] = None, 264 | connection_count: Optional[int] = None) -> Tuple[int, int, bool]: 265 | connection_count = connection_count or self._get_connection_count(file_size) 266 | part_size = (part_size_kb or utils.get_appropriated_part_size(file_size)) * 1024 267 | part_count = (file_size + part_size - 1) // part_size 268 | is_large = file_size > 10 * 1024 * 1024 269 | await self._init_upload(connection_count, file_id, part_count, is_large) 270 | return part_size, part_count, is_large 271 | 272 | async def upload(self, part: bytes) -> None: 273 | await self.senders[self.upload_ticker].next(part) 274 | self.upload_ticker = (self.upload_ticker + 1) % len(self.senders) 275 | 276 | async def finish_upload(self) -> None: 277 | await self._cleanup() 278 | 279 | async def download( 280 | self, file: TypeLocation, file_size: int, 281 | part_size_kb: Optional[int] = None, 282 | offset: Optional[int] = None, 283 | connection_count: Optional[int] = None 284 | ) -> AsyncGenerator[bytes, None]: 285 | 286 | assert not offset % 4096, 'Offset must be divisible by 4096' 287 | assert not offset % 524288, 'Offset must be divisible by 524288' 288 | 289 | connection_count = connection_count or self._get_connection_count(file_size) 290 | part_size = (part_size_kb or utils.get_appropriated_part_size(file_size)) * 1024 291 | part_count = math.ceil((file_size - offset) / part_size) 292 | 293 | logger.debug("Starting parallel download: " 294 | f"{connection_count} {part_size} {part_count} {file!s}") 295 | 296 | await self._init_download( 297 | connection_count, 298 | file, part_count, 299 | part_size, offset=offset 300 | ) 301 | part = 0 302 | while part < part_count: 303 | tasks = [] 304 | for sender in self.senders: 305 | tasks.append(self.loop.create_task(sender.next())) 306 | for task in tasks: 307 | data = await task 308 | if not data: 309 | break 310 | yield data 311 | part += 1 312 | logger.debug(f"Part {part} downloaded") 313 | 314 | logger.debug("Parallel download finished, cleaning up connections") 315 | await self._cleanup() 316 | 317 | 318 | parallel_transfer_locks: DefaultDict[int, asyncio.Lock] =\ 319 | defaultdict(lambda: asyncio.Lock()) 320 | 321 | async def stream_file( 322 | file_to_stream: BinaryIO, 323 | chunk_size = 1024 324 | ): 325 | """``file_to_stream.read`` can be coroutine.""" 326 | while True: 327 | data_read = file_to_stream.read(chunk_size) 328 | 329 | if inspect.iscoroutine(data_read): 330 | data_read = await data_read 331 | 332 | if data_read: 333 | yield data_read 334 | else: 335 | break 336 | 337 | async def _internal_transfer_to_telegram( 338 | client: TelegramClient, 339 | response: BinaryIO, 340 | progress_callback: callable, 341 | file_size: int = None, 342 | file_name: str = 'document', 343 | part_size_kb: int = 512, 344 | ) -> Tuple[TypeInputFile, int]: 345 | 346 | file_id = helpers.generate_random_long() 347 | 348 | if not file_size: 349 | file_size = os.path.getsize(response.name) 350 | 351 | hash_md5 = hashlib.md5() 352 | uploader = ParallelTransferrer(client) 353 | 354 | part_size_, part_count, is_large =\ 355 | await uploader.init_upload( 356 | file_id, file_size, 357 | part_size_kb=part_size_kb 358 | ) 359 | if not part_size_kb: 360 | part_size = part_size_ 361 | else: 362 | part_size = part_size_kb*1024 363 | 364 | buffer = bytearray() 365 | async for data in stream_file(response, part_size): 366 | if progress_callback: 367 | r = progress_callback(response.tell(), file_size) 368 | if inspect.isawaitable(r): 369 | await r 370 | 371 | if not is_large: 372 | hash_md5.update(data) 373 | 374 | if len(buffer) == 0 and len(data) == part_size: 375 | try: 376 | await uploader.upload(data) 377 | continue 378 | except FilePartsInvalidError as e: 379 | await uploader.finish_upload() 380 | raise e from None 381 | 382 | new_len = len(buffer) + len(data) 383 | if new_len >= part_size: 384 | cutoff = part_size - len(buffer) 385 | buffer.extend(data[:cutoff]) 386 | 387 | await uploader.upload(bytes(buffer)) 388 | 389 | buffer.clear() 390 | buffer.extend(data[cutoff:]) 391 | else: 392 | buffer.extend(data) 393 | 394 | if len(buffer) > 0: 395 | await uploader.upload(bytes(buffer)) 396 | 397 | await uploader.finish_upload() 398 | 399 | if is_large: 400 | return (InputFileBig(file_id, part_count, file_name), file_size) 401 | else: 402 | return (InputFile(file_id, part_count, file_name, hash_md5.hexdigest()), file_size) 403 | 404 | async def download_file( 405 | client: TelegramClient, 406 | location: TypeLocation, 407 | request_size: int=524288, 408 | offset: int=0, 409 | progress_callback: callable = None 410 | ) -> AsyncGenerator[bytes, None]: 411 | 412 | if isinstance(location, Photo): 413 | size = File(location).size 414 | else: 415 | size = location.size 416 | 417 | dc_id, location = utils.get_input_location(location) 418 | 419 | # We lock the transfers because telegram has connection count limits 420 | downloader = ParallelTransferrer(client, dc_id) 421 | 422 | downloaded = downloader.download( 423 | location, size, offset=offset, 424 | part_size_kb=int(request_size/1024) 425 | ) 426 | position = offset 427 | async for chunk in downloaded: 428 | position += len(chunk) 429 | if progress_callback: 430 | r = progress_callback(position, size) 431 | if inspect.isawaitable(r): 432 | await r 433 | yield chunk 434 | 435 | 436 | async def upload_file( 437 | client: TelegramClient, 438 | file: BinaryIO, 439 | progress_callback: callable = None, 440 | file_name: str = 'document', 441 | file_size: int = None, 442 | part_size_kb: int = 512 443 | ) -> TypeInputFile: 444 | 445 | return (await _internal_transfer_to_telegram( 446 | client, file, progress_callback, 447 | file_size = file_size, 448 | file_name = file_name, 449 | part_size_kb = part_size_kb 450 | ))[0] 451 | -------------------------------------------------------------------------------- /tgbox/keys.py: -------------------------------------------------------------------------------- 1 | """This module stores all keys and key making functions.""" 2 | 3 | from hmac import HMAC 4 | from os import urandom 5 | from random import SystemRandom 6 | 7 | from typing import ( 8 | AsyncGenerator, 9 | Union, Optional 10 | ) 11 | from base64 import ( 12 | urlsafe_b64encode, 13 | urlsafe_b64decode 14 | ) 15 | from .errors import IncorrectKey 16 | from . import defaults 17 | 18 | from .crypto import ( 19 | AESwState as AES, FAST_ENCRYPTION, 20 | Salt, BoxSalt, FileSalt) 21 | try: 22 | from hashlib import sha256, scrypt 23 | except ImportError: # No Scrypt installed 24 | if FAST_ENCRYPTION: 25 | from cryptography.hazmat.primitives.kdf.scrypt\ 26 | import Scrypt as cryptography_Scrypt 27 | 28 | def scrypt( 29 | password: bytes, *, salt=None, n=None, 30 | r=None, p=None, maxmem=0, dklen=64): # pylint: disable=unused-argument 31 | """ 32 | This is a little wrapper around the Scrypt 33 | from the cryptography library. 34 | """ 35 | s = cryptography_Scrypt(salt=salt, length=dklen, n=n, r=r, p=p) 36 | return s.derive(password) 37 | else: 38 | if not defaults.READTHEDOCS: # ReadTheDocs does not have Scrypt in hashlib 39 | raise RuntimeError('Could not find Scrypt. Install tgbox[fast]') 40 | 41 | if FAST_ENCRYPTION: # Is faster and more secure 42 | from cryptography.hazmat.primitives.asymmetric import ec 43 | from cryptography.hazmat.primitives.serialization import PublicFormat 44 | from cryptography.hazmat.primitives.serialization import Encoding 45 | else: 46 | from ecdsa.ecdh import ECDH 47 | from ecdsa.curves import SECP256k1 48 | from ecdsa.keys import SigningKey, VerifyingKey 49 | 50 | __all__ = [ 51 | 'Phrase', 52 | 53 | 'Key', 54 | 'BaseKey', 55 | 'MainKey', 56 | 'RequestKey', 57 | 'ShareKey', 58 | 'ImportKey', 59 | 'FileKey', 60 | 'EncryptedMainkey', 61 | 'DirectoryKey', 62 | 'HMACKey', 63 | 64 | 'make_basekey', 65 | 'make_mainkey', 66 | 'make_filekey', 67 | 'make_requestkey', 68 | 'make_sharekey', 69 | 'make_importkey', 70 | 'make_dirkey', 71 | 'make_hmackey' 72 | ] 73 | 74 | class Phrase: 75 | """This class represents passphrase""" 76 | def __init__(self, phrase: Union[bytes, str]): 77 | if isinstance(phrase, str): 78 | self._phrase = phrase.encode() 79 | 80 | elif isinstance(phrase, bytes): 81 | self._phrase = phrase 82 | else: 83 | raise TypeError('phrase must be Union[bytes, str]') 84 | 85 | def __repr__(self) -> str: 86 | class_name = self.__class__.__name__ 87 | return f'{class_name}({repr(self._phrase)}) # at {hex(id(self))}' 88 | 89 | def __str__(self) -> str: 90 | return self._phrase.decode() 91 | 92 | def __hash__(self) -> int: 93 | # Without 22 hash of bytes will be equal to object's 94 | return hash((self._phrase,22)) 95 | 96 | def __eq__(self, other) -> bool: 97 | return hash(self) == hash(other) 98 | 99 | @property 100 | def phrase(self) -> bytes: 101 | """Returns current raw phrase""" 102 | return self._phrase 103 | 104 | @classmethod 105 | def generate(cls, words_count: int=6) -> 'Phrase': 106 | """ 107 | Generates passphrase 108 | 109 | Arguments: 110 | words_count (``int``, optional): 111 | Words count in ``Phrase``. 112 | """ 113 | sysrnd = SystemRandom(urandom(32)) 114 | 115 | with open(defaults.WORDS_PATH,'rb') as words_file: 116 | words_list = words_file.readlines() 117 | 118 | phrase = [ 119 | sysrnd.choice(words_list).strip() 120 | for _ in range(words_count) 121 | ] 122 | return cls(b' '.join(phrase)) 123 | 124 | class Key: 125 | """Metaclass that represents all keys.""" 126 | def __init__(self, key: bytes, key_type: int): 127 | """ 128 | Arguments: 129 | key (``bytes``): 130 | Raw bytes key. 131 | 132 | key_type (``int``): 133 | Type of key, where: 134 | 1: ``BaseKey`` 135 | 2: ``MainKey`` 136 | 3: ``RequestKey`` 137 | 4: ``ShareKey`` 138 | 5: ``ImportKey`` 139 | 6: ``FileKey`` 140 | 7: ``EncryptedMainkey`` 141 | 8: ``DirectoryKey`` 142 | 9: ``HMACKey`` 143 | """ 144 | self._key = key 145 | self._key_type = key_type 146 | self._key_types = { 147 | 1: 'BaseKey', 148 | 2: 'MainKey', 149 | 3: 'RequestKey', 150 | 4: 'ShareKey', 151 | 5: 'ImportKey', 152 | 6: 'FileKey', 153 | 7: 'EncryptedMainkey', 154 | 8: 'DirectoryKey', 155 | 9: 'HMACKey' 156 | } 157 | def __hash__(self) -> int: 158 | return hash((self._key, self._key_type)) 159 | 160 | def __eq__(self, other) -> bool: 161 | return all(( 162 | isinstance(other, self.__class__), 163 | self._key == other.key, 164 | self._key_type == other.key_type 165 | )) 166 | def __repr__(self) -> str: 167 | return f'{self._key_types[self._key_type]}({self._key}) # at {hex(id(self))}' 168 | 169 | def __add__(self, other) -> bytes: 170 | if isinstance(other, Salt): 171 | return self._key + other.salt 172 | return self._key + other 173 | 174 | def __len__(self) -> int: 175 | return len(self._key) 176 | 177 | def __getitem__(self, key) -> int: 178 | return self._key[key] 179 | 180 | def __iter__(self) -> AsyncGenerator[int, None]: 181 | for i in self._key: 182 | yield i 183 | 184 | @property 185 | def key_types(self) -> dict: 186 | """Returns all key types""" 187 | return self._key_types.copy() 188 | 189 | @property 190 | def key_type(self) -> int: 191 | """Returns current key type""" 192 | return self._key_type 193 | 194 | @property 195 | def key(self) -> bytes: 196 | """Returns key in raw""" 197 | return self._key 198 | 199 | @classmethod 200 | def decode(cls, encoded_key: str) -> Union[ 201 | 'BaseKey','MainKey','RequestKey', 202 | 'ShareKey','ImportKey','FileKey', 203 | 'EncryptedMainkey', 'DirectoryKey', 204 | 'HMACKey']: 205 | """ 206 | Decodes Key by prefix and returns 207 | ``Key`` in one of ``Key`` classes. 208 | 209 | B: ``BaseKey`` 210 | M: ``MainKey`` 211 | R: ``RequestKey`` 212 | S: ``ShareKey`` 213 | I: ``ImportKey`` 214 | F: ``FileKey`` 215 | E: ``EncryptedMainkey`` 216 | D: ``DirectoryKey`` 217 | H: ``HMACKey`` 218 | 219 | Key example: 220 | ``MSGVsbG8hIEkgYW0gTm9uISBJdCdzIDI5LzExLzIwMjE=``. 221 | You can decode it with ``Key.decode``. 222 | """ 223 | try: 224 | ekey_types = { 225 | 'B': BaseKey, 'M': MainKey, 226 | 'R': RequestKey, 'S': ShareKey, 227 | 'I': ImportKey, 'F': FileKey, 228 | 229 | 'E': EncryptedMainkey, 230 | 'D': DirectoryKey, 231 | 'H': HMACKey 232 | } 233 | ekey_type = ekey_types[encoded_key[0]] 234 | return ekey_type(urlsafe_b64decode(encoded_key[1:])) 235 | except Exception as e: 236 | raise IncorrectKey() from e 237 | 238 | def encode(self) -> str: 239 | """Encode raw key with ``urlsafe_b64encode`` and add prefix.""" 240 | prefix = self._key_types[self._key_type][0] 241 | return prefix + urlsafe_b64encode(self._key).decode() 242 | 243 | def hex(self) -> str: 244 | """Returns key in hex representation""" 245 | return self._key.hex() 246 | 247 | class BaseKey(Key): 248 | """ 249 | This ``Key`` used for ``MainKey`` creation and 250 | cloned ``RemoteBox`` decryption. In API 251 | it's usually result of ``keys.make_basekey``. 252 | """ 253 | def __init__(self, key: bytes): 254 | super().__init__(key, 1) 255 | 256 | class MainKey(Key): 257 | """ 258 | ``MainKey`` may be referred as "Box key". This 259 | key encrypts all box data and used in ``FileKey`` 260 | creation. It's one of your most important ``Key``, 261 | as leakage of it will result in compromising all 262 | your encrypted files in *RemoteBox* & *LocalBox*. 263 | 264 | When you clone other's *RemoteBox*, Session data 265 | will be encrypted by ``BaseKey``, not ``MainKey``. 266 | 267 | Usually you will see this ``Key`` as a result of 268 | ``keys.make_mainkey`` function. 269 | """ 270 | def __init__(self, key: bytes): 271 | super().__init__(key, 2) 272 | 273 | class RequestKey(Key): 274 | """ 275 | The ``RequestKey`` is a key that *Requester* 276 | creates when [s]he wants to import *Giver's* 277 | file, directory or even clone other's 278 | *RemoteBox* and access all files. 279 | 280 | With ``RequestKey`` *Giver* makes ``ShareKey``. 281 | Run ``help(tgbox.keys.make_requestkey)`` for information. 282 | """ 283 | def __init__(self, key: bytes): 284 | super().__init__(key, 3) 285 | 286 | class ShareKey(Key): 287 | """ 288 | The ``ShareKey`` is a key that *Giver* creates 289 | when [s]he wants to share file, directory or 290 | even the whole *Box* with the *Requester*. 291 | 292 | With ``ShareKey`` *Requester* makes ``ImportKey``. 293 | Run ``help(tgbox.keys.make_sharekey)`` for information. 294 | """ 295 | def __init__(self, key: bytes): 296 | super().__init__(key, 4) 297 | 298 | class ImportKey(Key): 299 | """ 300 | The ``ImportKey`` is a key that *Requester* 301 | obtains after calling ``make_importkey`` 302 | function with the ``ShareKey``. This is 303 | a decryption key for the requested object. 304 | 305 | Run ``help(tgbox.keys.make_importkey)`` for information. 306 | """ 307 | def __init__(self, key: bytes): 308 | super().__init__(key, 5) 309 | 310 | class FileKey(Key): 311 | """ 312 | ``FileKey`` is a key that used for encrypting 313 | file's bytedata and its metadata on upload. The 314 | ``FileKey`` encrypts all of *secret metadata* values 315 | except the ``efile_path`` (encrypted file path), so 316 | user with which you share file from your *Box* 317 | will not know from which directory it was extracted. 318 | 319 | .. note:: 320 | Usually you will not work with this class, API 321 | converts ``DirectoryKey`` to ``FileKey`` under the hood, 322 | but you can make it with ``tgbox.keys.make_filekey``. 323 | """ 324 | def __init__(self, key: bytes): 325 | super().__init__(key, 6) 326 | 327 | class EncryptedMainkey(Key): 328 | """ 329 | This class represents encrypted mainkey. When 330 | you clone other's *RemoteBox* we encrypt its 331 | ``MainKey`` with your ``BaseKey``. 332 | """ 333 | def __init__(self, key: bytes): 334 | super().__init__(key, 7) 335 | 336 | class DirectoryKey(Key): 337 | """ 338 | ``DirectoryKey`` is a ``Key`` that was added in the 339 | ``v1.3``. In previous versions, ``FileKey`` was 340 | generated with the *SHA256* over the ``MainKey`` 341 | and ``FileSalt``. Now we will make it with the 342 | ``DirectoryKey``. See Docs for more information. 343 | """ 344 | def __init__(self, key: bytes): 345 | super().__init__(key, 8) 346 | 347 | class HMACKey(Key): 348 | """ 349 | ``HMACKey`` is a ``Key`` that is used to make a 350 | *HMAC* of the bytestring. Typically, ``HMACKey`` 351 | is a result of a ``tgbox.keys.make_hmackey`` func. 352 | """ 353 | def __init__(self, key: bytes): 354 | super().__init__(key, 9) 355 | 356 | def make_basekey( 357 | phrase: Union[bytes, Phrase], 358 | *, 359 | salt: Union[bytes, int] = defaults.Scrypt.SALT, 360 | n: Optional[int] = defaults.Scrypt.N, 361 | r: Optional[int] = defaults.Scrypt.R, 362 | p: Optional[int] = defaults.Scrypt.P, 363 | dklen: Optional[int] = defaults.Scrypt.DKLEN) -> BaseKey: 364 | """ 365 | Function to create ``BaseKey``. 366 | Uses the ``sha256(scrypt(...))``. 367 | 368 | .. warning:: 369 | RAM consumption is calculated by ``128 * r * (n + p + 2)``. 370 | 371 | Arguments: 372 | phrase (``bytes``, ``Phrase``): 373 | Passphrase from which 374 | ``BaseKey`` will be created. 375 | 376 | salt (``bytes``, ``int``, optional): 377 | Scrypt Salt. 378 | 379 | n (``int``, optional): 380 | Scrypt N. 381 | 382 | r (``int``, optional): 383 | Scrypt R. 384 | 385 | p (``int``, optional): 386 | Scrypt P. 387 | 388 | dklen (``int``, optional): 389 | Scrypt dklen. 390 | """ 391 | phrase = phrase.phrase if isinstance(phrase, Phrase) else phrase 392 | 393 | if isinstance(salt, int): 394 | bit_length = ((salt.bit_length() + 8) // 8) 395 | length = (bit_length * 8 ) // 8 396 | 397 | salt = int.to_bytes(salt, length, 'big') 398 | 399 | maxmem = 128 * r * (n + p + 2) 400 | scrypt_key = scrypt( 401 | phrase, n=n, r=r, dklen=dklen, 402 | p=p, salt=salt, maxmem=maxmem 403 | ) 404 | return BaseKey(sha256(scrypt_key).digest()) 405 | 406 | def make_mainkey(basekey: BaseKey, box_salt: BoxSalt) -> MainKey: 407 | """ 408 | Function to create ``MainKey``. 409 | 410 | Arguments: 411 | basekey (``bytes``): 412 | Key which you received with scrypt 413 | function or any other key you want. 414 | 415 | box_salt (``BoxSalt``): 416 | ``BoxSalt`` generated on *LocalBox* creation. 417 | """ 418 | return MainKey(sha256(basekey + box_salt).digest()) 419 | 420 | def make_filekey(key: Union[MainKey, DirectoryKey], file_salt: FileSalt) -> FileKey: 421 | """ 422 | Function to create ``FileKey``. 423 | 424 | The ``FileKey`` is a ``Key`` that we use to encrypt the file 425 | and its metadata (except ``efile_path``) on upload. Prior to 426 | the version **1.3** to make a ``FileKey`` we used ``MainKey`` 427 | and the ``FileSalt``, which is randomly generated (on file 428 | preparation) 32 bytes. Started from now, instead of the 429 | ``MainKey`` we will use the ``DirectoryKey``, but you can 430 | still generate old *FileKey(s)* with ``MainKey``, it's 431 | here only for backward compatibility and this is legacy. 432 | 433 | ``MainKey`` or ``DirectoryKey`` *can not* be restored 434 | from the ``FileKey``, so it's safe-to-share. 435 | 436 | The main benefit in using the ``DirectoryKey`` over 437 | ``MainKey`` is that in old versions you will need 438 | to share each of files from your *Box* separately, 439 | while now you can share the one ``DirectoryKey`` 440 | and *Requester* will be able to make all of the 441 | *FileKeys* to range of files in Dir by himself. 442 | 443 | You still can share files separately, though. 444 | 445 | See docs if you want to learn more about the 446 | *Keys hierarchy* structure & other things. 447 | 448 | Arguments: 449 | key (``MainKey`` (legacy), ``DirectoryKey``): 450 | Key which will be used to make a ``FileKey``. 451 | 452 | file_salt (``FileSalt``): 453 | ``FileSalt`` generated on file prepare. 454 | """ 455 | return FileKey(sha256(key + file_salt).digest()) 456 | 457 | def make_hmackey(filekey: FileKey, file_salt: FileSalt) -> HMACKey: 458 | """ 459 | Function to create ``HMACKey``. 460 | 461 | ``HMACKey`` is a ``Key`` that is used exclusively 462 | to derive a *HMAC* (by default *SHA256*) of a 463 | *File* or any target bytestring. 464 | 465 | Arguments: 466 | filekey (``FileKey``): 467 | ``FileKey`` which will be used to make a ``HMACKey``. 468 | 469 | file_salt (``FileSalt``): 470 | ``FileSalt`` that correspond to ``FileKey``. 471 | """ 472 | return HMACKey(HMAC(filekey.key, file_salt.salt, 'sha256').digest()) 473 | 474 | def make_dirkey(mainkey: MainKey, part_id: bytes) -> DirectoryKey: 475 | """ 476 | Function to create ``DirectoryKey``. 477 | 478 | ``DirectoryKey`` is generated from the unique 479 | path *PartID* and the ``MainKey``. We use the 480 | ``DirectoryKey`` to make a ``FileKey``. See 481 | ``help(tgbox.keys.DirectoryKey)`` and docs 482 | for more information about this type of ``Key``. 483 | """ 484 | sha256_mainkey = sha256(mainkey.key).digest() 485 | return DirectoryKey(sha256(sha256_mainkey + part_id).digest()) 486 | 487 | def make_requestkey(key: Union[MainKey, BaseKey], 488 | salt: Union[FileSalt, BoxSalt, bytes]) -> RequestKey: 489 | """ 490 | Function to create ``RequestKey``. 491 | 492 | All files in *RemoteBox* is encrypted with filekeys, so 493 | if you want to share (or import) file, then you need to 494 | get ``FileKey``. For this purpose you can create ``RequestKey``. 495 | 496 | Alice has file in her Box which she wants to share with Bob. 497 | Then: A sends file to B. B forwards file to his Box, takes 498 | ``FileSalt`` from A File and ``MainKey`` of his Box and calls 499 | ``make_requestkey(key=mainkey, salt=file_salt)``. 500 | 501 | ``RequestKey`` is a compressed pubkey of ECDH on *SECP256K1* curve, 502 | B makes privkey with ``sha256(mainkey + salt)`` & exports pubkey 503 | to make a shared secret bytes (key, with which A will 504 | encrypt her filekey/mainkey. The encrypted (file/main)key 505 | is called ``ShareKey``. Use help on ``make_sharekey``.). 506 | 507 | B sends received ``RequestKey`` to A. A makes ``ShareKey`` 508 | and sends it to B. B calls ``get_importkey`` and receives the 509 | ``ImportKey``, which is, in fact, a ``FileKey``. 510 | 511 | No one except Alice and Bob will have ``FileKey``. If Alice want 512 | to share entire Box (``MainKey``) with Bob, then Bob creates 513 | slightly different ``RequestKey`` with same function: 514 | ``make_requestkey(key=mainkey, salt=box_salt)``. 515 | 516 | Please note that ``FileKey`` can only decrypt a some 517 | *RemoteBox* with which it is associated. However, if 518 | Alice will want to share the entire *Directory* of 519 | her *Box* files (i.e */home/alice/Pictures* folder) 520 | then Bob can make a ``RequestKey`` to any file 521 | from this *Directory*, and Alice will make a 522 | ``ShareKey`` with a ``DirectoryKey`` instead 523 | of ``FileKey``. See help on ``make_sharekey``. 524 | 525 | .. note:: 526 | Functions in this module is low-level, you can make ``RequestKey`` for 527 | a forwarded from A file by calling ``get_requestkey(...)`` 528 | method on ``EncryptedRemoteBoxFile`` | ``EncryptedRemoteBox``. 529 | 530 | Arguments: 531 | key (``MainKey``, ``BaseKey``): 532 | Bob's *Key*. If you want to import other's 533 | *file*, then you need to specify here 534 | ``MainKey`` of your *LocalBox*, otherwise 535 | specify ``BaseKey`` (to clone *RemoteBox*) 536 | 537 | salt (``FileSalt``, ``BoxSalt``, ``bytes``): 538 | Most obvious ``salt`` is Alice's ``BoxSalt`` or 539 | ``FileSalt``, however, ``salt`` here is just 540 | some bytestring that will be hashed with the 541 | ``MainKey`` to make the output ECDH keys 542 | unique, so you can specify here any bytes 543 | value if you understand consequences (you 544 | will need to re-use it on ``make_importkey``). 545 | """ 546 | if not any((isinstance(salt, Salt), isinstance(salt, bytes))): 547 | raise ValueError('`salt` is not Union[Salt, bytes]') 548 | 549 | if FAST_ENCRYPTION: 550 | skey_data = int.from_bytes(sha256(key + salt).digest(), 'big') 551 | skey = ec.derive_private_key(skey_data, ec.SECP256K1()) 552 | 553 | vkey = skey.public_key().public_bytes( 554 | encoding=Encoding.X962, 555 | format=PublicFormat.CompressedPoint) 556 | else: 557 | skey = SigningKey.from_string( 558 | sha256(key + salt).digest(), 559 | curve=SECP256k1, hashfunc=sha256 560 | ) 561 | vkey = skey.get_verifying_key() 562 | vkey = vkey.to_string('compressed') 563 | 564 | return RequestKey(vkey) 565 | 566 | def make_sharekey( 567 | key: Union[FileKey, MainKey, DirectoryKey], 568 | salt: Optional[Union[FileSalt, BoxSalt, bytes]] = None, 569 | requestkey: Optional[RequestKey] = None) \ 570 | -> Union[ShareKey, ImportKey]: 571 | """ 572 | Function to create ``ShareKey``. 573 | 574 | .. note:: 575 | You may want to know what is ``RequestKey`` before reading 576 | this. Please, run help on ``make_requestkey`` to get info. 577 | 578 | Alice received ``RequestKey`` from Bob. But what she should do 579 | next? As reqkey is just EC-pubkey, she wants to make a *shared 580 | secret key*. A makes her own privkey, with ``sha256(mainkey 581 | + sha256(salt + requestkey))`` & initializes ECDH with B pubkey 582 | and her privkey. After this, A makes a hashed with SHA256 583 | *shared secret*, which will be used as 32-byte length AES-CBC 584 | key & encrypts her *File|Main|Directory* key. IV here is first 16 585 | bytes of the ``sha256(requestkey)``. After, she prepends her pubkey to 586 | the resulted encrypted *File|Main|Directory* key and sends it to Bob. 587 | 588 | With A pubkey, B can easily get the same shared secret and 589 | decrypt ``ShareKey`` to make the ``ImportKey``. 590 | 591 | The things will be much less complicated if Alice don't mind 592 | to share her File, Dir or Box with ALL peoples. In this case 593 | we don't even need to make a ``ShareKey``, ``ImportKey`` 594 | will be returned from the raw target ``Key``. 595 | 596 | Arguments: 597 | key (``MainKey``, ``FileKey``, ``DirectoryKey``): 598 | o If ``key`` is instance of ``MainKey``: Box key. 599 | Specify only this kwarg and ignore ``requestkey`` 600 | if you want to share your Box with **ALL** peoples. 601 | Your Box ``key`` -- ``MainKey`` will be NOT encrypted. 602 | 603 | o If ``key`` is instance of ``FileKey``: File key. 604 | Specify only this kwarg if you want to share your 605 | File with **ALL** peoples. **No encryption** if 606 | ``RequestKey`` (as ``requestkey``) is not specified. 607 | 608 | o If ``key`` is instance of ``DirectoryKey``: Dir key. 609 | Specify only this kwarg if you want to share your 610 | File with **ALL** peoples. **No encryption** if 611 | ``RequestKey`` (as ``requestkey``) is not specified. 612 | 613 | salt (``FileSalt``, ``BoxSalt``, ``bytes``, optional): 614 | Most obvious ``salt`` is Alice's ``BoxSalt`` or 615 | ``FileSalt``, however, ``salt`` here is just 616 | some bytestring that will be hashed with the 617 | ``MainKey`` to make the output ECDH keys 618 | unique, so you can specify here any bytes 619 | value if you understand consequences. For 620 | example, we will use PartID (``bytes``) as 621 | salt on ``DirectoryKey`` sharing. 622 | 623 | requestkey (``RequestKey``, optional): 624 | ``RequestKey`` of Bob. With this must be 625 | specified ``salt``. 626 | """ 627 | if not all((requestkey, salt)): 628 | return ImportKey(key.key) 629 | 630 | skey_salt = sha256(salt + requestkey.key).digest() 631 | 632 | if FAST_ENCRYPTION: 633 | skey_data = int.from_bytes(sha256(key + skey_salt).digest(), 'big') 634 | skey = ec.derive_private_key(skey_data, ec.SECP256K1()) 635 | 636 | vkey = skey.public_key().public_bytes( 637 | encoding=Encoding.X962, 638 | format=PublicFormat.CompressedPoint 639 | ) 640 | b_pubkey = ec.EllipticCurvePublicKey.from_encoded_point( 641 | curve=ec.SECP256K1(), 642 | data=requestkey.key 643 | ) 644 | enc_key = skey.exchange( 645 | algorithm=ec.ECDH(), 646 | peer_public_key=b_pubkey) 647 | else: 648 | skey = SigningKey.from_string( 649 | sha256(key + skey_salt).digest(), 650 | curve=SECP256k1, hashfunc=sha256 651 | ) 652 | vkey = skey.get_verifying_key() 653 | vkey = vkey.to_string('compressed') 654 | 655 | b_pubkey = VerifyingKey.from_string( 656 | requestkey.key, curve=SECP256k1 657 | ) 658 | ecdh = ECDH( 659 | curve=SECP256k1, 660 | private_key=skey, 661 | public_key=b_pubkey 662 | ) 663 | enc_key = ecdh.generate_sharedsecret_bytes() 664 | 665 | enc_key = sha256(enc_key).digest() 666 | iv = sha256(requestkey.key).digest()[:16] 667 | 668 | encrypted_key = AES(enc_key, iv).encrypt( 669 | key.key, pad=False, concat_iv=False 670 | ) 671 | return ShareKey(encrypted_key + vkey) 672 | 673 | def make_importkey( 674 | key: Union[MainKey, BaseKey], sharekey: ShareKey, 675 | salt: Optional[Union[FileSalt, BoxSalt, bytes]] = None) -> ImportKey: 676 | """ 677 | .. note:: 678 | You may want to know what is ``RequestKey`` and 679 | ``ShareKey`` before using this. Use ``help()`` on 680 | another ``Key`` *make* functions. 681 | 682 | ``ShareKey`` is a combination of encrypted by Alice 683 | (File/Main/Directory)Key and her pubkey. As Bob can create 684 | again ``RequestKey``, which is PubKey of ECDH from 685 | ``sha256(key + salt)`` PrivKey, and already have 686 | PubKey of A, -- B can create a shared secret, and 687 | decrypt A ``ShareKey`` to make an ``ImportKey``. 688 | 689 | Arguments: 690 | key (``MainKey``, ``BaseKey``): 691 | Bob's ``MainKey`` or ``BaseKey`` that 692 | was used on ``RequestKey`` creation. 693 | 694 | sharekey (``ShareKey``): 695 | Alice's ``ShareKey``. 696 | 697 | salt (``FileSalt``, ``BoxSalt``, ``bytes``, optional): 698 | Salt that was used on ``RequestKey`` creation. 699 | """ 700 | if len(sharekey) == 32: # Key isn't encrypted. 701 | return ImportKey(sharekey.key) 702 | 703 | if not salt: 704 | raise ValueError('`salt` must be specified.') 705 | 706 | requestkey = make_requestkey(key, salt) 707 | 708 | if FAST_ENCRYPTION: 709 | skey_data = int.from_bytes(sha256(key + salt).digest(), 'big') 710 | skey = ec.derive_private_key(skey_data, ec.SECP256K1()) 711 | 712 | a_pubkey = ec.EllipticCurvePublicKey.from_encoded_point( 713 | curve=ec.SECP256K1(), 714 | data=sharekey[32:] 715 | ) 716 | dec_key = skey.exchange( 717 | algorithm=ec.ECDH(), 718 | peer_public_key=a_pubkey) 719 | else: 720 | skey = SigningKey.from_string( 721 | sha256(key + salt).digest(), 722 | curve=SECP256k1, hashfunc=sha256 723 | ) 724 | a_pubkey = VerifyingKey.from_string( 725 | sharekey[32:], curve=SECP256k1 726 | ) 727 | ecdh = ECDH( 728 | curve=SECP256k1, 729 | private_key=skey, 730 | public_key=a_pubkey 731 | ) 732 | dec_key = ecdh.generate_sharedsecret_bytes() 733 | 734 | dec_key = sha256(dec_key).digest() 735 | iv = sha256(requestkey.key).digest()[:16] 736 | 737 | decrypted_key = AES(dec_key, iv).decrypt( 738 | sharekey[:32], unpad=False 739 | ) 740 | return ImportKey(decrypted_key) 741 | -------------------------------------------------------------------------------- /tgbox/other/tgbox_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NonProjects/tgbox/c55abb8b731af4c9b46795cde73c9bff436049ae/tgbox/other/tgbox_logo.png -------------------------------------------------------------------------------- /tgbox/other/words.txt: -------------------------------------------------------------------------------- 1 | abandon 2 | ability 3 | able 4 | about 5 | above 6 | absent 7 | absorb 8 | abstract 9 | absurd 10 | abuse 11 | access 12 | accident 13 | account 14 | accuse 15 | achieve 16 | acid 17 | acoustic 18 | acquire 19 | across 20 | act 21 | action 22 | actor 23 | actress 24 | actual 25 | adapt 26 | add 27 | addict 28 | address 29 | adjust 30 | admit 31 | adult 32 | advance 33 | advice 34 | aerobic 35 | affair 36 | afford 37 | afraid 38 | again 39 | age 40 | agent 41 | agree 42 | ahead 43 | aim 44 | air 45 | airport 46 | aisle 47 | alarm 48 | album 49 | alcohol 50 | alert 51 | alien 52 | all 53 | alley 54 | allow 55 | almost 56 | alone 57 | alpha 58 | already 59 | also 60 | alter 61 | always 62 | amateur 63 | amazing 64 | among 65 | amount 66 | amused 67 | analyst 68 | anchor 69 | ancient 70 | anger 71 | angle 72 | angry 73 | animal 74 | ankle 75 | announce 76 | annual 77 | another 78 | answer 79 | antenna 80 | antique 81 | anxiety 82 | any 83 | apart 84 | apology 85 | appear 86 | apple 87 | approve 88 | april 89 | arch 90 | arctic 91 | area 92 | arena 93 | argue 94 | arm 95 | armed 96 | armor 97 | army 98 | around 99 | arrange 100 | arrest 101 | arrive 102 | arrow 103 | art 104 | artefact 105 | artist 106 | artwork 107 | ask 108 | aspect 109 | assault 110 | asset 111 | assist 112 | assume 113 | asthma 114 | athlete 115 | atom 116 | attack 117 | attend 118 | attitude 119 | attract 120 | auction 121 | audit 122 | august 123 | aunt 124 | author 125 | auto 126 | autumn 127 | average 128 | avocado 129 | avoid 130 | awake 131 | aware 132 | away 133 | awesome 134 | awful 135 | awkward 136 | axis 137 | baby 138 | bachelor 139 | bacon 140 | badge 141 | bag 142 | balance 143 | balcony 144 | ball 145 | bamboo 146 | banana 147 | banner 148 | bar 149 | barely 150 | bargain 151 | barrel 152 | base 153 | basic 154 | basket 155 | battle 156 | beach 157 | bean 158 | beauty 159 | because 160 | become 161 | beef 162 | before 163 | begin 164 | behave 165 | behind 166 | believe 167 | below 168 | belt 169 | bench 170 | benefit 171 | best 172 | betray 173 | better 174 | between 175 | beyond 176 | bicycle 177 | bid 178 | bike 179 | bind 180 | biology 181 | bird 182 | birth 183 | bitter 184 | black 185 | blade 186 | blame 187 | blanket 188 | blast 189 | bleak 190 | bless 191 | blind 192 | blood 193 | blossom 194 | blouse 195 | blue 196 | blur 197 | blush 198 | board 199 | boat 200 | body 201 | boil 202 | bomb 203 | bone 204 | bonus 205 | book 206 | boost 207 | border 208 | boring 209 | borrow 210 | boss 211 | bottom 212 | bounce 213 | box 214 | boy 215 | bracket 216 | brain 217 | brand 218 | brass 219 | brave 220 | bread 221 | breeze 222 | brick 223 | bridge 224 | brief 225 | bright 226 | bring 227 | brisk 228 | broccoli 229 | broken 230 | bronze 231 | broom 232 | brother 233 | brown 234 | brush 235 | bubble 236 | buddy 237 | budget 238 | buffalo 239 | build 240 | bulb 241 | bulk 242 | bullet 243 | bundle 244 | bunker 245 | burden 246 | burger 247 | burst 248 | bus 249 | business 250 | busy 251 | butter 252 | buyer 253 | buzz 254 | cabbage 255 | cabin 256 | cable 257 | cactus 258 | cage 259 | cake 260 | call 261 | calm 262 | camera 263 | camp 264 | can 265 | canal 266 | cancel 267 | candy 268 | cannon 269 | canoe 270 | canvas 271 | canyon 272 | capable 273 | capital 274 | captain 275 | car 276 | carbon 277 | card 278 | cargo 279 | carpet 280 | carry 281 | cart 282 | case 283 | cash 284 | casino 285 | castle 286 | casual 287 | cat 288 | catalog 289 | catch 290 | category 291 | cattle 292 | caught 293 | cause 294 | caution 295 | cave 296 | ceiling 297 | celery 298 | cement 299 | census 300 | century 301 | cereal 302 | certain 303 | chair 304 | chalk 305 | champion 306 | change 307 | chaos 308 | chapter 309 | charge 310 | chase 311 | chat 312 | cheap 313 | check 314 | cheese 315 | chef 316 | cherry 317 | chest 318 | chicken 319 | chief 320 | child 321 | chimney 322 | choice 323 | choose 324 | chronic 325 | chuckle 326 | chunk 327 | churn 328 | cigar 329 | cinnamon 330 | circle 331 | citizen 332 | city 333 | civil 334 | claim 335 | clap 336 | clarify 337 | claw 338 | clay 339 | clean 340 | clerk 341 | clever 342 | click 343 | client 344 | cliff 345 | climb 346 | clinic 347 | clip 348 | clock 349 | clog 350 | close 351 | cloth 352 | cloud 353 | clown 354 | club 355 | clump 356 | cluster 357 | clutch 358 | coach 359 | coast 360 | coconut 361 | code 362 | coffee 363 | coil 364 | coin 365 | collect 366 | color 367 | column 368 | combine 369 | come 370 | comfort 371 | comic 372 | common 373 | company 374 | concert 375 | conduct 376 | confirm 377 | congress 378 | connect 379 | consider 380 | control 381 | convince 382 | cook 383 | cool 384 | copper 385 | copy 386 | coral 387 | core 388 | corn 389 | correct 390 | cost 391 | cotton 392 | couch 393 | country 394 | couple 395 | course 396 | cousin 397 | cover 398 | coyote 399 | crack 400 | cradle 401 | craft 402 | cram 403 | crane 404 | crash 405 | crater 406 | crawl 407 | crazy 408 | cream 409 | credit 410 | creek 411 | crew 412 | cricket 413 | crime 414 | crisp 415 | critic 416 | crop 417 | cross 418 | crouch 419 | crowd 420 | crucial 421 | cruel 422 | cruise 423 | crumble 424 | crunch 425 | crush 426 | cry 427 | crystal 428 | cube 429 | culture 430 | cup 431 | cupboard 432 | curious 433 | current 434 | curtain 435 | curve 436 | cushion 437 | custom 438 | cute 439 | cycle 440 | dad 441 | damage 442 | damp 443 | dance 444 | danger 445 | daring 446 | dash 447 | daughter 448 | dawn 449 | day 450 | deal 451 | debate 452 | debris 453 | decade 454 | december 455 | decide 456 | decline 457 | decorate 458 | decrease 459 | deer 460 | defense 461 | define 462 | defy 463 | degree 464 | delay 465 | deliver 466 | demand 467 | demise 468 | denial 469 | dentist 470 | deny 471 | depart 472 | depend 473 | deposit 474 | depth 475 | deputy 476 | derive 477 | describe 478 | desert 479 | design 480 | desk 481 | despair 482 | destroy 483 | detail 484 | detect 485 | develop 486 | device 487 | devote 488 | diagram 489 | dial 490 | diamond 491 | diary 492 | dice 493 | diesel 494 | diet 495 | differ 496 | digital 497 | dignity 498 | dilemma 499 | dinner 500 | dinosaur 501 | direct 502 | dirt 503 | disagree 504 | discover 505 | disease 506 | dish 507 | dismiss 508 | disorder 509 | display 510 | distance 511 | divert 512 | divide 513 | divorce 514 | dizzy 515 | doctor 516 | document 517 | dog 518 | doll 519 | dolphin 520 | domain 521 | donate 522 | donkey 523 | donor 524 | door 525 | dose 526 | double 527 | dove 528 | draft 529 | dragon 530 | drama 531 | drastic 532 | draw 533 | dream 534 | dress 535 | drift 536 | drill 537 | drink 538 | drip 539 | drive 540 | drop 541 | drum 542 | dry 543 | duck 544 | dumb 545 | dune 546 | during 547 | dust 548 | dutch 549 | duty 550 | dwarf 551 | dynamic 552 | eager 553 | eagle 554 | early 555 | earn 556 | earth 557 | easily 558 | east 559 | easy 560 | echo 561 | ecology 562 | economy 563 | edge 564 | edit 565 | educate 566 | effort 567 | egg 568 | eight 569 | either 570 | elbow 571 | elder 572 | electric 573 | elegant 574 | element 575 | elephant 576 | elevator 577 | elite 578 | else 579 | embark 580 | embody 581 | embrace 582 | emerge 583 | emotion 584 | employ 585 | empower 586 | empty 587 | enable 588 | enact 589 | end 590 | endless 591 | endorse 592 | enemy 593 | energy 594 | enforce 595 | engage 596 | engine 597 | enhance 598 | enjoy 599 | enlist 600 | enough 601 | enrich 602 | enroll 603 | ensure 604 | enter 605 | entire 606 | entry 607 | envelope 608 | episode 609 | equal 610 | equip 611 | era 612 | erase 613 | erode 614 | erosion 615 | error 616 | erupt 617 | escape 618 | essay 619 | essence 620 | estate 621 | eternal 622 | ethics 623 | evidence 624 | evil 625 | evoke 626 | evolve 627 | exact 628 | example 629 | excess 630 | exchange 631 | excite 632 | exclude 633 | excuse 634 | execute 635 | exercise 636 | exhaust 637 | exhibit 638 | exile 639 | exist 640 | exit 641 | exotic 642 | expand 643 | expect 644 | expire 645 | explain 646 | expose 647 | express 648 | extend 649 | extra 650 | eye 651 | eyebrow 652 | fabric 653 | face 654 | faculty 655 | fade 656 | faint 657 | faith 658 | fall 659 | false 660 | fame 661 | family 662 | famous 663 | fan 664 | fancy 665 | fantasy 666 | farm 667 | fashion 668 | fat 669 | fatal 670 | father 671 | fatigue 672 | fault 673 | favorite 674 | feature 675 | february 676 | federal 677 | fee 678 | feed 679 | feel 680 | female 681 | fence 682 | festival 683 | fetch 684 | fever 685 | few 686 | fiber 687 | fiction 688 | field 689 | figure 690 | file 691 | film 692 | filter 693 | final 694 | find 695 | fine 696 | finger 697 | finish 698 | fire 699 | firm 700 | first 701 | fiscal 702 | fish 703 | fit 704 | fitness 705 | fix 706 | flag 707 | flame 708 | flash 709 | flat 710 | flavor 711 | flee 712 | flight 713 | flip 714 | float 715 | flock 716 | floor 717 | flower 718 | fluid 719 | flush 720 | fly 721 | foam 722 | focus 723 | fog 724 | foil 725 | fold 726 | follow 727 | food 728 | foot 729 | force 730 | forest 731 | forget 732 | fork 733 | fortune 734 | forum 735 | forward 736 | fossil 737 | foster 738 | found 739 | fox 740 | fragile 741 | frame 742 | frequent 743 | fresh 744 | friend 745 | fringe 746 | frog 747 | front 748 | frost 749 | frown 750 | frozen 751 | fruit 752 | fuel 753 | fun 754 | funny 755 | furnace 756 | fury 757 | future 758 | gadget 759 | gain 760 | galaxy 761 | gallery 762 | game 763 | gap 764 | garage 765 | garbage 766 | garden 767 | garlic 768 | garment 769 | gas 770 | gasp 771 | gate 772 | gather 773 | gauge 774 | gaze 775 | general 776 | genius 777 | genre 778 | gentle 779 | genuine 780 | gesture 781 | ghost 782 | giant 783 | gift 784 | giggle 785 | ginger 786 | giraffe 787 | girl 788 | give 789 | glad 790 | glance 791 | glare 792 | glass 793 | glide 794 | glimpse 795 | globe 796 | gloom 797 | glory 798 | glove 799 | glow 800 | glue 801 | goat 802 | goddess 803 | gold 804 | good 805 | goose 806 | gorilla 807 | gospel 808 | gossip 809 | govern 810 | gown 811 | grab 812 | grace 813 | grain 814 | grant 815 | grape 816 | grass 817 | gravity 818 | great 819 | green 820 | grid 821 | grief 822 | grit 823 | grocery 824 | group 825 | grow 826 | grunt 827 | guard 828 | guess 829 | guide 830 | guilt 831 | guitar 832 | gun 833 | gym 834 | habit 835 | hair 836 | half 837 | hammer 838 | hamster 839 | hand 840 | happy 841 | harbor 842 | hard 843 | harsh 844 | harvest 845 | hat 846 | have 847 | hawk 848 | hazard 849 | head 850 | health 851 | heart 852 | heavy 853 | hedgehog 854 | height 855 | hello 856 | helmet 857 | help 858 | hen 859 | hero 860 | hidden 861 | high 862 | hill 863 | hint 864 | hip 865 | hire 866 | history 867 | hobby 868 | hockey 869 | hold 870 | hole 871 | holiday 872 | hollow 873 | home 874 | honey 875 | hood 876 | hope 877 | horn 878 | horror 879 | horse 880 | hospital 881 | host 882 | hotel 883 | hour 884 | hover 885 | hub 886 | huge 887 | human 888 | humble 889 | humor 890 | hundred 891 | hungry 892 | hunt 893 | hurdle 894 | hurry 895 | hurt 896 | husband 897 | hybrid 898 | ice 899 | icon 900 | idea 901 | identify 902 | idle 903 | ignore 904 | ill 905 | illegal 906 | illness 907 | image 908 | imitate 909 | immense 910 | immune 911 | impact 912 | impose 913 | improve 914 | impulse 915 | inch 916 | include 917 | income 918 | increase 919 | index 920 | indicate 921 | indoor 922 | industry 923 | infant 924 | inflict 925 | inform 926 | inhale 927 | inherit 928 | initial 929 | inject 930 | injury 931 | inmate 932 | inner 933 | innocent 934 | input 935 | inquiry 936 | insane 937 | insect 938 | inside 939 | inspire 940 | install 941 | intact 942 | interest 943 | into 944 | invest 945 | invite 946 | involve 947 | iron 948 | island 949 | isolate 950 | issue 951 | item 952 | ivory 953 | jacket 954 | jaguar 955 | jar 956 | jazz 957 | jealous 958 | jeans 959 | jelly 960 | jewel 961 | job 962 | join 963 | joke 964 | journey 965 | joy 966 | judge 967 | juice 968 | jump 969 | jungle 970 | junior 971 | junk 972 | just 973 | kangaroo 974 | keen 975 | keep 976 | ketchup 977 | key 978 | kick 979 | kid 980 | kidney 981 | kind 982 | kingdom 983 | kiss 984 | kit 985 | kitchen 986 | kite 987 | kitten 988 | kiwi 989 | knee 990 | knife 991 | knock 992 | know 993 | lab 994 | label 995 | labor 996 | ladder 997 | lady 998 | lake 999 | lamp 1000 | language 1001 | laptop 1002 | large 1003 | later 1004 | latin 1005 | laugh 1006 | laundry 1007 | lava 1008 | law 1009 | lawn 1010 | lawsuit 1011 | layer 1012 | lazy 1013 | leader 1014 | leaf 1015 | learn 1016 | leave 1017 | lecture 1018 | left 1019 | leg 1020 | legal 1021 | legend 1022 | leisure 1023 | lemon 1024 | lend 1025 | length 1026 | lens 1027 | leopard 1028 | lesson 1029 | letter 1030 | level 1031 | liar 1032 | liberty 1033 | library 1034 | license 1035 | life 1036 | lift 1037 | light 1038 | like 1039 | limb 1040 | limit 1041 | link 1042 | lion 1043 | liquid 1044 | list 1045 | little 1046 | live 1047 | lizard 1048 | load 1049 | loan 1050 | lobster 1051 | local 1052 | lock 1053 | logic 1054 | lonely 1055 | long 1056 | loop 1057 | lottery 1058 | loud 1059 | lounge 1060 | love 1061 | loyal 1062 | lucky 1063 | luggage 1064 | lumber 1065 | lunar 1066 | lunch 1067 | luxury 1068 | lyrics 1069 | machine 1070 | mad 1071 | magic 1072 | magnet 1073 | maid 1074 | mail 1075 | main 1076 | major 1077 | make 1078 | mammal 1079 | man 1080 | manage 1081 | mandate 1082 | mango 1083 | mansion 1084 | manual 1085 | maple 1086 | marble 1087 | march 1088 | margin 1089 | marine 1090 | market 1091 | marriage 1092 | mask 1093 | mass 1094 | master 1095 | match 1096 | material 1097 | math 1098 | matrix 1099 | matter 1100 | maximum 1101 | maze 1102 | meadow 1103 | mean 1104 | measure 1105 | meat 1106 | mechanic 1107 | medal 1108 | media 1109 | melody 1110 | melt 1111 | member 1112 | memory 1113 | mention 1114 | menu 1115 | mercy 1116 | merge 1117 | merit 1118 | merry 1119 | mesh 1120 | message 1121 | metal 1122 | method 1123 | middle 1124 | midnight 1125 | milk 1126 | million 1127 | mimic 1128 | mind 1129 | minimum 1130 | minor 1131 | minute 1132 | miracle 1133 | mirror 1134 | misery 1135 | miss 1136 | mistake 1137 | mix 1138 | mixed 1139 | mixture 1140 | mobile 1141 | model 1142 | modify 1143 | mom 1144 | moment 1145 | monitor 1146 | monkey 1147 | monster 1148 | month 1149 | moon 1150 | moral 1151 | more 1152 | morning 1153 | mosquito 1154 | mother 1155 | motion 1156 | motor 1157 | mountain 1158 | mouse 1159 | move 1160 | movie 1161 | much 1162 | muffin 1163 | mule 1164 | multiply 1165 | muscle 1166 | museum 1167 | mushroom 1168 | music 1169 | must 1170 | mutual 1171 | myself 1172 | mystery 1173 | myth 1174 | naive 1175 | name 1176 | napkin 1177 | narrow 1178 | nasty 1179 | nation 1180 | nature 1181 | near 1182 | neck 1183 | need 1184 | negative 1185 | neglect 1186 | neither 1187 | nephew 1188 | nerve 1189 | nest 1190 | net 1191 | network 1192 | neutral 1193 | never 1194 | news 1195 | next 1196 | nice 1197 | night 1198 | noble 1199 | noise 1200 | nominee 1201 | noodle 1202 | normal 1203 | north 1204 | nose 1205 | notable 1206 | note 1207 | nothing 1208 | notice 1209 | novel 1210 | now 1211 | nuclear 1212 | number 1213 | nurse 1214 | nut 1215 | oak 1216 | obey 1217 | object 1218 | oblige 1219 | obscure 1220 | observe 1221 | obtain 1222 | obvious 1223 | occur 1224 | ocean 1225 | october 1226 | odor 1227 | off 1228 | offer 1229 | office 1230 | often 1231 | oil 1232 | okay 1233 | old 1234 | olive 1235 | olympic 1236 | omit 1237 | once 1238 | one 1239 | onion 1240 | online 1241 | only 1242 | open 1243 | opera 1244 | opinion 1245 | oppose 1246 | option 1247 | orange 1248 | orbit 1249 | orchard 1250 | order 1251 | ordinary 1252 | organ 1253 | orient 1254 | original 1255 | orphan 1256 | ostrich 1257 | other 1258 | outdoor 1259 | outer 1260 | output 1261 | outside 1262 | oval 1263 | oven 1264 | over 1265 | own 1266 | owner 1267 | oxygen 1268 | oyster 1269 | ozone 1270 | pact 1271 | paddle 1272 | page 1273 | pair 1274 | palace 1275 | palm 1276 | panda 1277 | panel 1278 | panic 1279 | panther 1280 | paper 1281 | parade 1282 | parent 1283 | park 1284 | parrot 1285 | party 1286 | pass 1287 | patch 1288 | path 1289 | patient 1290 | patrol 1291 | pattern 1292 | pause 1293 | pave 1294 | payment 1295 | peace 1296 | peanut 1297 | pear 1298 | peasant 1299 | pelican 1300 | pen 1301 | penalty 1302 | pencil 1303 | people 1304 | pepper 1305 | perfect 1306 | permit 1307 | person 1308 | pet 1309 | phone 1310 | photo 1311 | phrase 1312 | physical 1313 | piano 1314 | picnic 1315 | picture 1316 | piece 1317 | pig 1318 | pigeon 1319 | pill 1320 | pilot 1321 | pink 1322 | pioneer 1323 | pipe 1324 | pistol 1325 | pitch 1326 | pizza 1327 | place 1328 | planet 1329 | plastic 1330 | plate 1331 | play 1332 | please 1333 | pledge 1334 | pluck 1335 | plug 1336 | plunge 1337 | poem 1338 | poet 1339 | point 1340 | polar 1341 | pole 1342 | police 1343 | pond 1344 | pony 1345 | pool 1346 | popular 1347 | portion 1348 | position 1349 | possible 1350 | post 1351 | potato 1352 | pottery 1353 | poverty 1354 | powder 1355 | power 1356 | practice 1357 | praise 1358 | predict 1359 | prefer 1360 | prepare 1361 | present 1362 | pretty 1363 | prevent 1364 | price 1365 | pride 1366 | primary 1367 | print 1368 | priority 1369 | prison 1370 | private 1371 | prize 1372 | problem 1373 | process 1374 | produce 1375 | profit 1376 | program 1377 | project 1378 | promote 1379 | proof 1380 | property 1381 | prosper 1382 | protect 1383 | proud 1384 | provide 1385 | public 1386 | pudding 1387 | pull 1388 | pulp 1389 | pulse 1390 | pumpkin 1391 | punch 1392 | pupil 1393 | puppy 1394 | purchase 1395 | purity 1396 | purpose 1397 | purse 1398 | push 1399 | put 1400 | puzzle 1401 | pyramid 1402 | quality 1403 | quantum 1404 | quarter 1405 | question 1406 | quick 1407 | quit 1408 | quiz 1409 | quote 1410 | rabbit 1411 | raccoon 1412 | race 1413 | rack 1414 | radar 1415 | radio 1416 | rail 1417 | rain 1418 | raise 1419 | rally 1420 | ramp 1421 | ranch 1422 | random 1423 | range 1424 | rapid 1425 | rare 1426 | rate 1427 | rather 1428 | raven 1429 | raw 1430 | razor 1431 | ready 1432 | real 1433 | reason 1434 | rebel 1435 | rebuild 1436 | recall 1437 | receive 1438 | recipe 1439 | record 1440 | recycle 1441 | reduce 1442 | reflect 1443 | reform 1444 | refuse 1445 | region 1446 | regret 1447 | regular 1448 | reject 1449 | relax 1450 | release 1451 | relief 1452 | rely 1453 | remain 1454 | remember 1455 | remind 1456 | remove 1457 | render 1458 | renew 1459 | rent 1460 | reopen 1461 | repair 1462 | repeat 1463 | replace 1464 | report 1465 | require 1466 | rescue 1467 | resemble 1468 | resist 1469 | resource 1470 | response 1471 | result 1472 | retire 1473 | retreat 1474 | return 1475 | reunion 1476 | reveal 1477 | review 1478 | reward 1479 | rhythm 1480 | rib 1481 | ribbon 1482 | rice 1483 | rich 1484 | ride 1485 | ridge 1486 | rifle 1487 | right 1488 | rigid 1489 | ring 1490 | riot 1491 | ripple 1492 | risk 1493 | ritual 1494 | rival 1495 | river 1496 | road 1497 | roast 1498 | robot 1499 | robust 1500 | rocket 1501 | romance 1502 | roof 1503 | rookie 1504 | room 1505 | rose 1506 | rotate 1507 | rough 1508 | round 1509 | route 1510 | royal 1511 | rubber 1512 | rude 1513 | rug 1514 | rule 1515 | run 1516 | runway 1517 | rural 1518 | sad 1519 | saddle 1520 | sadness 1521 | safe 1522 | sail 1523 | salad 1524 | salmon 1525 | salon 1526 | salt 1527 | salute 1528 | same 1529 | sample 1530 | sand 1531 | satisfy 1532 | satoshi 1533 | sauce 1534 | sausage 1535 | save 1536 | say 1537 | scale 1538 | scan 1539 | scare 1540 | scatter 1541 | scene 1542 | scheme 1543 | school 1544 | science 1545 | scissors 1546 | scorpion 1547 | scout 1548 | scrap 1549 | screen 1550 | script 1551 | scrub 1552 | sea 1553 | search 1554 | season 1555 | seat 1556 | second 1557 | secret 1558 | section 1559 | security 1560 | seed 1561 | seek 1562 | segment 1563 | select 1564 | sell 1565 | seminar 1566 | senior 1567 | sense 1568 | sentence 1569 | series 1570 | service 1571 | session 1572 | settle 1573 | setup 1574 | seven 1575 | shadow 1576 | shaft 1577 | shallow 1578 | share 1579 | shed 1580 | shell 1581 | sheriff 1582 | shield 1583 | shift 1584 | shine 1585 | ship 1586 | shiver 1587 | shock 1588 | shoe 1589 | shoot 1590 | shop 1591 | short 1592 | shoulder 1593 | shove 1594 | shrimp 1595 | shrug 1596 | shuffle 1597 | shy 1598 | sibling 1599 | sick 1600 | side 1601 | siege 1602 | sight 1603 | sign 1604 | silent 1605 | silk 1606 | silly 1607 | silver 1608 | similar 1609 | simple 1610 | since 1611 | sing 1612 | siren 1613 | sister 1614 | situate 1615 | six 1616 | size 1617 | skate 1618 | sketch 1619 | ski 1620 | skill 1621 | skin 1622 | skirt 1623 | skull 1624 | slab 1625 | slam 1626 | sleep 1627 | slender 1628 | slice 1629 | slide 1630 | slight 1631 | slim 1632 | slogan 1633 | slot 1634 | slow 1635 | slush 1636 | small 1637 | smart 1638 | smile 1639 | smoke 1640 | smooth 1641 | snack 1642 | snake 1643 | snap 1644 | sniff 1645 | snow 1646 | soap 1647 | soccer 1648 | social 1649 | sock 1650 | soda 1651 | soft 1652 | solar 1653 | soldier 1654 | solid 1655 | solution 1656 | solve 1657 | someone 1658 | song 1659 | soon 1660 | sorry 1661 | sort 1662 | soul 1663 | sound 1664 | soup 1665 | source 1666 | south 1667 | space 1668 | spare 1669 | spatial 1670 | spawn 1671 | speak 1672 | special 1673 | speed 1674 | spell 1675 | spend 1676 | sphere 1677 | spice 1678 | spider 1679 | spike 1680 | spin 1681 | spirit 1682 | split 1683 | spoil 1684 | sponsor 1685 | spoon 1686 | sport 1687 | spot 1688 | spray 1689 | spread 1690 | spring 1691 | spy 1692 | square 1693 | squeeze 1694 | squirrel 1695 | stable 1696 | stadium 1697 | staff 1698 | stage 1699 | stairs 1700 | stamp 1701 | stand 1702 | start 1703 | state 1704 | stay 1705 | steak 1706 | steel 1707 | stem 1708 | step 1709 | stereo 1710 | stick 1711 | still 1712 | sting 1713 | stock 1714 | stomach 1715 | stone 1716 | stool 1717 | story 1718 | stove 1719 | strategy 1720 | street 1721 | strike 1722 | strong 1723 | struggle 1724 | student 1725 | stuff 1726 | stumble 1727 | style 1728 | subject 1729 | submit 1730 | subway 1731 | success 1732 | such 1733 | sudden 1734 | suffer 1735 | sugar 1736 | suggest 1737 | suit 1738 | summer 1739 | sun 1740 | sunny 1741 | sunset 1742 | super 1743 | supply 1744 | supreme 1745 | sure 1746 | surface 1747 | surge 1748 | surprise 1749 | surround 1750 | survey 1751 | suspect 1752 | sustain 1753 | swallow 1754 | swamp 1755 | swap 1756 | swarm 1757 | swear 1758 | sweet 1759 | swift 1760 | swim 1761 | swing 1762 | switch 1763 | sword 1764 | symbol 1765 | symptom 1766 | syrup 1767 | system 1768 | table 1769 | tackle 1770 | tag 1771 | tail 1772 | talent 1773 | talk 1774 | tank 1775 | tape 1776 | target 1777 | task 1778 | taste 1779 | tattoo 1780 | taxi 1781 | teach 1782 | team 1783 | tell 1784 | ten 1785 | tenant 1786 | tennis 1787 | tent 1788 | term 1789 | test 1790 | text 1791 | thank 1792 | that 1793 | theme 1794 | then 1795 | theory 1796 | there 1797 | they 1798 | thing 1799 | this 1800 | thought 1801 | three 1802 | thrive 1803 | throw 1804 | thumb 1805 | thunder 1806 | ticket 1807 | tide 1808 | tiger 1809 | tilt 1810 | timber 1811 | time 1812 | tiny 1813 | tip 1814 | tired 1815 | tissue 1816 | title 1817 | toast 1818 | tobacco 1819 | today 1820 | toddler 1821 | toe 1822 | together 1823 | toilet 1824 | token 1825 | tomato 1826 | tomorrow 1827 | tone 1828 | tongue 1829 | tonight 1830 | tool 1831 | tooth 1832 | top 1833 | topic 1834 | topple 1835 | torch 1836 | tornado 1837 | tortoise 1838 | toss 1839 | total 1840 | tourist 1841 | toward 1842 | tower 1843 | town 1844 | toy 1845 | track 1846 | trade 1847 | traffic 1848 | tragic 1849 | train 1850 | transfer 1851 | trap 1852 | trash 1853 | travel 1854 | tray 1855 | treat 1856 | tree 1857 | trend 1858 | trial 1859 | tribe 1860 | trick 1861 | trigger 1862 | trim 1863 | trip 1864 | trophy 1865 | trouble 1866 | truck 1867 | true 1868 | truly 1869 | trumpet 1870 | trust 1871 | truth 1872 | try 1873 | tube 1874 | tuition 1875 | tumble 1876 | tuna 1877 | tunnel 1878 | turkey 1879 | turn 1880 | turtle 1881 | twelve 1882 | twenty 1883 | twice 1884 | twin 1885 | twist 1886 | two 1887 | type 1888 | typical 1889 | ugly 1890 | umbrella 1891 | unable 1892 | unaware 1893 | uncle 1894 | uncover 1895 | under 1896 | undo 1897 | unfair 1898 | unfold 1899 | unhappy 1900 | uniform 1901 | unique 1902 | unit 1903 | universe 1904 | unknown 1905 | unlock 1906 | until 1907 | unusual 1908 | unveil 1909 | update 1910 | upgrade 1911 | uphold 1912 | upon 1913 | upper 1914 | upset 1915 | urban 1916 | urge 1917 | usage 1918 | use 1919 | used 1920 | useful 1921 | useless 1922 | usual 1923 | utility 1924 | vacant 1925 | vacuum 1926 | vague 1927 | valid 1928 | valley 1929 | valve 1930 | van 1931 | vanish 1932 | vapor 1933 | various 1934 | vast 1935 | vault 1936 | vehicle 1937 | velvet 1938 | vendor 1939 | venture 1940 | venue 1941 | verb 1942 | verify 1943 | version 1944 | very 1945 | vessel 1946 | veteran 1947 | viable 1948 | vibrant 1949 | vicious 1950 | victory 1951 | video 1952 | view 1953 | village 1954 | vintage 1955 | violin 1956 | virtual 1957 | virus 1958 | visa 1959 | visit 1960 | visual 1961 | vital 1962 | vivid 1963 | vocal 1964 | voice 1965 | void 1966 | volcano 1967 | volume 1968 | vote 1969 | voyage 1970 | wage 1971 | wagon 1972 | wait 1973 | walk 1974 | wall 1975 | walnut 1976 | want 1977 | warfare 1978 | warm 1979 | warrior 1980 | wash 1981 | wasp 1982 | waste 1983 | water 1984 | wave 1985 | way 1986 | wealth 1987 | weapon 1988 | wear 1989 | weasel 1990 | weather 1991 | web 1992 | wedding 1993 | weekend 1994 | weird 1995 | welcome 1996 | west 1997 | wet 1998 | whale 1999 | what 2000 | wheat 2001 | wheel 2002 | when 2003 | where 2004 | whip 2005 | whisper 2006 | wide 2007 | width 2008 | wife 2009 | wild 2010 | will 2011 | win 2012 | window 2013 | wine 2014 | wing 2015 | wink 2016 | winner 2017 | winter 2018 | wire 2019 | wisdom 2020 | wise 2021 | wish 2022 | witness 2023 | wolf 2024 | woman 2025 | wonder 2026 | wood 2027 | wool 2028 | word 2029 | work 2030 | world 2031 | worry 2032 | worth 2033 | wrap 2034 | wreck 2035 | wrestle 2036 | wrist 2037 | write 2038 | wrong 2039 | yard 2040 | year 2041 | yellow 2042 | you 2043 | young 2044 | youth 2045 | zebra 2046 | zero 2047 | zone 2048 | zoo 2049 | -------------------------------------------------------------------------------- /tgbox/tools.py: -------------------------------------------------------------------------------- 1 | """This module stores utils required by API.""" 2 | 3 | import logging 4 | 5 | from asyncio import ( 6 | iscoroutine, get_event_loop 7 | ) 8 | from copy import deepcopy 9 | from pprint import pformat 10 | from hashlib import sha256 11 | from random import Random 12 | 13 | from typing import ( 14 | BinaryIO, Optional, Dict, 15 | Generator, Union 16 | ) 17 | from subprocess import PIPE, run as subprocess_run 18 | 19 | from io import BytesIO 20 | from functools import partial 21 | 22 | from os import urandom, PathLike 23 | try: 24 | # Try to use Third-party Regex if installed 25 | from regex import search as re_search 26 | except ImportError: 27 | from re import search as re_search 28 | 29 | from platform import system as platform_system 30 | from pathlib import PureWindowsPath, Path 31 | 32 | from .errors import ( 33 | ConcatError, 34 | PreviewImpossible, 35 | DurationImpossible 36 | ) 37 | from . import defaults 38 | 39 | __all__ = [ 40 | 'prbg', 'anext', 41 | 'SearchFilter', 42 | 'OpenPretender', 43 | 'PackedAttributes', 44 | 'int_to_bytes', 45 | 'bytes_to_int', 46 | 'get_media_duration', 47 | 'make_media_preview', 48 | 'ppart_id_generator', 49 | 'make_general_path', 50 | 'guess_path_type', 51 | 'make_safe_file_path', 52 | 'make_file_fingerprint' 53 | ] 54 | logger = logging.getLogger(__name__) 55 | 56 | class _TypeList: 57 | """ 58 | This is small version of ``list()`` that 59 | checks type on ``.append(...)``. 60 | 61 | You can specify multiply types with 62 | ``tuple``, e.g: ``_TypeList((int, float))`` 63 | 64 | * The list will try to change value type if \ 65 | ``isinstance(value, type_) is False`` to \ 66 | ``value = type_(value)``. Otherwise ``TypeError``. 67 | """ 68 | def __init__(self, type_, *args): 69 | self.type = type_ if isinstance(type_, tuple) else (type_,) 70 | self.list = [self.__check_type(i) for i in args] 71 | 72 | def __bool__(self): 73 | return bool(self.list) 74 | 75 | def __len__(self): 76 | return len(self.list) 77 | 78 | def __iter__(self): 79 | for i in self.list: 80 | yield i 81 | 82 | def __getitem__(self, sl): 83 | return self.list[sl] 84 | 85 | def __repr__(self) -> str: 86 | return f'_TypeList({self.type}, *{self.list})' 87 | 88 | def __check_type(self, value): 89 | for type_ in self.type: 90 | if isinstance(value, type_): 91 | return value 92 | 93 | for type_ in self.type: 94 | try: 95 | if isinstance(b'', type_): 96 | # bytes(str) doesn't work 97 | return type_(value,'utf-8') 98 | else: 99 | return type_(value) 100 | except: 101 | pass 102 | 103 | raise TypeError(f'Invalid type! Expected {self.type}, got {type(value)}') 104 | 105 | def append(self, value): 106 | self.list.append(self.__check_type(value)) 107 | 108 | def extend(self, list): 109 | self.list.extend([self.__check_type(i) for i in list]) 110 | 111 | def clear(self): 112 | self.list.clear() 113 | 114 | class SearchFilter: 115 | """ 116 | Container that filters search in ``DecryptedRemoteBox`` or 117 | ``DecryptedLocalBox``. 118 | 119 | The ``SearchFilter`` has **two** filters: the **Include** 120 | and **Exclude**. On search, all matching to **include** 121 | files will be **yielded**, while all matching to 122 | **exclude** will be **not yielded** (ignored). 123 | 124 | * The ``tgbox.tools.search_generator`` will firstly check for \ 125 | **include** filters, so its priority higher. 126 | 127 | * The ``tgbox.tools.search_generator`` will yield files that \ 128 | match **all** of filters, **not** one of it. 129 | 130 | * The ``SearchFilter`` accepts ``list`` as kwargs \ 131 | value. You can ``SearchFilter(id=[3,5,10])``. 132 | 133 | * The ``SearchFilter(**kwargs)`` will add all filters \ 134 | to the **include**. Also use ``SearchFilter.include(...)`` \ 135 | & ``SearchFilter.exclude(...)`` methods after initialization. 136 | 137 | All filters: 138 | * **scope** *str*: Define a path as search scope: 139 | The *scope* is an absolute directory in which 140 | we will search your file by other *filters*. By 141 | default, the ``tgbox.api.utils.search_generator`` 142 | will search over the entire *LocalBox*. This can 143 | be slow if you're have too many files. 144 | 145 | **Example**: let's imagine that You're a Linux user which 146 | share it's *Box* with the Windows user. In this case, 147 | Your *LocalBox* will contain a path parts on the 148 | ``'/'`` (Linux) and ``'C:\\'`` (Windows) roots. If You 149 | know that some file was uploaded by Your friend, 150 | then You can specify a ``scope='C:\\'`` to ignore 151 | all files uploaded from the Linux machine. This 152 | will significantly fasten the search process, 153 | because almost all filters require to select 154 | row from the LocalBox DB, decrypt Metadata and 155 | compare its values with ones from ``SearchFilter``. 156 | 157 | | !: The ``scope`` will be ignored on *RemoteBox* search. 158 | | !: The ``min_id`` & ``max_id`` will be ignored if ``scope`` used. 159 | 160 | * **id** *integer*: File ID 161 | 162 | * **cattrs** *dict*: File CustomAttributes: 163 | To search for CATTRS you need to specify a dict. 164 | 165 | E.g: If *file* ``cattrs={b'comment': b'hi!'}``, then 166 | *filter* ``cattrs={b'comment': b'h'}`` will match. 167 | 168 | By default, ``tgbox.api.utils.search_generator`` will 169 | use an ``in``, like ``b'h' in b'hi!'``, but you 170 | can set a ``re`` flag to use regular expressions, 171 | so *filter* ``cattrs={b'comment': b'hi(.)'}`` will match. 172 | 173 | * **file_path** *pathlib.Path*, *str* 174 | * **file_name** *str*: File name 175 | * **file_salt** *bytes/str*: File salt 176 | * **verbyte** *bytes*: File version byte 177 | * **mime** *str*: File mime type 178 | 179 | * **sender** *integer/str*: File sender name or ID: 180 | Only works on RemoteBox files and only on 181 | RemoteBox that enabled Sign Messages (& 182 | Show Author Profiles, optionally for IDs), 183 | otherwise will never return files. 184 | 185 | This filter can be Sender name or Sender ID, 186 | e.g sender='black sabbath' or sender=36265675; 187 | if 'sender' is string but .isnumeric(), Generator 188 | will also convert it to int and check against 189 | ID (if available), essentially, sender=36265675 190 | and sender='36265675' both valid, but latter 191 | will also check '36265675' in name. 192 | 193 | * **minor_version** *integer*: File minor version 194 | 195 | * **min_id** *integer*: File ID should be > min_id 196 | * **max_id** *integer*: File ID should be < max_id 197 | 198 | * **min_size** *integer*: File Size should be > min_size 199 | * **max_size** *integer*: File Size should be < max_size 200 | 201 | * **min_time** *integer/float*: Upload Time should be > min_time 202 | * **max_time** *integer/float*: Upload Time should be < max_time 203 | 204 | * **re** *bool*: re_search for every ``bytes`` 205 | * **imported** *bool*: Yield only imported files 206 | * **non_recursive_scope** *bool*: Disable recursive scope search 207 | """ 208 | def __init__(self, **kwargs): 209 | self.in_filters = { 210 | 'scope': _TypeList(str), 211 | 'cattrs': _TypeList(dict), 212 | 'file_path': _TypeList(str), 213 | 'file_name': _TypeList(str), 214 | 'file_salt': _TypeList((bytes,str)), 215 | 'verbyte': _TypeList(bytes), 216 | 'id': _TypeList(int), 217 | 'min_id': _TypeList(int), 218 | 'max_id': _TypeList(int), 219 | 'min_size': _TypeList(int), 220 | 'max_size': _TypeList(int), 221 | 'min_time': _TypeList((int,float)), 222 | 'max_time': _TypeList((int,float)), 223 | 'mime': _TypeList(str), 224 | 'sender': _TypeList((str,int)), 225 | 'imported': _TypeList(bool), 226 | 're': _TypeList(bool), 227 | 'minor_version': _TypeList(int), 228 | 'non_recursive_scope': _TypeList(bool), 229 | } 230 | self.ex_filters = deepcopy(self.in_filters) 231 | self.include(**kwargs) 232 | 233 | def __repr__(self) -> str: 234 | return pformat({ 235 | 'include': self.in_filters, 236 | 'exclude': self.ex_filters 237 | }) 238 | def include(self, **kwargs) -> 'SearchFilter': 239 | """Will extend included filters""" 240 | for k,v in kwargs.items(): 241 | if isinstance(v, list): 242 | self.in_filters[k].extend(v) 243 | else: 244 | self.in_filters[k].append(v) 245 | return self 246 | 247 | def exclude(self, **kwargs) -> 'SearchFilter': 248 | """Will extend excluded filters""" 249 | for k,v in kwargs.items(): 250 | if isinstance(v, list): 251 | self.ex_filters[k].extend(v) 252 | else: 253 | self.ex_filters[k].append(v) 254 | return self 255 | 256 | class PackedAttributes: 257 | """ 258 | This class is used to pack items to 259 | bytestring. We use it to pack file 260 | metadata as well as pack a user's 261 | custom attributes (cattrs), then 262 | encrypt it and attach to RBFile. 263 | 264 | We store key/value length in 3 bytes, 265 | so the max key/value length is 256^3-1. 266 | 267 | keyvalue<...> 268 | 269 | Every key/value will be randomly 270 | shuffled on the packing process. 271 | """ 272 | @staticmethod 273 | def pack( 274 | *, random_seed: Optional[bytes] = None, 275 | protected_keys: Optional[tuple] = None, 276 | **kwargs) -> bytes: 277 | """ 278 | Will make bytestring from your kwargs. 279 | Any kwarg **always** must be ``bytes``. 280 | 281 | ``make(x=5)`` will not work; 282 | ``make(x=b'\x05')`` is correct. 283 | 284 | We shuffle all key/value before packing, so 285 | you can specify ``random_seed``. Otherwise, 286 | ``urandom(32)`` will be used instead. 287 | 288 | Keys specified in the ``protect_key`` 289 | tuple will *never* be in the start or 290 | or in the end of the packed bytestring. 291 | 292 | ``protect_key`` is for internal usage. 293 | """ 294 | # We randomize all keys before packing 295 | random = Random(random_seed or urandom(32)) 296 | 297 | rnd_keys = list(kwargs.keys()) 298 | random.shuffle(rnd_keys) 299 | 300 | for pkey in protected_keys or (): 301 | _check = ( 302 | rnd_keys.index(pkey) == 0, # Start 303 | rnd_keys.index(pkey) == (len(rnd_keys) - 1) # End 304 | ) 305 | if any(_check): 306 | rnd_keys.pop(rnd_keys.index(pkey)) 307 | rnd_keys.insert(len(rnd_keys) // 2, pkey) 308 | 309 | pattr = bytes([0xFF]) 310 | for k in rnd_keys: 311 | if not isinstance(kwargs[k], bytes): 312 | raise TypeError('Values must be bytes') 313 | 314 | pattr += int_to_bytes(len(k), 3) + k.encode() 315 | pattr += int_to_bytes(len(kwargs[k]), 3) + kwargs[k] 316 | return pattr 317 | 318 | @staticmethod 319 | def unpack(pattr: bytes) -> Dict[str, bytes]: 320 | """ 321 | Will parse PackedAttributes.pack 322 | bytestring and convert it to the 323 | python dictionary. 324 | 325 | Every PackedAttributes bytestring 326 | must contain ``0xFF`` as first byte. 327 | If not, or if error, will return ``{}``. 328 | """ 329 | if not pattr: 330 | return {} 331 | try: 332 | assert pattr[0] == 0xFF 333 | pattr_d, pattr = {}, pattr[1:] 334 | 335 | while pattr: 336 | key_len = bytes_to_int(pattr[:3]) 337 | key = pattr[3:key_len+3] 338 | 339 | value_len = bytes_to_int(pattr[key_len+3:key_len+6]) 340 | pattr = pattr[key_len+6:] 341 | 342 | if value_len: 343 | value = pattr[:value_len] 344 | pattr = pattr[value_len:] 345 | else: 346 | value = b'' 347 | 348 | pattr_d[key.decode()] = value 349 | 350 | return pattr_d 351 | except: 352 | return {} 353 | 354 | class OpenPretender: 355 | """ 356 | Class to wrap Tgbox AES Generators and make it look 357 | like opened to "rb"-read file. 358 | """ 359 | def __init__( 360 | self, flo: BinaryIO, 361 | aes_state: 'tgbox.crypto.AESwState', 362 | hmac_state: 'hashlib.HMAC', 363 | file_size: int 364 | ): 365 | """ 366 | Arguments: 367 | flo (``BinaryIO``): 368 | File-like object. Like ``open('file','rb')``. 369 | 370 | aes_state (``AESwState``): 371 | ``AESwState`` with Key and IV. 372 | 373 | hmac_state (``hmac.HMAC``): 374 | ``HMAC`` initialized with ``HMACKey`` 375 | 376 | file_size (``int``): 377 | File size of ``flo``. 378 | """ 379 | self._aes_state = aes_state 380 | self._hmac_state = hmac_state 381 | self._flo = flo 382 | 383 | self._file_size = file_size 384 | self._current_size = file_size 385 | 386 | self._buffered_bytes = b'' 387 | self._stop_iteration = False 388 | 389 | self._hmac_returned = False 390 | self._padding_added = False 391 | 392 | self._concated_metadata_size = None 393 | self._position = 0 394 | 395 | def __repr__(self): 396 | return ( 397 | f'' 399 | ) 400 | def __str__(self): 401 | return ( 402 | f' # {self._position=}, {len(self._buffered_bytes)=}' 404 | ) 405 | def concat_metadata(self, metadata: bytes) -> None: 406 | """Concates metadata to the file as (metadata + file).""" 407 | if self._position: 408 | raise ConcatError('Concat must be before any usage of object.') 409 | else: 410 | self._buffered_bytes += metadata 411 | self._concated_metadata_size = len(metadata) 412 | 413 | def get_expected_size(self): 414 | """ 415 | Returns expected actual Telegram document size after 416 | upload. We use it in ``push_file()`` 417 | """ 418 | if not self._concated_metadata_size: 419 | raise Exception('You need to concat metadata firstly') 420 | # self._file_size already include size of Metadata, but 421 | # we need to calculate size of encrypted File *with* 422 | # padding, so firstly we are required to subtract 423 | # self._concated_metadata_size from self._file_size 424 | # for correct calculation. 32 here is HMAC blob 425 | expected = self._file_size - self._concated_metadata_size 426 | expected = (expected + (16 - expected % 16)) + 32 427 | return expected + self._concated_metadata_size 428 | 429 | async def read(self, size: int=-1) -> bytes: 430 | """ 431 | Returns ``size`` bytes from async Generator. 432 | 433 | This method is async only because we use 434 | ``File`` uploading from the async library. You 435 | can use ``tgbox.sync`` in your sync code for reading. 436 | 437 | Arguments: 438 | size (``int``): 439 | Amount of bytes to return. By default is 440 | negative (return all). Must be divisible 441 | by 16, and >= 64. 442 | """ 443 | if self._stop_iteration: 444 | raise Exception('Stream was closed') 445 | 446 | if (size % 16 or size < 64) and not size == -1: 447 | raise ValueError( 448 | 'size must be -1 (return all), or >= 64, divisible by 16' 449 | ) 450 | if self._current_size is None: 451 | self._current_size = self._flo.seek(0,2) # Move to file end 452 | self._flo.seek(0,0) # Move to file start 453 | self._current_size += len(self._buffered_bytes) 454 | 455 | self._current_size -= size 456 | 457 | if size < 0 or self._current_size < 0: 458 | self._current_size = 0 459 | 460 | if (self._current_size == 0 and self._padding_added)\ 461 | or (size <= len(self._buffered_bytes) and size != -1): 462 | if self._hmac_returned: 463 | return b'' 464 | 465 | elif self._current_size == 0: 466 | if len(self._buffered_bytes) + 32 < size: # + 32 is HMAC 467 | block = self._buffered_bytes + self._hmac_state.digest() 468 | self._buffered_bytes = b'' 469 | self._hmac_returned = True 470 | else: 471 | block = self._buffered_bytes[:size] 472 | self._buffered_bytes = self._buffered_bytes[size:] 473 | else: 474 | block = self._buffered_bytes[:size] 475 | self._buffered_bytes = self._buffered_bytes[size:] 476 | else: 477 | buffered = self._buffered_bytes 478 | self._buffered_bytes = b'' 479 | 480 | if size == -1: 481 | chunk = self._flo.read() 482 | chunk = await chunk if iscoroutine(chunk) else chunk 483 | 484 | logger.debug(f'Trying to read all bytes, got {len(chunk)=}') 485 | 486 | self._hmac_state.update(chunk) 487 | 488 | block = buffered + self._aes_state.encrypt( 489 | chunk, pad=True, concat_iv=False) 490 | 491 | block += self._hmac_state.digest() 492 | self._hmac_returned = True 493 | self._padding_added = True 494 | else: 495 | chunk = self._flo.read(size) 496 | chunk = await chunk if iscoroutine(chunk) else chunk 497 | 498 | logger.debug(f'Trying to read {size=}, got {len(chunk)=}') 499 | 500 | self._hmac_state.update(chunk) 501 | 502 | if len(chunk) < size: 503 | chunk = buffered + self._aes_state.encrypt( 504 | chunk, pad=True, concat_iv=False) 505 | 506 | self._padding_added = True 507 | else: 508 | chunk = buffered + self._aes_state.encrypt( 509 | chunk, pad=False, concat_iv=False) 510 | 511 | if len(chunk) > size: 512 | shift = size 513 | else: 514 | shift = None 515 | 516 | if shift is not None: 517 | self._buffered_bytes = chunk[shift:] 518 | 519 | block = chunk[:shift] 520 | 521 | logger.debug(f'Return {len(block)=}; {self._current_size=}') 522 | 523 | self._position += len(block) 524 | return block 525 | 526 | def tell(self) -> int: 527 | return self._position 528 | 529 | def seekable(self, *args, **kwargs) -> bool: # pylint: disable=unused-argument 530 | return False 531 | 532 | def close(self) -> None: 533 | self._stop_iteration = True 534 | 535 | def pad_request_size(request_size: int, bsize: int=4096) -> int: 536 | """ 537 | This function pads ``request_size`` to divisible 538 | by BSIZE bytes. If ``request_size`` < BSIZE, then 539 | it's not padded. This function designed for Telethon's 540 | ``GetFileRequest``. See issue #3 on TGBOX GitHub. 541 | 542 | .. note:: 543 | You will need to strip extra bytes from result, as 544 | request_size can be bigger than bytecount you want. 545 | 546 | Arguments: 547 | request_size (``int``): 548 | Amount of requested bytes, 549 | max is 1048576 (1MiB). 550 | 551 | bsize (``int``, optional): 552 | Size of block. Typically we 553 | don't need to change this. 554 | """ 555 | assert request_size <= 1048576, 'Max 1MiB' 556 | 557 | if request_size <= bsize: 558 | return request_size 559 | 560 | while 1048576 % request_size: 561 | request_size = ((request_size + bsize) // bsize) * bsize 562 | return request_size 563 | 564 | def ppart_id_generator(path: Path, mainkey: 'MainKey') -> Generator[tuple, None, None]: 565 | """ 566 | This generator will iterate over path parts and 567 | yield their unique IDs. We will use this to better 568 | navigate over *abstract* Folders in the LocalBox. 569 | 570 | The path **shouldn't** contain a file name, 571 | otherwise directory will contain it as folder. 572 | 573 | */home/user/* is **OK** 574 | */home/user/file.txt* is **NOT** 575 | 576 | Will yield a tuple (PART, PARENT_PART_ID, PART_ID) 577 | """ 578 | path = make_general_path(path) # Convert Win-like paths to UNIX-like 579 | parent_part_id = b'' # The root (/ anchor) doesn't have parent 580 | 581 | for part in path.parts: 582 | part_id = sha256( 583 | mainkey\ 584 | + sha256(part.encode()).digest()\ 585 | + parent_part_id 586 | ) 587 | yield (part, parent_part_id, part_id.digest()) 588 | parent_part_id = part_id.digest() 589 | 590 | def prbg(size: int) -> bytes: 591 | """Will generate ``size`` pseudo-random bytes.""" 592 | random = Random() 593 | return bytes([random.randrange(256) for _ in range(size)]) 594 | 595 | def int_to_bytes( 596 | int_: int, length: Optional[int] = None, 597 | signed: Optional[bool] = False) -> bytes: 598 | 599 | """Converts int to bytes with Big byteorder.""" 600 | 601 | bit_length = int_.bit_length() 602 | 603 | if not length: 604 | if signed and not (int_ >= -128 and int_ <= 127): 605 | divide_with = 16 606 | else: 607 | divide_with = 8 608 | 609 | bit_length = ((bit_length + divide_with) // divide_with) 610 | length = (bit_length * divide_with) // 8 611 | 612 | return int.to_bytes(int_, length, 'big', signed=signed) 613 | 614 | def bytes_to_int(bytes_: bytes, signed: Optional[bool] = False) -> int: 615 | """Converts bytes to int with Big byteorder.""" 616 | return int.from_bytes(bytes_, 'big', signed=signed) 617 | 618 | def guess_path_type(path: Union[str, Path]) -> str: 619 | """ 620 | This function will try to guess file path 621 | type. It can be Windows-like or Unix-like 622 | 623 | Returns 'windows' or 'unix' 624 | """ 625 | path = path if isinstance(path, str) else str(path) 626 | 627 | # If path has Letter drive (i.e C:) then 628 | # it's definitely a Windows-like path 629 | if PureWindowsPath(path).drive: 630 | return 'windows' 631 | 632 | # If user specified 'path' is the same as converted 633 | # to pathlib.Path (contains only one part) it means 634 | # that it's most probably a Windows path that 635 | # doesn't have drive letter. 636 | if str(path) == Path(path).parts[0]\ 637 | and str(path) not in ('/', '//'): 638 | return 'windows' 639 | 640 | return 'unix' 641 | 642 | def make_general_path(path: Union[str, Path]) -> Path: 643 | """ 644 | This function will make a valid 645 | UNIX-like Path from the Windows-like 646 | on the UNIX-like systems. 647 | """ 648 | # Windows can support UNIX-like paths 649 | if platform_system().lower() == 'windows': 650 | return Path(path) if isinstance(path, str) else path 651 | 652 | if guess_path_type(path) == 'windows': 653 | return Path(*PureWindowsPath(path).parts) 654 | 655 | return Path(path) 656 | 657 | def make_safe_file_path(path: Union[str, Path]) -> Path: 658 | """ 659 | This function will make a safe file path (a 660 | file path that can be easily inserted into 661 | another path). This is mostly for internal 662 | purposes, i.e ``DecryptedRemoteBox.download()`` 663 | 664 | This function will make a 665 | @/home/non/test from /home/non/test 666 | C\\Users\\non\\test from C:\\Users\\non\\test 667 | 668 | ...so this path can be easily inserted into 669 | another, i.e DownloadsTGBOX/@/home/non/test 670 | 671 | ``path`` *must* be absolute. 672 | """ 673 | path_type = guess_path_type(path) 674 | path = make_general_path(path) 675 | 676 | if path_type == 'unix': 677 | # /home/non -> @/home/non 678 | if str(path)[0] == '/': 679 | return Path(str(path).replace('/','@/',1)) 680 | else: 681 | return Path(str(path).replace('\\','@\\',1)) 682 | 683 | elif path_type == 'windows': 684 | if len(path.parts) < 2: 685 | return path 686 | # C:\Users\user -> C\Users\User 687 | drive_letter = path.parts[0][0] 688 | return Path(drive_letter, *path.parts[1:]) 689 | 690 | def make_file_fingerprint(mainkey: 'MainKey', file_path: Union[str, Path]) -> bytes: 691 | """ 692 | Function to make a file Fingerprint. 693 | 694 | Fingerprint is a SHA256 over ``mainkey`` and 695 | ``file_path`` parts, not a hash of a file. We 696 | use it to check if prepared file is unique 697 | or not (and raise error if Box already have 698 | some file with the same name and path). 699 | 700 | Arguments: 701 | mainkey (``MainKey``): 702 | The ``MainKey`` of your *Box* 703 | 704 | file_path (``Union[str, Path]``): 705 | A file path from which we will make a 706 | Fingerprint. It **should** include a 707 | file name! 708 | 709 | /home/user/ (directory) is NOT OK! 710 | /home/user/file.txt (file) is OK! 711 | """ 712 | file_path = make_general_path(file_path) 713 | fingerprint = sha256() 714 | # Here we update fingerprint SHA256 by path 715 | # parts plus b'/' symbol except the last 716 | # path part. This way we will guarantee 717 | # that Fingerprints would be the same 718 | # on all platforms and at least will not 719 | # break old f-prints on Linux, as it's 720 | # currently main system tgbox is used 721 | # on (as I believe) [changed in the v1.6] 722 | path_parts = list(file_path.parts) 723 | while path_parts: 724 | part = path_parts.pop(0).encode() 725 | 726 | # We also don't concat a '/' to '/' 727 | if path_parts and part != b'/': 728 | part += b'/' 729 | 730 | fingerprint.update(part) 731 | 732 | fingerprint.update(mainkey.key) 733 | return fingerprint.digest() 734 | 735 | async def anext(aiterator, default=...): 736 | """Analogue to Python 3.10+ anext()""" 737 | try: 738 | return await aiterator.__anext__() 739 | except StopAsyncIteration as e: 740 | if default is not Ellipsis: 741 | return default 742 | raise e 743 | 744 | async def get_media_duration(file_path: str) -> int: 745 | """Returns video/audio duration with ffmpeg in seconds.""" 746 | func = partial(subprocess_run, 747 | args=[defaults.FFMPEG, '-i', file_path], 748 | stdout=None, stderr=PIPE 749 | ) 750 | try: 751 | loop = get_event_loop() 752 | stderr = (await loop.run_in_executor(None, func)).stderr 753 | duration = re_search(b'Duration: (.)+,', stderr).group() 754 | d = duration.decode().split('.')[0].split(': ')[1].split(':') 755 | return int(d[0]) * 60**2 + int(d[1]) * 60 + int(d[2]) 756 | except Exception as e: 757 | raise DurationImpossible(f'Can\'t get media duration: {e}') from e 758 | 759 | async def make_media_preview(file_path: PathLike, x: int=128, y: int=-1) -> BinaryIO: 760 | """ 761 | Makes x:y sized thumbnail of the video/audio 762 | with ffmpeg. "-1" preserves one of side size. 763 | """ 764 | sp_func = partial(subprocess_run, 765 | args=[ 766 | defaults.FFMPEG, '-i', file_path, '-frames:v', '1', '-filter:v', 767 | f'scale={x}:{y}', '-an', '-loglevel', 'quiet', '-q:v', '2', '-f', 768 | 'mjpeg', 'pipe:1' 769 | ], 770 | capture_output = True 771 | ) 772 | loop = get_event_loop() 773 | try: 774 | sp_result = await loop.run_in_executor(None, sp_func) 775 | assert sp_result.stdout, 'Preview bytes is empty' 776 | return BytesIO(sp_result.stdout) 777 | except Exception as e: 778 | raise PreviewImpossible(f'Can\'t make thumbnail: {e}') from e 779 | -------------------------------------------------------------------------------- /tgbox/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '1.7.0b0' 2 | --------------------------------------------------------------------------------