├── .github └── workflows │ └── lint.yml ├── LICENSE ├── README.md ├── TODO ├── _skeleton.py ├── admin_tools.py ├── afk.py ├── autoprofile.py ├── b_emoji.py ├── forward.py ├── full.txt ├── google.py ├── info.py ├── insult.py ├── lmgtfy.py ├── lydia.py ├── lyrics.py ├── manifest.txt ├── minimal.txt ├── misc.py ├── mock.py ├── none.txt ├── nopm.py ├── notes.py ├── purge.py ├── quicktype.py ├── quotes.py ├── recentactions.py ├── setup.cfg ├── setup_for_development.sh ├── spam.py ├── speedtest.py ├── stickers.py ├── terminal.py ├── transfersh.py ├── translate.py ├── tts.py ├── typer.py ├── urbandictionary.py ├── userinfo.py ├── weather.py ├── xda.py └── yesno.py /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 5 11 | matrix: 12 | python-version: [3.6, 3.7, 3.8] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v1 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r https://raw.githubusercontent.com/friendly-telegram/friendly-telegram/master/requirements.txt 24 | pip install flake8 flake8-print flake8-quotes 25 | - name: Check for showstoppers 26 | run: | 27 | # stop the build if there are Python syntax errors or undefined names 28 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 29 | - name: Lint for typos 30 | run: | 31 | # exit-zero treats all errors as warnings. 32 | flake8 . --count --show-source --statistics 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modules-repo 2 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | insult 2 | lydia 3 | lyrics 4 | misc 5 | quicktype 6 | recentactions 7 | speedtest 8 | stickers 9 | terminal 10 | transfersh 11 | translate 12 | tts 13 | urbandictionary 14 | userinfo 15 | weather 16 | xda 17 | yesno 18 | 19 | -------------------------------------------------------------------------------- /_skeleton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | 21 | from .. import loader, utils 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def register(cb): 27 | cb(YourMod()) 28 | 29 | 30 | class YourMod(loader.Module): 31 | """Description for module""" 32 | def __init__(self): 33 | self.config = loader.ModuleConfig("CONFIG_STRING", _("hello"), 34 | "This is what is said, you can edit me with the configurator") 35 | self.name = _("A Name") 36 | 37 | async def examplecmd(self, message): 38 | """Does something when you type .example""" 39 | logger.debug("We logged something!") 40 | await utils.answer(message, self.config["CONFIG_STRING"]) 41 | -------------------------------------------------------------------------------- /admin_tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | from .. import loader, utils 20 | import logging 21 | 22 | from telethon.tl.types import ChatAdminRights, ChatBannedRights, PeerUser, PeerChannel 23 | from telethon.errors import BadRequestError 24 | from telethon.tl.functions.channels import EditAdminRequest, EditBannedRequest 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def register(cb): 30 | cb(BanMod()) 31 | 32 | 33 | @loader.tds 34 | class BanMod(loader.Module): 35 | """Group administration tasks""" 36 | strings = {"name": "Administration", 37 | "ban_not_supergroup": "I can't ban someone unless they're in a supergroup!", 38 | "unban_not_supergroup": "I can't unban someone unless they're banned from a supergroup!", 39 | "kick_not_group": "I can't kick someone unless they're in a group!", 40 | "ban_none": "I can't ban no-one, can I?", 41 | "unban_none": "I need someone to unbanned here.", 42 | "kick_none": "I need someone to be kicked out of the chat.", 43 | "promote_none": "I can't promote no one, can I?", 44 | "demote_none": "I can't demote no one, can I?", 45 | "who": "Who the hell is that?", 46 | "not_admin": "Am I an admin here?", 47 | "banned": "Banned {} from the chat!", 48 | "unbanned": "Unbanned {} from the chat!", 49 | "kicked": "Kicked {} from the chat!", 50 | "promoted": "{} is now powered with admin rights!", 51 | "demoted": "{} is now stripped off of their admin rights!"} 52 | 53 | def __init__(self): 54 | self.name = self.strings["name"] 55 | 56 | async def bancmd(self, message): 57 | """Ban the user from the group""" 58 | if not isinstance(message.to_id, PeerChannel): 59 | return await utils.answer(message, self.strings["not_supergroup"]) 60 | if message.is_reply: 61 | user = await utils.get_user(await message.get_reply_message()) 62 | else: 63 | args = utils.get_args(message) 64 | if len(args) == 0: 65 | return await utils.answer(message, self.strings["ban_none"]) 66 | user = await self.client.get_entity(args[0]) 67 | if not user: 68 | return await utils.answer(message, self.strings["who"]) 69 | logger.debug(user) 70 | try: 71 | await self.client(EditBannedRequest(message.chat_id, user.id, 72 | ChatBannedRights(until_date=None, view_messages=True))) 73 | except BadRequestError: 74 | await utils.answer(message, self.strings["not_admin"]) 75 | else: 76 | await self.allmodules.log("ban", group=message.chat_id, affected_uids=[user.id]) 77 | await utils.answer(message, self.strings["banned"].format(utils.escape_html(ascii(user.first_name)))) 78 | 79 | async def unbancmd(self, message): 80 | """Lift the ban off the user.""" 81 | if not isinstance(message.to_id, PeerChannel): 82 | return await utils.answer(message, self.strings["unban_not_supergroup"]) 83 | if message.is_reply: 84 | user = await utils.get_user(await message.get_reply_message()) 85 | else: 86 | args = utils.get_args(message) 87 | if len(args) == 0: 88 | return await utils.answer(message, self.strings["unban_none"]) 89 | user = await self.client.get_entity(args[0]) 90 | if not user: 91 | return await utils.answer(message, self.strings["who"]) 92 | logger.debug(user) 93 | try: 94 | await self.client(EditBannedRequest(message.chat_id, user.id, 95 | ChatBannedRights(until_date=None, view_messages=False))) 96 | except BadRequestError: 97 | await utils.answer(message, self.strings["not_admin"]) 98 | else: 99 | await self.allmodules.log("unban", group=message.chat_id, affected_uids=[user.id]) 100 | await utils.answer(message, self.strings["unbanned"].format(utils.escape_html(ascii(user.first_name)))) 101 | 102 | async def kickcmd(self, message): 103 | """Kick the user out of the group""" 104 | if isinstance(message.to_id, PeerUser): 105 | return await utils.answer(message, self.strings["kick_not_group"]) 106 | if message.is_reply: 107 | user = await utils.get_user(await message.get_reply_message()) 108 | else: 109 | args = utils.get_args(message) 110 | if len(args) == 0: 111 | return await utils.answer(message, self.strings["kick_none"]) 112 | user = await self.client.get_entity(args[0]) 113 | if not user: 114 | return await utils.answer(message, self.strings["who"]) 115 | logger.debug(user) 116 | try: 117 | await self.client.kick_participant(message.chat_id, user.id) 118 | except BadRequestError: 119 | await utils.answer(message, self.strings["not_admin"]) 120 | else: 121 | await self.allmodules.log("kick", group=message.chat_id, affected_uids=[user.id]) 122 | await utils.answer(message, self.strings["kicked"].format(utils.escape_html(ascii(user.first_name)))) 123 | 124 | async def promotecmd(self, message): 125 | """Provides admin rights to the specified user.""" 126 | if message.is_reply: 127 | user = await utils.get_user(await message.get_reply_message()) 128 | else: 129 | args = utils.get_args(message) 130 | if len(args) == 0: 131 | return await utils.answer(message, self.strings["promote_none"]) 132 | user = await self.client.get_entity(args[0]) 133 | if not user: 134 | return await utils.answer(message, self.strings["who"]) 135 | logger.debug(user) 136 | try: 137 | await self.client(EditAdminRequest(message.chat_id, user.id, 138 | ChatAdminRights(post_messages=None, 139 | add_admins=None, 140 | invite_users=None, 141 | change_info=None, 142 | ban_users=None, 143 | delete_messages=True, 144 | pin_messages=True, 145 | edit_messages=None), "Admin")) 146 | except BadRequestError: 147 | await utils.answer(message, self.strings["not_admin"]) 148 | else: 149 | await self.allmodules.log("promote", group=message.chat_id, affected_uids=[user.id]) 150 | await utils.answer(message, self.strings["promoted"].format(utils.escape_html(ascii(user.first_name)))) 151 | 152 | async def demotecmd(self, message): 153 | """Removes admin rights of the specified group admin.""" 154 | if message.is_reply: 155 | user = await utils.get_user(await message.get_reply_message()) 156 | else: 157 | args = utils.get_args(message) 158 | if len(args) == 0: 159 | return await utils.answer(message, self.strings["demote_none"]) 160 | user = await self.client.get_entity(args[0]) 161 | if not user: 162 | return await utils.answer(message, self.strings["who"]) 163 | logger.debug(user) 164 | try: 165 | await self.client(EditAdminRequest(message.chat_id, user.id, 166 | ChatAdminRights(post_messages=None, 167 | add_admins=None, 168 | invite_users=None, 169 | change_info=None, 170 | ban_users=None, 171 | delete_messages=None, 172 | pin_messages=None, 173 | edit_messages=None), "Admin")) 174 | except BadRequestError: 175 | await utils.answer(message, self.strings["not_admin"]) 176 | else: 177 | await self.allmodules.log("demote", group=message.chat_id, affected_uids=[user.id]) 178 | await utils.answer(message, self.strings["demoted"].format(utils.escape_html(ascii(user.first_name)))) 179 | 180 | async def client_ready(self, client, db): 181 | self.client = client 182 | -------------------------------------------------------------------------------- /afk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | from .. import loader, utils 20 | 21 | import logging 22 | import datetime 23 | import time 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | def register(cb): 29 | cb(AFKMod()) 30 | 31 | 32 | @loader.tds 33 | class AFKMod(loader.Module): 34 | """Provides a message saying that you are unavailable""" 35 | strings = {"name": "AFK", 36 | "gone": "I'm goin' AFK", 37 | "back": "I'm no longer AFK", 38 | "afk": "I'm AFK right now (since {} ago).", 39 | "afk_reason": "I'm AFK right now (since {} ago).\nReason: {}"} 40 | 41 | def config_complete(self): 42 | self.name = self.strings["name"] 43 | 44 | async def client_ready(self, client, db): 45 | self._db = db 46 | self._me = await client.get_me() 47 | 48 | async def afkcmd(self, message): 49 | """.afk [message]""" 50 | if utils.get_args_raw(message): 51 | self._db.set(__name__, "afk", utils.get_args_raw(message)) 52 | else: 53 | self._db.set(__name__, "afk", True) 54 | self._db.set(__name__, "gone", time.time()) 55 | self._db.set(__name__, "ratelimit", []) 56 | await self.allmodules.log("afk", data=utils.get_args_raw(message) or None) 57 | await utils.answer(message, self.strings["gone"]) 58 | 59 | async def unafkcmd(self, message): 60 | """Remove the AFK status""" 61 | self._db.set(__name__, "afk", False) 62 | self._db.set(__name__, "gone", None) 63 | self._db.set(__name__, "ratelimit", []) 64 | await self.allmodules.log("unafk") 65 | await utils.answer(message, self.strings["back"]) 66 | 67 | async def watcher(self, message): 68 | if message.mentioned or getattr(message.to_id, "user_id", None) == self._me.id: 69 | logger.debug("tagged!") 70 | ratelimit = self._db.get(__name__, "ratelimit", []) 71 | if utils.get_chat_id(message) in ratelimit: 72 | return 73 | else: 74 | self._db.setdefault(__name__, {}).setdefault("ratelimit", []).append(utils.get_chat_id(message)) 75 | self._db.save() 76 | user = await utils.get_user(message) 77 | if user.is_self or user.bot or user.verified: 78 | logger.debug("User is self, bot or verified.") 79 | return 80 | if self.get_afk() is False: 81 | return 82 | now = datetime.datetime.now().replace(microsecond=0) 83 | gone = datetime.datetime.fromtimestamp(self._db.get(__name__, "gone")).replace(microsecond=0) 84 | diff = now - gone 85 | if self.get_afk() is True: 86 | ret = self.strings["afk"].format(diff) 87 | elif self.get_afk() is not False: 88 | ret = self.strings["afk_reason"].format(diff, self.get_afk()) 89 | await utils.answer(message, ret) 90 | 91 | def get_afk(self): 92 | return self._db.get(__name__, "afk", False) 93 | -------------------------------------------------------------------------------- /autoprofile.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import asyncio 18 | import ast 19 | import time 20 | import logging 21 | from io import BytesIO 22 | from telethon.tl import functions 23 | from .. import loader, utils 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | try: 28 | from PIL import Image 29 | except ImportError: 30 | pil_installed = False 31 | else: 32 | pil_installed = True 33 | 34 | 35 | def register(cb): 36 | cb(AutoProfileMod()) 37 | 38 | 39 | @loader.tds 40 | class AutoProfileMod(loader.Module): 41 | """Automatic stuff for your profile :P""" 42 | strings = {"name": "Automatic Profile", 43 | "missing_pil": "You don't have Pillow installed", 44 | "missing_pfp": "You don't have a profile picture to rotate", 45 | "invalid_args": "Missing parameters, please read the docs", 46 | "invalid_degrees": "Invalid number of degrees to rotate, please read the docs", 47 | "invalid_delete": "Please specify whether to delete the old pictures or not", 48 | "enabled_pfp": "Enabled profile picture rotation", 49 | "pfp_not_enabled": "Profile picture rotation is not enabled", 50 | "pfp_disabled": "Profile picture rotation disabled", 51 | "missing_time": "Time was not specified in bio", 52 | "enabled_bio": "Enabled bio clock", 53 | "bio_not_enabled": "Bio clock is not enabled", 54 | "disabled_bio": "Disabled bio clock", 55 | "enabled_name": "Enabled name clock", 56 | "name_not_enabled": "Name clock is not enabled", 57 | "disabled_name": "Name clock disabled", 58 | "how_many_pfps": "Please specify how many profile pictures should be removed", 59 | "invalid_pfp_count": "Invalid number of profile pictures to remove", 60 | "removed_pfps": "Removed {} profile pic(s)"} 61 | 62 | def __init__(self): 63 | self.bio_enabled = False 64 | self.name_enabled = False 65 | self.pfp_enabled = False 66 | self.raw_bio = None 67 | self.raw_name = None 68 | 69 | def config_complete(self): 70 | self.name = self.strings["name"] 71 | 72 | async def client_ready(self, client, db): 73 | self.client = client 74 | 75 | async def autopfpcmd(self, message): 76 | """Rotates your profile picture every 60 seconds with x degrees, usage: 77 | .autopfp 78 | 79 | Degrees - 60, -10, etc 80 | Remove last pfp - True/1/False/0, case sensitive""" 81 | 82 | if not pil_installed: 83 | return await utils.answer(message, self.strings["missing_pil"]) 84 | 85 | if not await self.client.get_profile_photos("me", limit=1): 86 | return await utils.answer(message, self.strings["missing_pfp"]) 87 | 88 | msg = utils.get_args(message) 89 | if len(msg) != 2: 90 | return await utils.answer(message, self.strings["invalid_args"]) 91 | 92 | try: 93 | degrees = int(msg[0]) 94 | except ValueError: 95 | return await utils.answer(message, self.strings["invalid_degrees"]) 96 | 97 | try: 98 | delete_previous = ast.literal_eval(msg[1]) 99 | except (ValueError, SyntaxError): 100 | return await utils.answer(message, self.strings["invalid_delete"]) 101 | 102 | with BytesIO() as pfp: 103 | await self.client.download_profile_photo("me", file=pfp) 104 | raw_pfp = Image.open(pfp) 105 | 106 | self.pfp_enabled = True 107 | pfp_degree = 0 108 | await self.allmodules.log("start_autopfp") 109 | await utils.answer(message, self.strings["enabled_pfp"]) 110 | 111 | while self.pfp_enabled: 112 | pfp_degree = (pfp_degree + degrees) % 360 113 | rotated = raw_pfp.rotate(pfp_degree) 114 | with BytesIO() as buf: 115 | rotated.save(buf, format="JPEG") 116 | buf.seek(0) 117 | 118 | if delete_previous: 119 | await self.client(functions.photos. 120 | DeletePhotosRequest(await self.client.get_profile_photos("me", limit=1))) 121 | 122 | await self.client(functions.photos.UploadProfilePhotoRequest(await self.client.upload_file(buf))) 123 | buf.close() 124 | await asyncio.sleep(60) 125 | 126 | async def stopautopfpcmd(self, message): 127 | """Stop autobio cmd.""" 128 | 129 | if self.pfp_enabled is False: 130 | return await utils.answer(message, self.strings["pfp_not_enabled"]) 131 | else: 132 | self.pfp_enabled = False 133 | 134 | await self.client(functions.photos.DeletePhotosRequest( 135 | await self.client.get_profile_photos("me", limit=1) 136 | )) 137 | await self.allmodules.log("stop_autopfp") 138 | await utils.answer(message, self.strings["pfp_disabled"]) 139 | 140 | async def autobiocmd(self, message): 141 | """Automatically changes your account's bio with current time, usage: 142 | .autobio ''""" 143 | 144 | msg = utils.get_args(message) 145 | if len(msg) != 1: 146 | return await utils.answer(message, self.strings["invalid_args"]) 147 | raw_bio = msg[0] 148 | if "{time}" not in raw_bio: 149 | return await utils.answer(message, self.strings["missing_time"]) 150 | 151 | self.bio_enabled = True 152 | self.raw_bio = raw_bio 153 | await self.allmodules.log("start_autobio") 154 | await utils.answer(message, self.strings["enabled_bio"]) 155 | 156 | while self.bio_enabled is True: 157 | current_time = time.strftime("%H:%M") 158 | bio = raw_bio.format(time=current_time) 159 | await self.client(functions.account.UpdateProfileRequest( 160 | about=bio 161 | )) 162 | await asyncio.sleep(60) 163 | 164 | async def stopautobiocmd(self, message): 165 | """Stop autobio cmd.""" 166 | 167 | if self.bio_enabled is False: 168 | return await utils.answer(message, self.strings["bio_not_enabled"]) 169 | else: 170 | self.bio_enabled = False 171 | await self.allmodules.log("stop_autobio") 172 | await utils.answer(message, self.strings["disabled_bio"]) 173 | await self.client(functions.account.UpdateProfileRequest(about=self.raw_bio.format(time=""))) 174 | 175 | async def autonamecmd(self, message): 176 | """Automatically changes your Telegram name with current time, usage: 177 | .autoname ''""" 178 | 179 | msg = utils.get_args(message) 180 | if len(msg) != 1: 181 | return await utils.answer(message, self.strings["invalid_args"]) 182 | raw_name = msg[0] 183 | if "{time}" not in raw_name: 184 | return await utils.answer(message, self.strings["missing_time"]) 185 | 186 | self.name_enabled = True 187 | self.raw_name = raw_name 188 | await self.allmodules.log("start_autoname") 189 | await utils.answer(message, self.strings["enabled_name"]) 190 | 191 | while self.name_enabled is True: 192 | current_time = time.strftime("%H:%M") 193 | name = raw_name.format(time=current_time) 194 | await self.client(functions.account.UpdateProfileRequest( 195 | first_name=name 196 | )) 197 | await asyncio.sleep(60) 198 | 199 | async def stopautonamecmd(self, message): 200 | """ Stop autoname cmd.""" 201 | 202 | if self.name_enabled is False: 203 | return await utils.answer(message, self.strings["name_not_enabled"]) 204 | else: 205 | self.name_enabled = False 206 | await self.allmodules.log("stop_autoname") 207 | await utils.answer(message, self.strings["disabled_name"]) 208 | await self.client(functions.account.UpdateProfileRequest( 209 | first_name=self.raw_name.format(time="") 210 | )) 211 | 212 | async def delpfpcmd(self, message): 213 | """ Remove x profile pic(s) from your profile. 214 | .delpfp """ 215 | 216 | args = utils.get_args(message) 217 | if not args: 218 | return await utils.answer(message, self.strings["how_many_pfps"]) 219 | if args[0].lower() == "unlimited": 220 | pfps_count = None 221 | else: 222 | try: 223 | pfps_count = int(args[0]) 224 | except ValueError: 225 | return await utils.answer(message, self.strings["invalid_pfp_count"]) 226 | if pfps_count <= 0: 227 | return await utils.answer(message, self.strings["invalid_pfp_count"]) 228 | 229 | await self.client(functions.photos.DeletePhotosRequest(await self.client.get_profile_photos("me", 230 | limit=pfps_count))) 231 | 232 | if pfps_count is None: 233 | pfps_count = _("all") 234 | await self.allmodules.log("delpfp") 235 | await utils.answer(message, self.strings["removed_pfps"].format(str(pfps_count))) 236 | -------------------------------------------------------------------------------- /b_emoji.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader, utils 18 | import logging 19 | import random 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def register(cb): 25 | cb(BEmojiMod()) 26 | 27 | 28 | @loader.tds 29 | class BEmojiMod(loader.Module): 30 | """🅱️-ifies things""" 31 | strings = {"name": "🅱️", 32 | "replacable_chars_cfg_doc": "Characters that can be replaced with 🅱️", 33 | "no_text": "There's nothing to 🅱️-ify"} 34 | 35 | def __init__(self): 36 | self.config = loader.ModuleConfig("REPLACABLE_CHARS", "bdfgpv", "Characters that can be replaced with 🅱️") 37 | 38 | def config_complete(self): 39 | self.name = self.strings["name"] 40 | 41 | async def bcmd(self, message): 42 | """Use in reply to another message or as .b """ 43 | if message.is_reply: 44 | text = (await message.get_reply_message()).message 45 | else: 46 | text = utils.get_args_raw(message.message) 47 | if text is None or len(text) == 0: 48 | await message.edit(self.strings["no_text"]) 49 | return 50 | text = list(text) 51 | n = 0 52 | for c in text: 53 | if c.lower() == c.upper(): 54 | n += 1 55 | continue 56 | if len(self.config["REPLACABLE_CHARS"]) == 0: 57 | if n % 2 == random.randint(0, 1): 58 | text[n] = "🅱️" 59 | else: 60 | text[n] = c 61 | else: 62 | if c.lower() in self.config["REPLACABLE_CHARS"]: 63 | text[n] = "🅱️" 64 | else: 65 | text[n] = c 66 | n += 1 67 | text = "".join(text) 68 | logger.debug(text) 69 | await utils.answer(message, text) 70 | -------------------------------------------------------------------------------- /forward.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | 21 | from .. import loader, utils 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def register(cb): 27 | cb(ForwardMod()) 28 | 29 | 30 | @loader.tds 31 | class ForwardMod(loader.Module): 32 | """Forwards messages""" 33 | strings = {"name": "Forwarding", 34 | "error": "Invalid chat to forward to", 35 | "done": "Forwarded all messages"} 36 | 37 | def config_complete(self): 38 | self.name = self.strings["name"] 39 | 40 | async def fwdallcmd(self, message): 41 | """.fwdall 42 | Forwards all messages in chat""" 43 | try: 44 | user = await message.client.get_input_entity(utils.get_args(message)[0]) 45 | except ValueError: 46 | await utils.answer(self.strings["error"]) 47 | msgs = [] 48 | async for msg in message.client.iter_messages( 49 | entity=message.to_id, 50 | reverse=True): 51 | msgs += [msg.id] 52 | if len(msgs) >= 100: 53 | logger.debug(msgs) 54 | await message.client.forward_messages(user, msgs, message.from_id) 55 | msgs = [] 56 | if len(msgs) > 0: 57 | logger.debug(msgs) 58 | await message.client.forward_messages(user, msgs, message.from_id) 59 | await utils.answer(message, self.strings["done"]) 60 | -------------------------------------------------------------------------------- /full.txt: -------------------------------------------------------------------------------- 1 | admin_tools 2 | afk 3 | autoprofile 4 | b_emoji 5 | forward 6 | google 7 | info 8 | insult 9 | lmgtfy 10 | lydia 11 | lyrics 12 | misc 13 | mock 14 | nopm 15 | notes 16 | purge 17 | quicktype 18 | quotes 19 | recentactions 20 | spam 21 | speedtest 22 | stickers 23 | terminal 24 | transfersh 25 | translate 26 | tts 27 | typer 28 | urbandictionary 29 | userinfo 30 | weather 31 | xda 32 | yesno 33 | -------------------------------------------------------------------------------- /google.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | 21 | from search_engine_parser import GoogleSearch 22 | 23 | from .. import loader, utils 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | def register(cb): 29 | cb(GoogleSearchMod()) 30 | 31 | 32 | @loader.tds 33 | class GoogleSearchMod(loader.Module): 34 | """Make a Google search, right in your chat!""" 35 | strings = {"name": "Google Search", 36 | "no_term": "I can't Google nothing", 37 | "no_results": "Could not find anything about {} on Google", 38 | "results": "These came back from a Google search for {}:\n\n", 39 | "result": "{}\n\n{}\n"} 40 | 41 | def config_complete(self): 42 | self.name = self.strings["name"] 43 | 44 | async def googlecmd(self, message): 45 | """Shows Google search results.""" 46 | text = utils.get_args_raw(message.message) 47 | if not text: 48 | text = (await message.get_reply_message()).message 49 | if not text: 50 | await utils.answer(message, self.strings["no_term"]) 51 | return 52 | # TODO: add ability to specify page number. 53 | gsearch = GoogleSearch() 54 | gresults = await gsearch.async_search(text, 1) 55 | if not gresults: 56 | await utils.answer(message, self.strings["no_results"].format(text)) 57 | return 58 | msg = "" 59 | results = zip(gresults["titles"], gresults["links"], gresults["descriptions"]) 60 | for result in results: 61 | msg += self.strings["result"].format(utils.escape_html(result[0]), utils.escape_html(result[1]), 62 | utils.escape_html(result[2])) 63 | await utils.answer(message, self.strings["results"].format(utils.escape_html(text)) + msg) 64 | -------------------------------------------------------------------------------- /info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | import platform 21 | import asyncio 22 | import shutil 23 | import sys 24 | 25 | import telethon 26 | 27 | from .. import loader, utils 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | def register(cb): 33 | cb(InfoMod()) 34 | 35 | 36 | @loader.tds 37 | class InfoMod(loader.Module): 38 | """Provides system information about the computer hosting this bot""" 39 | strings = {"name": "System Info", 40 | "info_title": "System Info", 41 | "kernel": "Kernel: {}", 42 | "arch": "Arch: {}", 43 | "os": "OS: {}", 44 | "distro": "Linux Distribution: {}", 45 | "android_sdk": "Android SDK: {}", 46 | "android_ver": "Android Version: {}", 47 | "android_patch": "Android Security Patch: {}", 48 | "unknown_distro": "Could not determine Linux distribution.", 49 | "python_version": "Python version: {}", 50 | "telethon_version": "Telethon version: {}"} 51 | 52 | def __init__(self): 53 | self.name = self.strings["name"] 54 | 55 | async def infocmd(self, message): 56 | """Shows system information""" 57 | reply = self.strings["info_title"] 58 | reply += "\n" + self.strings["kernel"].format(utils.escape_html(platform.release())) 59 | reply += "\n" + self.strings["arch"].format(utils.escape_html(platform.architecture()[0])) 60 | reply += "\n" + self.strings["os"].format(utils.escape_html(platform.system())) 61 | 62 | if platform.system() == "Linux": 63 | done = False 64 | try: 65 | a = open("/etc/os-release").readlines() 66 | b = {} 67 | for line in a: 68 | b[line.split("=")[0]] = line.split("=")[1].strip().strip("\"") 69 | reply += "\n" + self.strings["distro"].format(utils.escape_html(b["PRETTY_NAME"])) 70 | done = True 71 | except FileNotFoundError: 72 | getprop = shutil.which("getprop") 73 | if getprop is not None: 74 | sdk = await asyncio.create_subprocess_exec(getprop, "ro.build.version.sdk", 75 | stdout=asyncio.subprocess.PIPE) 76 | ver = await asyncio.create_subprocess_exec(getprop, "ro.build.version.release", 77 | stdout=asyncio.subprocess.PIPE) 78 | sec = await asyncio.create_subprocess_exec(getprop, "ro.build.version.security_patch", 79 | stdout=asyncio.subprocess.PIPE) 80 | sdks, unused = await sdk.communicate() 81 | vers, unused = await ver.communicate() 82 | secs, unused = await sec.communicate() 83 | if sdk.returncode == 0 and ver.returncode == 0 and sec.returncode == 0: 84 | reply += "\n" + self.strings["android_sdk"].format(sdks.decode("utf-8").strip()) 85 | reply += "\n" + self.strings["android_ver"].format(vers.decode("utf-8").strip()) 86 | reply += "\n" + self.strings["android_patch"].format(secs.decode("utf-8").strip()) 87 | done = True 88 | if not done: 89 | reply += "\n" + self.strings["unknown_distro"] 90 | reply += "\n" + self.strings["python_version"].format(utils.escape_html(sys.version)) 91 | reply += "\n" + self.strings["telethon_version"].format(utils.escape_html(telethon.__version__)) 92 | await utils.answer(message, reply) 93 | -------------------------------------------------------------------------------- /insult.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader 18 | 19 | import logging 20 | import random 21 | 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def register(cb): 27 | cb(InsultMod()) 28 | 29 | 30 | class InsultMod(loader.Module): 31 | """Shouts at people""" 32 | def __init__(self): 33 | self.name = _("Insulter") 34 | 35 | async def insultcmd(self, message): 36 | """Use when angry""" 37 | # TODO localisation? 38 | adjectives_start = ["salty", "fat", "fucking", "shitty", "stupid", "retarded", "self conscious", "tiny"] 39 | adjectives_mid = ["little", "vitamin D deficient", "idiotic", "incredibly stupid"] 40 | nouns = ["cunt", "pig", "pedophile", "beta male", "bottom", "retard", "ass licker", "cunt nugget", 41 | "PENIS", "dickhead", "flute", "idiot", "motherfucker", "loner", "creep"] 42 | starts = ["You're a", "You", "Fuck off you", "Actually die you", "Listen up you", 43 | "What the fuck is wrong with you, you"] 44 | ends = ["!!!!", "!", ""] 45 | start = random.choice(starts) 46 | adjective_start = random.choice(adjectives_start) 47 | adjective_mid = random.choice(adjectives_mid) 48 | noun = random.choice(nouns) 49 | end = random.choice(ends) 50 | insult = start + " " + adjective_start + " " + adjective_mid + (" " if adjective_mid else "") + noun + end 51 | logger.debug(insult) 52 | await message.edit(insult) 53 | -------------------------------------------------------------------------------- /lmgtfy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | from .. import loader, utils 20 | import logging 21 | import urllib 22 | import requests 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def register(cb): 28 | cb(LetMeGoogleThatForYou()) 29 | 30 | 31 | @loader.tds 32 | class LetMeGoogleThatForYou(loader.Module): 33 | """Let me Google that for you, coz you too lazy to do that yourself.""" 34 | strings = {"name": "LetMeGoogleThatForYou", 35 | "result": "Here you go, help yourself.\n{}", 36 | "default": "How to use Google?"} 37 | 38 | def __init__(self): 39 | self.name = self.strings["name"] 40 | 41 | async def lmgtfycmd(self, message): 42 | """Use in reply to another message or as .lmgtfy """ 43 | text = utils.get_args_raw(message) 44 | if not text: 45 | if message.is_reply: 46 | text = (await message.get_reply_message()).message 47 | else: 48 | text = self.strings["default"] 49 | query_encoded = urllib.parse.quote_plus(text) 50 | lmgtfy_url = f"http://lmgtfy.com/?s=g&iie=1&q={query_encoded}" 51 | payload = {"format": "json", "url": lmgtfy_url} 52 | r = requests.get("http://is.gd/create.php", params=payload) 53 | await utils.answer(message, self.strings["result"].format((await utils.run_sync(r.json))["shorturl"], text)) 54 | -------------------------------------------------------------------------------- /lydia.py: -------------------------------------------------------------------------------- 1 | # Some parts are Copyright (C) Diederik Noordhuis (@AntiEngineer) 2019 2 | # All licensed under project license 3 | 4 | # Friendly Telegram (telegram userbot) 5 | # Copyright (C) 2018-2019 The Authors 6 | 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | 20 | # The API is not yet public. To get a key, go to https://t.me/Intellivoid then ask Qián Zhào. 21 | 22 | from .. import loader, utils 23 | import logging 24 | import asyncio 25 | import time 26 | import random 27 | import coffeehouse 28 | from telethon import functions, types 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | 33 | def register(cb): 34 | cb(LydiaMod()) 35 | 36 | 37 | class LydiaMod(loader.Module): 38 | """Talks to a robot instead of a human""" 39 | def __init__(self): 40 | self.config = loader.ModuleConfig("CLIENT_KEY", None, _("The API key for lydia, acquire from @IntellivoidDev"), 41 | "IGNORE_NO_COMMON", False, _("Boolean to ignore users who have no chats " 42 | + "in common with you")) 43 | self.name = _("Lydia anti-PM") 44 | self._ratelimit = [] 45 | self._cleanup = None 46 | 47 | async def client_ready(self, client, db): 48 | self._db = db 49 | self._lydia = coffeehouse.LydiaAI(self.config["CLIENT_KEY"]) if self.config["CLIENT_KEY"] else None 50 | # Schedule cleanups 51 | self._cleanup = asyncio.ensure_future(self.schedule_cleanups()) 52 | 53 | async def schedule_cleanups(self): 54 | """Cleans up dead sessions and reschedules itself to run when the next session expiry takes place""" 55 | sessions = self._db.get(__name__, "sessions", {}) 56 | if len(sessions) == 0: 57 | return 58 | nsessions = {} 59 | t = time.time() 60 | for ident, session in sessions.items(): 61 | if not session["expires"] < t: 62 | nsessions.update({ident: session}) 63 | else: 64 | break # workaround server bug 65 | session = await utils.run_sync(self._lydia.get_session, session["session_id"]) 66 | if session.available: 67 | nsessions.update({ident: session}) 68 | if len(nsessions) > 1: 69 | next = min(*[v["expires"] for k, v in nsessions.items()]) 70 | elif len(nsessions) == 1: 71 | [next] = [v["expires"] for k, v in nsessions.items()] 72 | else: 73 | next = t + 86399 74 | if nsessions != sessions: 75 | self._db.set(__name__, "sessions", nsessions) 76 | # Don't worry about the 1 day limit below 3.7.1, if it isn't expired we will just reschedule, 77 | # as nothing will be matched for deletion. 78 | await asyncio.sleep(min(next - t, 86399)) 79 | 80 | await self.schedule_cleanups() 81 | 82 | async def enlydiacmd(self, message): 83 | """Enables Lydia for target user""" 84 | old = self._db.get(__name__, "allow", []) 85 | if message.is_reply: 86 | user = (await message.get_reply_message()).from_id 87 | else: 88 | user = getattr(message.to_id, "user_id", None) 89 | if user is None: 90 | await utils.answer(message, _("The AI service cannot be enabled or disabled in this chat. " 91 | + "Is this a group chat?")) 92 | return 93 | try: 94 | old.remove(user) 95 | self._db.set(__name__, "allow", old) 96 | except ValueError: 97 | await utils.answer(message, _("The AI service cannot be enabled for this user." 98 | + "Perhaps it wasn't disabled?")) 99 | return 100 | await utils.answer(message, _("AI enabled for this user. ")) 101 | 102 | async def forcelydiacmd(self, message): 103 | """Enables Lydia for user in specific chat""" 104 | if message.is_reply: 105 | user = (await message.get_reply_message()).from_id 106 | else: 107 | user = getattr(message.to_id, "user_id", None) 108 | if user is None: 109 | await utils.answer(message, _("Cannot find that user.")) 110 | return 111 | self._db.set(__name__, "force", self._db.get(__name__, "force", []) + [[utils.get_chat_id(message), user]]) 112 | await utils.answer(message, _("AI enabled for that user in this chat.")) 113 | 114 | async def dislydiacmd(self, message): 115 | """Disables Lydia for the target user""" 116 | if message.is_reply: 117 | user = (await message.get_reply_message()).from_id 118 | else: 119 | user = getattr(message.to_id, "user_id", None) 120 | if user is None: 121 | await utils.answer(message, _("The AI service cannot be enabled or disabled in this chat. " 122 | + "Is this a group chat?")) 123 | return 124 | 125 | old = self._db.get(__name__, "force") 126 | try: 127 | old.remove([utils.get_chat_id(message), user]) 128 | self._db.set(__name__, "force", old) 129 | except (ValueError, TypeError, AttributeError): 130 | pass 131 | self._db.set(__name__, "allow", self._db.get(__name__, "allow", []) + [user]) 132 | await message.edit(_("AI disabled for this user.")) 133 | 134 | async def cleanlydiadisabledcmd(self, message): 135 | """ Remove all lydia-disabled users from DB. """ 136 | self._db.set(__name__, "allow", []) 137 | return await utils.answer(message, _("Successfully cleaned up lydia-disabled IDs")) 138 | 139 | async def cleanlydiasessionscmd(self, message): 140 | """Remove all active and not active lydia sessions from DB""" 141 | self._db.set(__name__, "sessions", {}) 142 | return await utils.answer(message, _("Successfully cleaned up lydia sessions.")) 143 | 144 | async def watcher(self, message): 145 | if not self.config["CLIENT_KEY"]: 146 | logger.debug("no key set for lydia, returning") 147 | return 148 | if self._lydia is None: 149 | self._lydia = coffeehouse.LydiaAI(self.config["CLIENT_KEY"]) 150 | if (isinstance(message.to_id, types.PeerUser) and not self.get_allowed(message.from_id)) or \ 151 | (self.is_forced(utils.get_chat_id(message), message.from_id) 152 | and not isinstance(message.to_id, types.PeerUser)): 153 | user = await utils.get_user(message) 154 | if user.is_self or user.bot or user.verified: 155 | logger.debug("User is self, bot or verified.") 156 | return 157 | else: 158 | if not isinstance(message.message, str): 159 | return 160 | if len(message.message) == 0: 161 | return 162 | if self.config["IGNORE_NO_COMMON"] and not self.is_forced(utils.get_chat_id(message), message.from_id): 163 | fulluser = await message.client(functions.users.GetFullUserRequest(await utils.get_user(message))) 164 | if fulluser.common_chats_count == 0: 165 | return 166 | await message.client(functions.messages.SetTypingRequest( 167 | peer=await utils.get_user(message), 168 | action=types.SendMessageTypingAction() 169 | )) 170 | try: 171 | # Get a session 172 | sessions = self._db.get(__name__, "sessions", {}) 173 | session = sessions.get(utils.get_chat_id(message), None) 174 | if session is None or session["expires"] < time.time(): 175 | session = await utils.run_sync(self._lydia.create_session) 176 | session = {"session_id": session.id, "expires": session.expires} 177 | logger.debug(session) 178 | sessions[utils.get_chat_id(message)] = session 179 | logger.debug(sessions) 180 | self._db.set(__name__, "sessions", sessions) 181 | if self._cleanup is not None: 182 | self._cleanup.cancel() 183 | self._cleanup = asyncio.ensure_future(self.schedule_cleanups()) 184 | logger.debug(session) 185 | # AI Response method 186 | msg = message.message 187 | airesp = await utils.run_sync(self._lydia.think_thought, session["session_id"], str(msg)) 188 | logger.debug("AI says %s", airesp) 189 | if random.randint(0, 1) and isinstance(message.to_id, types.PeerUser): 190 | await message.respond(airesp) 191 | else: 192 | await message.reply(airesp) 193 | finally: 194 | await message.client(functions.messages.SetTypingRequest( 195 | peer=await utils.get_user(message), 196 | action=types.SendMessageCancelAction() 197 | )) 198 | 199 | def get_allowed(self, id): 200 | return id in self._db.get(__name__, "allow", []) 201 | 202 | def is_forced(self, chat, user_id): 203 | forced = self._db.get(__name__, "force", []) 204 | if [chat, user_id] in forced: 205 | return True 206 | else: 207 | return False 208 | -------------------------------------------------------------------------------- /lyrics.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader, utils 18 | 19 | import logging 20 | import lyricsgenius 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | def register(cb): 26 | cb(LyricsMod()) 27 | 28 | 29 | class LyricsMod(loader.Module): 30 | """Sings songs""" 31 | def __init__(self): 32 | self.config = loader.ModuleConfig("GENIUS_API_TOKEN", "", 33 | "The LyricsGenius API token from http://genius.com/api-clients") 34 | self.name = _("Lyrics") 35 | 36 | def config_complete(self): 37 | self.genius = lyricsgenius.Genius(self.config["GENIUS_API_TOKEN"]) 38 | 39 | async def lyricscmd(self, message): 40 | """.lyrics Song, Artist""" 41 | args = utils.get_args_split_by(message, ",") 42 | if len(args) != 2: 43 | logger.debug(args) 44 | await message.edit(_("Please specify song and artist.")) 45 | return 46 | logger.debug("getting song lyrics for " + args[0] + ", " + args[1]) 47 | try: 48 | song = await utils.run_sync(self.genius.search_song, args[0], args[1]) 49 | except TypeError: 50 | # Song not found causes internal library error 51 | song = None 52 | if song is None: 53 | await message.edit(_("Song not found.")) 54 | return 55 | logger.debug(song) 56 | logger.debug(song.lyrics) 57 | await utils.answer(message, utils.escape_html(song.lyrics)) 58 | -------------------------------------------------------------------------------- /manifest.txt: -------------------------------------------------------------------------------- 1 | admin_tools 2 | afk 3 | autoprofile 4 | b_emoji 5 | forward 6 | google 7 | info 8 | insult 9 | lmgtfy 10 | lydia 11 | lyrics 12 | misc 13 | mock 14 | nopm 15 | notes 16 | purge 17 | quicktype 18 | recentactions 19 | spam 20 | speedtest 21 | stickers 22 | terminal 23 | transfersh 24 | translate 25 | tts 26 | typer 27 | urbandictionary 28 | userinfo 29 | weather 30 | xda 31 | yesno 32 | -------------------------------------------------------------------------------- /minimal.txt: -------------------------------------------------------------------------------- 1 | admin_tools 2 | afk 3 | info 4 | notes 5 | purge 6 | recentactions 7 | speedtest 8 | stickers 9 | terminal 10 | translate 11 | typer 12 | userinfo 13 | weather 14 | -------------------------------------------------------------------------------- /misc.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader, utils 18 | from ..loader import ModuleConfig as mc 19 | 20 | import logging 21 | import random 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def register(cb): 27 | cb(MiscMod()) 28 | 29 | 30 | class MiscMod(loader.Module): 31 | """Miscellaneous tasks""" 32 | def __init__(self): 33 | self.config = mc("VOLTE_TEXT", "To be fair, you have to have a very high IQ to understand VoLTE. " 34 | + "The technology is extremely subtle, and without a solid grasp of cell towers most " 35 | + "of the signal will go over a typical user’s head. There's also Mukesh Ambani’s " 36 | + "omniscient outlook, which is deftly woven into his characterisation - his personal " 37 | + "philosophy draws heavily from Indian literature, for instance. The users understand " 38 | + "this stuff; they have the intellectual capacity to truly appreciate the depths of " 39 | + "this technology, to realize that they're not just powerful- they say something deep " 40 | + "about LIFE. As a consequence people who dislike reliance jio truly ARE idiots- of " 41 | + "course they wouldn't appreciate, for instance, the humour in Mukesh’s existencial " 42 | + "catchphrase \"does this roms supports volte????” which itself is a cryptic reference " 43 | + "to Turgenev's Russian epic Fathers and Sons I'm smirking right now just imagining one " 44 | + "of those addlepated simpletons scratching their heads in confusion as Mukesh Ambani’s " 45 | + "genius unfolds itself on their phone screens. What fools... how I pity them. 😂 And yes " 46 | + "by the way, I DO have a reliance jio tattoo. And no, you cannot see it. It's for the " 47 | + "ladies' eyes only- And even they have to demonstrate that they're phones even supports " 48 | + "voltes beforehand.\"", "", "HUAWEI_TEXT", "Do you even know what a huawei is, i bet you do" 49 | + "nt, well i made it with one goal, to make a very nice looking ui so i can collect peoples " 50 | + "data(nudes) and send them to my indian friends, i bet you are jealous😂😂, and i bet you " 51 | + "dont even know how to write a proper OS, well I do, I hired 200 Africian Slaves to work on " 52 | + "my new project called HongmengOS, it has 90% better performance than android, and it can " 53 | + "even run android apps😍. Infact donald trump almost fucked me in the ass one time, but " 54 | + "when I promised to share the \"data\" with him, and send him all the nudes captured from " 55 | + "Clinton's CCTV, he let me and my company off the hook, imagine a blonde looking at your " 56 | + "dick😂😂, I mean, its not like ive already got yours already, infact I'm working on a " 57 | + "new EMUI update to every huawei phone to make it auto-capture every time you jerk off " 58 | + "😋, even Tim Cook wants take the hua-way of collecting data, but I bet he doesnt even " 59 | + "know how to use AI to capture good nudes. But there is more, I encrypt the nudes on your " 60 | + "device so you cant access them, but we can😜, Also take note that I already know your " 61 | + "bank details and I'm selling them to tech support scammers, and by the huaway, EMUI 9.2 " 62 | + "will have a new A.I in the camera app to enhance dick pics, but you need to agree to " 63 | + "the new privacy policy of sharing the nudes you capture to us, for \"product improvement\"", 64 | "", "F_LENGTHS", [5, 1, 1, 4, 1, 1, 1], "List to customise size of F shape", "BLUE_TEXT", 65 | "/BLUE /TEXT\n/MUST /CLICK\n/I /AM /A /STUPID /ANIMAL /THAT /IS /ATTRACTED /TO /COLORS", 66 | "Blue text must click!11!!1!1") 67 | self.name = _("Miscellaneous") 68 | 69 | async def voltecmd(self, message): 70 | """Use when the bholit just won't work""" 71 | await message.edit(self.config["VOLTE_TEXT"]) 72 | 73 | async def fcmd(self, message): 74 | """Pays respects""" 75 | args = utils.get_args_raw(message) 76 | if len(args) == 0: 77 | r = random.randint(0, 3) 78 | logger.debug(r) 79 | if r == 0: 80 | await message.edit("┏━━━┓\n┃┏━━┛\n┃┗━━┓\n┃┏━━┛\n┃┃\n┗┛") 81 | elif r == 1: 82 | await message.edit("╭━━━╮\n┃╭━━╯\n┃╰━━╮\n┃╭━━╯\n┃┃\n╰╯") 83 | else: 84 | args = "F" 85 | if len(args) > 0: 86 | out = "" 87 | for line in self.config["F_LENGTHS"]: 88 | c = max(round(line / len(args)), 1) 89 | out += (args * c) + "\n" 90 | await message.edit("" + utils.escape_html(out) + "") 91 | 92 | async def huaweicmd(self, message): 93 | """Use when your country is "investing" in Huawei 5G modems""" 94 | await message.edit(self.config["HUAWEI_TEXT"]) 95 | 96 | async def btcmd(self, message): 97 | """Blue text must click""" 98 | await message.edit(self.config["BLUE_TEXT"]) 99 | -------------------------------------------------------------------------------- /mock.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader, utils 18 | import logging 19 | import random 20 | import re 21 | from pyfiglet import Figlet, FigletFont, FontNotFound 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def register(cb): 27 | cb(MockMod()) 28 | 29 | 30 | @loader.tds 31 | class MockMod(loader.Module): 32 | """mOcKs PeOpLe""" 33 | strings = {"name": "Memes", 34 | "mock_args": "rEpLy To A mEsSaGe To MoCk It (Or TyPe ThE mEsSaGe AfTeR tHe CoMmAnD)", 35 | "figlet_args": "Supply a font and some text to render with figlet", 36 | "no_font": "Font not found", 37 | "uwu_args": "I nyeed some text fow the nyeko.", 38 | "shout_args": "You can't shout nothing."} 39 | 40 | def config_complete(self): 41 | self.name = self.strings["name"] 42 | 43 | async def mockcmd(self, message): 44 | """Use in reply to another message or as .mock """ 45 | text = utils.get_args_raw(message.message) 46 | if len(text) == 0: 47 | if message.is_reply: 48 | text = (await message.get_reply_message()).message 49 | else: 50 | await utils.answer(message, self.strings["mock_args"]) 51 | return 52 | text = list(text) 53 | n = 0 54 | rn = 0 55 | for c in text: 56 | if n % 2 == random.randint(0, 1): 57 | text[rn] = c.upper() 58 | else: 59 | text[rn] = c.lower() 60 | if c.lower() != c.upper(): 61 | n += 1 62 | rn += 1 63 | text = "".join(text) 64 | logger.debug(text) 65 | await message.edit(text) 66 | 67 | async def figletcmd(self, message): 68 | """.figlet """ 69 | # We can't localise figlet due to a lack of fonts 70 | args = utils.get_args(message) 71 | if len(args) < 2: 72 | await utils.answer(message, self.strings["figlet_args"]) 73 | return 74 | text = " ".join(args[1:]) 75 | mode = args[0] 76 | if mode == "random": 77 | mode = random.choice(FigletFont.getFonts()) 78 | try: 79 | fig = Figlet(font=mode, width=30) 80 | except FontNotFound: 81 | await utils.answer(message, self.strings["no_font"]) 82 | return 83 | await message.edit("\u206a" + utils.escape_html(fig.renderText(text)) + "") 84 | 85 | async def uwucmd(self, message): 86 | """Use in wepwy to anyothew message ow as .uwu """ 87 | text = utils.get_args_raw(message.message) 88 | if not text: 89 | if message.is_reply: 90 | text = (await message.get_reply_message()).message 91 | else: 92 | await utils.answer(message, self.strings["uwu_args"]) 93 | return 94 | reply_text = re.sub(r"(r|l)", "w", text) 95 | reply_text = re.sub(r"(R|L)", "W", reply_text) 96 | reply_text = re.sub(r"n([aeiouAEIOU])", r"ny\1", reply_text) 97 | reply_text = re.sub(r"N([aeiouAEIOU])", r"Ny\1", reply_text) 98 | reply_text = reply_text.replace("ove", "uv") 99 | await message.edit(reply_text) 100 | 101 | async def shoutcmd(self, message): 102 | """.shout makes the text massive""" 103 | text = utils.get_args_raw(message) 104 | if not text: 105 | if message.is_reply: 106 | text = (await message.get_reply_message()).message 107 | else: 108 | await utils.answer(message, self.strings["shout_args"]) 109 | return 110 | result = " ".join(text) + "\n" + "\n".join(sym + " " * (pos * 2 + 1) + sym for pos, sym in enumerate(text[1:])) 111 | await utils.answer(message, "" + utils.escape_html(result) + "") 112 | -------------------------------------------------------------------------------- /none.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nopm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | from .. import loader, utils 20 | 21 | import logging 22 | 23 | from telethon import functions, types 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def register(cb): 28 | cb(AntiPMMod()) 29 | 30 | 31 | @loader.tds 32 | class AntiPMMod(loader.Module): 33 | """Prevents people sending you unsolicited private messages""" 34 | strings = {"name": "Anti PM", 35 | "limit_cfg_doc": "Max number of PMs before user is blocked, or None", 36 | "who_to_block": "Specify whom to block", 37 | "blocked": ("I don't want any PM from you, " 38 | "so you have been blocked!"), 39 | "who_to_unblock": "Specify whom to unblock", 40 | "unblocked": ("Alright fine! I'll forgive them this time. PM has been unblocked for " 41 | "this user"), 42 | "who_to_allow": "Who shall I allow to PM?", 43 | "allowed": "I have allowed you to PM now", 44 | "who_to_report": "Who shall I report?", 45 | "reported": "You just got reported spam!", 46 | "who_to_deny": "Who shall I deny to PM?", 47 | "denied": ("I have denied you " 48 | "of your PM permissions."), 49 | "notif_off": "Notifications from denied PMs are silenced.", 50 | "notif_on": "Notifications from denied PMs are now activated.", 51 | "go_away": ("Hey there! Unfortunately, I don't accept private messages from " 52 | "strangers.\n\nPlease contact me in a group, or wait " 53 | "for me to approve you."), 54 | "triggered": ("Hey! I don't appreciate you barging into my PM like this! " 55 | "Did you even ask me for approving you to PM? No? Goodbye then." 56 | "\n\nPS: you've been reported as spam already.")} 57 | 58 | def __init__(self): 59 | self.config = loader.ModuleConfig("PM_BLOCK_LIMIT", None, lambda: self.strings["limit_cfg_doc"]) 60 | self._me = None 61 | self._ratelimit = [] 62 | 63 | def config_complete(self): 64 | self.name = self.strings["name"] 65 | 66 | async def client_ready(self, client, db): 67 | self._db = db 68 | self._client = client 69 | self._me = await client.get_me(True) 70 | 71 | async def blockcmd(self, message): 72 | """Block this user to PM without being warned""" 73 | user = await utils.get_target(message) 74 | if not user: 75 | await utils.answer(message, self.strings["who_to_block"]) 76 | return 77 | await message.client(functions.contacts.BlockRequest(user)) 78 | await utils.answer(message, self.strings["blocked"].format(user)) 79 | 80 | async def unblockcmd(self, message): 81 | """Unblock this user to PM""" 82 | user = await utils.get_target(message) 83 | if not user: 84 | await utils.answer(message, self.strings["who_to_unblock"]) 85 | return 86 | await message.client(functions.contacts.UnblockRequest(user)) 87 | await utils.answer(message, self.strings["unblocked"].format(user)) 88 | 89 | async def allowcmd(self, message): 90 | """Allow this user to PM""" 91 | user = await utils.get_target(message) 92 | if not user: 93 | await utils.answer(message, self.strings["who_to_allow"]) 94 | return 95 | self._db.set(__name__, "allow", list(set(self._db.get(__name__, "allow", [])).union({user}))) 96 | await utils.answer(message, self.strings["allowed"].format(user)) 97 | 98 | async def reportcmd(self, message): 99 | """Report the user spam. Use only in PM""" 100 | user = await utils.get_target(message) 101 | if not user: 102 | await utils.answer(message, self.strings["who_to_report"]) 103 | return 104 | self._db.set(__name__, "allow", list(set(self._db.get(__name__, "allow", [])).difference({user}))) 105 | if message.is_reply and isinstance(message.to_id, types.PeerChannel): 106 | # Report the message 107 | await message.client(functions.messages.ReportRequest(peer=message.chat_id, 108 | id=[message.reply_to_msg_id], 109 | reason=types.InputReportReasonSpam())) 110 | else: 111 | await message.client(functions.messages.ReportSpamRequest(peer=message.to_id)) 112 | await utils.answer(message, self.strings["reported"]) 113 | 114 | async def denycmd(self, message): 115 | """Deny this user to PM without being warned""" 116 | user = await utils.get_target(message) 117 | if not user: 118 | await utils.answer(message, self.strings["who_to_deny"]) 119 | return 120 | self._db.set(__name__, "allow", list(set(self._db.get(__name__, "allow", [])).difference({user}))) 121 | await utils.answer(message, self.strings["denied"].format(user)) 122 | 123 | async def notifoffcmd(self, message): 124 | """Disable the notifications from denied PMs""" 125 | self._db.set(__name__, "notif", True) 126 | await utils.answer(message, self.strings["notif_off"]) 127 | 128 | async def notifoncmd(self, message): 129 | """Enable the notifications from denied PMs""" 130 | self._db.set(__name__, "notif", False) 131 | await utils.answer(message, self.strings["notif_on"]) 132 | 133 | async def watcher(self, message): 134 | if getattr(message.to_id, "user_id", None) == self._me.user_id: 135 | logger.debug("pm'd!") 136 | if message.from_id in self._ratelimit: 137 | self._ratelimit.remove(message.from_id) 138 | return 139 | else: 140 | self._ratelimit += [message.from_id] 141 | user = await utils.get_user(message) 142 | if user.is_self or user.bot or user.verified: 143 | logger.debug("User is self, bot or verified.") 144 | return 145 | if self.get_allowed(message.from_id): 146 | logger.debug("Authorised pm detected") 147 | else: 148 | await utils.answer(message, self.strings["go_away"]) 149 | if isinstance(self.config["PM_BLOCK_LIMIT"], int): 150 | limit = self._db.get(__name__, "limit", {}) 151 | if limit.get(message.from_id, 0) >= self.config["PM_BLOCK_LIMIT"]: 152 | await utils.answer(message, self.strings["triggered"]) 153 | await message.client(functions.contacts.BlockRequest(message.from_id)) 154 | await message.client(functions.messages.ReportSpamRequest(peer=message.from_id)) 155 | del limit[message.from_id] 156 | self._db.set(__name__, "limit", limit) 157 | else: 158 | self._db.set(__name__, "limit", {**limit, message.from_id: limit.get(message.from_id, 0) + 1}) 159 | if self._db.get(__name__, "notif", False): 160 | await message.client.send_read_acknowledge(message.chat_id) 161 | 162 | def get_allowed(self, id): 163 | return id in self._db.get(__name__, "allow", []) 164 | -------------------------------------------------------------------------------- /notes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | 21 | from .. import loader, utils 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def register(cb): 27 | cb(NotesMod()) 28 | 29 | 30 | @loader.tds 31 | class NotesMod(loader.Module): 32 | """Stores global notes (aka snips)""" 33 | strings = {"name": "Notes", 34 | "what_note": "What note should be retrieved?", 35 | "no_note": "Note not found", 36 | "save_what": "You must reply to a message to save it to a note, or type the note.", 37 | "what_name": "You must specify what the note should be called?", 38 | "saved": "Note saved", 39 | "notes_header": "Saved notes:\n\n", 40 | "notes_item": " {}", 41 | "delnote_args": "What note should be deleted?", 42 | "delnote_done": "Note deleted", 43 | "delnotes_none": "There are no notes to be cleared", 44 | "delnotes_done": "All notes cleared", 45 | "notes_none": "There are no saved notes"} 46 | 47 | def config_complete(self): 48 | self.name = self.strings["name"] 49 | 50 | async def notecmd(self, message): 51 | """Gets the note specified""" 52 | args = utils.get_args(message) 53 | if not args: 54 | await utils.answer(message, self.strings["what_note"]) 55 | return 56 | asset_id = self._db.get(__name__, "notes", {}).get(args[0], None) 57 | logger.debug(asset_id) 58 | if asset_id is None: 59 | await utils.answer(message, self.strings["no_note"]) 60 | return 61 | await utils.answer(message, await self._db.fetch_asset(asset_id)) 62 | 63 | async def delallnotescmd(self, message): 64 | """Deletes all the saved notes""" 65 | if not self._db.get(__name__, "notes", {}): 66 | await utils.answer(message, self.strings["delnotes_none"]) 67 | return 68 | self._db.get(__name__, "notes", {}).clear() 69 | await utils.answer(message, self.strings["delnotes_done"]) 70 | 71 | async def savecmd(self, message): 72 | """Save a new note. Must be used in reply with one parameter (note name)""" 73 | args = utils.get_args(message) 74 | if not args: 75 | await utils.answer(message, self.strings["what_name"]) 76 | return 77 | if not message.is_reply: 78 | if len(args) < 2: 79 | await utils.answer(message, self.strings["save_what"]) 80 | return 81 | else: 82 | message.entities = None 83 | message.message = args[1] 84 | target = message 85 | logger.debug(target.message) 86 | else: 87 | target = await message.get_reply_message() 88 | asset_id = await self._db.store_asset(target) 89 | self._db.set(__name__, "notes", {**self._db.get(__name__, "notes", {}), args[0]: asset_id}) 90 | await utils.answer(message, self.strings["saved"]) 91 | 92 | async def delnotecmd(self, message): 93 | """Deletes a note, specified by note name""" 94 | args = utils.get_args(message) 95 | if not args: 96 | await utils.answer(message, self.strings["delnote_args"]) 97 | old = self._db.get(__name__, "notes", {}) 98 | del old[args[0]] 99 | self._db.set(__name__, "notes", old) 100 | await utils.answer(message, self.strings["delnote_done"]) 101 | 102 | async def notescmd(self, message): 103 | """List the saved notes""" 104 | if not self._db.get(__name__, "notes", {}): 105 | await utils.answer(message, self.strings["notes_none"]) 106 | return 107 | await utils.answer(message, self.strings["notes_header"] 108 | + "\n".join(self.strings["notes_item"].format(key) 109 | for key in self._db.get(__name__, "notes", {}))) 110 | 111 | async def client_ready(self, client, db): 112 | self._db = db 113 | -------------------------------------------------------------------------------- /purge.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader, utils 18 | import logging 19 | import telethon 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def register(cb): 25 | cb(PurgeMod()) 26 | 27 | 28 | @loader.tds 29 | class PurgeMod(loader.Module): 30 | """Deletes your messages""" 31 | strings = {"name": "Purge", 32 | "from_where": "From where shall I purge?"} 33 | 34 | def config_complete(self): 35 | self.name = self.strings["name"] 36 | 37 | async def purgecmd(self, message): 38 | """Purge from the replied message""" 39 | if not message.is_reply: 40 | await message.edit(self.strings["from_where"]) 41 | return 42 | 43 | from_users = set() 44 | args = utils.get_args(message) 45 | for arg in args: 46 | try: 47 | entity = await message.client.get_entity(arg) 48 | if isinstance(entity, telethon.tl.types.User): 49 | from_users.add(entity.id) 50 | except ValueError: 51 | pass 52 | 53 | msgs = [] 54 | from_ids = set() 55 | async for msg in message.client.iter_messages( 56 | entity=message.to_id, 57 | min_id=message.reply_to_msg_id - 1, 58 | reverse=True): 59 | if from_users and msg.from_id not in from_users: 60 | continue 61 | msgs.append(msg.id) 62 | from_ids.add(msg.from_id) 63 | if len(msgs) >= 99: 64 | logger.debug(msgs) 65 | await message.client.delete_messages(message.to_id, msgs) 66 | msgs.clear() 67 | # No async list comprehension in 3.5 68 | if msgs: 69 | logger.debug(msgs) 70 | await message.client.delete_messages(message.to_id, msgs) 71 | await self.allmodules.log("purge", group=message.to_id, affected_uids=from_ids) 72 | 73 | async def delcmd(self, message): 74 | """Delete the replied message""" 75 | msgs = [message.id] 76 | if not message.is_reply: 77 | msg = await message.client.iter_messages(message.to_id, 1, max_id=message.id).__anext__() 78 | else: 79 | msg = await message.get_reply_message() 80 | msgs.append(msg.id) 81 | logger.debug(msgs) 82 | await message.client.delete_messages(message.to_id, msgs) 83 | await self.allmodules.log("delete", group=message.to_id, affected_uids=[msg.from_id]) 84 | -------------------------------------------------------------------------------- /quicktype.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader, utils 18 | 19 | import logging 20 | import asyncio 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | def register(cb): 26 | cb(QuickTypeMod()) 27 | 28 | 29 | class QuickTypeMod(loader.Module): 30 | """Deletes your message after a timeout""" 31 | def __init__(self): 32 | self.name = _("Quick Typer") 33 | 34 | async def quicktypecmd(self, message): 35 | """.quicktype """ 36 | args = utils.get_args(message) 37 | logger.debug(args) 38 | if len(args) == 0: 39 | await message.edit(_("U wot? I need something to type")) 40 | return 41 | if len(args) == 1: 42 | await message.edit(_("Go type it urself m8")) 43 | return 44 | t = args[0] 45 | mess = " ".join(args[1:]) 46 | try: 47 | t = float(t) 48 | except ValueError: 49 | await message.edit(_("Nice number bro")) 50 | return 51 | await message.edit(mess) 52 | await asyncio.sleep(t) 53 | await message.delete() 54 | -------------------------------------------------------------------------------- /quotes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | import requests 21 | import base64 22 | import json 23 | import telethon 24 | 25 | from .. import loader, utils 26 | from PIL import Image 27 | from io import BytesIO 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | def register(cb): 33 | cb(QuotesMod()) 34 | 35 | 36 | @loader.tds 37 | class QuotesMod(loader.Module): 38 | """Quote a message.""" 39 | strings = { 40 | "name": "Quotes", 41 | "api_token_cfg_doc": "API Key/Token for Quotes.", 42 | "api_url_cfg_doc": "API URL for Quotes.", 43 | "colors_cfg_doc": "Username colors", 44 | "default_username_color_cfg_doc": "Default color for the username.", 45 | "no_reply": "You didn't reply to a message.", 46 | "no_template": "You didn't specify the template.", 47 | "delimiter": ", ", 48 | "server_error": "Server error. Please report to developer.", 49 | "invalid_token": "You've set an invalid token.", 50 | "unauthorized": "You're unauthorized to do this.", 51 | "not_enough_permissions": "Wrong template. You can use only the default one.", 52 | "templates": "Available Templates: {}", 53 | "cannot_send_stickers": "You cannot send stickers in this chat.", 54 | "admin": "admin", 55 | "creator": "creator", 56 | "hidden": "hidden", 57 | "channel": "Channel", 58 | "filename": "file.png" 59 | } 60 | 61 | def __init__(self): 62 | self.config = loader.ModuleConfig("api_token", None, lambda: self.strings["api_token_cfg_doc"], 63 | "api_url", "http://api.antiddos.systems", 64 | lambda: self.strings["api_url_cfg_doc"], 65 | "username_colors", ["#fb6169", "#faa357", "#b48bf2", "#85de85", 66 | "#62d4e3", "#65bdf3", "#ff5694"], 67 | lambda: self.strings["colors_cfg_doc"], 68 | "default_username_color", "#b48bf2", 69 | lambda: self.strings["default_username_color_cfg_doc"]) 70 | 71 | def config_complete(self): 72 | self.name = self.strings["name"] 73 | 74 | async def client_ready(self, client, db): 75 | self.client = client 76 | 77 | async def quotecmd(self, message): # noqa: C901 78 | """Quote a message. 79 | Usage: .quote [template] [file/force_file] 80 | If template is missing, possible templates are fetched. 81 | If no args provided, default template will be used, quote sent as sticker""" 82 | args = utils.get_args(message) 83 | reply = await message.get_reply_message() 84 | 85 | if not reply: 86 | return await utils.answer(message, self.strings["no_reply"]) 87 | 88 | username_color = username = admintitle = user_id = None 89 | profile_photo_url = reply.from_id 90 | 91 | admintitle = "" 92 | pfp = None 93 | if isinstance(reply.to_id, telethon.tl.types.PeerChannel) and reply.fwd_from: 94 | user = reply.forward.chat 95 | elif isinstance(reply.to_id, telethon.tl.types.PeerChat): 96 | chat = await self.client(telethon.tl.functions.messages.GetFullChatRequest(reply.to_id)) 97 | participants = chat.full_chat.participants.participants 98 | participant = next(filter(lambda x: x.user_id == reply.from_id, participants), None) 99 | if isinstance(participant, telethon.tl.types.ChatParticipantCreator): 100 | admintitle = self.strings["creator"] 101 | elif isinstance(participant, telethon.tl.types.ChatParticipantAdmin): 102 | admintitle = self.strings["admin"] 103 | user = await reply.get_sender() 104 | else: 105 | user = await reply.get_sender() 106 | 107 | username = telethon.utils.get_display_name(user) 108 | if reply.fwd_from is not None and reply.fwd_from.post_author is not None: 109 | username += f" ({reply.fwd_from.post_author})" 110 | user_id = reply.from_id 111 | 112 | if reply.fwd_from: 113 | if reply.fwd_from.saved_from_peer: 114 | profile_photo_url = reply.forward.chat 115 | admintitle = self.strings["channel"] 116 | elif reply.fwd_from.from_name: 117 | username = reply.fwd_from.from_name 118 | profile_photo_url = None 119 | admintitle = "" 120 | elif reply.forward.sender: 121 | username = telethon.utils.get_display_name(reply.forward.sender) 122 | profile_photo_url = reply.forward.sender.id 123 | admintitle = "" 124 | elif reply.forward.chat: 125 | admintitle = self.strings["channel"] 126 | profile_photo_url = user 127 | else: 128 | if isinstance(reply.to_id, telethon.tl.types.PeerUser) is False: 129 | try: 130 | user = await self.client(telethon.tl.functions.channels.GetParticipantRequest(message.chat_id, 131 | user)) 132 | if isinstance(user.participant, telethon.tl.types.ChannelParticipantCreator): 133 | admintitle = user.participant.rank or self.strings["creator"] 134 | elif isinstance(user.participant, telethon.tl.types.ChannelParticipantAdmin): 135 | admintitle = user.participant.rank or self.strings["admin"] 136 | user = user.users[0] 137 | except telethon.errors.rpcerrorlist.UserNotParticipantError: 138 | pass 139 | if profile_photo_url is not None: 140 | pfp = await self.client.download_profile_photo(profile_photo_url, bytes) 141 | 142 | if pfp is not None: 143 | profile_photo_url = "data:image/png;base64, " + base64.b64encode(pfp).decode() 144 | else: 145 | profile_photo_url = "" 146 | 147 | if user_id is not None: 148 | username_color = self.config["username_colors"][user_id % 7] 149 | else: 150 | username_color = self.config["default_username_color"] 151 | 152 | reply_username = "" 153 | reply_text = "" 154 | if reply.is_reply is True: 155 | reply_to = await reply.get_reply_message() 156 | reply_peer = None 157 | if reply_to.fwd_from is not None: 158 | if reply_to.forward.chat is not None: 159 | reply_peer = reply_to.forward.chat 160 | elif reply_to.fwd_from.from_id is not None: 161 | try: 162 | user_id = reply_to.fwd_from.from_id 163 | user = await self.client(telethon.tl.functions.users.GetFullUserRequest(user_id)) 164 | reply_peer = user.user 165 | except ValueError: 166 | pass 167 | else: 168 | reply_username = reply_to.fwd_from.from_name 169 | elif reply_to.from_id is not None: 170 | reply_user = await self.client(telethon.tl.functions.users.GetFullUserRequest(reply_to.from_id)) 171 | reply_peer = reply_user.user 172 | 173 | if reply_username is None or reply_username == "": 174 | reply_username = telethon.utils.get_display_name(reply_peer) 175 | reply_text = reply_to.message 176 | 177 | date = "" 178 | if reply.fwd_from is not None: 179 | date = reply.fwd_from.date.strftime("%H:%M") 180 | else: 181 | date = reply.date.strftime("%H:%M") 182 | 183 | request = json.dumps({ 184 | "ProfilePhotoURL": profile_photo_url, 185 | "usernameColor": username_color, 186 | "username": username, 187 | "adminTitle": admintitle, 188 | "Text": reply.message, 189 | "Markdown": get_markdown(reply), 190 | "ReplyUsername": reply_username, 191 | "ReplyText": reply_text, 192 | "Date": date, 193 | "Template": args[0] if len(args) > 0 else "default", 194 | "APIKey": self.config["api_token"] 195 | }) 196 | 197 | resp = await utils.run_sync(requests.post, self.config["api_url"] + "/api/v2/quote", data=request) 198 | resp.raise_for_status() 199 | resp = await utils.run_sync(resp.json) 200 | 201 | if resp["status"] == 500: 202 | return await utils.answer(message, self.strings["server_error"]) 203 | elif resp["status"] == 401: 204 | if resp["message"] == "ERROR_TOKEN_INVALID": 205 | return await utils.answer(message, self.strings["invalid_token"]) 206 | else: 207 | raise ValueError("Invalid response from server", resp) 208 | elif resp["status"] == 403: 209 | if resp["message"] == "ERROR_UNAUTHORIZED": 210 | return await utils.answer(message, self.strings["unauthorized"]) 211 | else: 212 | raise ValueError("Invalid response from server", resp) 213 | elif resp["status"] == 404: 214 | if resp["message"] == "ERROR_TEMPLATE_NOT_FOUND": 215 | newreq = await utils.run_sync(requests.post, self.config["api_url"] + "/api/v1/getalltemplates", data={ 216 | "token": self.config["api_token"] 217 | }) 218 | newreq = await utils.run_sync(newreq.json) 219 | 220 | if newreq["status"] == "NOT_ENOUGH_PERMISSIONS": 221 | return await utils.answer(message, self.strings["not_enough_permissions"]) 222 | elif newreq["status"] == "SUCCESS": 223 | templates = self.strings["delimiter"].join(newreq["message"]) 224 | return await utils.answer(message, self.strings["templates"].format(templates)) 225 | elif newreq["status"] == "INVALID_TOKEN": 226 | return await utils.answer(message, self.strings["invalid_token"]) 227 | else: 228 | raise ValueError("Invalid response from server", newreq) 229 | else: 230 | raise ValueError("Invalid response from server", resp) 231 | elif resp["status"] != 200: 232 | raise ValueError("Invalid response from server", resp) 233 | 234 | req = await utils.run_sync(requests.get, self.config["api_url"] + "/cdn/" + resp["message"]) 235 | req.raise_for_status() 236 | file = BytesIO(req.content) 237 | file.seek(0) 238 | 239 | if len(args) == 2: 240 | if args[1] == "file": 241 | await utils.answer(message, file) 242 | elif args[1] == "force_file": 243 | file.name = self.strings["filename"] 244 | await utils.answer(message, file, force_document=True) 245 | else: 246 | img = await utils.run_sync(Image.open, file) 247 | with BytesIO() as sticker: 248 | await utils.run_sync(img.save, sticker, "webp") 249 | sticker.name = "sticker.webp" 250 | sticker.seek(0) 251 | try: 252 | await utils.answer(message, sticker) 253 | except telethon.errors.rpcerrorlist.ChatSendStickersForbiddenError: 254 | await utils.answer(message, self.strings["cannot_send_stickers"]) 255 | file.close() 256 | 257 | 258 | def get_markdown(reply): 259 | if not reply.entities: 260 | return [] 261 | 262 | markdown = [] 263 | for entity in reply.entities: 264 | md_item = { 265 | "Type": None, 266 | "Start": entity.offset, 267 | "End": entity.offset + entity.length - 1 268 | } 269 | if isinstance(entity, telethon.tl.types.MessageEntityBold): 270 | md_item["Type"] = "bold" 271 | elif isinstance(entity, telethon.tl.types.MessageEntityItalic): 272 | md_item["Type"] = "italic" 273 | elif isinstance(entity, (telethon.tl.types.MessageEntityMention, telethon.tl.types.MessageEntityTextUrl, 274 | telethon.tl.types.MessageEntityMentionName, telethon.tl.types.MessageEntityHashtag, 275 | telethon.tl.types.MessageEntityCashtag, telethon.tl.types.MessageEntityBotCommand, 276 | telethon.tl.types.MessageEntityUrl)): 277 | md_item["Type"] = "link" 278 | elif isinstance(entity, telethon.tl.types.MessageEntityCode): 279 | md_item["Type"] = "code" 280 | elif isinstance(entity, telethon.tl.types.MessageEntityStrike): 281 | md_item["Type"] = "stroke" 282 | elif isinstance(entity, telethon.tl.types.MessageEntityUnderline): 283 | md_item["Type"] = "underline" 284 | else: 285 | logger.warning("Unknown entity: " + str(entity)) 286 | 287 | markdown.append(md_item) 288 | return markdown 289 | -------------------------------------------------------------------------------- /recentactions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import telethon 20 | 21 | from .. import loader, utils 22 | 23 | 24 | def register(cb): 25 | cb(RecentActionsMod()) 26 | 27 | 28 | class RecentActionsMod(loader.Module): 29 | """Reads recent actions""" 30 | def __init__(self): 31 | self.name = _("Recent Actions") 32 | 33 | async def recoverdeletedcmd(self, message): 34 | """Restores deleted messages sent after replied message (optionally specify how many to recover)""" 35 | msgs = message.client.iter_admin_log(message.to_id, delete=True) 36 | if not message.is_reply: 37 | await utils.answer(message, _("Reply to a message to specify where to start")) 38 | return 39 | if not isinstance(message.to_id, telethon.tl.types.PeerChannel): 40 | await utils.answer(message, _("This isn't a supergroup or channel")) 41 | return 42 | target = (await message.get_reply_message()).date 43 | ret = [] 44 | try: 45 | async for msg in msgs: 46 | if msg.original.date < target: 47 | break 48 | if msg.original.action.message.date < target: 49 | continue 50 | ret += [msg] 51 | except telethon.errors.rpcerrorlist.ChatAdminRequiredError: 52 | await utils.answer(message, _("Admin is required to read deleted messages")) 53 | args = utils.get_args(message) 54 | if len(args) > 0: 55 | try: 56 | count = int(args[0]) 57 | ret = ret[-count:] 58 | except ValueError: 59 | pass 60 | for msg in reversed(ret): 61 | orig = msg.original.action.message 62 | deldate = msg.original.date.isoformat() 63 | origdate = orig.date.isoformat() 64 | await message.respond(_("Deleted message {} recovered. Originally sent at {} by {}, deleted at {} by {}") 65 | .format(msg.id, origdate, orig.from_id, deldate, msg.user_id)) 66 | if isinstance(orig, telethon.tl.types.MessageService): 67 | await message.respond("" + utils.escape_html(orig.stringify()) + "") 68 | else: 69 | await message.respond(orig) 70 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # W503 is actually not a PEP8 violation. It was superseded by W504 which does the exact opposite. https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator 3 | ignore = W503,Q003 4 | max-line-length = 120 5 | # Only needed in modules/ but flake8 doesn't let us tell it that 6 | builtins = _ 7 | max-complexity = 15 8 | # We use " not ' 9 | inline-quotes = double 10 | -------------------------------------------------------------------------------- /setup_for_development.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Not copyright, it's from documentation 3 | python3.7 -m pip install --user flake8 flake8-print flake8-quotes 4 | git config --bool flake8.strict true 5 | git config --bool flake8.lazy true 6 | python3.7 -m flake8 --install-hook git 7 | -------------------------------------------------------------------------------- /spam.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader, utils 18 | import logging 19 | import asyncio 20 | 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | def register(cb): 26 | cb(SpamMod()) 27 | 28 | 29 | @loader.tds 30 | class SpamMod(loader.Module): 31 | """Annoys people really effectively""" 32 | strings = {"name": "Spam", 33 | "need_spam": "U wot? I need something to spam.", 34 | "spam_urself": "Go spam urself.", 35 | "nice_number": "Nice number bro.", 36 | "much_spam": "Haha, much spam."} 37 | 38 | def __init__(self): 39 | self.name = self.strings["name"] 40 | 41 | async def spamcmd(self, message): 42 | """.spam """ 43 | use_reply = False 44 | args = utils.get_args(message) 45 | logger.debug(args) 46 | if len(args) == 0: 47 | await utils.answer(message, self.strings["need_spam"]) 48 | return 49 | if len(args) == 1: 50 | if message.is_reply: 51 | use_reply = True 52 | else: 53 | await utils.answer(message, self.strings["spam_urself"]) 54 | return 55 | count = args[0] 56 | spam = (await message.get_reply_message()) if use_reply else message 57 | spam.message = " ".join(args[1:]) 58 | try: 59 | count = int(count) 60 | except ValueError: 61 | await utils.answer(message, self.strings["nice_number"]) 62 | return 63 | if count < 1: 64 | await utils.answer(message, self.strings["much_spam"]) 65 | return 66 | await message.delete() 67 | if count > 20: 68 | # Be kind to other people 69 | sleepy = 2 70 | else: 71 | sleepy = 0 72 | i = 0 73 | size = 1 if sleepy else 100 74 | while i < count: 75 | await asyncio.gather(*[message.respond(spam) for x in range(min(count, size))]) 76 | await asyncio.sleep(sleepy) 77 | i += size 78 | await self.allmodules.log("spam", group=message.to_id, data=spam.message + " (" + str(count) + ")") 79 | -------------------------------------------------------------------------------- /speedtest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | import speedtest 21 | 22 | from .. import loader, utils 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def register(cb): 28 | cb(SpeedtestMod()) 29 | 30 | 31 | class SpeedtestMod(loader.Module): 32 | """Uses speedtest.net""" 33 | def __init__(self): 34 | self.name = _("Speedtest") 35 | 36 | async def speedtestcmd(self, message): 37 | """Tests your internet speed""" 38 | await utils.answer(message, _("Running speedtest...")) 39 | args = utils.get_args(message) 40 | servers = [] 41 | for server in args: 42 | try: 43 | servers += [int(server)] 44 | except ValueError: 45 | logger.warning("server failed") 46 | results = await utils.run_sync(self.speedtest, servers) 47 | ret = _("Speedtest Results:") + "\n\n" 48 | ret += _("Download: {} MiB/s").format(round(results["download"] / 2**20, 2)) + "\n" 49 | ret += _("Upload: {} MiB/s").format(round(results["upload"] / 2**20, 2)) + "\n" 50 | ret += _("Ping: {} milliseconds").format(round(results["ping"], 2)) + "\n" 51 | await utils.answer(message, ret) 52 | 53 | def speedtest(self, servers): 54 | speedtester = speedtest.Speedtest() 55 | speedtester.get_servers(servers) 56 | speedtester.get_best_server() 57 | speedtester.download(threads=None) 58 | speedtester.upload(threads=None) 59 | return speedtester.results.dict() 60 | -------------------------------------------------------------------------------- /stickers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | from .. import loader, utils 20 | 21 | import logging 22 | import warnings 23 | import itertools 24 | import asyncio 25 | 26 | from io import BytesIO 27 | from PIL import Image 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | try: 32 | import tgs 33 | except OSError: 34 | logger.exception("TGS not available") 35 | 36 | warnings.simplefilter("error", Image.DecompressionBombWarning) 37 | 38 | 39 | def register(cb): 40 | cb(StickersMod()) 41 | 42 | 43 | class StickersMod(loader.Module): 44 | """Tasks with stickers""" 45 | def __init__(self): 46 | self.config = loader.ModuleConfig("STICKERS_USERNAME", "Stickers", "Bot username to create stickers via", 47 | "STICKER_SIZE", (512, 512), "The size of one sticker", 48 | "DEFAULT_STICKER_EMOJI", u"🤔", "The emoji to use for stickers by default") 49 | self.name = _("Stickers") 50 | self._lock = asyncio.Lock() 51 | 52 | async def kangcmd(self, message): # noqa: C901 # TODO: reduce complexity a LOT 53 | """Use in reply or with an attached media: 54 | .kang [emojis] 55 | If pack is not matched the most recently created will be used instead""" 56 | args = utils.get_args(message) 57 | if len(args) != 1 and len(args) != 2: 58 | logger.debug("wrong args len(%s) or bad args(%s)", len(args), args) 59 | await message.edit(_("Provide a pack name and optionally emojis too")) 60 | return 61 | 62 | if not message.is_reply: 63 | if message.sticker or message.photo: 64 | logger.debug("user sent photo/sticker directly not reply") 65 | sticker = message 66 | else: 67 | logger.debug("user didnt send any sticker/photo or reply") 68 | async for sticker in message.client.iter_messages(message.to_id, 10): 69 | if sticker.sticker or sticker.photo: 70 | break # Changes message into the right one 71 | else: 72 | sticker = await message.get_reply_message() 73 | if not (sticker.sticker or sticker.photo): 74 | await message.edit(_("Reply to a sticker or photo to nick it")) 75 | return 76 | logger.debug("user did send photo/sticker") 77 | if len(args) > 1: 78 | emojis = args[1] 79 | elif sticker.sticker: 80 | emojis = sticker.file.emoji 81 | else: 82 | emojis = self.config["DEFAULT_STICKER_EMOJI"] 83 | logger.debug(emojis) 84 | animated = sticker.file.mime_type == "application/x-tgsticker" 85 | try: 86 | img = BytesIO() 87 | await sticker.download_media(file=img) 88 | img.seek(0) 89 | logger.debug(img) 90 | if animated: 91 | async with self._lock: 92 | conv = message.client.conversation("t.me/" + self.config["STICKERS_USERNAME"], 93 | timeout=5, exclusive=True) 94 | async with conv: 95 | first = await conv.send_message("/cancel") 96 | await conv.get_response() 97 | await conv.send_message("/addsticker") 98 | buttons = (await conv.get_response()).buttons 99 | if buttons is not None: 100 | logger.debug("there are buttons, good") 101 | button = click_buttons(buttons, args[0]) 102 | await button.click() 103 | else: 104 | logger.warning("there's no buttons!") 105 | await message.client.send_message("t.me/" + self.config["STICKERS_USERNAME"], "/cancel") 106 | await message.edit("Something went wrong") 107 | return 108 | # We have sent the pack we wish to modify. 109 | # Upload sticker 110 | r0 = await conv.get_response() 111 | if ".PSD" in r0.message: 112 | logger.error("bad response from stickerbot 0") 113 | logger.error(r0) 114 | await message.edit(_("That isn't an animated sticker pack")) 115 | msgs = [] 116 | async for msg in message.client.iter_messages(entity="t.me/" 117 | + self.config["STICKERS_USERNAME"], 118 | min_id=first.id, reverse=True): 119 | msgs += [msg.id] 120 | logger.debug(msgs) 121 | await message.client.delete_messages("t.me/" + self.config["STICKERS_USERNAME"], 122 | msgs + [first]) 123 | return 124 | uploaded = await message.client.upload_file(img, file_name="AnimatedSticker.tgs") 125 | m1 = await conv.send_file(uploaded, force_document=True) 126 | m2 = await conv.send_message(emojis) 127 | await conv.send_message("/done") 128 | # Block now so that we mark it all as read 129 | await message.client.send_read_acknowledge(conv.chat_id) 130 | r1 = await conv.get_response(m1) 131 | r2 = await conv.get_response(m2) 132 | if "/done" not in r2.message: 133 | # That's an error 134 | logger.error("Bad response from StickerBot 1") 135 | logger.error(r0) 136 | logger.error(r1) 137 | logger.error(r2) 138 | await message.edit(_("Something went wrong internally!")) 139 | return 140 | msgs = [] 141 | async for msg in message.client.iter_messages(entity="t.me/" + self.config["STICKERS_USERNAME"], 142 | min_id=first.id, 143 | reverse=True): 144 | msgs += [msg.id] 145 | logger.debug(msgs) 146 | await message.client.delete_messages("t.me/" + self.config["STICKERS_USERNAME"], msgs + [first]) 147 | if "emoji" in r2.message: 148 | # The emoji(s) are invalid. 149 | logger.error("Bad response from StickerBot 2") 150 | logger.error(r2) 151 | await message.edit(_("Please provide valid emoji(s).")) 152 | return 153 | 154 | else: 155 | try: 156 | thumb = BytesIO() 157 | task = asyncio.ensure_future(utils.run_sync(resize_image, img, self.config["STICKER_SIZE"], thumb)) 158 | thumb.name = "sticker.png" 159 | # The data is now in thumb. 160 | # Lock access to @Stickers 161 | async with self._lock: 162 | # Without t.me/ there is ambiguity; Stickers could be a name, 163 | # in which case the wrong entity could be returned 164 | # TODO should this be translated? 165 | conv = message.client.conversation("t.me/" + self.config["STICKERS_USERNAME"], 166 | timeout=5, exclusive=True) 167 | async with conv: 168 | first = await conv.send_message("/cancel") 169 | await conv.get_response() 170 | await conv.send_message("/addsticker") 171 | r0 = await conv.get_response() 172 | buttons = r0.buttons 173 | if buttons is not None: 174 | logger.debug("there are buttons, good") 175 | button = click_buttons(buttons, args[0]) 176 | m0 = await button.click() 177 | elif "/newpack" in r0.message: 178 | await message.edit("Please create a pack first") 179 | return 180 | else: 181 | logger.warning("there's no buttons!") 182 | m0 = await message.client.send_message("t.me/" + self.config["STICKERS_USERNAME"], 183 | "/cancel") 184 | await message.edit("Something went wrong") 185 | return 186 | # We have sent the pack we wish to modify. 187 | # Upload sticker 188 | r0 = await conv.get_response() 189 | if ".TGS" in r0.message: 190 | logger.error("bad response from stickerbot 0") 191 | logger.error(r0) 192 | await message.edit(_("That's an animated pack")) 193 | msgs = [] 194 | async for msg in message.client.iter_messages(entity="t.me/" 195 | + self.config["STICKERS_USERNAME"], 196 | min_id=first.id, 197 | reverse=True): 198 | msgs += [msg.id] 199 | logger.debug(msgs) 200 | await message.client.delete_messages("t.me/" + self.config["STICKERS_USERNAME"], 201 | msgs + [first]) 202 | return 203 | if "120" in r0.message: 204 | logger.error("bad response from stickerbot 0") 205 | logger.error(r0) 206 | await message.edit(_("That pack is full. Delete some stickers or try making a " 207 | "new pack.")) 208 | msgs = [] 209 | async for msg in message.client.iter_messages(entity="t.me/" 210 | + self.config["STICKERS_USERNAME"], 211 | min_id=first.id, 212 | reverse=True): 213 | if msg.id != m0.id: 214 | msgs += [msg.id] 215 | logger.debug(msgs) 216 | await message.client.delete_messages("t.me/" + self.config["STICKERS_USERNAME"], 217 | msgs + [first]) 218 | return 219 | await task # We can resize the thumbnail while the sticker bot is processing other data 220 | thumb.seek(0) 221 | m1 = await conv.send_file(thumb, allow_cache=False, force_document=True) 222 | r1 = await conv.get_response(m1) 223 | m2 = await conv.send_message(emojis) 224 | r2 = await conv.get_response(m2) 225 | if "/done" in r2.message: 226 | await conv.send_message("/done") 227 | else: 228 | logger.error(r1) 229 | logger.error(r2) 230 | logger.error("Bad response from StickerBot 0") 231 | await message.edit(_("Something went wrong internally")) 232 | await message.client.send_read_acknowledge(conv.chat_id) 233 | if "/done" not in r2.message: 234 | # That's an error 235 | logger.error("Bad response from StickerBot 1") 236 | logger.error(r1) 237 | logger.error(r2) 238 | await message.edit(_("Something went wrong internally!")) 239 | return 240 | msgs = [] 241 | async for msg in message.client.iter_messages(entity="t.me/" 242 | + self.config["STICKERS_USERNAME"], 243 | min_id=first.id, 244 | reverse=True): 245 | msgs += [msg.id] 246 | logger.debug(msgs) 247 | await message.client.delete_messages("t.me/" + self.config["STICKERS_USERNAME"], msgs + [first]) 248 | if "emoji" in r2.message: 249 | # The emoji(s) are invalid. 250 | logger.error("Bad response from StickerBot 2") 251 | logger.error(r2) 252 | await message.edit(_("Please provide valid emoji(s).")) 253 | return 254 | finally: 255 | thumb.close() 256 | finally: 257 | img.close() 258 | packurl = utils.escape_html(f"https://t.me/addstickers/{button.text}") 259 | await message.edit(_("Sticker added to pack!").format(packurl)) 260 | 261 | async def gififycmd(self, message): 262 | """Convert the replied animated sticker to a GIF""" 263 | args = utils.get_args(message) 264 | fps = 5 265 | quality = 256 266 | try: 267 | if len(args) == 1: 268 | fps = int(args[0]) 269 | elif len(args) == 2: 270 | quality = int(args[0]) 271 | fps = int(args[1]) 272 | except ValueError: 273 | logger.exception("Failed to parse quality/fps") 274 | target = await message.get_reply_message() 275 | if target is None or target.file is None or target.file.mime_type != "application/x-tgsticker": 276 | await utils.answer(message, _("Please provide an animated sticker to convert to a GIF")) 277 | return 278 | try: 279 | file = BytesIO() 280 | await target.download_media(file) 281 | file.seek(0) 282 | anim = await utils.run_sync(tgs.parsers.tgs.parse_tgs, file) 283 | file.close() 284 | result = BytesIO() 285 | result.name = "animation.gif" 286 | await utils.run_sync(tgs.exporters.gif.export_gif, anim, result, quality, fps) 287 | result.seek(0) 288 | await utils.answer(message, result) 289 | finally: 290 | try: 291 | file.close() 292 | except UnboundLocalError: 293 | pass 294 | try: 295 | result.close() 296 | except UnboundLocalError: 297 | pass 298 | 299 | 300 | def click_buttons(buttons, target_pack): 301 | buttons = list(itertools.chain.from_iterable(buttons)) 302 | # Process in reverse order; most difficult to match first 303 | try: 304 | return buttons[int(target_pack)] 305 | except (IndexError, ValueError): 306 | pass 307 | logger.debug(buttons) 308 | for button in buttons: 309 | logger.debug(button) 310 | if button.text == target_pack: 311 | return button 312 | for button in buttons: 313 | if target_pack in button.text: 314 | return button 315 | for button in buttons: 316 | if target_pack.lower() in button.text.lower(): 317 | return button 318 | return buttons[-1] 319 | 320 | 321 | def resize_image(img, size, dest): 322 | # Wrapper for asyncio purposes 323 | try: 324 | im = Image.open(img) 325 | # We used to use thumbnail(size) here, but it returns with a *max* dimension of 512,512 326 | # rather than making one side exactly 512 so we have to calculate dimensions manually :( 327 | if im.width == im.height: 328 | size = (512, 512) 329 | elif im.width < im.height: 330 | size = (int(512 * im.width / im.height), 512) 331 | else: 332 | size = (512, int(512 * im.height / im.width)) 333 | logger.debug("Resizing to %s", size) 334 | im.resize(size).save(dest, "PNG") 335 | finally: 336 | im.close() 337 | img.close() 338 | del im 339 | -------------------------------------------------------------------------------- /terminal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | from .. import loader, utils 20 | import logging 21 | import asyncio 22 | import telethon 23 | import os 24 | import re 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def register(cb): 30 | cb(TerminalMod()) 31 | 32 | 33 | class TerminalMod(loader.Module): 34 | """Runs commands""" 35 | def __init__(self): 36 | self.commands = {"terminal": self.terminalcmd, "terminate": self.terminatecmd, "kill": self.killcmd, 37 | "apt": self.aptcmd, "neofetch": self.neocmd, "uptime": self.upcmd} 38 | self.config = loader.ModuleConfig("FLOOD_WAIT_PROTECT", 2, "How long to wait in seconds during commands") 39 | self.name = _("Terminal") 40 | self.activecmds = {} 41 | 42 | async def terminalcmd(self, message): 43 | """.terminal """ 44 | await self.runcmd(message, utils.get_args_raw(message)) 45 | 46 | async def aptcmd(self, message): 47 | """Shorthand for '.terminal apt'""" 48 | await self.runcmd(message, ("apt " if os.geteuid() == 0 else "sudo -S apt ") 49 | + utils.get_args_raw(message) + " -y", 50 | RawMessageEditor(message, "apt " + utils.get_args_raw(message), self.config, True)) 51 | 52 | async def runcmd(self, message, cmd, editor=None): 53 | if len(cmd.split(" ")) > 1 and cmd.split(" ")[0] == "sudo": 54 | needsswitch = True 55 | for word in cmd.split(" ", 1)[1].split(" "): 56 | if word[0] != "-": 57 | break 58 | if word == "-S": 59 | needsswitch = False 60 | if needsswitch: 61 | cmd = " ".join([cmd.split(" ", 1)[0], "-S", cmd.split(" ", 1)[1]]) 62 | sproc = await asyncio.create_subprocess_shell(cmd, stdin=asyncio.subprocess.PIPE, 63 | stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, 64 | cwd=utils.get_base_dir()) 65 | if editor is None: 66 | editor = SudoMessageEditor(message, cmd, self.config) 67 | editor.update_process(sproc) 68 | self.activecmds[hash_msg(message)] = sproc 69 | await editor.redraw(True) 70 | await asyncio.gather(read_stream(editor.update_stdout, sproc.stdout, self.config["FLOOD_WAIT_PROTECT"]), 71 | read_stream(editor.update_stderr, sproc.stderr, self.config["FLOOD_WAIT_PROTECT"])) 72 | await editor.cmd_ended(await sproc.wait()) 73 | del self.activecmds[hash_msg(message)] 74 | 75 | async def terminatecmd(self, message): 76 | """Use in reply to send SIGTERM to a process""" 77 | if not message.is_reply: 78 | await message.edit(_("Reply to a terminal command to terminate it.")) 79 | return 80 | if hash_msg(await message.get_reply_message()) in self.activecmds: 81 | try: 82 | self.activecmds[hash_msg(await message.get_reply_message())].terminate() 83 | except Exception: 84 | await message.edit(_("Could not kill!")) 85 | else: 86 | await message.edit(_("Killed!")) 87 | else: 88 | await message.edit(_("No command is running in that message.")) 89 | 90 | async def killcmd(self, message): 91 | """Use in reply to send SIGKILL to a process""" 92 | if not message.is_reply: 93 | await message.edit(_("Reply to a terminal command to kill it.")) 94 | return 95 | if hash_msg(await message.get_reply_message()) in self.activecmds: 96 | try: 97 | self.activecmds[hash_msg(await message.get_reply_message())].kill() 98 | except Exception: 99 | await message.edit(_("Could not kill!")) 100 | else: 101 | await message.edit(_("Killed!")) 102 | else: 103 | await message.edit(_("No command is running in that message.")) 104 | 105 | async def neocmd(self, message): 106 | """Show system stats via neofetch""" 107 | await self.runcmd(message, "neofetch --stdout", RawMessageEditor(message, "neofetch --stdout", self.config)) 108 | 109 | async def upcmd(self, message): 110 | """Show system uptime""" 111 | await self.runcmd(message, "uptime", RawMessageEditor(message, "uptime", self.config)) 112 | 113 | 114 | def hash_msg(message): 115 | return str(utils.get_chat_id(message)) + "/" + str(message.id) 116 | 117 | 118 | async def read_stream(func, stream, delay): 119 | last_task = None 120 | data = b"" 121 | while True: 122 | dat = (await stream.read(1)) 123 | if not dat: 124 | # EOF 125 | if last_task: 126 | # Send all pending data 127 | last_task.cancel() 128 | await func(data.decode("utf-8")) 129 | # If there is no last task there is inherently no data, so theres no point sending a blank string 130 | break 131 | data += dat 132 | if last_task: 133 | last_task.cancel() 134 | last_task = asyncio.ensure_future(sleep_for_task(func, data, delay)) 135 | 136 | 137 | async def sleep_for_task(func, data, delay): 138 | await asyncio.sleep(delay) 139 | await func(data.decode("utf-8")) 140 | 141 | 142 | class MessageEditor(): 143 | def __init__(self, message, command, config): 144 | self.message = message 145 | self.command = command 146 | self.stdout = "" 147 | self.stderr = "" 148 | self.rc = None 149 | self.redraws = 0 150 | self.config = config 151 | 152 | async def update_stdout(self, stdout): 153 | self.stdout = stdout 154 | await self.redraw() 155 | 156 | async def update_stderr(self, stderr): 157 | self.stderr = stderr 158 | await self.redraw() 159 | 160 | async def redraw(self, skip_wait=False): 161 | text = _("Running command: {}").format(utils.escape_html(self.command)) + "\n" 162 | if self.rc is not None: 163 | text += _("Process exited with code {}").format(utils.escape_html(str(self.rc))) 164 | text += "\n" + _("Stdout:") + "\n" 165 | text += utils.escape_html(self.stdout[max(len(self.stdout) - 2048, 0):]) + "\n\n" + _("Stderr:") + "\n" 166 | text += utils.escape_html(self.stderr[max(len(self.stdout) - 1024, 0):]) + "" 167 | try: 168 | await self.message.edit(text) 169 | except telethon.errors.rpcerrorlist.MessageNotModifiedError: 170 | pass 171 | except telethon.errors.rpcerrorlist.MessageTooLongError as e: 172 | logger.error(e) 173 | logger.error(text) 174 | # The message is never empty due to the template header 175 | 176 | async def cmd_ended(self, rc): 177 | self.rc = rc 178 | self.state = 4 179 | await self.redraw(True) 180 | 181 | def update_process(self, process): 182 | pass 183 | 184 | 185 | class SudoMessageEditor(MessageEditor): 186 | PASS_REQ = "[sudo] password for" 187 | WRONG_PASS = r"\[sudo\] password for (.*): Sorry, try again\." 188 | TOO_MANY_TRIES = r"\[sudo\] password for (.*): sudo: [0-9]+ incorrect password attempts" 189 | 190 | def __init__(self, message, command, config): 191 | super().__init__(message, command, config) 192 | self.process = None 193 | self.state = 0 194 | self.authmsg = None 195 | 196 | def update_process(self, process): 197 | logger.debug("got sproc obj %s", process) 198 | self.process = process 199 | 200 | async def update_stderr(self, stderr): 201 | logger.debug("stderr update " + stderr) 202 | self.stderr = stderr 203 | lines = stderr.strip().split("\n") 204 | lastline = lines[-1] 205 | lastlines = lastline.rsplit(" ", 1) 206 | handled = False 207 | if len(lines) > 1 and re.fullmatch(self.WRONG_PASS, 208 | lines[-2]) and lastlines[0] == self.PASS_REQ and self.state == 1: 209 | logger.debug("switching state to 0") 210 | await self.authmsg.edit(_("Authentication failed, please try again.")) 211 | self.state = 0 212 | handled = True 213 | await asyncio.sleep(2) 214 | await self.authmsg.delete() 215 | logger.debug("got here") 216 | if lastlines[0] == self.PASS_REQ and self.state == 0: 217 | logger.debug("Success to find sudo log!") 218 | text = r"" 221 | text += _("Interactive authentication required.") 222 | text += r"" 223 | try: 224 | await self.message.edit(text) 225 | except telethon.errors.rpcerrorlist.MessageNotModifiedError as e: 226 | logger.debug(e) 227 | logger.debug("edited message with link to self") 228 | self.authmsg = await self.message.client.send_message("me", _("Please edit this message to the password " 229 | + "for user {user} to run command {command}") 230 | .format(command="" 231 | + utils.escape_html(self.command) + "", 232 | user=utils.escape_html(lastlines[1][:-1]))) 233 | logger.debug("sent message to self") 234 | self.message.client.remove_event_handler(self.on_message_edited) 235 | self.message.client.add_event_handler(self.on_message_edited, 236 | telethon.events.messageedited.MessageEdited(chats=["me"])) 237 | logger.debug("registered handler") 238 | handled = True 239 | if len(lines) > 1 and (re.fullmatch(self.TOO_MANY_TRIES, lastline) 240 | and (self.state == 1 or self.state == 3 or self.state == 4)): 241 | logger.debug("password wrong lots of times") 242 | await self.message.edit(_("Authentication failed, please try again later.")) 243 | await self.authmsg.delete() 244 | self.state = 2 245 | handled = True 246 | if not handled: 247 | logger.debug("Didn't find sudo log.") 248 | if self.authmsg is not None: 249 | await self.authmsg.delete() 250 | self.authmsg = None 251 | self.state = 2 252 | await self.redraw() 253 | logger.debug(self.state) 254 | 255 | async def update_stdout(self, stdout): 256 | self.stdout = stdout 257 | if self.state != 2: 258 | self.state = 3 # Means that we got stdout only 259 | if self.authmsg is not None: 260 | await self.authmsg.delete() 261 | self.authmsg = None 262 | await self.redraw() 263 | 264 | async def on_message_edited(self, message): 265 | # Message contains sensitive information. 266 | if self.authmsg is None: 267 | return 268 | logger.debug("got message edit update in self " + str(message.id)) 269 | if hash_msg(message) == hash_msg(self.authmsg): 270 | # The user has provided interactive authentication. Send password to stdin for sudo. 271 | try: 272 | self.authmsg = await message.edit(_("Authenticating...")) 273 | except telethon.errors.rpcerrorlist.MessageNotModifiedError: 274 | # Try to clear personal info if the edit fails 275 | await message.delete() 276 | self.state = 1 277 | self.process.stdin.write(message.message.message.split("\n", 1)[0].encode("utf-8") + b"\n") 278 | 279 | 280 | class RawMessageEditor(SudoMessageEditor): 281 | def __init__(self, message, command, config, show_done=False): 282 | super().__init__(message, command, config) 283 | self.show_done = show_done 284 | 285 | async def redraw(self, skip_wait=False): 286 | logger.debug(self.rc) 287 | if self.rc is None: 288 | text = "" + utils.escape_html(self.stdout[max(len(self.stdout) - 4095, 0):]) + "" 289 | elif self.rc == 0: 290 | text = "" + utils.escape_html(self.stdout[max(len(self.stdout) - 4090, 0):]) + "" 291 | else: 292 | text = "" + utils.escape_html(self.stderr[max(len(self.stderr) - 4095, 0):]) + "" 293 | if self.rc is not None and self.show_done: 294 | text += "\n" + _("Done") 295 | logger.debug(text) 296 | try: 297 | await self.message.edit(text) 298 | except telethon.errors.rpcerrorlist.MessageNotModifiedError: 299 | pass 300 | except telethon.errors.rpcerrorlist.MessageEmptyError: 301 | pass 302 | except telethon.errors.rpcerrorlist.MessageTooLongError as e: 303 | logger.error(e) 304 | logger.error(text) 305 | -------------------------------------------------------------------------------- /transfersh.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | import requests 19 | import asyncio 20 | 21 | from .. import loader, utils 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def register(cb): 27 | cb(TransferShMod()) 28 | 29 | 30 | def sgen(agen, loop): 31 | while True: 32 | try: 33 | yield utils.run_async(loop, agen.__anext__()) 34 | except StopAsyncIteration: 35 | return 36 | 37 | 38 | class TransferShMod(loader.Module): 39 | """Upload to and from transfer.sh""" 40 | def __init__(self): 41 | self.config = loader.ModuleConfig("UPLOAD_URL", "https://transfer.sh/{}", "Url to PUT to") 42 | self.name = _("transfer.sh support") 43 | 44 | async def uploadshcmd(self, message): 45 | """Uploads to transfer.sh""" 46 | if message.file: 47 | msg = message 48 | else: 49 | msg = (await message.get_reply_message()) 50 | doc = msg.media 51 | if doc is None: 52 | await message.edit(_("Provide a file to upload")) 53 | return 54 | doc = message.client.iter_download(doc) 55 | logger.debug("begin transfer") 56 | await message.edit(_("Uploading...")) 57 | r = await utils.run_sync(requests.put, self.config["UPLOAD_URL"].format(msg.file.name), 58 | data=sgen(doc, asyncio.get_event_loop())) 59 | logger.debug(r) 60 | r.raise_for_status() 61 | logger.debug(r.headers) 62 | await message.edit(_("Uploaded!").format(r.text)) 63 | 64 | # This code doesn't work. 65 | # async def downloadshcmd(self, message): 66 | # """Downloads from transfer.sh""" 67 | # args = utils.get_args(message) 68 | # if len(args) < 1: 69 | # await message.edit("Provide a link to download") 70 | # return 71 | # url = args[0] 72 | # if url.startswith("http://"): 73 | # url = message.message.replace("http://", "https://", 1) 74 | # elif not url.startswith("https://"): 75 | # url = "https://" + url 76 | # if url.startswith("https://transfer.sh/"): 77 | # url = url.replace("https://transfer.sh/", "https://transfer.sh/get/", 1) 78 | # logger.error(url) 79 | # await utils.answer(message, url, asfile=True) 80 | -------------------------------------------------------------------------------- /translate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | from Yandex import Translate 21 | 22 | from .. import loader, utils 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def register(cb): 28 | cb(TranslateMod()) 29 | 30 | 31 | class TranslateMod(loader.Module): 32 | """Translator""" 33 | def __init__(self): 34 | self.commands = {"translate": self.translatecmd} 35 | self.config = loader.ModuleConfig("DEFAULT_LANG", "en", "Language to translate to by default", 36 | "API_KEY", "", "API key from https://translate.yandex.com/developers/keys") 37 | self.name = _("Translator") 38 | 39 | def config_complete(self): 40 | self.tr = Translate(self.config["API_KEY"]) 41 | 42 | async def translatecmd(self, message): 43 | """.translate [from_lang->][->to_lang] """ 44 | args = utils.get_args(message) 45 | 46 | if len(args) == 0 or "->" not in args[0]: 47 | text = " ".join(args) 48 | args = ["", self.config["DEFAULT_LANG"]] 49 | else: 50 | text = " ".join(args[1:]) 51 | args = args[0].split("->") 52 | 53 | if len(text) == 0 and message.is_reply: 54 | text = (await message.get_reply_message()).message 55 | if len(text) == 0: 56 | await message.edit(_("Invalid text to translate")) 57 | return 58 | if args[0] == "": 59 | args[0] = self.tr.detect(text) 60 | if len(args) == 3: 61 | del args[1] 62 | if len(args) == 1: 63 | logging.error("python split() error, if there is -> in the text, it must split!") 64 | raise RuntimeError() 65 | if args[1] == "": 66 | args[1] = self.config["DEFAULT_LANG"] 67 | args[0] = args[0].lower() 68 | logger.debug(args) 69 | translated = self.tr.translate(text, args[1], args[0]) 70 | ret = _("Translated {text}\nfrom {frlang} to " 71 | + "{to} and it reads\n{output}") 72 | ret = ret.format(text=utils.escape_html(text), frlang=utils.escape_html(args[0]), 73 | to=utils.escape_html(args[1]), output=utils.escape_html(translated)) 74 | await utils.answer(message, ret) 75 | -------------------------------------------------------------------------------- /tts.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from gtts import gTTS 18 | 19 | from io import BytesIO 20 | 21 | from .. import loader, utils 22 | 23 | 24 | def register(cb): 25 | cb(TTSMod()) 26 | 27 | 28 | class TTSMod(loader.Module): 29 | def __init__(self): 30 | self.name = "Text to speech" 31 | 32 | async def ttscmd(self, message): 33 | """Convert text to speech with Google APIs""" 34 | args = utils.get_args_raw(message) 35 | if not args: 36 | args = (await message.get_reply_message()).message 37 | 38 | tts = await utils.run_sync(gTTS, args) 39 | voice = BytesIO() 40 | tts.write_to_fp(voice) 41 | voice.seek(0) 42 | voice.name = "voice.mp3" 43 | 44 | await utils.answer(message, voice, voice_note=True) 45 | -------------------------------------------------------------------------------- /typer.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader, utils 18 | 19 | from telethon.errors.rpcerrorlist import MessageNotModifiedError 20 | 21 | import logging 22 | import asyncio 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def register(cb): 28 | cb(TyperMod()) 29 | 30 | 31 | @loader.tds 32 | class TyperMod(loader.Module): 33 | """Makes your messages type slower""" 34 | strings = {"name": "Typewriter", 35 | "no_message": "You can't type nothing!", 36 | "type_char_cfg_doc": "Character for typewriter"} 37 | 38 | def __init__(self): 39 | self.config = loader.ModuleConfig("TYPE_CHAR", "▒", lambda: self.strings["type_char_cfg_doc"]) 40 | self.name = self.strings["name"] 41 | 42 | async def typecmd(self, message): 43 | """.type """ 44 | a = utils.get_args_raw(message) 45 | if not a: 46 | await utils.answer(message, self.strings["no_message"]) 47 | return 48 | m = "" 49 | for c in a: 50 | m += self.config["TYPE_CHAR"] 51 | message = await update_message(message, m) 52 | await asyncio.sleep(0.04) 53 | m = m[:-1] + c 54 | message = await update_message(message, m) 55 | await asyncio.sleep(0.02) 56 | 57 | 58 | async def update_message(message, m): 59 | try: 60 | return await message.edit(m) 61 | except MessageNotModifiedError: 62 | return message # space doesnt count 63 | -------------------------------------------------------------------------------- /urbandictionary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | 21 | import asyncurban 22 | from .. import loader, utils 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def register(cb): 28 | cb(UrbanDictionaryMod()) 29 | 30 | 31 | class UrbanDictionaryMod(loader.Module): 32 | """Define word meaning using UrbanDictionary.""" 33 | def __init__(self): 34 | self.name = _("Urban Dictionary") 35 | self.urban = asyncurban.UrbanDictionary() 36 | 37 | async def urbancmd(self, message): 38 | """Define word meaning. Usage: 39 | .urban """ 40 | 41 | args = utils.get_args_raw(message) 42 | 43 | if not args: 44 | return await utils.answer(message, _("Provide a word(s) to define.")) 45 | 46 | try: 47 | definition = await self.urban.get_word(args) 48 | except asyncurban.WordNotFoundError: 49 | return await utils.answer(message, _("Couldn't find definition for that.")) 50 | 51 | await utils.answer(message, _("Text: {}\nMeaning: {}\nExample: " 52 | "{}").format(definition.word, definition.definition, definition.example)) 53 | -------------------------------------------------------------------------------- /userinfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | from .. import loader, utils 20 | import logging 21 | 22 | from telethon.tl.functions.users import GetFullUserRequest 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def register(cb): 28 | cb(UserInfoMod()) 29 | 30 | 31 | class UserInfoMod(loader.Module): 32 | """Tells you about people""" 33 | def __init__(self): 34 | self.name = _("User Info") 35 | 36 | async def userinfocmd(self, message): 37 | """Use in reply to get user info""" 38 | if message.is_reply: 39 | full = await self.client(GetFullUserRequest((await message.get_reply_message()).from_id)) 40 | else: 41 | args = utils.get_args(message) 42 | if not args: 43 | return await utils.answer(message, "No args or reply was provided.") 44 | try: 45 | full = await self.client(GetFullUserRequest(args[0])) 46 | except ValueError: 47 | return await utils.answer(message, _("Couldn't find that user.")) 48 | logger.debug(full) 49 | reply = _("First name: {}").format(utils.escape_html(ascii(full.user.first_name))) 50 | if full.user.last_name is not None: 51 | reply += _("\nLast name: {}").format(utils.escape_html(ascii(full.user.last_name))) 52 | reply += _("\nBio: {}").format(utils.escape_html(ascii(full.about))) 53 | reply += _("\nRestricted: {}").format(utils.escape_html(str(full.user.restricted))) 54 | reply += _("\nDeleted: {}").format(utils.escape_html(str(full.user.deleted))) 55 | reply += _("\nBot: {}").format(utils.escape_html(str(full.user.bot))) 56 | reply += _("\nVerified: {}").format(utils.escape_html(str(full.user.verified))) 57 | if full.user.photo: 58 | reply += _("\nDC ID: {}").format(utils.escape_html(str(full.user.photo.dc_id))) 59 | await message.edit(reply) 60 | 61 | async def permalinkcmd(self, message): 62 | """Get permalink to user based on ID or username""" 63 | args = utils.get_args(message) 64 | if len(args) < 1: 65 | await message.edit(_("Provide a user to locate")) 66 | return 67 | try: 68 | user = int(args[0]) 69 | except ValueError: 70 | user = args[0] 71 | try: 72 | user = await self.client.get_entity(user) 73 | except ValueError as e: 74 | logger.debug(e) 75 | # look for the user 76 | await message.edit(_("Searching for user...")) 77 | await self.client.get_dialogs() 78 | try: 79 | user = await self.client.get_entity(user) 80 | except ValueError: 81 | await message.edit(_("Can't find user.")) 82 | return 83 | if len(args) > 1: 84 | await utils.answer(message, "{txt}".format(uid=user.id, txt=args[1])) 85 | else: 86 | await message.edit(_("Permalink to {uid}").format(uid=user.id)) 87 | 88 | async def client_ready(self, client, db): 89 | self.client = client 90 | -------------------------------------------------------------------------------- /weather.py: -------------------------------------------------------------------------------- 1 | # -*- coding: future_fstrings -*- 2 | 3 | # Friendly Telegram (telegram userbot) 4 | # Copyright (C) 2018-2019 The Authors 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | import logging 20 | import pyowm 21 | import math 22 | 23 | from .. import loader, utils 24 | 25 | from ..utils import escape_html as eh 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | def register(cb): 31 | cb(WeatherMod()) 32 | 33 | 34 | def deg_to_text(deg): 35 | if deg is None: 36 | return _("unknown") 37 | return ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", 38 | "SW", "WSW", "W", "WNW", "NW", "NNW"][round(deg / 22.5) % 16] 39 | 40 | 41 | def round_to_sf(n, digits): 42 | return round(n, digits - 1 - int(math.floor(math.log10(abs(n))))) 43 | 44 | 45 | class WeatherMod(loader.Module): 46 | """Checks the weather 47 | Get an API key at https://openweathermap.org/appid""" 48 | def __init__(self): 49 | self.config = loader.ModuleConfig("DEFAULT_LOCATION", None, "OpenWeatherMap City ID", 50 | "API_KEY", None, "API Key from https://openweathermap.org/appid", 51 | "TEMP_UNITS", "celsius", "Temperature unit as English") 52 | self.name = _("Weather") 53 | self._owm = None 54 | 55 | def config_complete(self): 56 | self._owm = pyowm.OWM(self.config["API_KEY"]) 57 | 58 | async def weathercmd(self, message): 59 | """.weather [location]""" 60 | if self.config["API_KEY"] is None: 61 | await message.edit(_("Please provide an API key via the configuration mode.")) 62 | return 63 | args = utils.get_args_raw(message) 64 | func = None 65 | if not args: 66 | func = self._owm.weather_at_id 67 | args = [self.config["DEFAULT_LOCATION"]] 68 | else: 69 | try: 70 | args = [int(args)] 71 | func = self._owm.weather_at_id 72 | except ValueError: 73 | coords = utils.get_args_split_by(message, ",") 74 | if len(coords) == 2: 75 | try: 76 | args = [int(coord.strip()) for coord in coords] 77 | func = self._owm.weather_at_coords 78 | except ValueError: 79 | pass 80 | if func is None: 81 | func = self._owm.weather_at_place 82 | args = [args] 83 | logger.debug(func) 84 | logger.debug(args) 85 | w = await utils.run_sync(func, *args) 86 | logger.debug(_("Weather at {args} is {w}").format(args=args, w=w)) 87 | try: 88 | weather = w.get_weather() 89 | temp = weather.get_temperature(self.config["TEMP_UNITS"]) 90 | except ValueError: 91 | await message.edit(_("Invalid temperature units provided. Please reconfigure the module.")) 92 | return 93 | ret = _("Weather in {loc} is {w} with a high of {high} and a low of {low}, " 94 | + "averaging at {avg} with {humid}% humidity and a {ws}mph {wd} wind.") 95 | ret = ret.format(loc=eh(w.get_location().get_name()), w=eh(w.get_weather().get_detailed_status().lower()), 96 | high=eh(temp["temp_max"]), low=eh(temp["temp_min"]), avg=eh(temp["temp"]), 97 | humid=eh(weather.get_humidity()), 98 | ws=eh(round_to_sf(weather.get_wind("miles_hour")["speed"], 3)), 99 | wd=eh(deg_to_text(weather.get_wind().get("deg", None)))) 100 | await message.edit(ret) 101 | -------------------------------------------------------------------------------- /xda.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader 18 | import random 19 | 20 | 21 | def register(cb): 22 | cb(XDAMod()) 23 | 24 | 25 | RANDOM_WORDS = {"sur": 6, "Sir": 6, "bro": 6, "yes": 5, "no": 5, "bolte": 2, "bolit": 2, "bholit": 3, "volit": 3, 26 | "mustah": 4, "fap": 5, "lit": 3, "lmao": 6, "iz": 7, "jiosim": 8, "ijo": 4, "nut": 7, "workz": 4, 27 | "workang": 4, "flashabl zip": 6, "bateri": 6, "bacup": 6, "bad englis": 5, "sar": 5, "treble wen": 2, 28 | "gsi": 6, "fox bag": 3, "bag fox": 3, "fine": 4, "bast room": 5, "fax": 3, "trable": 3, "kenzo": 4, 29 | "plz make room": 3, "andreid pai": 2, "when": 4, "port": 5, "mtk": 3, "send moni": 3, "bad rom": 2, 30 | "dot": 4, "rr": 4, "linage": 4, "arrows": 4, "kernal": 4} 31 | 32 | # Workaround for 3.5 33 | WORDS_WEIGHTED = [word for word, count in RANDOM_WORDS.items() for i in range(count)] 34 | 35 | 36 | class XDAMod(loader.Module): 37 | """Gibes bholte bro""" 38 | def __init__(self): 39 | self.config = loader.ModuleConfig("XDA_RANDOM_WORDS", RANDOM_WORDS, "Random words from XDA as dict & weight") 40 | self.name = "XDA" 41 | 42 | async def xdacmd(self, message): 43 | """Send random XDA posts""" 44 | length = random.randint(3, 10) 45 | # Workaround for 3.5 46 | string = [random.choice(WORDS_WEIGHTED) for dummy in range(length)] 47 | 48 | # Unsupported in python 3.5 49 | # string = random.choices(self.config["XDA_RANDOM_WORDS"], weights=self.config["XDA_WEIGHT_WORDS"], k=length) 50 | 51 | random.shuffle(string) 52 | await message.edit(" ".join(string)) 53 | -------------------------------------------------------------------------------- /yesno.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2019 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from .. import loader 18 | import random 19 | 20 | 21 | def register(cb): 22 | cb(YesNoMod()) 23 | 24 | 25 | class YesNoMod(loader.Module): 26 | """Helps you make important life choices""" 27 | def __init__(self): 28 | self.name = _("YesNo") 29 | 30 | async def yesnocmd(self, message): 31 | """Make a life choice""" 32 | # TODO translate 33 | yes = ["Yes", "Yup", "Absolutely", "Non't"] 34 | no = ["No", "Nope", "Nah", "Yesn't"] 35 | if random.getrandbits(1): 36 | await message.edit(random.choice(yes)) 37 | else: 38 | await message.edit(random.choice(no)) 39 | --------------------------------------------------------------------------------