From 6142c2a5e5303f9bdf17ab4ae3f7687438b106a5 Mon Sep 17 00:00:00 2001 From: yasir Date: Thu, 1 Dec 2022 22:27:03 +0700 Subject: [PATCH] Rilis Pertama --- .github/workflows/notify.yml | 26 + .github/workflows/pylint.yml | 32 ++ Calistoga-Regular.ttf | Bin 0 -> 128660 bytes Dockerfile | 12 + LICENSE | 339 ++++++++++++ README.md | 64 +++ config.env.sample | 7 + database/__init__.py | 12 + database/afk_db.py | 31 ++ database/karma_db.py | 66 +++ database/users_chats_db.py | 105 ++++ database/warn_db.py | 53 ++ img/bg.png | Bin 0 -> 21728 bytes img/profilepic.png | Bin 0 -> 10264 bytes logging.conf | 32 ++ misskaty/__init__.py | 31 ++ misskaty/__main__.py | 342 ++++++++++++ misskaty/core/decorator/errors.py | 59 ++ misskaty/core/decorator/permissions.py | 109 ++++ misskaty/core/keyboard.py | 26 + misskaty/helper/__init__.py | 1 + misskaty/helper/ffmpeg_helper.py | 61 +++ misskaty/helper/files.py | 82 +++ misskaty/helper/functions.py | 109 ++++ misskaty/helper/http.py | 78 +++ misskaty/helper/human_read.py | 54 ++ misskaty/helper/media_helper.py | 67 +++ misskaty/helper/misc.py | 82 +++ misskaty/helper/pyro_progress.py | 75 +++ misskaty/helper/stickerset.py | 73 +++ misskaty/helper/time_gap.py | 18 + misskaty/helper/tools.py | 83 +++ misskaty/helper/ytdl_helper.py | 42 ++ misskaty/plugins/__init__.py | 45 ++ misskaty/plugins/__main__.py | 0 misskaty/plugins/admin.py | 715 +++++++++++++++++++++++++ misskaty/plugins/afk.py | 175 ++++++ misskaty/plugins/auto_approve.py | 44 ++ misskaty/plugins/auto_forwarder.py | 93 ++++ misskaty/plugins/banned.py | 40 ++ misskaty/plugins/broadcast.py | 44 ++ misskaty/plugins/bypass.py | 73 +++ misskaty/plugins/code_tester.py | 710 ++++++++++++++++++++++++ misskaty/plugins/copy_forward.py | 77 +++ misskaty/plugins/detect_afk.py | 220 ++++++++ misskaty/plugins/dev.py | 144 +++++ misskaty/plugins/download_upload.py | 198 +++++++ misskaty/plugins/filter_request.py | 199 +++++++ misskaty/plugins/genss.py | 142 +++++ misskaty/plugins/grup_tools.py | 347 ++++++++++++ misskaty/plugins/inkick_user.py | 233 ++++++++ misskaty/plugins/inline_search.py | 607 +++++++++++++++++++++ misskaty/plugins/json.py | 25 + misskaty/plugins/karma.py | 201 +++++++ misskaty/plugins/mediainfo.py | 81 +++ misskaty/plugins/memify.py | 154 ++++++ misskaty/plugins/misc_tools.py | 645 ++++++++++++++++++++++ misskaty/plugins/nightmode.py | 213 ++++++++ misskaty/plugins/ocr.py | 38 ++ misskaty/plugins/paste.py | 195 +++++++ misskaty/plugins/ping.py | 55 ++ misskaty/plugins/quotly.py | 280 ++++++++++ misskaty/plugins/scrapwebsite.py | 417 ++++++++++++++ misskaty/plugins/sed.py | 53 ++ misskaty/plugins/sub_extractor.py | 150 ++++++ misskaty/plugins/tes_session.py | 15 + misskaty/plugins/ubot_plugin.py | 180 +++++++ misskaty/plugins/webss.py | 76 +++ misskaty/plugins/ytdl_download.py | 386 +++++++++++++ misskaty/vars.py | 69 +++ utils.py | 100 ++++ 71 files changed, 9610 insertions(+) create mode 100644 .github/workflows/notify.yml create mode 100644 .github/workflows/pylint.yml create mode 100644 Calistoga-Regular.ttf create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.env.sample create mode 100644 database/__init__.py create mode 100644 database/afk_db.py create mode 100644 database/karma_db.py create mode 100644 database/users_chats_db.py create mode 100644 database/warn_db.py create mode 100644 img/bg.png create mode 100644 img/profilepic.png create mode 100644 logging.conf create mode 100644 misskaty/__init__.py create mode 100644 misskaty/__main__.py create mode 100644 misskaty/core/decorator/errors.py create mode 100644 misskaty/core/decorator/permissions.py create mode 100644 misskaty/core/keyboard.py create mode 100644 misskaty/helper/__init__.py create mode 100644 misskaty/helper/ffmpeg_helper.py create mode 100644 misskaty/helper/files.py create mode 100644 misskaty/helper/functions.py create mode 100644 misskaty/helper/http.py create mode 100644 misskaty/helper/human_read.py create mode 100644 misskaty/helper/media_helper.py create mode 100644 misskaty/helper/misc.py create mode 100644 misskaty/helper/pyro_progress.py create mode 100644 misskaty/helper/stickerset.py create mode 100644 misskaty/helper/time_gap.py create mode 100644 misskaty/helper/tools.py create mode 100644 misskaty/helper/ytdl_helper.py create mode 100644 misskaty/plugins/__init__.py create mode 100644 misskaty/plugins/__main__.py create mode 100644 misskaty/plugins/admin.py create mode 100644 misskaty/plugins/afk.py create mode 100644 misskaty/plugins/auto_approve.py create mode 100644 misskaty/plugins/auto_forwarder.py create mode 100644 misskaty/plugins/banned.py create mode 100644 misskaty/plugins/broadcast.py create mode 100644 misskaty/plugins/bypass.py create mode 100644 misskaty/plugins/code_tester.py create mode 100644 misskaty/plugins/copy_forward.py create mode 100644 misskaty/plugins/detect_afk.py create mode 100644 misskaty/plugins/dev.py create mode 100644 misskaty/plugins/download_upload.py create mode 100644 misskaty/plugins/filter_request.py create mode 100644 misskaty/plugins/genss.py create mode 100644 misskaty/plugins/grup_tools.py create mode 100644 misskaty/plugins/inkick_user.py create mode 100644 misskaty/plugins/inline_search.py create mode 100644 misskaty/plugins/json.py create mode 100644 misskaty/plugins/karma.py create mode 100644 misskaty/plugins/mediainfo.py create mode 100644 misskaty/plugins/memify.py create mode 100644 misskaty/plugins/misc_tools.py create mode 100644 misskaty/plugins/nightmode.py create mode 100644 misskaty/plugins/ocr.py create mode 100644 misskaty/plugins/paste.py create mode 100644 misskaty/plugins/ping.py create mode 100644 misskaty/plugins/quotly.py create mode 100644 misskaty/plugins/scrapwebsite.py create mode 100644 misskaty/plugins/sed.py create mode 100644 misskaty/plugins/sub_extractor.py create mode 100644 misskaty/plugins/tes_session.py create mode 100644 misskaty/plugins/ubot_plugin.py create mode 100644 misskaty/plugins/webss.py create mode 100644 misskaty/plugins/ytdl_download.py create mode 100644 misskaty/vars.py create mode 100644 utils.py diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml new file mode 100644 index 00000000..cea9f59a --- /dev/null +++ b/.github/workflows/notify.yml @@ -0,0 +1,26 @@ +name: Notify on Telegram + +on: + fork: + push: + release: + issue_comment: + types: created + watch: + types: started + pull_request_review_comment: + types: created + pull_request: + types: [opened, closed, reopened] + issues: + types: [opened, pinned, closed, reopened] +jobs: + notify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Notify the commit on Telegram. + uses: EverythingSuckz/github-telegram-notify@main + with: + bot_token: '${{ secrets.BOT_TOKEN }}' + chat_id: '${{ secrets.CHAT_ID }}' diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 00000000..4fc59685 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,32 @@ +name: PyLint + +on: [pull_request, workflow_dispatch] + +jobs: + Pylint_Fix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install Python lint libraries + run: | + pip install autoflake black + - name: Remove unused imports and variables + run: | + autoflake --in-place --recursive --exclude "__main__.py" --remove-all-unused-imports --remove-unused-variables --ignore-init-module-imports . + - name: lint with black + run: | + black --exclude "__main__.py" --line-length 250 --target-version py310 . + # commit changes + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: 'reformating: code' + commit_options: '--no-verify' + repository: . + commit_user_name: yasirarism + commit_user_email: mail@yasir.eu.org + commit_author: yasirarism diff --git a/Calistoga-Regular.ttf b/Calistoga-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6ba77fb3765a5dc7513f9b84c6f295eda9900510 GIT binary patch literal 128660 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 L{>XKbtIXdYPLkR9 literal 0 HcmV?d00001 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c02df7e3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +# * @author Yasir Aris M +# * @date 2022-12-01 09:12:27 +# * @lastModified 2022-12-01 09:27:31 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved + +# Base Docker +FROM yasirarism/misskaty-docker:latest + +COPY . . +# Set CMD Bot +CMD ["python3", "-m", "misskaty"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3943ec99 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +

+ MissKaty Bot based on Pyrogram +

+ +## NOTES +This repo has many bugs and i dont have time to fix it. If you can help me, please open pull request (PR). + +## Demo +You can check demo this repo in this bot [MissKatyPyro](https://t.me/MissKatyRoBot) + +## Features +I'm forget about it, try asking doraemon maybe know.. :) + +## Variables + +### Required Variables +* `BOT_TOKEN`: Create a bot using [@BotFather](https://telegram.dog/BotFather), and get the Telegram API token. +* `API_ID`: Get this value from [telegram.org](https://my.telegram.org/apps) +* `API_HASH`: Get this value from [telegram.org](https://my.telegram.org/apps) +* `ADMINS`: Username or ID of Admin. Separate multiple Admins by space +* `DATABASE_URI`: [mongoDB](https://www.mongodb.com) URI. Get this value from [mongoDB](https://www.mongodb.com). For more help watch this [video](https://youtu.be/1G1XwEOnxxo) +* `DATABASE_NAME`: Name of the database in [mongoDB](https://www.mongodb.com). For more help watch this [video](https://youtu.be/1G1XwEOnxxo) +* `LOG_CHANNEL` : A channel to log the activities of bot. Make sure bot is an admin in the channel. +### Optional Variables +Check by yourself for optional vars. + +## Deploy (Must Use Docker to Run This Bot) + +- Start Docker daemon (Skip if already running): +``` +sudo dockerd +``` +- Build Docker image: +``` +sudo docker build . -t misskaty +``` +- Run the image: +``` +sudo docker run misskaty +``` +- To stop the image: +``` +sudo docker ps +``` +``` +sudo docker stop id +``` + +---- + + +## Thanks to + - Thanks To Allah Swt. + - Thanks To Dan For His Awesome [Library](https://github.com/pyrogram/pyrogram). + - Thanks To [The Hamker Cat](https://github.com/TheHamkerCat) For Some Code. + - Thanks To [Team Yukki](https://github.com/TeamYukki) For Some Code. + - Thanks To [Wrench](https://github.com/EverythingSuckz) For Some Code. + - And All People Who Help Me In My Life... + If your code used in this repo and want to give credit please open issue.. + +## Disclaimer +[![GNU Affero General Public License 2.0](https://www.gnu.org/graphics/agplv3-155x51.png)](https://www.gnu.org/licenses/agpl-3.0.en.html#header) +Licensed under [GNU AGPL 2.0.](https://github.com/yasirarism/MissKatyPyro/blob/master/LICENSE) +Selling The Codes To Other People For Money Is *Strictly Prohibited*. God always sees you. diff --git a/config.env.sample b/config.env.sample new file mode 100644 index 00000000..fac7aa6f --- /dev/null +++ b/config.env.sample @@ -0,0 +1,7 @@ +API_HASH= +DATABASE_NAME= +API_ID= +SUPPORT_CHAT=YasirPediaChannel +DATABASE_URI=mongodb+srv:// +BOT_TOKEN= +LOG_CHANNEL=- diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 00000000..94a04c6d --- /dev/null +++ b/database/__init__.py @@ -0,0 +1,12 @@ +""" + * @author yasir + * @date 2022-09-06 10:12:09 + * @lastModified 2022-12-01 09:34:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +from motor.motor_asyncio import AsyncIOMotorClient as MongoClient +from misskaty.vars import DATABASE_URI + +mongo = MongoClient(DATABASE_URI) +dbname = mongo.MissKatyDB diff --git a/database/afk_db.py b/database/afk_db.py new file mode 100644 index 00000000..73cc890e --- /dev/null +++ b/database/afk_db.py @@ -0,0 +1,31 @@ +from database import dbname + +usersdb = dbname.users + + +async def is_afk(user_id: int) -> bool: + user = await usersdb.find_one({"user_id": user_id}) + return (True, user["reason"]) if user else (False, {}) + + +async def add_afk(user_id: int, mode): + await usersdb.update_one({"user_id": user_id}, {"$set": { + "reason": mode + }}, + upsert=True) + + +async def remove_afk(user_id: int): + user = await usersdb.find_one({"user_id": user_id}) + if user: + return await usersdb.delete_one({"user_id": user_id}) + + +async def get_afk_users() -> list: + users = usersdb.find({"user_id": {"$gt": 0}}) + if not users: + return [] + users_list = [] + for user in await users.to_list(length=1000000000): + users_list.append(user) + return users_list diff --git a/database/karma_db.py b/database/karma_db.py new file mode 100644 index 00000000..20b71c65 --- /dev/null +++ b/database/karma_db.py @@ -0,0 +1,66 @@ +from typing import Dict, Union +from misskaty.helper.functions import int_to_alpha +from database import dbname + +karmadb = dbname.karma + + +async def get_karmas_count() -> dict: + chats_count = 0 + karmas_count = 0 + async for chat in karmadb.find({"chat_id": {"$lt": 0}}): + for i in chat["karma"]: + karma_ = chat["karma"][i]["karma"] + if karma_ > 0: + karmas_count += karma_ + chats_count += 1 + return {"chats_count": chats_count, "karmas_count": karmas_count} + + +async def user_global_karma(user_id) -> int: + total_karma = 0 + async for chat in karmadb.find({"chat_id": {"$lt": 0}}): + karma = await get_karma(chat["chat_id"], await int_to_alpha(user_id)) + if karma and (int(karma["karma"]) > 0): + total_karma += int(karma["karma"]) + return total_karma + + +async def get_karmas(chat_id: int) -> Dict[str, int]: + karma = await karmadb.find_one({"chat_id": chat_id}) + return karma["karma"] if karma else {} + + +async def get_karma(chat_id: int, name: str) -> Union[bool, dict]: + name = name.lower().strip() + karmas = await get_karmas(chat_id) + if name in karmas: + return karmas[name] + + +async def update_karma(chat_id: int, name: str, karma: dict): + name = name.lower().strip() + karmas = await get_karmas(chat_id) + karmas[name] = karma + await karmadb.update_one( + {"chat_id": chat_id}, {"$set": {"karma": karmas}}, upsert=True + ) + + +async def is_karma_on(chat_id: int) -> bool: + chat = await karmadb.find_one({"chat_id_toggle": chat_id}) + return bool(chat) + + +async def karma_on(chat_id: int): + is_karma = await is_karma_on(chat_id) + if is_karma: + return + return await karmadb.delete_one({"chat_id_toggle": chat_id}) + + +async def karma_off(chat_id: int): + is_karma = await is_karma_on(chat_id) + if not is_karma: + return + return await karmadb.insert_one({"chat_id_toggle": chat_id}) diff --git a/database/users_chats_db.py b/database/users_chats_db.py new file mode 100644 index 00000000..03474408 --- /dev/null +++ b/database/users_chats_db.py @@ -0,0 +1,105 @@ +import motor.motor_asyncio +from misskaty.vars import DATABASE_NAME, DATABASE_URI + + +class Database: + def __init__(self, uri, database_name): + self._client = motor.motor_asyncio.AsyncIOMotorClient(uri) + self.db = self._client[database_name] + self.col = self.db.users + self.grp = self.db.groups + + def new_user(self, id, name): + return dict( + id=id, + name=name, + ban_status=dict( + is_banned=False, + ban_reason="", + ), + ) + + def new_group(self, id, title): + return dict( + id=id, + title=title, + chat_status=dict( + is_disabled=False, + reason="", + ), + ) + + async def add_user(self, id, name): + user = self.new_user(id, name) + await self.col.insert_one(user) + + async def is_user_exist(self, id): + user = await self.col.find_one({"id": int(id)}) + return bool(user) + + async def total_users_count(self): + return await self.col.count_documents({}) + + async def remove_ban(self, id): + ban_status = dict(is_banned=False, ban_reason="") + await self.col.update_one({"id": id}, {"$set": {"ban_status": ban_status}}) + + async def ban_user(self, user_id, ban_reason="No Reason"): + ban_status = dict(is_banned=True, ban_reason=ban_reason) + await self.col.update_one({"id": user_id}, {"$set": {"ban_status": ban_status}}) + + async def get_ban_status(self, id): + default = dict(is_banned=False, ban_reason="") + user = await self.col.find_one({"id": int(id)}) + return user.get("ban_status", default) if user else default + + async def get_all_users(self): + return self.col.find({}) + + async def delete_user(self, user_id): + await self.col.delete_many({"id": int(user_id)}) + + async def get_banned(self): + users = self.col.find({"ban_status.is_banned": True}) + chats = self.grp.find({"chat_status.is_disabled": True}) + b_chats = [chat["id"] async for chat in chats] + b_users = [user["id"] async for user in users] + return b_users, b_chats + + async def add_chat(self, chat, title): + chat = self.new_group(chat, title) + await self.grp.insert_one(chat) + + async def get_chat(self, chat): + chat = await self.grp.find_one({"id": int(chat)}) + return chat.get("chat_status") if chat else False + + async def re_enable_chat(self, id): + chat_status = dict( + is_disabled=False, + reason="", + ) + await self.grp.update_one( + {"id": int(id)}, {"$set": {"chat_status": chat_status}} + ) + + async def disable_chat(self, chat, reason="No Reason"): + chat_status = dict( + is_disabled=True, + reason=reason, + ) + await self.grp.update_one( + {"id": int(chat)}, {"$set": {"chat_status": chat_status}} + ) + + async def total_chat_count(self): + return await self.grp.count_documents({}) + + async def get_all_chats(self): + return self.grp.find({}) + + async def get_db_size(self): + return (await self.db.command("dbstats"))["dataSize"] + + +db = Database(DATABASE_URI, DATABASE_NAME) diff --git a/database/warn_db.py b/database/warn_db.py new file mode 100644 index 00000000..953f7779 --- /dev/null +++ b/database/warn_db.py @@ -0,0 +1,53 @@ +from typing import Dict, Union +from database import dbname + +warnsdb = dbname.warn + + +async def get_warns_count() -> dict: + chats_count = 0 + warns_count = 0 + async for chat in warnsdb.find({"chat_id": {"$lt": 0}}): + for user in chat["warns"]: + warns_count += chat["warns"][user]["warns"] + chats_count += 1 + return {"chats_count": chats_count, "warns_count": warns_count} + + +async def get_warns(chat_id: int) -> Dict[str, int]: + warns = await warnsdb.find_one({"chat_id": chat_id}) + return warns["warns"] if warns else {} + + +async def get_warn(chat_id: int, name: str) -> Union[bool, dict]: + name = name.lower().strip() + warns = await get_warns(chat_id) + if name in warns: + return warns[name] + + +async def add_warn(chat_id: int, name: str, warn: dict): + name = name.lower().strip() + warns = await get_warns(chat_id) + warns[name] = warn + + await warnsdb.update_one({"chat_id": chat_id}, {"$set": { + "warns": warns + }}, + upsert=True) + + +async def remove_warns(chat_id: int, name: str) -> bool: + warnsd = await get_warns(chat_id) + name = name.lower().strip() + if name in warnsd: + del warnsd[name] + await warnsdb.update_one( + {"chat_id": chat_id}, + {"$set": { + "warns": warnsd + }}, + upsert=True, + ) + return True + return False diff --git a/img/bg.png b/img/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..39bec0ef83ed2c1c89c721f3cccc9e191d06f193 GIT binary patch literal 21728 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 + * @date 2022-12-01 09:12:27 + * @lastModified 2022-12-01 09:33:16 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved + """ +import asyncio, importlib, re, logging +from misskaty import app, user, HELPABLE +from misskaty.plugins import ALL_MODULES +from misskaty.helper import paginate_modules +from misskaty.helper.tools import bot_sys_stats +from database.users_chats_db import db +from misskaty.vars import LOG_CHANNEL +from utils import temp +from logging import info as log_info +from pyrogram.raw.all import layer +from pyrogram import idle, __version__, filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +loop = asyncio.get_event_loop() + + +# Run Bot +async def start_bot(): + global HELPABLE + + for module in ALL_MODULES: + imported_module = importlib.import_module(f"misskaty.plugins.{module}") + if hasattr(imported_module, "__MODULE__") and imported_module.__MODULE__: + imported_module.__MODULE__ = imported_module.__MODULE__ + if hasattr(imported_module, "__HELP__") and imported_module.__HELP__: + HELPABLE[imported_module.__MODULE__.lower()] = imported_module + bot_modules = "" + j = 1 + for i in ALL_MODULES: + if j == 4: + bot_modules += "|{:<15}|\n".format(i) + j = 0 + else: + bot_modules += "|{:<15}".format(i) + j += 1 + await app.start() + await user.start() + me = await app.get_me() + ubot = await user.get_me() + log_info("+===============================================================+") + log_info("| MissKatyPyro |") + log_info("+===============+===============+===============+===============+") + log_info(bot_modules) + log_info("+===============+===============+===============+===============+") + log_info(f"[INFO]: BOT STARTED AS @{me.username}!") + + try: + log_info("[INFO]: SENDING ONLINE STATUS") + await app.send_message( + 617426792, + f"USERBOT AND BOT STARTED with Pyrogram v{__version__}..\nUserBot: {ubot.first_name}\nBot: {me.first_name}\n\nwith Pyrogram v{__version__} (Layer {layer}) started on @{me.username}.", + ) + except Exception: + pass + + await idle() + await app.stop() + await user.stop() + print("[INFO]: Bye!") + + +home_keyboard_pm = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(text="Commands ā“", callback_data="bot_commands"), + InlineKeyboardButton( + text="Github Repo šŸ› ", + url="https://github.com/yasirarism/MissKatyPyro", + ), + ], + [ + InlineKeyboardButton( + text="System Stats šŸ–„", + callback_data="stats_callback", + ), + InlineKeyboardButton(text="Dev šŸ‘Ø", url="https://t.me/YasirArisM"), + ], + [ + InlineKeyboardButton( + text="Add Me To Your Group šŸŽ‰", + url="http://t.me/MissKatyRoBot?startgroup=new", + ) + ], + ] +) + +home_text_pm = f"Hey there! My name is MissKatyRoBot. I have many useful features for you, feel free to add me to your group.\n\nIf you want give coffee to my owner you can send /donate command for more info." + +keyboard = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(text="Help ā“", url="t.me/MissKatyRoBot?start=help"), + InlineKeyboardButton( + text="Github Repo ļæ½", + url="https://github.com/yasirarism/MissKatyPyro", + ), + ], + [ + InlineKeyboardButton( + text="System Stats šŸ’»", + callback_data="stats_callback", + ), + InlineKeyboardButton(text="Dev šŸ‘Ø", url="https://t.me/YasirArisM"), + ], + ] +) + + +@app.on_message(filters.command("start")) +async def start(_, message): + if message.chat.type.value != "private": + if not await db.get_chat(message.chat.id): + total = await app.get_chat_members_count(message.chat.id) + await app.send_message( + LOG_CHANNEL, + f"#NewGroup\nGroup = {message.chat.title}({message.chat.id})\nMembers Count = {total}\nAdded by - Unknown", + ) + + await db.add_chat(message.chat.id, message.chat.title) + nama = ( + message.from_user.mention + if message.from_user + else message.sender_chat.title + ) + return await message.reply_photo( + photo="https://telegra.ph/file/90e9a448bc2f8b055b762.jpg", + caption=f"Hi {nama}, Pm Me For More Info About Me.", + reply_markup=keyboard, + ) + if not await db.is_user_exist(message.from_user.id): + await db.add_user(message.from_user.id, message.from_user.first_name) + await app.send_message( + LOG_CHANNEL, + f"#NewUser\nID - {message.from_user.id}\nName - {message.from_user.mention}", + ) + + if len(message.text.split()) > 1: + name = (message.text.split(None, 1)[1]).lower() + if "_" in name: + module = name.split("_", 1)[1] + text = ( + f"Here is the help for **{HELPABLE[module].__MODULE__}**:\n" + + HELPABLE[module].__HELP__ + ) + await message.reply(text, disable_web_page_preview=True) + elif name == "help": + text, keyb = await help_parser(message.from_user.first_name) + await message.reply( + text, + reply_markup=keyb, + ) + else: + await message.reply_photo( + photo="https://telegra.ph/file/90e9a448bc2f8b055b762.jpg", + caption=home_text_pm, + reply_markup=home_keyboard_pm, + ) + + +@app.on_callback_query(filters.regex("bot_commands")) +async def commands_callbacc(_, CallbackQuery): + text, keyboard = await help_parser(CallbackQuery.from_user.mention) + await app.send_message( + CallbackQuery.message.chat.id, + text=text, + reply_markup=keyboard, + ) + + await CallbackQuery.message.delete() + + +@app.on_callback_query(filters.regex("stats_callback")) +async def stats_callbacc(_, CallbackQuery): + text = await bot_sys_stats() + await app.answer_callback_query(CallbackQuery.id, text, show_alert=True) + + +@app.on_message(filters.command("help")) +async def help_command(_, message): + if message.chat.type.value != "private": + if not await db.get_chat(message.chat.id): + total = await app.get_chat_members_count(message.chat.id) + await app.send_message( + LOG_CHANNEL, + f"#NewGroup\nGroup = {message.chat.title}({message.chat.id})\nMembers Count = {total}\nAdded by - Unknown", + ) + + await db.add_chat(message.chat.id, message.chat.title) + if len(message.command) >= 2: + name = (message.text.split(None, 1)[1]).replace(" ", "_").lower() + if str(name) in HELPABLE: + key = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text="Click here", + url=f"t.me/MissKatyRoBot?start=help_{name}", + ) + ], + ] + ) + await message.reply( + f"Click on the below button to get help about {name}", + reply_markup=key, + ) + else: + await message.reply("PM Me For More Details.", reply_markup=keyboard) + else: + await message.reply("Pm Me For More Details.", reply_markup=keyboard) + else: + if not await db.is_user_exist(message.from_user.id): + await db.add_user(message.from_user.id, message.from_user.first_name) + await app.send_message( + LOG_CHANNEL, + f"#NewUser\nID - {message.from_user.id}\nName - {message.from_user.mention}", + ) + + if len(message.command) >= 2: + name = (message.text.split(None, 1)[1]).replace(" ", "_").lower() + if str(name) in HELPABLE: + text = ( + f"Here is the help for **{HELPABLE[name].__MODULE__}**:\n" + + HELPABLE[name].__HELP__ + ) + await message.reply(text, disable_web_page_preview=True) + else: + text, help_keyboard = await help_parser(message.from_user.first_name) + await message.reply( + text, + reply_markup=help_keyboard, + disable_web_page_preview=True, + ) + else: + text, help_keyboard = await help_parser(message.from_user.first_name) + await message.reply( + text, reply_markup=help_keyboard, disable_web_page_preview=True + ) + return + + +async def help_parser(name, keyboard=None): + if not keyboard: + keyboard = InlineKeyboardMarkup(paginate_modules(0, HELPABLE, "help")) + return ( + """Hello {first_name}, My name is {bot_name}. +I'm a bot with some useful features. +You can choose an option below, by clicking a button. + +If you want give coffee to my owner you can send /donate command for more info. +""".format( + first_name=name, + bot_name="MissKaty", + ), + keyboard, + ) + + +@app.on_callback_query(filters.regex(r"help_(.*?)")) +async def help_button(client, query): + home_match = re.match(r"help_home\((.+?)\)", query.data) + mod_match = re.match(r"help_module\((.+?)\)", query.data) + prev_match = re.match(r"help_prev\((.+?)\)", query.data) + next_match = re.match(r"help_next\((.+?)\)", query.data) + back_match = re.match(r"help_back", query.data) + create_match = re.match(r"help_create", query.data) + top_text = f""" +Hello {query.from_user.first_name}, My name is MissKaty. +I'm a bot with some usefule features. +You can choose an option below, by clicking a button below. + +General command are: + - /start: Start the bot + - /help: Give this message + """ + if mod_match: + module = mod_match[1].replace(" ", "_") + text = f"Here is the help for **{HELPABLE[module].__MODULE__}**:\n{HELPABLE[module].__HELP__}" + + await query.message.edit( + text=text, + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("back", callback_data="help_back")]] + ), + disable_web_page_preview=True, + ) + elif home_match: + await app.send_message( + query.from_user.id, + text=home_text_pm, + reply_markup=home_keyboard_pm, + ) + await query.message.delete() + elif prev_match: + curr_page = int(prev_match[1]) + await query.message.edit( + text=top_text, + reply_markup=InlineKeyboardMarkup( + paginate_modules(curr_page - 1, HELPABLE, "help") + ), + disable_web_page_preview=True, + ) + + elif next_match: + next_page = int(next_match[1]) + await query.message.edit( + text=top_text, + reply_markup=InlineKeyboardMarkup( + paginate_modules(next_page + 1, HELPABLE, "help") + ), + disable_web_page_preview=True, + ) + + elif back_match: + await query.message.edit( + text=top_text, + reply_markup=InlineKeyboardMarkup(paginate_modules(0, HELPABLE, "help")), + disable_web_page_preview=True, + ) + + elif create_match: + text, keyboard = await help_parser(query) + await query.message.edit( + text=text, + reply_markup=keyboard, + disable_web_page_preview=True, + ) + + return await client.answer_callback_query(query.id) + + +if __name__ == "__main__": + try: + loop.run_until_complete(start_bot()) + except KeyboardInterrupt: + logging.info("----------------------- Service Stopped -----------------------") diff --git a/misskaty/core/decorator/errors.py b/misskaty/core/decorator/errors.py new file mode 100644 index 00000000..bd7d6650 --- /dev/null +++ b/misskaty/core/decorator/errors.py @@ -0,0 +1,59 @@ +import traceback, asyncio +from functools import wraps +from pyrogram.errors.exceptions.forbidden_403 import ChatWriteForbidden +from misskaty.vars import LOG_CHANNEL +from misskaty import app + + +def asyncify(func): + async def inner(*args, **kwargs): + loop = asyncio.get_running_loop() + func_out = await loop.run_in_executor(None, func, *args, **kwargs) + return func_out + + return inner + + +def split_limits(text): + if len(text) < 2048: + return [text] + + lines = text.splitlines(True) + small_msg = "" + result = [] + for line in lines: + if len(small_msg) + len(line) < 2048: + small_msg += line + else: + result.append(small_msg) + small_msg = line + result.append(small_msg) + + return result + + +def capture_err(func): + @wraps(func) + async def capture(client, message, *args, **kwargs): + try: + return await func(client, message, *args, **kwargs) + except ChatWriteForbidden: + await app.leave_chat(message.chat.id) + return + except Exception as err: + exc = traceback.format_exc() + error_feedback = split_limits( + "**ERROR** | `{}` | `{}`\n\n```{}```\n\n```{}```\n".format( + message.from_user.id if message.from_user else 0, + message.chat.id if message.chat else 0, + message.text or message.caption, + exc, + ) + ) + + for x in error_feedback: + await app.send_message(LOG_CHANNEL, x) + await message.reply(x) + raise err + + return capture diff --git a/misskaty/core/decorator/permissions.py b/misskaty/core/decorator/permissions.py new file mode 100644 index 00000000..dcd5974f --- /dev/null +++ b/misskaty/core/decorator/permissions.py @@ -0,0 +1,109 @@ +from functools import wraps +from traceback import format_exc as err +from pyrogram.errors.exceptions.forbidden_403 import ChatWriteForbidden +from pyrogram.types import Message +from pyrogram import enums +from misskaty import app +from misskaty.vars import SUDO +from time import time + + +async def member_permissions(chat_id: int, user_id: int): + perms = [] + try: + member = await app.get_chat_member(chat_id, user_id) + perijinan = member.privileges + except Exception: + return [] + if member.status != enums.ChatMemberStatus.MEMBER: + if perijinan.can_post_messages: + perms.append("can_post_messages") + if perijinan.can_edit_messages: + perms.append("can_edit_messages") + if perijinan.can_delete_messages: + perms.append("can_delete_messages") + if perijinan.can_restrict_members: + perms.append("can_restrict_members") + if perijinan.can_promote_members: + perms.append("can_promote_members") + if perijinan.can_change_info: + perms.append("can_change_info") + if perijinan.can_invite_users: + perms.append("can_invite_users") + if perijinan.can_pin_messages: + perms.append("can_pin_messages") + if perijinan.can_manage_video_chats: + perms.append("can_manage_video_chats") + return perms + + +admins_in_chat = {} + + +async def list_admins(chat_id: int): + global admins_in_chat + if chat_id in admins_in_chat: + interval = time() - admins_in_chat[chat_id]["last_updated_at"] + if interval < 3600: + return admins_in_chat[chat_id]["data"] + + admins_in_chat[chat_id] = { + "last_updated_at": time(), + "data": [member.user.id async for member in app.get_chat_members(chat_id, filter=enums.ChatMembersFilter.ADMINISTRATORS)], + } + return admins_in_chat[chat_id]["data"] + + +async def authorised(func, subFunc2, client, message, *args, **kwargs): + chatID = message.chat.id + try: + await func(client, message, *args, **kwargs) + except ChatWriteForbidden: + await app.leave_chat(chatID) + except Exception as e: + try: + await message.reply_text(str(e.MESSAGE)) + except AttributeError: + await message.reply_text(str(e)) + e = err() + print(e) + return subFunc2 + + +async def unauthorised(message: Message, permission, subFunc2): + chatID = message.chat.id + text = "You don't have the required permission to perform this action." + f"\n**Permission:** __{permission}__" + try: + await message.reply_text(text) + except ChatWriteForbidden: + await app.leave_chat(chatID) + return subFunc2 + + +def adminsOnly(permission): + def subFunc(func): + @wraps(func) + async def subFunc2(client, message: Message, *args, **kwargs): + chatID = message.chat.id + if not message.from_user: + # For anonymous admins + if message.sender_chat and message.sender_chat.id == message.chat.id: + return await authorised( + func, + subFunc2, + client, + message, + *args, + **kwargs, + ) + return await unauthorised(message, permission, subFunc2) + # For admins and sudo users + userID = message.from_user.id + permissions = await member_permissions(chatID, userID) + if userID not in SUDO and permission not in permissions: + return await unauthorised(message, permission, subFunc2) + return await authorised(func, subFunc2, client, message, *args, **kwargs) + + return subFunc2 + + return subFunc diff --git a/misskaty/core/keyboard.py b/misskaty/core/keyboard.py new file mode 100644 index 00000000..1736ff66 --- /dev/null +++ b/misskaty/core/keyboard.py @@ -0,0 +1,26 @@ +from pykeyboard import InlineKeyboard +from pyrogram.types import InlineKeyboardButton as Ikb + +from misskaty.helper.functions import get_urls_from_text as is_url + + +def keyboard(buttons_list, row_width: int = 2): + """ + Buttons builder, pass buttons in a list and it will + return pyrogram.types.IKB object + Ex: keyboard([["click here", "https://google.com"]]) + if theres, a url, it will make url button, else callback button + """ + buttons = InlineKeyboard(row_width=row_width) + data = [Ikb(text=str(i[0]), url=str(i[1])) if is_url(i[1]) else Ikb(text=str(i[0]), callback_data=str(i[1])) for i in buttons_list] + + buttons.add(*data) + return buttons + + +def ikb(data: dict, row_width: int = 2): + """ + Converts a dict to pyrogram buttons + Ex: dict_to_keyboard({"click here": "this is callback data"}) + """ + return keyboard(data.items(), row_width=row_width) diff --git a/misskaty/helper/__init__.py b/misskaty/helper/__init__.py new file mode 100644 index 00000000..e20f7f0c --- /dev/null +++ b/misskaty/helper/__init__.py @@ -0,0 +1 @@ +from .misc import paginate_modules diff --git a/misskaty/helper/ffmpeg_helper.py b/misskaty/helper/ffmpeg_helper.py new file mode 100644 index 00000000..b76c230f --- /dev/null +++ b/misskaty/helper/ffmpeg_helper.py @@ -0,0 +1,61 @@ +import asyncio +import os +import time +from pyrogram.types import InputMediaPhoto +from misskaty.plugins.dev import shell_exec +from pyrogram.errors import FloodWait + + +def hhmmss(seconds): + return time.strftime("%H:%M:%S", time.gmtime(seconds)) + + +async def take_ss(video_file): + out_put_file_name = f"genss{str(time.time())}.png" + cmd = f"ssmedia '{video_file}' -t -w 1340 -g 4x4 --ffmpeg-name mediaextract --quality 100 --end-delay-percent 20 --metadata-font-size 30 --timestamp-font-size 20 -o {out_put_file_name}" + await shell_exec(cmd) + return out_put_file_name if os.path.lexists(out_put_file_name) else None + + +async def ssgen_link(video, output_directory, ttl): + out_put_file_name = f"{output_directory}/{str(time.time())}.png" + cmd = [ + "mediaextract", + "-ss", + str(ttl), + "-i", + video, + "-vframes", + "1", + "-f", + "image2", + out_put_file_name, + ] + process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + + stdout, stderr = await process.communicate() + stderr.decode().strip() + stdout.decode().strip() + return out_put_file_name if os.path.isfile(out_put_file_name) else None + + +async def genss_link(msg, video_link, output_directory, min_duration, no_of_photos): + metadata = (await shell_exec(f"ffprobe -i {video_link} -show_entries format=duration -v quiet -of csv='p=0'"))[0] + duration = round(float(metadata)) + if duration > min_duration: + images = [] + ttl_step = duration // no_of_photos + current_ttl = ttl_step + for looper in range(no_of_photos): + ss_img = await ssgen_link(video_link, output_directory, current_ttl) + images.append(InputMediaPhoto(media=ss_img, caption=f"Screenshot at {hhmmss(current_ttl)}")) + try: + await msg.edit(f"šŸ“ø Take Screenshoot:\n{looper+1} of {no_of_photos} screenshot generated..") + except FloodWait as e: + await asyncio.sleep(e.value) + await msg.edit(f"šŸ“ø Take Screenshoot:\n{looper+1} of {no_of_photos} screenshot generated..") + current_ttl = current_ttl + ttl_step + await asyncio.sleep(2) + return images + else: + return None diff --git a/misskaty/helper/files.py b/misskaty/helper/files.py new file mode 100644 index 00000000..c5a91845 --- /dev/null +++ b/misskaty/helper/files.py @@ -0,0 +1,82 @@ +""" +MIT License +Copyright (c) 2021 TheHamkerCat +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import math +import os + +from PIL import Image +from pyrogram import Client, raw +from pyrogram.file_id import FileId + +STICKER_DIMENSIONS = (512, 512) + + +async def resize_file_to_sticker_size(file_path: str) -> str: + im = Image.open(file_path) + if (im.width, im.height) < STICKER_DIMENSIONS: + size1 = im.width + size2 = im.height + if im.width > im.height: + scale = STICKER_DIMENSIONS[0] / size1 + size1new = STICKER_DIMENSIONS[0] + size2new = size2 * scale + else: + scale = STICKER_DIMENSIONS[1] / size2 + size1new = size1 * scale + size2new = STICKER_DIMENSIONS[1] + size1new = math.floor(size1new) + size2new = math.floor(size2new) + sizenew = (size1new, size2new) + im = im.resize(sizenew) + else: + im.thumbnail(STICKER_DIMENSIONS) + try: + os.remove(file_path) + return f"{file_path}.png" + finally: + im.save(file_path) + + +async def upload_document(client: Client, file_path: str, chat_id: int) -> raw.base.InputDocument: + media = await client.send( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + mime_type=client.guess_mime_type(file_path) or "application/zip", + file=await client.save_file(file_path), + attributes=[raw.types.DocumentAttributeFilename(file_name=os.path.basename(file_path))], + ), + ) + ) + return raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + + +async def get_document_from_file_id( + file_id: str, +) -> raw.base.InputDocument: + decoded = FileId.decode(file_id) + return raw.types.InputDocument( + id=decoded.media_id, + access_hash=decoded.access_hash, + file_reference=decoded.file_reference, + ) diff --git a/misskaty/helper/functions.py b/misskaty/helper/functions.py new file mode 100644 index 00000000..7220a746 --- /dev/null +++ b/misskaty/helper/functions.py @@ -0,0 +1,109 @@ +from pyrogram import enums +from datetime import datetime, timedelta +from string import ascii_lowercase +from re import findall + + +def get_urls_from_text(text: str) -> bool: + regex = r"""(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-] + [.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|( + \([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\ + ()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?Ā«Ā»ā€œā€ā€˜ā€™]))""".strip() + return [x[0] for x in findall(regex, text)] + + +async def alpha_to_int(user_id_alphabet: str) -> int: + alphabet = list(ascii_lowercase)[:10] + user_id = "" + for i in user_id_alphabet: + index = alphabet.index(i) + user_id += str(index) + return int(user_id) + + +async def int_to_alpha(user_id: int) -> str: + alphabet = list(ascii_lowercase)[:10] + user_id = str(user_id) + return "".join(alphabet[int(i)] for i in user_id) + + +async def extract_userid(message, text: str): + """ + NOT TO BE USED OUTSIDE THIS FILE + """ + + def is_int(text: str): + try: + int(text) + except ValueError: + return False + return True + + text = text.strip() + + if is_int(text): + return int(text) + + entities = message.entities + app = message._client + if len(entities) < 2: + return (await app.get_users(text)).id + entity = entities[1] + if entity.type == enums.MessageEntityType.MENTION: + return (await app.get_users(text)).id + if entity.type == enums.MessageEntityType.MENTION: + return entity.user.id + return None + + +async def extract_user_and_reason(message, sender_chat=False): + args = message.text.strip().split() + text = message.text + user = None + reason = None + if message.reply_to_message: + reply = message.reply_to_message + # if reply to a message and no reason is given + if reply.from_user: + id_ = reply.from_user.id + + elif reply.sender_chat and reply.sender_chat != message.chat.id and sender_chat: + id_ = reply.sender_chat.id + else: + return None, None + reason = None if len(args) < 2 else text.split(None, 1)[1] + return id_, reason + + # if not reply to a message and no reason is given + if len(args) == 2: + user = text.split(None, 1)[1] + return await extract_userid(message, user), None + + # if reason is given + if len(args) > 2: + user, reason = text.split(None, 2)[1:] + return await extract_userid(message, user), reason + + return user, reason + + +async def extract_user(message): + return (await extract_user_and_reason(message))[0] + + +async def time_converter(message, time_value: str) -> int: + unit = ["m", "h", "d"] # m == minutes | h == hours | d == days + check_unit = "".join(list(filter(time_value[-1].lower().endswith, unit))) + currunt_time = datetime.now() + time_digit = time_value[:-1] + if not time_digit.isdigit(): + return await message.reply_text("Incorrect time specified") + if check_unit == "m": + temp_time = currunt_time + timedelta(minutes=int(time_digit)) + elif check_unit == "h": + temp_time = currunt_time + timedelta(hours=int(time_digit)) + elif check_unit == "d": + temp_time = currunt_time + timedelta(days=int(time_digit)) + else: + return await message.reply_text("Incorrect time specified.") + return int(datetime.timestamp(temp_time)) diff --git a/misskaty/helper/http.py b/misskaty/helper/http.py new file mode 100644 index 00000000..1d459cca --- /dev/null +++ b/misskaty/helper/http.py @@ -0,0 +1,78 @@ +""" +MIT License +Copyright (c) 2021 TheHamkerCat +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import httpx +from asyncio import gather +from aiohttp import ClientSession + +# Aiohttp Async Client +session = ClientSession() + +# HTTPx Async Client +http = httpx.AsyncClient( + http2=True, + timeout=httpx.Timeout(40), +) + + +async def get(url: str, *args, **kwargs): + async with session.get(url, *args, **kwargs) as resp: + try: + data = await resp.json() + except Exception: + data = await resp.text() + return data + + +async def head(url: str, *args, **kwargs): + async with session.head(url, *args, **kwargs) as resp: + try: + data = await resp.json() + except Exception: + data = await resp.text() + return data + + +async def post(url: str, *args, **kwargs): + async with session.post(url, *args, **kwargs) as resp: + try: + data = await resp.json() + except Exception: + data = await resp.text() + return data + + +async def multiget(url: str, times: int, *args, **kwargs): + return await gather(*[get(url, *args, **kwargs) for _ in range(times)]) + + +async def multihead(url: str, times: int, *args, **kwargs): + return await gather(*[head(url, *args, **kwargs) for _ in range(times)]) + + +async def multipost(url: str, times: int, *args, **kwargs): + return await gather(*[post(url, *args, **kwargs) for _ in range(times)]) + + +async def resp_get(url: str, *args, **kwargs): + return await session.get(url, *args, **kwargs) + + +async def resp_post(url: str, *args, **kwargs): + return await session.post(url, *args, **kwargs) diff --git a/misskaty/helper/human_read.py b/misskaty/helper/human_read.py new file mode 100644 index 00000000..9050aa49 --- /dev/null +++ b/misskaty/helper/human_read.py @@ -0,0 +1,54 @@ +SIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"] + + +def get_readable_file_size(size_in_bytes) -> str: + if size_in_bytes is None: + return "0B" + index = 0 + while size_in_bytes >= 1024: + size_in_bytes /= 1024 + index += 1 + try: + return f"{round(size_in_bytes, 2)}{SIZE_UNITS[index]}" + except IndexError: + return "File too large" + + +def get_readable_time(seconds: int) -> str: + result = "" + (days, remainder) = divmod(seconds, 86400) + days = int(days) + if days != 0: + result += f"{days} day " + (hours, remainder) = divmod(remainder, 3600) + hours = int(hours) + if hours != 0: + result += f"{hours} hour " + (minutes, seconds) = divmod(remainder, 60) + minutes = int(minutes) + if minutes != 0: + result += f"{minutes} minute " + seconds = int(seconds) + result += f"{seconds} second " + return result + + +def get_readable_time2(seconds: int) -> str: + count = 0 + ping_time = "" + time_list = [] + time_suffix_list = ["second", "minute", "hour", "day", "week", "month", "year"] + while count < 4: + count += 1 + remainder, result = divmod(seconds, 60) if count < 3 else divmod(seconds, 24) + if seconds == 0 and remainder == 0: + break + time_list.append(int(result)) + seconds = int(remainder) + for i in range(len(time_list)): + time_list[i] = str(time_list[i]) + time_suffix_list[i] + if len(time_list) == 4: + ping_time += f"{time_list.pop()}, " + time_list.reverse() + ping_time += ":".join(time_list) + return ping_time diff --git a/misskaty/helper/media_helper.py b/misskaty/helper/media_helper.py new file mode 100644 index 00000000..4c316f95 --- /dev/null +++ b/misskaty/helper/media_helper.py @@ -0,0 +1,67 @@ +import os +import asyncio +from html_telegraph_poster import TelegraphPoster +from typing import Tuple +import shlex + + +def post_to_telegraph(a_title: str, content: str) -> str: + """Create a Telegram Post using HTML Content""" + post_client = TelegraphPoster(use_api=True) + auth_name = "MissKaty Bot" + post_client.create_api_token(auth_name) + post_page = post_client.post( + title=a_title, + author=auth_name, + author_url="https://www.yasir.my.id", + text=content, + ) + return post_page["url"] + + +async def run_subprocess(cmd): + process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + return await process.communicate() + + +async def get_media_info(file_link): + ffprobe_cmd = [ + "ffprobe", + "-headers", + "IAM:''", + "-i", + file_link, + "-v", + "quiet", + "-of", + "json", + "-show_streams", + "-show_format", + "-show_chapters", + "-show_programs", + ] + data, err = await run_subprocess(ffprobe_cmd) + return data + + +async def runcmd(cmd: str) -> Tuple[str, str, int, int]: + """run command in terminal""" + args = shlex.split(cmd) + process = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + stdout, stderr = await process.communicate() + return ( + stdout.decode("utf-8", "replace").strip(), + stderr.decode("utf-8", "replace").strip(), + process.returncode, + process.pid, + ) + + +# Solves ValueError: No closing quotation by removing ' or " in file name +def safe_filename(path_): + if path_ is None: + return + safename = path_.replace("'", "").replace('"', "") + if safename != path_: + os.rename(path_, safename) + return safename diff --git a/misskaty/helper/misc.py b/misskaty/helper/misc.py new file mode 100644 index 00000000..e45bf0e6 --- /dev/null +++ b/misskaty/helper/misc.py @@ -0,0 +1,82 @@ +from math import ceil + +from pyrogram.types import InlineKeyboardButton + +from misskaty import MOD_LOAD, MOD_NOLOAD + + +class EqInlineKeyboardButton(InlineKeyboardButton): + def __eq__(self, other): + return self.text == other.text + + def __lt__(self, other): + return self.text < other.text + + def __gt__(self, other): + return self.text > other.text + + +def paginate_modules(page_n, module_dict, prefix, chat=None): + modules = ( + sorted( + [ + EqInlineKeyboardButton( + x.__MODULE__, + callback_data=f"{prefix}_module({chat},{x.__MODULE__.lower()})", + ) + for x in module_dict.values() + ] + ) + if chat + else sorted( + [ + EqInlineKeyboardButton( + x.__MODULE__, + callback_data=f"{prefix}_module({x.__MODULE__.lower()})", + ) + for x in module_dict.values() + ] + ) + ) + + pairs = list(zip(modules[::3], modules[1::3], modules[2::3])) + i = 0 + for m in pairs: + for _ in m: + i += 1 + if len(modules) - i == 1: + pairs.append((modules[-1],)) + elif len(modules) - i == 2: + pairs.append( + ( + modules[-2], + modules[-1], + ) + ) + + COLUMN_SIZE = 4 + + max_num_pages = ceil(len(pairs) / COLUMN_SIZE) + modulo_page = page_n % max_num_pages + + # can only have a certain amount of buttons side by side + if len(pairs) > COLUMN_SIZE: + pairs = pairs[modulo_page * COLUMN_SIZE : COLUMN_SIZE * (modulo_page + 1)] + [ + ( + EqInlineKeyboardButton( + "ā®", callback_data=f"{prefix}_prev({modulo_page})" + ), + EqInlineKeyboardButton( + "Back", callback_data=f"{prefix}_home({modulo_page})" + ), + EqInlineKeyboardButton( + "āÆ", callback_data=f"{prefix}_next({modulo_page})" + ), + ) + ] + + return pairs + + +def is_module_loaded(name): + return (not MOD_LOAD or name in MOD_LOAD) and name not in MOD_NOLOAD diff --git a/misskaty/helper/pyro_progress.py b/misskaty/helper/pyro_progress.py new file mode 100644 index 00000000..7332a01d --- /dev/null +++ b/misskaty/helper/pyro_progress.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# (c) Shrimadhav U K + +import asyncio +import math +import time +from pyrogram.errors import MessageNotModified, FloodWait + + +async def progress_for_pyrogram(current, total, ud_type, message, start): + """generic progress display for Telegram Upload / Download status""" + now = time.time() + diff = now - start + if round(diff % 10.00) == 0 or current == total: + # if round(current / total * 100, 0) % 5 == 0: + percentage = current * 100 / total + elapsed_time = round(diff) + if elapsed_time == 0: + return + speed = current / diff + time_to_completion = round((total - current) / speed) + estimated_total_time = elapsed_time + time_to_completion + + elapsed_time = time_formatter(elapsed_time) + estimated_total_time = time_formatter(estimated_total_time) + + progress = "[{0}{1}] \nP: {2}%\n".format( + "".join(["ā–ˆ" for _ in range(math.floor(percentage / 5))]), + "".join(["ā–‘" for _ in range(20 - math.floor(percentage / 5))]), + round(percentage, 2), + ) + + tmp = progress + "{0} of {1}\nSpeed: {2}/s\nETA: {3}\n".format( + humanbytes(current), + humanbytes(total), + humanbytes(speed), + # elapsed_time if elapsed_time != "" else "0 s", + estimated_total_time if estimated_total_time != "" else "0 s", + ) + try: + await message.edit(f"{ud_type}\n {tmp}") + except FloodWait as e: + await asyncio.sleep(e.value) + await message.edit(f"{ud_type}\n {tmp}") + except MessageNotModified: + pass + + +def humanbytes(size: int) -> str: + """converts bytes into human readable format""" + # https://stackoverflow.com/a/49361727/4723940 + # 2**10 = 1024 + if not size: + return "" + power = 2**10 + number = 0 + dict_power_n = {0: " ", 1: "Ki", 2: "Mi", 3: "Gi", 4: "Ti"} + while size > power: + size /= power + number += 1 + return f"{str(round(size, 2))} {dict_power_n[number]}B" + + +def time_formatter(seconds: int) -> str: + result = "" + v_m = 0 + remainder = seconds + r_ange_s = {"days": (24 * 60 * 60), "hours": (60 * 60), "minutes": 60, "seconds": 1} + for age, divisor in r_ange_s.items(): + v_m, remainder = divmod(remainder, divisor) + v_m = int(v_m) + if v_m != 0: + result += f" {v_m} {age} " + return result diff --git a/misskaty/helper/stickerset.py b/misskaty/helper/stickerset.py new file mode 100644 index 00000000..a1e8bca7 --- /dev/null +++ b/misskaty/helper/stickerset.py @@ -0,0 +1,73 @@ +""" +MIT License +Copyright (c) 2021 TheHamkerCat +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from typing import List + +from pyrogram import Client, errors, raw + + +async def get_sticker_set_by_name(client: Client, name: str) -> raw.base.messages.StickerSet: + try: + return await client.invoke( + raw.functions.messages.GetStickerSet( + stickerset=raw.types.InputStickerSetShortName(short_name=name), + hash=0, + ) + ) + except errors.exceptions.not_acceptable_406.StickersetInvalid: + return None + + +# Known errors: (I don't see a reason to catch them as we, for sure, won't face them right now): +# errors.exceptions.bad_request_400.PackShortNameInvalid -> pack name needs to end with _by_botname +# errors.exceptions.bad_request_400.ShortnameOccupyFailed -> pack's name is already in use + + +async def create_sticker_set( + client: Client, + owner: int, + title: str, + short_name: str, + stickers: List[raw.base.InputStickerSetItem], +) -> raw.base.messages.StickerSet: + return await client.invoke( + raw.functions.stickers.CreateStickerSet( + user_id=await client.resolve_peer(owner), + title=title, + short_name=short_name, + stickers=stickers, + ) + ) + + +async def add_sticker_to_set( + client: Client, + stickerset: raw.base.messages.StickerSet, + sticker: raw.base.InputStickerSetItem, +) -> raw.base.messages.StickerSet: + return await client.invoke( + raw.functions.stickers.AddStickerToSet( + stickerset=raw.types.InputStickerSetShortName(short_name=stickerset.set.short_name), + sticker=sticker, + ) + ) + + +async def create_sticker(sticker: raw.base.InputDocument, emoji: str) -> raw.base.InputStickerSetItem: + return raw.types.InputStickerSetItem(document=sticker, emoji=emoji) diff --git a/misskaty/helper/time_gap.py b/misskaty/helper/time_gap.py new file mode 100644 index 00000000..ace9e29d --- /dev/null +++ b/misskaty/helper/time_gap.py @@ -0,0 +1,18 @@ +import time + +GAP = {} + + +async def check_time_gap(user_id: int): + """A Function for checking user time gap! + :parameter user_id Telegram User ID""" + + if str(user_id) in GAP: + current_time = time.time() + previous_time = GAP[str(user_id)] + if round(current_time - previous_time) < 10: + return True, round(previous_time - current_time + 10) + del GAP[str(user_id)] + else: + GAP[str(user_id)] = time.time() + return False, None diff --git a/misskaty/helper/tools.py b/misskaty/helper/tools.py new file mode 100644 index 00000000..5f1a0795 --- /dev/null +++ b/misskaty/helper/tools.py @@ -0,0 +1,83 @@ +import random +import string +import psutil +import time +import os +from misskaty import botStartTime +from misskaty.plugins import ALL_MODULES +from misskaty.helper.human_read import get_readable_time +from misskaty.helper.http import http +from http.cookies import SimpleCookie + +GENRES_EMOJI = { + "Action": "šŸ‘Š", + "Adventure": random.choice(["šŸŖ‚", "šŸ§—ā€ā™€", "šŸŒ‹"]), + "Family": "šŸ‘Øā€", + "Musical": "šŸŽø", + "Comedy": "🤣", + "Drama": " šŸŽ­", + "Ecchi": random.choice(["šŸ’‹", "🄵"]), + "Fantasy": random.choice(["šŸ§ž", "šŸ§žā€ā™‚", "šŸ§žā€ā™€", "šŸŒ—"]), + "Hentai": "šŸ”ž", + "History": "šŸ“œ", + "Horror": "☠", + "Mahou Shoujo": "☯", + "Mecha": "šŸ¤–", + "Music": "šŸŽø", + "Mystery": "šŸ”®", + "Psychological": "ā™Ÿ", + "Romance": "šŸ’ž", + "Sci-Fi": "šŸ›ø", + "Slice of Life": random.choice(["☘", "šŸ"]), + "Sports": "āš½ļø", + "Supernatural": "🫧", + "Thriller": random.choice(["🄶", "šŸ”Ŗ", "🤯"]), +} + + +async def bot_sys_stats(): + bot_uptime = int(time.time() - botStartTime) + cpu = psutil.cpu_percent(interval=0.5) + mem = psutil.virtual_memory().percent + disk = psutil.disk_usage("/").percent + process = psutil.Process(os.getpid()) + return f""" +YasirArisM@MissKatyRoBot +------------------ +UPTIME: {get_readable_time(bot_uptime)} +BOT: {round(process.memory_info()[0] / 1024**2)} MB +CPU: {cpu}% +RAM: {mem}% +DISK: {disk}% + +TOTAL PLUGINS: {len(ALL_MODULES)} +""" + + +def get_random_string(length): + # choose from all lowercase letter + letters = string.ascii_lowercase + return "".join(random.choice(letters) for _ in range(length)) + + +async def rentry(teks): + # buat dapetin cookie + cookie = SimpleCookie() + kuki = (await http.get("https://rentry.co")).cookies + cookie.load(kuki) + kukidict = {key: value.value for key, value in cookie.items()} + # headernya + header = {"Referer": "https://rentry.co"} + payload = {"csrfmiddlewaretoken": kukidict["csrftoken"], "text": teks} + return ( + ( + await http.post( + "https://rentry.co/api/new", + data=payload, + headers=header, + cookies=kukidict, + ) + ) + .json() + .get("url") + ) diff --git a/misskaty/helper/ytdl_helper.py b/misskaty/helper/ytdl_helper.py new file mode 100644 index 00000000..7a0c877e --- /dev/null +++ b/misskaty/helper/ytdl_helper.py @@ -0,0 +1,42 @@ +import string +import os +import requests +import time +import random +from misskaty.helper.human_read import get_readable_file_size + + +def random_char(y): + return "".join(random.choice(string.ascii_letters) for _ in range(y)) + + +def DetectFileSize(url): + r = requests.get(url, allow_redirects=True, stream=True) + return int(r.headers.get("content-length", 0)) + + +def DownLoadFile(url, file_name, chunk_size, client, ud_type, message_id, chat_id): + if os.path.exists(file_name): + os.remove(file_name) + if not url: + return file_name + r = requests.get(url, allow_redirects=True, stream=True) + # https://stackoverflow.com/a/47342052/4723940 + total_size = int(r.headers.get("content-length", 0)) + downloaded_size = 0 + with open(file_name, "wb") as fd: + for chunk in r.iter_content(chunk_size=chunk_size): + if chunk: + fd.write(chunk) + downloaded_size += chunk_size + if client is not None and ((total_size // downloaded_size) % 5) == 0: + time.sleep(0.3) + try: + client.edit_message_text( + chat_id, + message_id, + text=f"{ud_type}: {get_readable_file_size(downloaded_size)} of {get_readable_file_size(total_size)}", + ) + except: + pass + return file_name diff --git a/misskaty/plugins/__init__.py b/misskaty/plugins/__init__.py new file mode 100644 index 00000000..25fde160 --- /dev/null +++ b/misskaty/plugins/__init__.py @@ -0,0 +1,45 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @lastModified 2022-12-01 09:27:31 + * @projectName MissKatyPyro + * Copyright Ā©YasirPedia All rights reserved + """ +import glob +import importlib +import sys +from logging import info as log_info +from os.path import basename, dirname, isfile +from misskaty import MOD_LOAD, MOD_NOLOAD + + +def __list_all_modules(): + # This generates a list of modules in this + # folder for the * in __main__ to work. + mod_paths = glob.glob(f"{dirname(__file__)}/*.py") + all_modules = [ + basename(f)[:-3] for f in mod_paths if isfile(f) and f.endswith(".py") + and not f.endswith("__init__.py") and not f.endswith("__main__.py") + ] + + if MOD_LOAD or MOD_NOLOAD: + to_load = MOD_LOAD + if to_load: + if not all( + any(mod == module_name for module_name in all_modules) + for mod in to_load): + sys.exit() + + else: + to_load = all_modules + + return [item for item in to_load + if item not in MOD_NOLOAD] if MOD_NOLOAD else to_load + + return all_modules + + +log_info("[INFO]: IMPORTING PLUGINS") +importlib.import_module("misskaty.plugins.__main__") +ALL_MODULES = sorted(__list_all_modules()) +__all__ = ALL_MODULES + ["ALL_MODULES"] diff --git a/misskaty/plugins/__main__.py b/misskaty/plugins/__main__.py new file mode 100644 index 00000000..e69de29b diff --git a/misskaty/plugins/admin.py b/misskaty/plugins/admin.py new file mode 100644 index 00000000..bbfa3c56 --- /dev/null +++ b/misskaty/plugins/admin.py @@ -0,0 +1,715 @@ +import logging +import asyncio, re +from misskaty import app +from misskaty.helper.functions import ( + extract_user_and_reason, + time_converter, + extract_user, + int_to_alpha, +) +from time import time +from pyrogram import filters, enums +from pyrogram.errors import FloodWait, ChatAdminRequired +from pyrogram.types import ChatPermissions +from misskaty.core.decorator.permissions import ( + adminsOnly, + admins_in_chat, + list_admins, + member_permissions, +) +from misskaty.core.decorator.errors import capture_err +from misskaty.core.keyboard import ikb +from misskaty.vars import SUDO, COMMAND_HANDLER +from database.warn_db import get_warn, remove_warns, add_warn + +__MODULE__ = "Admin" +__HELP__ = """ +/ban - Ban A User From A Group +/dban - Delete the replied message banning its sender +/tban - Ban A User For Specific Time +/unban - Unban A User +/listban - Ban a user from groups listed in a message +/listunban - Unban a user from groups listed in a message +/warn - Warn A User +/dwarn - Delete the replied message warning its sender +/rmwarns - Remove All Warning of A User +/warns - Show Warning Of A User +/kick - Kick A User +/dkick - Delete the replied message kicking its sender +/purge - Purge Messages +/purge [n] - Purge "n" number of messages from replied message +/del - Delete Replied Message +/promote - Promote A Member +/fullpromote - Promote A Member With All Rights +/demote - Demote A Member +/pin - Pin A Message +/mute - Mute A User +/tmute - Mute A User For Specific Time +/unmute - Unmute A User +/ban_ghosts - Ban Deleted Accounts +/report | @admins | @admin - Report A Message To Admins. +""" + + +# Admin cache reload +@app.on_chat_member_updated() +async def admin_cache_func(_, cmu): + if cmu.old_chat_member and cmu.old_chat_member.promoted_by: + admins_in_chat[cmu.chat.id] = { + "last_updated_at": time(), + "data": [ + member.user.id + async for member in app.get_chat_members( + cmu.chat.id, filter=enums.ChatMembersFilter.ADMINISTRATORS + ) + ], + } + logging.info(f"Updated admin cache for {cmu.chat.id} [{cmu.chat.title}]") + + +# Purge CMD +@app.on_message(filters.command("purge", COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_delete_messages") +async def purge(_, message): + repliedmsg = message.reply_to_message + await message.delete() + + if not repliedmsg: + return await message.reply_text("Reply to a message to purge from.") + + cmd = message.command + if len(cmd) > 1 and cmd[1].isdigit(): + purge_to = repliedmsg.id + int(cmd[1]) + if purge_to > message.id: + purge_to = message.id + else: + purge_to = message.id + + chat_id = message.chat.id + message_ids = [] + del_total = 0 + + for message_id in range( + repliedmsg.id, + purge_to, + ): + message_ids.append(message_id) + + # Max message deletion limit is 100 + if len(message_ids) == 100: + await app.delete_messages( + chat_id=chat_id, + message_ids=message_ids, + revoke=True, # For both sides + ) + del_total += len(message_ids) + # To delete more than 100 messages, start again + message_ids = [] + + # Delete if any messages left + if len(message_ids) > 0: + await app.delete_messages( + chat_id=chat_id, + message_ids=message_ids, + revoke=True, + ) + del_total += len(message_ids) + await message.reply(f"Successfully deleted {del_total} messages..") + + +# Kick members +@app.on_message(filters.command(["kick", "dkick"], COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_restrict_members") +async def kickFunc(_, message): + user_id, reason = await extract_user_and_reason(message) + if not user_id: + return await message.reply_text("I can't find that user.") + if user_id == 1507530289: + return await message.reply_text("I can't kick myself, i can leave if you want.") + if user_id in SUDO: + return await message.reply_text("Wow, you wanna kick my owner?") + if user_id in (await list_admins(message.chat.id)): + return await message.reply_text("Lol, it's crazy if i can kick an admin.") + mention = (await app.get_users(user_id)).mention + msg = f""" +**Kicked User:** {mention} +**Kicked By:** {message.from_user.mention if message.from_user else 'Anon'} +**Reason:** {reason or '-'}""" + if message.command[0][0] == "d": + await message.reply_to_message.delete() + await message.chat.ban_member(user_id) + await message.reply_text(msg) + await asyncio.sleep(1) + await message.chat.unban_member(user_id) + + +# Ban/DBan/TBan User +@app.on_message( + filters.command(["ban", "dban", "tban"], COMMAND_HANDLER) & ~filters.private +) +@adminsOnly("can_restrict_members") +async def banFunc(_, message): + user_id, reason = await extract_user_and_reason(message, sender_chat=True) + + if not user_id: + return await message.reply_text("I can't find that user.") + if user_id == 1507530289: + return await message.reply_text("I can't ban myself, i can leave if you want.") + if user_id in SUDO: + return await message.reply_text("You Wanna Ban The Elevated One?, RECONSIDER!") + if user_id in (await list_admins(message.chat.id)): + return await message.reply_text( + "I can't ban an admin, You know the rules, so do i." + ) + + try: + mention = (await app.get_users(user_id)).mention + except IndexError: + mention = ( + message.reply_to_message.sender_chat.title + if message.reply_to_message + else "Anon" + ) + + msg = ( + f"**Banned User:** {mention}\n" + f"**Banned By:** {message.from_user.mention if message.from_user else 'Anon'}\n" + ) + if message.command[0][0] == "d": + await message.reply_to_message.delete() + if message.command[0] == "tban": + split = reason.split(None, 1) + time_value = split[0] + temp_reason = split[1] if len(split) > 1 else "" + temp_ban = await time_converter(message, time_value) + msg += f"**Banned For:** {time_value}\n" + if temp_reason: + msg += f"**Reason:** {temp_reason}" + try: + if len(time_value[:-1]) < 3: + await message.chat.ban_member(user_id, until_date=temp_ban) + await message.reply_text(msg) + else: + await message.reply_text("You can't use more than 99") + except AttributeError: + pass + return + if reason: + msg += f"**Reason:** {reason}" + keyboard = ikb({"🚨 Unban 🚨": f"unban_{user_id}"}) + await message.chat.ban_member(user_id) + await message.reply_text(msg, reply_markup=keyboard) + + +# Unban members +@app.on_message(filters.command("unban", COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_restrict_members") +async def unban_func(_, message): + # we don't need reasons for unban, also, we + # don't need to get "text_mention" entity, because + # normal users won't get text_mention if the user + # they want to unban is not in the group. + reply = message.reply_to_message + + if reply and reply.sender_chat and reply.sender_chat != message.chat.id: + return await message.reply_text("You cannot unban a channel") + + if len(message.command) == 2: + user = message.text.split(None, 1)[1] + elif len(message.command) == 1 and reply: + user = message.reply_to_message.from_user.id + else: + return await message.reply_text( + "Provide a username or reply to a user's message to unban." + ) + await message.chat.unban_member(user) + umention = (await app.get_users(user)).mention + await message.reply_text(f"Unbanned! {umention}") + + +# Ban users listed in a message +@app.on_message( + filters.user(SUDO) & filters.command("listban", COMMAND_HANDLER) & ~filters.private +) +async def list_ban_(c, message): + userid, msglink_reason = await extract_user_and_reason(message) + if not userid or not msglink_reason: + return await message.reply_text( + "Provide a userid/username along with message link and reason to list-ban" + ) + if len(msglink_reason.split(" ")) == 1: # message link included with the reason + return await message.reply_text("You must provide a reason to list-ban") + # seperate messge link from reason + lreason = msglink_reason.split() + messagelink, reason = lreason[0], " ".join(lreason[1:]) + + if not re.search( + r"(https?://)?t(elegram)?\.me/\w+/\d+", messagelink + ): # validate link + return await message.reply_text("Invalid message link provided") + + if userid == 1507530289: + return await message.reply_text("I can't ban myself.") + if userid in SUDO: + return await message.reply_text("You Wanna Ban The Elevated One?, RECONSIDER!") + splitted = messagelink.split("/") + uname, mid = splitted[-2], int(splitted[-1]) + m = await message.reply_text( + "`Banning User from multiple groups. This may take some time`" + ) + try: + msgtext = (await app.get_messages(uname, mid)).text + gusernames = re.findall("@\w+", msgtext) + except: + return await m.edit_text("Could not get group usernames") + count = 0 + for username in gusernames: + try: + await app.ban_chat_member(username.strip("@"), userid) + await asyncio.sleep(1) + except FloodWait as e: + await asyncio.sleep(e.value) + except: + continue + count += 1 + mention = (await app.get_users(userid)).mention + + msg = f""" +**List-Banned User:** {mention} +**Banned User ID:** `{userid}` +**Admin:** {message.from_user.mention} +**Affected chats:** `{count}` +**Reason:** {reason} +""" + await m.edit_text(msg) + + +# Unban users listed in a message +@app.on_message( + filters.user(SUDO) + & filters.command("listunban", COMMAND_HANDLER) + & ~filters.private +) +async def list_unban_(c, message): + userid, msglink = await extract_user_and_reason(message) + if not userid or not msglink: + return await message.reply_text( + "Provide a userid/username along with message link to list-unban" + ) + + if not re.search(r"(https?://)?t(elegram)?\.me/\w+/\d+", msglink): # validate link + return await message.reply_text("Invalid message link provided") + + splitted = msglink.split("/") + uname, mid = splitted[-2], int(splitted[-1]) + m = await message.reply_text( + "`Unbanning User from multiple groups. \ + This may take some time`" + ) + try: + msgtext = (await app.get_messages(uname, mid)).text + gusernames = re.findall("@\w+", msgtext) + except: + return await m.edit_text("Could not get the group usernames") + count = 0 + for username in gusernames: + try: + await app.unban_chat_member(username.strip("@"), userid) + await asyncio.sleep(1) + except FloodWait as e: + await asyncio.sleep(e.x) + except: + continue + count += 1 + mention = (await app.get_users(userid)).mention + msg = f""" +**List-Unbanned User:** {mention} +**Unbanned User ID:** `{userid}` +**Admin:** {message.from_user.mention} +**Affected chats:** `{count}` +""" + await m.edit_text(msg) + + +# Delete messages +@app.on_message(filters.command("del", COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_delete_messages") +async def deleteFunc(_, message): + if not message.reply_to_message: + return await message.reply_text("Reply To A Message To Delete It") + await message.reply_to_message.delete() + await message.delete() + + +# Promote Members +@app.on_message( + filters.command(["promote", "fullpromote"], COMMAND_HANDLER) & ~filters.private +) +@adminsOnly("can_promote_members") +async def promoteFunc(_, message): + user_id = await extract_user(message) + umention = (await app.get_users(user_id)).mention + if not user_id: + return await message.reply_text("I can't find that user.") + bot = await app.get_chat_member(message.chat.id, 1507530289) + if user_id == 1507530289: + return await message.reply_text("I can't promote myself.") + if not bot.can_promote_members: + return await message.reply_text("I don't have enough permissions") + if message.command[0][0] == "f": + await message.chat.promote_member( + user_id=user_id, + can_change_info=bot.can_change_info, + can_invite_users=bot.can_invite_users, + can_delete_messages=bot.can_delete_messages, + can_restrict_members=bot.can_restrict_members, + can_pin_messages=bot.can_pin_messages, + can_promote_members=bot.can_promote_members, + can_manage_chat=bot.can_manage_chat, + can_manage_voice_chats=bot.can_manage_voice_chats, + ) + return await message.reply_text(f"Fully Promoted! {umention}") + + await message.chat.promote_member( + user_id=user_id, + can_change_info=False, + can_invite_users=bot.can_invite_users, + can_delete_messages=bot.can_delete_messages, + can_restrict_members=False, + can_pin_messages=False, + can_promote_members=False, + can_manage_chat=bot.can_manage_chat, + can_manage_voice_chats=bot.can_manage_voice_chats, + ) + await message.reply_text(f"Promoted! {umention}") + + +# Demote Member +@app.on_message(filters.command("demote", COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_promote_members") +async def demote(_, message): + user_id = await extract_user(message) + if not user_id: + return await message.reply_text("I can't find that user.") + if user_id == 1507530289: + return await message.reply_text("I can't demote myself.") + if user_id in SUDO: + return await message.reply_text("You wanna demote the elevated one?") + await message.chat.promote_member( + user_id=user_id, + can_change_info=False, + can_invite_users=False, + can_delete_messages=False, + can_restrict_members=False, + can_pin_messages=False, + can_promote_members=False, + can_manage_chat=False, + can_manage_voice_chats=False, + ) + umention = (await app.get_users(user_id)).mention + await message.reply_text(f"Demoted! {umention}") + + +# Pin Messages +@app.on_message(filters.command(["pin", "unpin"], COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_pin_messages") +async def pin(_, message): + if not message.reply_to_message: + return await message.reply_text("Reply to a message to pin/unpin it.") + r = message.reply_to_message + try: + if message.command[0][0] == "u": + await r.unpin() + return await message.reply_text( + f"**Unpinned [this]({r.link}) message.**", + disable_web_page_preview=True, + ) + await r.pin(disable_notification=True) + await message.reply( + f"**Pinned [this]({r.link}) message.**", + disable_web_page_preview=True, + ) + except ChatAdminRequired: + await message.reply( + "Please give me admin access to use this command.", + disable_web_page_preview=True, + ) + + +# Mute members +@app.on_message(filters.command(["mute", "tmute"], COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_restrict_members") +async def mute(_, message): + user_id, reason = await extract_user_and_reason(message) + if not user_id: + return await message.reply_text("I can't find that user.") + if user_id == 1507530289: + return await message.reply_text("I can't mute myself.") + if user_id in SUDO: + return await message.reply_text("You wanna mute the elevated one?, RECONSIDER!") + if user_id in (await list_admins(message.chat.id)): + return await message.reply_text( + "I can't mute an admin, You know the rules, so do i." + ) + mention = (await app.get_users(user_id)).mention + keyboard = ikb({"🚨 Unmute 🚨": f"unmute_{user_id}"}) + msg = ( + f"**Muted User:** {mention}\n" + f"**Muted By:** {message.from_user.mention if message.from_user else 'Anon'}\n" + ) + if message.command[0] == "tmute": + split = reason.split(None, 1) + time_value = split[0] + temp_reason = split[1] if len(split) > 1 else "" + temp_mute = await time_converter(message, time_value) + msg += f"**Muted For:** {time_value}\n" + if temp_reason: + msg += f"**Reason:** {temp_reason}" + try: + if len(time_value[:-1]) < 3: + await message.chat.restrict_member( + user_id, + permissions=ChatPermissions(), + until_date=temp_mute, + ) + await message.reply_text(msg, reply_markup=keyboard) + else: + await message.reply_text("You can't use more than 99") + except AttributeError: + pass + return + if reason: + msg += f"**Reason:** {reason}" + await message.chat.restrict_member(user_id, permissions=ChatPermissions()) + await message.reply_text(msg, reply_markup=keyboard) + + +# Unmute members +@app.on_message(filters.command("unmute", COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_restrict_members") +async def unmute(_, message): + user_id = await extract_user(message) + if not user_id: + return await message.reply_text("I can't find that user.") + await message.chat.unban_member(user_id) + umention = (await app.get_users(user_id)).mention + await message.reply_text(f"Unmuted! {umention}") + + +# Ban deleted accounts +@app.on_message(filters.command("ban_ghosts", COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_restrict_members") +async def ban_deleted_accounts(_, message): + chat_id = message.chat.id + deleted_users = [] + m = await message.reply("Finding ghosts...") + + async for i in app.iter_chat_members(chat_id): + if i.user.is_deleted: + deleted_users.append(i.user.id) + if deleted_users: + banned_users = 0 + for deleted_user in deleted_users: + try: + await message.chat.ban_member(deleted_user) + except Exception: + pass + banned_users += 1 + await m.edit(f"Banned {banned_users} Deleted Accounts") + else: + await m.edit("There are no deleted accounts in this chat") + + +@app.on_message(filters.command(["warn", "dwarn"], COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_restrict_members") +async def warn_user(_, message): + user_id, reason = await extract_user_and_reason(message) + chat_id = message.chat.id + if not user_id: + return await message.reply_text("I can't find that user.") + if user_id == 1507530289: + return await message.reply_text("I can't warn myself, i can leave if you want.") + if user_id in SUDO: + return await message.reply_text("You Wanna Warn The Elevated One?, RECONSIDER!") + if user_id in (await list_admins(chat_id)): + return await message.reply_text( + "I can't warn an admin, You know the rules, so do i." + ) + user, warns = await asyncio.gather( + app.get_users(user_id), + get_warn(chat_id, await int_to_alpha(user_id)), + ) + mention = user.mention + keyboard = ikb({"🚨 Remove Warn 🚨": f"unwarn_{user_id}"}) + warns = warns["warns"] if warns else 0 + if message.command[0][0] == "d": + await message.reply_to_message.delete() + if warns >= 2: + await message.chat.ban_member(user_id) + await message.reply_text(f"Number of warns of {mention} exceeded, BANNED!") + await remove_warns(chat_id, await int_to_alpha(user_id)) + else: + warn = {"warns": warns + 1} + msg = f""" +**Warned User:** {mention} +**Warned By:** {message.from_user.mention if message.from_user else 'Anon'} +**Reason:** {reason or 'No Reason Provided.'} +**Warns:** {warns + 1}/3""" + await message.reply_text(msg, reply_markup=keyboard) + await add_warn(chat_id, await int_to_alpha(user_id), warn) + + +@app.on_callback_query(filters.regex("unwarn_")) +async def remove_warning(_, cq): + from_user = cq.from_user + chat_id = cq.message.chat.id + permissions = await member_permissions(chat_id, from_user.id) + permission = "can_restrict_members" + if permission not in permissions: + return await cq.answer( + "You don't have enough permissions to perform this action.\n" + + f"Permission needed: {permission}", + show_alert=True, + ) + user_id = cq.data.split("_")[1] + warns = await get_warn(chat_id, await int_to_alpha(user_id)) + if warns: + warns = warns["warns"] + if not warns or warns == 0: + return await cq.answer("User has no warnings.") + warn = {"warns": warns - 1} + await add_warn(chat_id, await int_to_alpha(user_id), warn) + text = cq.message.text.markdown + text = f"~~{text}~~\n\n" + text += f"__Warn removed by {from_user.mention}__" + await cq.message.edit(text) + + +@app.on_callback_query(filters.regex("unmute_")) +async def unmute_user(_, cq): + from_user = cq.from_user + chat_id = cq.message.chat.id + permissions = await member_permissions(chat_id, from_user.id) + permission = "can_restrict_members" + if permission not in permissions: + return await cq.answer( + "You don't have enough permissions to perform this action.\n" + + f"Permission needed: {permission}", + show_alert=True, + ) + user_id = cq.data.split("_")[1] + text = cq.message.text.markdown + text = f"~~{text}~~\n\n" + text += f"__Mute removed by {from_user.mention}__" + await cq.message.chat.unban_member(user_id) + await cq.message.edit(text) + + +@app.on_callback_query(filters.regex("unban_")) +async def unban_user(_, cq): + from_user = cq.from_user + chat_id = cq.message.chat.id + permissions = await member_permissions(chat_id, from_user.id) + permission = "can_restrict_members" + if permission not in permissions: + return await cq.answer( + "You don't have enough permissions to perform this action.\n" + + f"Permission needed: {permission}", + show_alert=True, + ) + user_id = cq.data.split("_")[1] + (await app.get_users(user_id)).mention + text = cq.message.text.markdown + text = f"~~{text}~~\n\n" + text += f"__Banned removed by {from_user.mention}__" + await cq.message.chat.unban_member(user_id) + await cq.message.edit(text) + + +# Remove Warn +@app.on_message(filters.command("rmwarn", COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_restrict_members") +async def remove_warnings(_, message): + if not message.reply_to_message: + return await message.reply_text( + "Reply to a message to remove a user's warnings." + ) + user_id = message.reply_to_message.from_user.id + mention = message.reply_to_message.from_user.mention + chat_id = message.chat.id + warns = await get_warn(chat_id, await int_to_alpha(user_id)) + if warns: + warns = warns["warns"] + if warns == 0 or not warns: + await message.reply_text(f"{mention} have no warnings.") + else: + await remove_warns(chat_id, await int_to_alpha(user_id)) + await message.reply_text(f"Removed warnings of {mention}.") + + +# Warns +@app.on_message(filters.command("warns", COMMAND_HANDLER) & ~filters.private) +@capture_err +async def check_warns(_, message): + user_id = await extract_user(message) + if not user_id: + return await message.reply_text("I can't find that user.") + warns = await get_warn(message.chat.id, await int_to_alpha(user_id)) + mention = (await app.get_users(user_id)).mention + if warns: + warns = warns["warns"] + else: + return await message.reply_text(f"{mention} has no warnings.") + return await message.reply_text(f"{mention} has {warns}/3 warnings.") + + +# Report User in Group +@app.on_message( + ( + filters.command("report", COMMAND_HANDLER) + | filters.command(["admins", "admin"], prefixes="@") + ) + & ~filters.private +) +@capture_err +async def report_user(_, message): + if not message.reply_to_message: + return await message.reply_text("Reply to a message to report that user.") + reply = message.reply_to_message + reply_id = reply.from_user.id if reply.from_user else reply.sender_chat.id + user_id = message.from_user.id if message.from_user else message.sender_chat.id + if reply_id == user_id: + return await message.reply_text("Why are you reporting yourself ?") + + list_of_admins = await list_admins(message.chat.id) + linked_chat = (await app.get_chat(message.chat.id)).linked_chat + if linked_chat is None: + if reply_id in list_of_admins or reply_id == message.chat.id: + return await message.reply_text( + "Do you know that the user you are replying is an admin ?" + ) + + elif ( + reply_id in list_of_admins + or reply_id == message.chat.id + or reply_id == linked_chat.id + ): + return await message.reply_text( + "Do you know that the user you are replying is an admin ?" + ) + user_mention = ( + reply.from_user.mention if reply.from_user else reply.sender_chat.title + ) + text = f"Reported {user_mention} to admins!" + admin_data = [ + m + async for m in app.get_chat_members( + message.chat.id, filter=enums.ChatMembersFilter.ADMINISTRATORS + ) + ] + for admin in admin_data: + if admin.user.is_bot or admin.user.is_deleted: + # return bots or deleted admins + continue + text += f"[\u2063](tg://user?id={admin.user.id})" + await message.reply_to_message.reply_text(text) diff --git a/misskaty/plugins/afk.py b/misskaty/plugins/afk.py new file mode 100644 index 00000000..2a7bd9fd --- /dev/null +++ b/misskaty/plugins/afk.py @@ -0,0 +1,175 @@ +# Sample menggunakan modul motor mongodb +import time, asyncio +from misskaty import app +from pyrogram import filters +from misskaty.vars import COMMAND_HANDLER +from database.afk_db import remove_afk, is_afk, add_afk +from misskaty.helper.human_read import get_readable_time2 +from misskaty.core.decorator.errors import capture_err + +__MODULE__ = "AFK" +__HELP__ = """/afk [Reason > Optional] - Tell others that you are AFK (Away From Keyboard), so that your boyfriend or girlfriend won't look for you šŸ’”. +Just type something in group to remove AFK Status.""" + + +# Handle set AFK Command +@capture_err +@app.on_message(filters.command(["afk"], COMMAND_HANDLER)) +async def active_afk(_, message): + if message.sender_chat: + return + user_id = message.from_user.id + verifier, reasondb = await is_afk(user_id) + if verifier: + await remove_afk(user_id) + try: + afktype = reasondb["type"] + timeafk = reasondb["time"] + data = reasondb["data"] + reasonafk = reasondb["reason"] + seenago = get_readable_time2((int(time.time() - timeafk))) + if afktype == "text": + return await message.reply_text( + f"**{message.from_user.first_name}** is back online and was away for {seenago}", + disable_web_page_preview=True, + ) + if afktype == "text_reason": + return await message.reply_text( + f"**{message.from_user.first_name}** is back online and was away for {seenago}\n\n**Reason:** {reasonafk}", + disable_web_page_preview=True, + ) + if afktype == "animation": + return ( + await message.reply_animation( + data, + caption=f"**{message.from_user.first_name}** is back online and was away for {seenago}", + ) + if str(reasonafk) == "None" + else await message.reply_animation( + data, + caption=f"**{message.from_user.first_name}** is back online and was away for {seenago}\n\n**Reason:** {reasonafk}", + ) + ) + + elif afktype == "photo": + return ( + await message.reply_photo( + photo=f"downloads/{user_id}.jpg", + caption=f"**{message.from_user.first_name}** is back online and was away for {seenago}", + ) + if str(reasonafk) == "None" + else await message.reply_photo( + photo=f"downloads/{user_id}.jpg", + caption=f"**{message.from_user.first_name}** is back online and was away for {seenago}\n\n**Reason:** {reasonafk}", + ) + ) + + except Exception: + return await message.reply_text( + f"**{message.from_user.first_name}** is back online.", + disable_web_page_preview=True, + ) + if len(message.command) == 1 and not message.reply_to_message: + details = { + "type": "text", + "time": time.time(), + "data": None, + "reason": None, + } + elif len(message.command) > 1 and not message.reply_to_message: + _reason = (message.text.split(None, 1)[1].strip())[:100] + details = { + "type": "text_reason", + "time": time.time(), + "data": None, + "reason": _reason, + } + elif len(message.command) == 1 and message.reply_to_message.animation: + _data = message.reply_to_message.animation.file_id + details = { + "type": "animation", + "time": time.time(), + "data": _data, + "reason": None, + } + elif len(message.command) > 1 and message.reply_to_message.animation: + _data = message.reply_to_message.animation.file_id + _reason = (message.text.split(None, 1)[1].strip())[:100] + details = { + "type": "animation", + "time": time.time(), + "data": _data, + "reason": _reason, + } + elif len(message.command) == 1 and message.reply_to_message.photo: + await app.download_media(message.reply_to_message, file_name=f"{user_id}.jpg") + details = { + "type": "photo", + "time": time.time(), + "data": None, + "reason": None, + } + elif len(message.command) > 1 and message.reply_to_message.photo: + await app.download_media(message.reply_to_message, file_name=f"{user_id}.jpg") + _reason = message.text.split(None, 1)[1].strip() + details = { + "type": "photo", + "time": time.time(), + "data": None, + "reason": _reason, + } + elif len(message.command) == 1 and message.reply_to_message.sticker: + if message.reply_to_message.sticker.is_animated: + details = { + "type": "text", + "time": time.time(), + "data": None, + "reason": None, + } + else: + await app.download_media( + message.reply_to_message, file_name=f"{user_id}.jpg" + ) + details = { + "type": "photo", + "time": time.time(), + "data": None, + "reason": None, + } + elif len(message.command) > 1 and message.reply_to_message.sticker: + _reason = (message.text.split(None, 1)[1].strip())[:100] + if message.reply_to_message.sticker.is_animated: + details = { + "type": "text_reason", + "time": time.time(), + "data": None, + "reason": _reason, + } + else: + await app.download_media( + message.reply_to_message, file_name=f"{user_id}.jpg" + ) + details = { + "type": "photo", + "time": time.time(), + "data": None, + "reason": _reason, + } + else: + details = { + "type": "text", + "time": time.time(), + "data": None, + "reason": None, + } + + await add_afk(user_id, details) + pesan = await message.reply_text( + f"{message.from_user.mention} [{message.from_user.id}] is now AFK! This message will be deleted in 10s." + ) + await asyncio.sleep(10) + await pesan.delete() + try: + await message.delete() + except: + pass diff --git a/misskaty/plugins/auto_approve.py b/misskaty/plugins/auto_approve.py new file mode 100644 index 00000000..c476afd1 --- /dev/null +++ b/misskaty/plugins/auto_approve.py @@ -0,0 +1,44 @@ +from misskaty import app +from pyrogram import filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from pyrogram.errors import UserIsBlocked, UserAlreadyParticipant +from misskaty.core.decorator.errors import capture_err + + +@capture_err +@app.on_chat_join_request(filters.chat(-1001686184174)) +async def approve_join_chat(c, m): + try: + markup = InlineKeyboardMarkup([[InlineKeyboardButton(text="Sudah", callback_data=f"approve_{m.chat.id}"), InlineKeyboardButton(text="Belum", callback_data=f"declined_{m.chat.id}")]]) + await c.send_message( + m.from_user.id, + "PERMINTAAN JOIN CHANNEL YMOVIEZ REBORN\n\nSebelum masuk ke channel ada tes kejujuran, apakah anda sudah membaca catatan di @YMovieZ_New? Jika sudah silahkan klik Sudah, jika kamu berbohong resiko kamu tanggung sendiri šŸ˜¶ā€šŸŒ«ļø.\n\nBot by @YasirPediaChannel", + disable_web_page_preview=True, + reply_markup=markup, + ) + except UserIsBlocked: + await m.decline() + + +@app.on_callback_query(filters.regex(r"^approve")) +async def approve_chat(c, q): + i, chat = q.data.split("_") + try: + await q.message.edit("Yeayy, selamat kamu bisa bergabung di Channel YMovieZ Reborn...") + await c.approve_chat_join_request(chat, q.from_user.id) + except UserAlreadyParticipant: + await q.message.edit("Kamu sudah di acc join grup, jadi ga perlu menekan button.") + except Exception as err: + await q.message.edit(err) + + +@app.on_callback_query(filters.regex(r"^declined")) +async def decline_chat(c, q): + i, chat = q.data.split("_") + try: + await q.message.edit("Yahh, kamu ditolak join channel. Biasakan rajin membaca yahhh..") + await c.decline_chat_join_request(chat, q.from_user.id) + except UserAlreadyParticipant: + await q.message.edit("Kamu sudah di acc join grup, jadi ga perlu menekan button.") + except Exception as err: + await q.message.edit(err) diff --git a/misskaty/plugins/auto_forwarder.py b/misskaty/plugins/auto_forwarder.py new file mode 100644 index 00000000..c8b0c09d --- /dev/null +++ b/misskaty/plugins/auto_forwarder.py @@ -0,0 +1,93 @@ +# Code copy from https://github.com/AbirHasan2005/Forward-Client +import logging +from misskaty import user +from pyrogram import filters +from asyncio import sleep +from pyrogram.types import Message +from pyrogram.errors import FloodWait +from misskaty.vars import ( + FORWARD_FILTERS, + BLOCK_FILES_WITHOUT_EXTENSIONS, + BLOCKED_EXTENSIONS, + FORWARD_FROM_CHAT_ID, + FORWARD_TO_CHAT_ID, + MINIMUM_FILE_SIZE, +) + + +async def FilterMessage(message: Message): + if (message.forward_from or message.forward_from_chat) and ( + "forwarded" not in FORWARD_FILTERS + ): + return 400 + if (len(FORWARD_FILTERS) == 9) or ( + (message.video and ("video" in FORWARD_FILTERS)) + or (message.document and ("document" in FORWARD_FILTERS)) + or (message.photo and ("photo" in FORWARD_FILTERS)) + or (message.audio and ("audio" in FORWARD_FILTERS)) + or (message.text and ("text" in FORWARD_FILTERS)) + or (message.animation and ("gif" in FORWARD_FILTERS)) + or (message.poll and ("poll" in FORWARD_FILTERS)) + or (message.sticker and ("sticker" in FORWARD_FILTERS)) + ): + return 200 + else: + return 400 + + +async def CheckBlockedExt(event: Message): + media = event.document or event.video or event.audio or event.animation + if (BLOCK_FILES_WITHOUT_EXTENSIONS is True) and ("." not in media.file_name): + return True + if (media is not None) and (media.file_name is not None): + _file = media.file_name.rsplit(".", 1) + if len(_file) == 2: + return ( + _file[-1].lower() in BLOCKED_EXTENSIONS + or _file[-1].upper() in BLOCKED_EXTENSIONS + ) + + else: + return False + + +async def CheckFileSize(msg: Message): + media = msg.video or msg.document or msg.audio or msg.photo or msg.animation + return MINIMUM_FILE_SIZE is None or media.file_size >= int(MINIMUM_FILE_SIZE) + + +async def ForwardMessage(client: user, msg: Message): + try: + ## --- Check 1 --- ## + can_forward = await FilterMessage(message=msg) + if can_forward == 400: + return 400 + ## --- Check 2 --- ## + has_blocked_ext = await CheckBlockedExt(event=msg) + if has_blocked_ext is True: + return 400 + ## --- Check 3 --- ## + file_size_passed = await CheckFileSize(msg=msg) + if file_size_passed is False: + return 400 + ## --- Check 4 --- ## + for i in range(len(FORWARD_TO_CHAT_ID)): + try: + await msg.copy(FORWARD_TO_CHAT_ID[i]) + except FloodWait as e: + await sleep(e.value) + logging.warning(f"#FloodWait: Stopped Forwarder for {e.x}s!") + await ForwardMessage(client, msg) + except Exception as err: + logging.warning( + f"#ERROR: {err}\n\nUnable to Forward Message to {str(FORWARD_TO_CHAT_ID[i])}, reason: {err}" + ) + except Exception as err: + logging.warning(f"#ERROR: {err}") + + +@user.on_message((filters.text | filters.media) & filters.chat(FORWARD_FROM_CHAT_ID)) +async def forwardubot(client: user, message: Message): + try_forward = await ForwardMessage(client, message) + if try_forward == 400: + return diff --git a/misskaty/plugins/banned.py b/misskaty/plugins/banned.py new file mode 100644 index 00000000..6c22eb8f --- /dev/null +++ b/misskaty/plugins/banned.py @@ -0,0 +1,40 @@ +from pyrogram import filters +from utils import temp +from pyrogram.types import Message +from database.users_chats_db import db +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from misskaty.vars import SUPPORT_CHAT +from misskaty import app + + +async def banned_users(_, client, message: Message): + return (message.from_user is not None or not message.sender_chat) and message.from_user.id in temp.BANNED_USERS + + +banned_user = filters.create(banned_users) + + +async def disabled_chat(_, client, message: Message): + return message.chat.id in temp.BANNED_CHATS + + +disabled_group = filters.create(disabled_chat) + + +@app.on_message(filters.private & banned_user & filters.incoming) +async def ban_reply(bot, message): + ban = await db.get_ban_status(message.from_user.id) + await message.reply(f'Sorry Dude, You are Banned to use Me. \nBan Reason: {ban["ban_reason"]}') + + +@app.on_message(filters.group & disabled_group & filters.incoming) +async def grp_bd(bot, message): + buttons = [[InlineKeyboardButton("Support", url=f"https://t.me/{SUPPORT_CHAT}")]] + reply_markup = InlineKeyboardMarkup(buttons) + vazha = await db.get_chat(message.chat.id) + k = await message.reply(text=f"CHAT NOT ALLOWED šŸž\n\nMy admins has restricted me from working here ! If you want to know more about it contact support..\nReason : {vazha['reason']}.", reply_markup=reply_markup) + try: + await k.pin() + except: + pass + await bot.leave_chat(message.chat.id) diff --git a/misskaty/plugins/broadcast.py b/misskaty/plugins/broadcast.py new file mode 100644 index 00000000..ebf0bbba --- /dev/null +++ b/misskaty/plugins/broadcast.py @@ -0,0 +1,44 @@ +from pyrogram import filters +import datetime +import time +from database.users_chats_db import db +from misskaty.vars import SUDO +from utils import broadcast_messages +import asyncio +from misskaty import app + + +@app.on_message(filters.command("broadcast") & filters.user(SUDO) & filters.reply) +async def broadcast(bot, message): + users = await db.get_all_users() + b_msg = message.reply_to_message + sts = await message.reply_text(text="Broadcasting your messages...") + start_time = time.time() + total_users = await db.total_users_count() + done = 0 + blocked = 0 + deleted = 0 + failed = 0 + + success = 0 + async for user in users: + pti, sh = await broadcast_messages(int(user["id"]), b_msg) + if pti: + success += 1 + elif pti == False: + if sh == "Bocked": + blocked += 1 + elif sh == "Deleted": + deleted += 1 + elif sh == "Error": + failed += 1 + done += 1 + await asyncio.sleep(2) + if not done % 20: + await sts.edit( + f"Broadcast in progress:\n\nTotal Users {total_users}\nCompleted: {done} / {total_users}\nSuccess: {success}\nBlocked: {blocked}\nDeleted: {deleted}" + ) + time_taken = datetime.timedelta(seconds=int(time.time() - start_time)) + await sts.edit( + f"Broadcast Completed:\nCompleted in {time_taken} seconds.\n\nTotal Users {total_users}\nCompleted: {done} / {total_users}\nSuccess: {success}\nBlocked: {blocked}\nDeleted: {deleted}" + ) diff --git a/misskaty/plugins/bypass.py b/misskaty/plugins/bypass.py new file mode 100644 index 00000000..a8a08db2 --- /dev/null +++ b/misskaty/plugins/bypass.py @@ -0,0 +1,73 @@ +import re +from misskaty.helper.http import http +from misskaty import app +from pyrogram import filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from pyrogram.errors import MessageTooLong, EntitiesTooLong +from misskaty.vars import COMMAND_HANDLER +from misskaty.helper.tools import rentry +from urllib.parse import unquote +from misskaty.core.decorator.errors import capture_err +from misskaty.helper.human_read import get_readable_file_size + +LIST_LINK = """ +- Pling and all aliases. +- Other link soon... +""" + +__MODULE__ = "Bypass" +__HELP__ = f""" +/directurl [Link] - Bypass URL. + +Supported Link: +{LIST_LINK} +""" + + +async def pling_bypass(url): + try: + id_url = re.search(r"https?://(store.kde.org|www.pling.com)\/p\/(\d+)", url)[2] + link = f"https://www.pling.com/p/{id_url}/loadFiles" + res = await http.get(link) + json_dic_files = res.json().pop("files") + msg = f"\n**Source Link** :\n`{url}`\n**Direct Link :**\n" + msg += "\n".join( + f'**→ [{i["name"]}]({unquote(i["url"])}) ({get_readable_file_size(int(i["size"]))})**' + for i in json_dic_files + ) + return msg + except Exception as e: + return e + + +@app.on_message(filters.command(["directurl"], COMMAND_HANDLER)) +@capture_err +async def bypass(_, message): + if len(message.command) == 1: + return await message.reply( + f"Gunakan perintah /{message.command[0]} untuk bypass url" + ) + url = message.command[1] + msg = await message.reply("Bypassing URL..", quote=True) + mention = f"**Bypasser:** {message.from_user.mention} ({message.from_user.id})" + if re.match(r"https?://(store.kde.org|www.pling.com)\/p\/(\d+)", url): + data = await pling_bypass(url) + try: + await msg.edit(f"{data}\n\n{mention}") + except (MessageTooLong, EntitiesTooLong): + result = await rentry(data) + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("Open Link", url=result), + InlineKeyboardButton("Raw Link", url=f"{result}/raw"), + ] + ] + ) + await msg.edit( + f"{result}\n\nBecause your bypassed url is too long, so your link will be pasted to rentry.\n{mention}", + reply_markup=markup, + disable_web_page_preview=True, + ) + else: + await msg.edit("Unsupported link..") diff --git a/misskaty/plugins/code_tester.py b/misskaty/plugins/code_tester.py new file mode 100644 index 00000000..1367e5e8 --- /dev/null +++ b/misskaty/plugins/code_tester.py @@ -0,0 +1,710 @@ +import aiohttp +from pyrogram import enums, filters +from pyrogram.errors import MessageTooLong +from misskaty import app +from misskaty.helper.tools import rentry +from misskaty.vars import COMMAND_HANDLER + +__MODULE__ = "CodeTester" +__HELP__ = """ +This feature allows you to run multiple programming languages through this bot via the Glot.io api. The following is a list of supported languages, for temporary commands only support with a "!" like the example below. + +List of Supported Programming Languages: +~> assembly +~> ats +~> bash +~> c +~> clojure +~> cobol +~> coffeescript +~> cpp +~> crystal +~> csharp +~> d +~> elixir +~> elm +~> erlang +~> fsharp +~> go +~> groovy +~> haskell +~> idris +~> java +~> javascript +~> julia +~> kotlin +~> lua +~> mercury +~> nim +~> nix +~> ocaml +~> perl +~> php +~> python +~> raku +~> ruby +~> rust +~> scala +~> swift +~> typescript + +**Example:** +~> `!python print("Hai aku MissKatyRoBot")` +""" + + +async def listcode(): + async with aiohttp.ClientSession() as session: + r = await session.get("https://glot.io/api/run") + return await r.json() + + +async def glot(lang, langcode, code): + async with aiohttp.ClientSession() as session: + data = {"files": [{"name": f"misskaty.{langcode}", "content": code}]} + headers = { + "content-type": "application/json", + "Authorization": "Token b8a2b75a-a078-4089-869c-e53d448b1ebb", + } + r = await session.post( + f"https://glot.io/api/run/{lang}/latest", headers=headers, json=data + ) + return await r.json() + + +@app.on_message(filters.command(["codelist"], COMMAND_HANDLER)) +async def list_lang(client, message): + daftarlang = await listcode() + list_ = "".join(f"~> {i['name']}\n" for i in daftarlang) + return await message.reply( + f"List of Supported Programming Languages:\n{list_}" + ) + + +@app.on_message(filters.command(["assembly"], "!")) +@app.on_edited_message(filters.command(["assembly"], "!")) +async def assembly(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "asm", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["ats"], "!")) +@app.on_edited_message(filters.command(["ats"], "!")) +async def ats(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "dats", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["bash"], "!")) +@app.on_edited_message(filters.command(["bash"], "!")) +async def bash(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "sh", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["c"], "!")) +@app.on_edited_message(filters.command(["c"], "!")) +async def c(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "c", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["clojure"], "!")) +@app.on_edited_message(filters.command(["clojure"], "!")) +async def clojure(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "clj", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["cobol"], "!")) +@app.on_edited_message(filters.command(["cobol"], "!")) +async def cobol(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "cob", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["coffeescript"], "!")) +@app.on_edited_message(filters.command(["coffeescript"], "!")) +async def coffeescript(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "coffee", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["cpp"], "!")) +@app.on_edited_message(filters.command(["cpp"], "!")) +async def cpp(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "cpp", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["crystal"], "!")) +@app.on_edited_message(filters.command(["crystal"], "!")) +async def crystal(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "cr", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["csharp"], "!")) +@app.on_edited_message(filters.command(["csharp"], "!")) +async def csharp(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "cs", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["d"], "!")) +@app.on_edited_message(filters.command(["d"], "!")) +async def d(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "d", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["elixir"], "!")) +@app.on_edited_message(filters.command(["elixir"], "!")) +async def elixir(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "ex", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["elm"], "!")) +@app.on_edited_message(filters.command(["elm"], "!")) +async def elm(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "elm", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["erlang"], "!")) +@app.on_edited_message(filters.command(["erlang"], "!")) +async def erlang(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "erl", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["fsharp"], "!")) +@app.on_edited_message(filters.command(["fsharp"], "!")) +async def fsharp(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "fs", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["go"], "!")) +@app.on_edited_message(filters.command(["go"], "!")) +async def go(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "go", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["groovy"], "!")) +@app.on_edited_message(filters.command(["groovy"], "!")) +async def groovy(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "groovy", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["haskell"], "!")) +@app.on_edited_message(filters.command(["haskell"], "!")) +async def haskell(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "hs", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["idris"], "!")) +@app.on_edited_message(filters.command(["idris"], "!")) +async def idris(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "idr", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["java"], "!")) +@app.on_edited_message(filters.command(["java"], "!")) +async def java(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "java", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["javascript"], "!")) +@app.on_edited_message(filters.command(["javascript"], "!")) +async def javascript(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "js", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["julia"], "!")) +@app.on_edited_message(filters.command(["julia"], "!")) +async def julia(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "jl", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["kotlin"], "!")) +@app.on_edited_message(filters.command(["kotlin"], "!")) +async def kotlin(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "kt", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["lua"], "!")) +@app.on_edited_message(filters.command(["lua"], "!")) +async def lua(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "lua", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["mercury"], "!")) +@app.on_edited_message(filters.command(["mercury"], "!")) +async def mercury(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "m", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["nim"], "!")) +@app.on_edited_message(filters.command(["nim"], "!")) +async def nim(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "nim", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["nix"], "!")) +@app.on_edited_message(filters.command(["nix"], "!")) +async def nix(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "nix", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["ocaml"], "!")) +@app.on_edited_message(filters.command(["ocaml"], "!")) +async def ocaml(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "ml", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["perl"], "!")) +@app.on_edited_message(filters.command(["perl"], "!")) +async def perl(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "pl", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["php"], "!")) +@app.on_edited_message(filters.command(["php"], "!")) +async def php(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "php", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["python"], "!")) +@app.on_edited_message(filters.command(["python"], "!")) +async def python(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "py", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["raku"], "!")) +@app.on_edited_message(filters.command(["raku"], "!")) +async def raku(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "raku", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["ruby"], "!")) +@app.on_edited_message(filters.command(["ruby"], "!")) +async def ruby(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "rb", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["rust"], "!")) +@app.on_edited_message(filters.command(["rust"], "!")) +async def rust(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "rs", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["scala"], "!")) +@app.on_edited_message(filters.command(["scala"], "!")) +async def scala(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "scala", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["swift"], "!")) +@app.on_edited_message(filters.command(["swift"], "!")) +async def swift(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "swift", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) + + +@app.on_message(filters.command(["typescript"], "!")) +@app.on_edited_message(filters.command(["typescript"], "!")) +async def typescript(client, message): + if len(message.command) < 2: + return await message.reply("Please enter the code you want to run.") + res = await glot(message.command[0], "ts", message.text.split(None, 1)[1]) + hasil = res["stdout"] or res["stderr"] + hasil = f"Result :\n{hasil}" + try: + return await message.reply(hasil, parse_mode=enums.ParseMode.DISABLED) + except MessageTooLong: + post = await rentry(hasil) + return await message.reply(f"View Result in Rentry:\n{post}") + except Exception as e: + return await message.reply(e, parse_mode=enums.ParseMode.DISABLED) diff --git a/misskaty/plugins/copy_forward.py b/misskaty/plugins/copy_forward.py new file mode 100644 index 00000000..2eeba8b8 --- /dev/null +++ b/misskaty/plugins/copy_forward.py @@ -0,0 +1,77 @@ +from pyrogram import filters, enums +from pyrogram.errors import UserIsBlocked, UserNotParticipant +from misskaty.vars import COMMAND_HANDLER +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from misskaty.core.decorator.errors import capture_err +from misskaty import app + + +@app.on_message(filters.command(["copy"], COMMAND_HANDLER)) +async def copy(client, message): + if len(message.command) == 1: + if not message.reply_to_message: + return await message.reply("Silahkan balas pesan yang mau dicopy.") + try: + await message.reply_to_message.copy(message.from_user.id, caption_entities=message.reply_to_message.entities, reply_markup=message.reply_to_message.reply_markup) + return await message.reply_text("Pesan berhasil dikirim..") + except UserIsBlocked: + return await message.reply("Silahkan PM Saya untuk mengcopy pesan ke chat pribadi..", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="šŸ’¬ Chat Aku Yahh", url="https://t.me/MissKatyRoBot")]])) + except Exception as e: + return await message.reply(f"ERROR: {str(e)}") + elif message.reply_to_message: + try: + idtujuan = message.command[1] + userstat = await app.get_chat_member(-1001686184174, message.from_user.id) + if ( + userstat.status + not in [ + enums.ChatMemberStatus.ADMINISTRATOR, + enums.ChatMemberStatus.OWNER, + ] + and message.from_user.id != 2024984460 + ): + return await message.reply_text("šŸ¦‰šŸ¦‰šŸ¦‰") + await message.reply_to_message.copy(idtujuan, caption_entities=message.reply_to_message.entities, reply_markup=message.reply_to_message.reply_markup) + return await message.reply_text("Pesan berhasil dikirim..") + except UserNotParticipant: + return await message.reply("Command ini hanya untuk admin YMoviezNew") + except Exception as e: + return await message.reply(f"ERROR: {e}") + else: + await message.reply("Silahkan balas pesan yang mau dicopy.") + + +@app.on_message(filters.command(["forward"], COMMAND_HANDLER)) +@capture_err +async def forward(client, message): + if len(message.command) == 1: + if not message.reply_to_message: + return await message.reply("Silahkan balas pesan yang mau dicopy.") + try: + await message.reply_to_message.forward(message.from_user.id) + return await message.reply_text("Pesan berhasil dikirim..") + except UserIsBlocked: + return await message.reply("Silahkan PM Saya untuk memforward pesan ke chat pribadi..", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="šŸ’¬ Chat Aku Yahh", url="https://t.me/MissKatyRoBot")]])) + except Exception as e: + return await message.reply(f"ERROR: {str(e)}") + elif message.reply_to_message: + try: + idtujuan = message.command[1] + userstat = await app.get_chat_member(-1001686184174, message.from_user.id) + if ( + userstat.status + not in [ + enums.ChatMemberStatus.ADMINISTRATOR, + enums.ChatMemberStatus.OWNER, + ] + and message.from_user.id != 2024984460 + ): + return await message.reply_text("šŸ¦‰šŸ¦‰šŸ¦‰") + await message.reply_to_message.forward(idtujuan) + return await message.reply_text("Pesan berhasil dikirim..") + except UserNotParticipant: + return await message.reply("Comman ini hanya untuk admin YMoviezNew") + except Exception as e: + return await message.reply(f"ERROR: {e}") + else: + await message.reply("Silahkan balas pesan yang mau diforward.") diff --git a/misskaty/plugins/detect_afk.py b/misskaty/plugins/detect_afk.py new file mode 100644 index 00000000..151abc9f --- /dev/null +++ b/misskaty/plugins/detect_afk.py @@ -0,0 +1,220 @@ +import re +import time +import asyncio +from misskaty import app +from pyrogram import filters, enums +from database.afk_db import remove_afk, is_afk +from misskaty.helper.human_read import get_readable_time2 + +# Detect user that AFK based on Yukki Repo +@app.on_message( + filters.group & ~filters.bot & ~filters.via_bot, + group=1, +) +async def chat_watcher_func(_, message): + if message.sender_chat: + return + userid = message.from_user.id + user_name = message.from_user.first_name + if message.entities: + possible = ["/afk", f"/afk@MissKatyRoBot", "!afk"] + message_text = message.text or message.caption + for entity in message.entities: + if entity.type == enums.MessageEntityType.BOT_COMMAND: + if (message_text[0 : 0 + entity.length]).lower() in possible: + return + + msg = "" + replied_user_id = 0 + + # Self AFK + verifier, reasondb = await is_afk(userid) + if verifier: + await remove_afk(userid) + try: + afktype = reasondb["type"] + timeafk = reasondb["time"] + data = reasondb["data"] + reasonafk = reasondb["reason"] + seenago = get_readable_time2((int(time.time() - timeafk))) + if afktype == "text": + msg += f"**{user_name[:25]}** is back online and was away for {seenago}\n\n" + if afktype == "text_reason": + msg += f"**{user_name[:25]}** is back online and was away for {seenago}\n\n**Reason:** {reasonafk}\n\n" + if afktype == "animation": + if str(reasonafk) == "None": + await message.reply_animation( + data, + caption=f"**{user_name[:25]}** is back online and was away for {seenago}\n\n", + ) + else: + await message.reply_animation( + data, + caption=f"**{user_name[:25]}** is back online and was away for {seenago}\n\n**Reason:** {reasonafk}\n\n", + ) + if afktype == "photo": + if str(reasonafk) == "None": + await message.reply_photo( + photo=f"downloads/{userid}.jpg", + caption=f"**{user_name[:25]}** is back online and was away for {seenago}\n\n", + ) + else: + await message.reply_photo( + photo=f"downloads/{userid}.jpg", + caption=f"**{user_name[:25]}** is back online and was away for {seenago}\n\n**Reason:** {reasonafk}\n\n", + ) + except: + msg += f"**{user_name[:25]}** is back online.\n\n" + + # Replied to a User which is AFK + if message.reply_to_message: + try: + replied_first_name = message.reply_to_message.from_user.first_name + replied_user_id = message.reply_to_message.from_user.id + verifier, reasondb = await is_afk(replied_user_id) + if verifier: + try: + afktype = reasondb["type"] + timeafk = reasondb["time"] + data = reasondb["data"] + reasonafk = reasondb["reason"] + seenago = get_readable_time2((int(time.time() - timeafk))) + if afktype == "text": + msg += f"**{replied_first_name[:25]}** is AFK since {seenago} ago.\n\n" + if afktype == "text_reason": + msg += f"**{replied_first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason:** {reasonafk}\n\n" + if afktype == "animation": + if str(reasonafk) == "None": + await message.reply_animation( + data, + caption=f"**{replied_first_name[:25]}** is AFK since {seenago} ago.\n\n", + ) + else: + await message.reply_animation( + data, + caption=f"**{replied_first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason:** {reasonafk}\n\n", + ) + if afktype == "photo": + if str(reasonafk) == "None": + await message.reply_photo( + photo=f"downloads/{replied_user_id}.jpg", + caption=f"**{replied_first_name[:25]}** is AFK since {seenago} ago.\n\n", + ) + else: + await message.reply_photo( + photo=f"downloads/{replied_user_id}.jpg", + caption=f"**{replied_first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason:** {reasonafk}\n\n", + ) + except Exception: + msg += f"**{replied_first_name}** is AFK\n\n" + except: + pass + + # If username or mentioned user is AFK + if message.entities: + entity = message.entities + j = 0 + for x in range(len(entity)): + if (entity[j].type) == enums.MessageEntityType.MENTION: + found = re.findall("@([_0-9a-zA-Z]+)", message.text) + try: + get_user = found[j] + user = await app.get_users(get_user) + if user.id == replied_user_id: + j += 1 + continue + except: + j += 1 + continue + verifier, reasondb = await is_afk(user.id) + if verifier: + try: + afktype = reasondb["type"] + timeafk = reasondb["time"] + data = reasondb["data"] + reasonafk = reasondb["reason"] + seenago = get_readable_time2((int(time.time() - timeafk))) + if afktype == "text": + msg += f"**{user.first_name[:25]}** is AFK since {seenago} ago.\n\n" + if afktype == "text_reason": + msg += f"**{user.first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason:** {reasonafk}\n\n" + if afktype == "animation": + if str(reasonafk) == "None": + await message.reply_animation( + data, + caption=f"**{user.first_name[:25]}** is AFK since {seenago} ago.\n\n", + ) + else: + await message.reply_animation( + data, + caption=f"**{user.first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason**: {reasonafk}\n\n", + ) + if afktype == "photo": + if str(reasonafk) == "None": + await message.reply_photo( + photo=f"downloads/{user.id}.jpg", + caption=f"**{user.first_name[:25]}** is AFK since {seenago} ago.\n\n", + ) + else: + await message.reply_photo( + photo=f"downloads/{user.id}.jpg", + caption=f"**{user.first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason:** {reasonafk}\n\n", + ) + except: + msg += f"**{user.first_name[:25]}** is AFK\n\n" + elif (entity[j].type) == enums.MessageEntityType.TEXT_MENTION: + try: + user_id = entity[j].user.id + if user_id == replied_user_id: + j += 1 + continue + first_name = entity[j].user.first_name + except: + j += 1 + continue + verifier, reasondb = await is_afk(user_id) + if verifier: + try: + afktype = reasondb["type"] + timeafk = reasondb["time"] + data = reasondb["data"] + reasonafk = reasondb["reason"] + seenago = get_readable_time2((int(time.time() - timeafk))) + if afktype == "text": + msg += ( + f"**{first_name[:25]}** is AFK since {seenago} ago.\n\n" + ) + if afktype == "text_reason": + msg += f"**{first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason:** {reasonafk}\n\n" + if afktype == "animation": + if str(reasonafk) == "None": + await message.reply_animation( + data, + caption=f"**{first_name[:25]}** is AFK since {seenago} ago.\n\n", + ) + else: + await message.reply_animation( + data, + caption=f"**{first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason:** {reasonafk}\n\n", + ) + if afktype == "photo": + if str(reasonafk) == "None": + await message.reply_photo( + photo=f"downloads/{user_id}.jpg", + caption=f"**{first_name[:25]}** is AFK since {seenago} ago.\n\n", + ) + else: + await message.reply_photo( + photo=f"downloads/{user_id}.jpg", + caption=f"**{first_name[:25]}** is AFK since {seenago} ago.\n\n**Reason:** {reasonafk}\n\n", + ) + except: + msg += f"**{first_name[:25]}** is AFK\n\n" + j += 1 + if msg != "": + try: + pesan = await message.reply_text(msg, disable_web_page_preview=True) + await asyncio.sleep(20) + await pesan.delete() + except: + return diff --git a/misskaty/plugins/dev.py b/misskaty/plugins/dev.py new file mode 100644 index 00000000..c7f5c245 --- /dev/null +++ b/misskaty/plugins/dev.py @@ -0,0 +1,144 @@ +import io +import sys +import os +import traceback +import asyncio +from pyrogram import filters, enums +from misskaty.vars import COMMAND_HANDLER, SUDO +from misskaty import app + +__MODULE__ = "DevCommand" +__HELP__ = """ +**For Owner Bot Only.** +/run [args] - Run eval CMD +/shell [args] - Run Exec/Terminal CMD +/download [link/reply_to_telegram_file] - Download file from Telegram + +**For Public Use** +/json - Send structure message Telegram in JSON using Pyrogram Style. +""" + + +@app.on_message(filters.command(["logs"]) & filters.user(SUDO)) +async def log_file(bot, message): + """Send log file""" + try: + await message.reply_document("MissKatyLogs.txt", caption="Log Bot MissKatyPyro") + except Exception as e: + await message.reply(str(e)) + + +@app.on_message(filters.command(["donate"], COMMAND_HANDLER)) +async def donate(_, message): + await message.reply_photo( + "AgACAgQAAxkBAAECsVNjbMvjxbN4gRafvNBH-Kv-Zqml8wACzq4xG95tbVPDeZ_UusonbAAIAQADAgADeQAHHgQ", + caption=f"Hai {message.from_user.mention}, jika kamu merasa bot ini berguna bisa melakukan donasi dengan scan kode QRIS diatas untuk kebutuhan server dan lainnya. Terimakasih..", + ) + + +@app.on_message( + filters.command(["balas"], COMMAND_HANDLER) & filters.user(SUDO) & filters.reply +) +async def balas(c, m): + pesan = m.text.split(" ", 1) + await m.delete() + await m.reply(pesan[1], reply_to_message_id=m.reply_to_message.id) + + +@app.on_message(filters.command(["neofetch"], COMMAND_HANDLER) & filters.user(SUDO)) +async def neofetch(c, m): + neofetch = (await shell_exec("neofetch --stdout"))[0] + await m.reply(f"{neofetch}") + + +@app.on_message(filters.command(["shell", "sh"], COMMAND_HANDLER) & filters.user(SUDO)) +@app.on_edited_message( + filters.command(["shell", "sh"], COMMAND_HANDLER) & filters.user(SUDO) +) +async def shell(client, message): + cmd = message.text.split(" ", 1) + if len(cmd) == 1: + return await message.reply("No command to execute was given.") + shell = (await shell_exec(cmd[1]))[0] + if len(shell) > 3000: + with open("shell_output.txt", "w") as file: + file.write(shell) + with open("shell_output.txt", "rb") as doc: + await message.reply_document(document=doc, file_name=doc.name) + try: + os.remove("shell_output.txt") + except: + pass + elif len(shell) != 0: + await message.reply(shell, parse_mode=enums.ParseMode.HTML) + else: + await message.reply("No Reply") + + +@app.on_message(filters.command(["ev", "run"]) & filters.user(SUDO)) +@app.on_edited_message(filters.command(["ev", "run"]) & filters.user(SUDO)) +async def evaluation_cmd_t(client, message): + status_message = await message.reply("__Processing eval pyrogram...__") + try: + cmd = message.text.split(" ", maxsplit=1)[1] + except IndexError: + return await status_message.edit("__No evaluate message!__") + old_stderr = sys.stderr + old_stdout = sys.stdout + redirected_output = sys.stdout = io.StringIO() + redirected_error = sys.stderr = io.StringIO() + stdout, stderr, exc = None, None, None + + try: + await aexec(cmd, client, message) + except Exception: + exc = traceback.format_exc() + + stdout = redirected_output.getvalue() + stderr = redirected_error.getvalue() + sys.stdout = old_stdout + sys.stderr = old_stderr + + evaluation = "" + if exc: + evaluation = exc + elif stderr: + evaluation = stderr + elif stdout: + evaluation = stdout + else: + evaluation = "Success" + + final_output = f"**EVAL**:\n`{cmd}`\n\n**OUTPUT**:\n`{evaluation.strip()}`\n" + + if len(final_output) > 4096: + with open("MissKatyEval.txt", "w+", encoding="utf8") as out_file: + out_file.write(final_output) + await status_message.reply_document( + document="MissKatyEval.txt", + caption=cmd[: 4096 // 4 - 1], + disable_notification=True, + ) + os.remove("MissKatyEval.txt") + await status_message.delete() + else: + await status_message.edit(final_output, parse_mode=enums.ParseMode.MARKDOWN) + + +async def aexec(code, client, message): + exec( + "async def __aexec(client, message): " + + "".join(f"\n {l_}" for l_ in code.split("\n")) + ) + return await locals()["__aexec"](client, message) + + +async def shell_exec(code, treat=True): + process = await asyncio.create_subprocess_shell( + code, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT + ) + + stdout = (await process.communicate())[0] + if treat: + stdout = stdout.decode().strip() + return stdout, process diff --git a/misskaty/plugins/download_upload.py b/misskaty/plugins/download_upload.py new file mode 100644 index 00000000..bb7f3186 --- /dev/null +++ b/misskaty/plugins/download_upload.py @@ -0,0 +1,198 @@ +import time +import asyncio +import math +import os +import logging +import aiohttp +import json +from misskaty.helper.http import http +from bs4 import BeautifulSoup +from misskaty import app +from pySmartDL import SmartDL +from datetime import datetime +from misskaty.core.decorator.errors import capture_err +from misskaty.vars import COMMAND_HANDLER, SUDO +from pyrogram import filters +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from misskaty.helper.pyro_progress import ( + progress_for_pyrogram, + humanbytes, +) + +__MODULE__ = "Download/Upload" +__HELP__ = """ +/download [url] - Download file from URL (Sudo Only) +/download [reply_to_TG_File] - Download TG File +/tiktokdl [link] - Download TikTok Video +/fbdl [link] - Download Facebook Video +/anon [link] - Upload files to Anonfiles +/ytdown [link] - Download YouTube dengan YT-DLP +""" + + +@app.on_message(filters.command(["anon"], COMMAND_HANDLER)) +async def upload(bot, message): + if not message.reply_to_message: + return await message.reply("Please reply to media file.") + if message.reply_to_message is not None: + vid = [message.reply_to_message.video, message.reply_to_message.document] + for v in vid: + if v is not None: + break + m = await message.reply("Download your file to my Server...") + now = time.time() + fileku = await message.reply_to_message.download( + progress=progress_for_pyrogram, + progress_args=("Trying to download, please wait..", m, now), + ) + try: + files = {"file": open(fileku, "rb")} + await m.edit("Uploading to Anonfile, Please Wait||") + callapi = await http.post("https://api.anonfiles.com/upload", files=files) + text = callapi.json() + output = f'File Uploaded to Anonfile\n\nšŸ“‚ File Name: {text["data"]["file"]["metadata"]["name"]}\n\nšŸ“¦ File Size: {text["data"]["file"]["metadata"]["size"]["readable"]}\n\nšŸ“„ Download Link: {text["data"]["file"]["url"]["full"]}' + + btn = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "šŸ“„ Download šŸ“„", url=f"{text['data']['file']['url']['full']}" + ) + ] + ] + ) + await m.edit(output, reply_markup=btn) + except Exception as e: + await bot.send_message(message.chat.id, text=f"Something Went Wrong!\n\n{e}") + os.remove(sed) + + +@app.on_message(filters.command(["download"], COMMAND_HANDLER) & filters.user(SUDO)) +@capture_err +async def download(client, message): + pesan = await message.reply_text("Processing...", quote=True) + if message.reply_to_message is not None: + start_t = datetime.now() + c_time = time.time() + the_real_download_location = await client.download_media( + message=message.reply_to_message, + progress=progress_for_pyrogram, + progress_args=("trying to download, sabar yakk..", pesan, c_time), + ) + end_t = datetime.now() + ms = (end_t - start_t).seconds + await pesan.edit( + f"Downloaded to {the_real_download_location} in {ms} seconds." + ) + elif len(message.command) > 1: + start_t = datetime.now() + the_url_parts = " ".join(message.command[1:]) + url = the_url_parts.strip() + custom_file_name = os.path.basename(url) + if "|" in the_url_parts: + url, custom_file_name = the_url_parts.split("|") + url = url.strip() + custom_file_name = custom_file_name.strip() + download_file_path = os.path.join("downloads/", custom_file_name) + downloader = SmartDL(url, download_file_path, progress_bar=False) + downloader.start(blocking=False) + c_time = time.time() + while not downloader.isFinished(): + total_length = downloader.filesize or None + downloaded = downloader.get_dl_size() + display_message = "" + now = time.time() + diff = now - c_time + percentage = downloader.get_progress() * 100 + speed = downloader.get_speed() + round(diff) * 1000 + progress_str = "[{0}{1}]\nProgress: {2}%".format( + "".join(["ā–ˆ" for _ in range(math.floor(percentage / 5))]), + "".join(["ā–‘" for _ in range(20 - math.floor(percentage / 5))]), + round(percentage, 2), + ) + + estimated_total_time = downloader.get_eta(human=True) + try: + current_message = "trying to download...\n" + current_message += f"URL: {url}\n" + current_message += f"File Name: {custom_file_name}\n" + current_message += f"Speed: {speed}\n" + current_message += f"{progress_str}\n" + current_message += ( + f"{humanbytes(downloaded)} of {humanbytes(total_length)}\n" + ) + current_message += f"ETA: {estimated_total_time}" + if round(diff % 10.00) == 0 and current_message != display_message: + await pesan.edit( + disable_web_page_preview=True, text=current_message + ) + display_message = current_message + await asyncio.sleep(10) + except Exception as e: + logging.info(str(e)) + if os.path.exists(download_file_path): + end_t = datetime.now() + ms = (end_t - start_t).seconds + await pesan.edit( + f"Downloaded to {download_file_path} in {ms} seconds" + ) + else: + await pesan.edit( + "Reply to a Telegram Media, to download it to my local server." + ) + + +@app.on_message(filters.command(["tiktokdl"], COMMAND_HANDLER)) +@capture_err +async def tiktokdl(client, message): + if len(message.command) == 1: + return await message.reply( + f"Use command /{message.command[0]} [link] to download tiktok video." + ) + link = message.command[1] + msg = await message.reply("Trying download...") + try: + r = (await http.get(f"https://api.hayo.my.id/api/tiktok/4?url={link}")).json() + await message.reply_video( + r["linkori"], + caption=f"Title: {r['name']}\n\nUploaded for {message.from_user.mention} [{message.from_user.id}]", + ) + await msg.delete() + except Exception as e: + await message.reply(f"Failed to download tiktok video..\n\nReason: {e}") + await msg.delete() + + +@app.on_message(filters.command(["fbdl"], COMMAND_HANDLER)) +@capture_err +async def fbdl(client, message): + if len(message.command) == 1: + return await message.reply( + f"Use command /{message.command[0]} [link] to download Facebook video." + ) + link = message.command[1] + msg = await message.reply("Trying download...") + try: + resjson = (await http.get(f"https://yasirapi.eu.org/fbdl?link={link}")).json() + try: + url = resjson["result"]["links"]["hd"].replace("&", "&") + except: + url = resjson["result"]["links"]["sd"].replace("&", "&") + obj = SmartDL(url, progress_bar=False) + obj.start() + path = obj.get_dest() + await message.reply_video( + path, + caption=f"{os.path.basename(path)}\n\nUploaded for {message.from_user.mention} [{message.from_user.id}]", + ) + await msg.delete() + try: + os.remove(path) + except: + pass + except Exception as e: + await message.reply( + f"Failed to download Facebook video..\n\nReason: {e}" + ) + await msg.delete() diff --git a/misskaty/plugins/filter_request.py b/misskaty/plugins/filter_request.py new file mode 100644 index 00000000..656ac802 --- /dev/null +++ b/misskaty/plugins/filter_request.py @@ -0,0 +1,199 @@ +import re +import random +from misskaty import app +from pyrogram import enums, filters +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from pyrogram.errors import UserNotParticipant, PeerIdInvalid +from misskaty.core.decorator.errors import capture_err +from misskaty.helper.time_gap import check_time_gap + +chat = [-1001128045651, -1001255283935, -1001455886928] +REQUEST_DB = {} + + +@app.on_message(filters.regex(r"alamu'?ala[iy]ku+m", re.I) & filters.chat(chat)) +async def start(_, message): + await message.reply_text(text=f"Wa'alaikumsalam {message.from_user.mention} šŸ˜‡") + + +@app.on_message(filters.regex(r"#request|#req", re.I) & (filters.text | filters.photo) & filters.chat(-1001255283935) & ~filters.channel) +@capture_err +async def request_user(client, message): + if message.sender_chat: + return await message.reply(f"{message.from_user.mention} mohon gunakan akun asli saat request.") + is_in_gap, sleep_time = await check_time_gap(message.from_user.id) + if is_in_gap: + return await message.reply("Sabar dikit napa.. šŸ™„") + markup = InlineKeyboardMarkup( + [ + [InlineKeyboardButton(text="šŸ’¬ Lihat Pesan", url=f"https://t.me/c/1255283935/{message.id}")], + [InlineKeyboardButton(text="🚫 Tolak", callback_data=f"rejectreq_{message.id}_{message.chat.id}"), InlineKeyboardButton(text="āœ… Done", callback_data=f"donereq_{message.id}_{message.chat.id}")], + [InlineKeyboardButton(text="āš ļø Tidak Tersedia", callback_data=f"unavailablereq_{message.id}_{message.chat.id}")], + [InlineKeyboardButton(text="šŸ” Sudah Ada", callback_data=f"dahada_{message.id}_{message.chat.id}")], + ] + ) + try: + user_id = message.from_user.id + if user_id in REQUEST_DB: + REQUEST_DB[user_id] += 1 + else: + REQUEST_DB[user_id] = 1 + if REQUEST_DB[user_id] > 3: + return await message.reply(f"Mohon maaf {message.from_user.mention}, maksimal request hanya 3x perhari. Kalo mau tambah 5k per request šŸ˜šŸ˜.") + if message.text: + forward = await client.send_message(-1001575525902, f"Request by {message.from_user.first_name} (#id{message.from_user.id})\n\n{message.text}", reply_markup=markup) + markup2 = InlineKeyboardMarkup([[InlineKeyboardButton(text="ā³ Cek Status Request", url=f"https://t.me/c/1575525902/{forward.id}")]]) + if message.photo: + forward = await client.send_photo( + -1001575525902, message.photo.file_id, caption=f"Request by {message.from_user.first_name} (#id{message.from_user.id})\n\n{message.caption}", reply_markup=markup + ) + markup2 = InlineKeyboardMarkup([[InlineKeyboardButton(text="ā³ Cek Status Request", url=f"https://t.me/c/1575525902/{forward.id}")]]) + await message.reply_text(text=f"Hai {message.from_user.mention}, request kamu sudah dikirim yaa. Harap bersabar mungkin admin juga punya kesibukan lain.\n\nSisa Request: {3 - REQUEST_DB[user_id]}x", quote=True, reply_markup=markup2) + except: + pass + + +async def clear_reqdict(): + REQUEST_DB.clear() + + +# @app.on_message(filters.regex(r"makasi|thank|terimakasih|terima kasih|mksh", re.I) & filters.chat(chat)) +async def start(_, message): + pesan = [ + f"Sama-sama {message.from_user.first_name}", + f"You're Welcome {message.from_user.first_name}", + "Oke..", + "Yoi..", + "Terimakasih Kembali..", + "Sami-Sami...", + "Sama-sama, senang bisa membantu..", + f"Yups, Sama-sama {message.from_user.first_name}", + "Okayyy...", + ] + await message.reply_text(text=random.choice(pesan)) + + +@app.on_callback_query(filters.regex(r"^donereq")) +async def _callbackreq(c, q): + try: + user = await c.get_chat_member(-1001201566570, q.from_user.id) + if user.status in [enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER]: + i, msg_id, chat_id = q.data.split("_") + await c.send_message( + chat_id=chat_id, + text="#Done\nDone āœ…, Selamat menonton. Jika request tidak bisa dilihat digrup silahkan join channel melalui link private yang ada di @YMovieZ_New ...", + reply_to_message_id=int(msg_id), + ) + + if q.message.caption: + await q.message.edit_text(f"COMPLETED\n\n{q.message.caption}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="āœ… Request Completed", callback_data="reqcompl")]])) + else: + await q.message.edit_text(f"COMPLETED\n\n{q.message.text}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="āœ… Request Completed", callback_data="reqcompl")]])) + await q.answer("Request berhasil diselesaikan āœ…") + else: + await q.answer("Apa motivasi kamu menekan tombol ini?", show_alert=True) + except UserNotParticipant: + return await q.answer("Apa motivasi kamu menekan tombol ini?", show_alert=True, cache_time=10) + except PeerIdInvalid: + return await q.answer("Silahkan kirim pesan digrup supaya bot bisa merespon.", show_alert=True, cache_time=10) + + +@app.on_callback_query(filters.regex(r"^dahada")) +async def _callbackreqada(c, q): + try: + user = await c.get_chat_member(-1001201566570, q.from_user.id) + if user.status in [enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER]: + i, msg_id, chat_id = q.data.split("_") + await c.send_message( + chat_id=chat_id, + text="#SudahAda\nFilm/series yang direquest sudah ada sebelumnya. Biasakan mencari terlebih dahulu..", + reply_to_message_id=int(msg_id), + ) + + if q.message.caption: + await q.message.edit_text(f"#AlreadyAvailable\n\n{q.message.caption}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="šŸ” Request Sudah Ada", callback_data="reqavailable")]])) + else: + await q.message.edit_text(f"Already Available\n\n{q.message.text}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="šŸ” Request Sudah Ada", callback_data="reqavailable")]])) + await q.answer("Done āœ”ļø") + else: + await q.answer("Apa motivasi kamu menekan tombol ini?", show_alert=True) + except UserNotParticipant: + return await q.answer("Apa motivasi kamu menekan tombol ini?", show_alert=True, cache_time=10) + except PeerIdInvalid: + return await q.answer("Silahkan kirim pesan digrup supaya bot bisa merespon.", show_alert=True, cache_time=10) + + +@app.on_callback_query(filters.regex(r"^rejectreq")) +async def _callbackreject(c, q): + try: + user = await c.get_chat_member(-1001201566570, q.from_user.id) + if user.status in [enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER]: + i, msg_id, chat_id = q.data.split("_") + await c.send_message( + chat_id=chat_id, + text="Mohon maaf, request kamu ditolak karena tidak sesuai rules. Harap baca rules grup no.6 yaa šŸ™ƒ.", + reply_to_message_id=int(msg_id), + ) + + if q.message.caption: + await q.message.edit_text(f"REJECTED\n\n{q.message.caption}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="🚫 Request Rejected", callback_data="reqreject")]])) + else: + await q.message.edit_text(f"REJECTED\n\n{q.message.text}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="🚫 Request Rejected", callback_data="reqreject")]])) + await q.answer("Request berhasil ditolak 🚫") + else: + await q.answer("Apa motivasi kamu menekan tombol ini?", show_alert=True) + except UserNotParticipant: + await q.answer("Apa motivasi kamu menekan tombol ini?", show_alert=True, cache_time=10) + except PeerIdInvalid: + return await q.answer("Silahkan kirim pesan digrup supaya bot bisa merespon.", show_alert=True, cache_time=10) + + +@app.on_callback_query(filters.regex(r"^unavailablereq")) +async def _callbackunav(c, q): + try: + user = await c.get_chat_member(-1001201566570, q.from_user.id) + if user.status in [enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER]: + i, msg_id, chat_id = q.data.split("_") + await c.send_message( + chat_id=chat_id, + text="Mohon maaf, request kamu tidak tersedia. Silahkan baca beberapa alasannya di channel @YMovieZ_New", + reply_to_message_id=int(msg_id), + ) + + if q.message.caption: + await q.message.edit_text(f"UNAVAILABLE\n\n{q.message.caption}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="āš ļø Request Unavailable", callback_data="requnav")]])) + else: + await q.message.edit_text(f"UNAVAILABLE\n\n{q.message.text}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(text="āš ļø Request Unavailable", callback_data="requnav")]])) + await q.answer("Request tidak tersedia, mungkin belum rilis atau memang tidak tersedia versi digital.") + else: + await q.answer("Apa motivasi kamu menekan tombol ini?", show_alert=True, cache_time=1000) + except UserNotParticipant: + await q.answer("Apa motivasi kamu menekan tombol ini?", show_alert=True, cache_time=10) + except PeerIdInvalid: + return await q.answer("Silahkan kirim pesan digrup supaya bot bisa merespon.", show_alert=True, cache_time=10) + + +@app.on_callback_query(filters.regex(r"^reqcompl$")) +async def _callbackaft_done(c, q): + await q.answer("Request ini sudah terselesaikan 🄳, silahkan cek di channel atau grup yaa..", show_alert=True, cache_time=1000) + + +@app.on_callback_query(filters.regex(r"^reqreject$")) +async def _callbackaft_rej(c, q): + await q.answer("Request ini ditolak šŸ’”, silahkan cek rules grup yaa.", show_alert=True, cache_time=1000) + + +@app.on_callback_query(filters.regex(r"^requnav$")) +async def _callbackaft_unav(c, q): + await q.answer("Request ini tidak tersedia ā˜¹ļø, mungkin filmnya belum rilis atau memang tidak tersedia versi digital.", show_alert=True, cache_time=1000) + + +@app.on_callback_query(filters.regex(r"^reqavailable$")) +async def _callbackaft_dahada(c, q): + await q.answer("Request ini sudah ada, silahkan cari šŸ” di channelnya yaa šŸ˜‰..", show_alert=True) + + +scheduler = AsyncIOScheduler(timezone="Asia/Jakarta") +scheduler.add_job(clear_reqdict, trigger="cron", hour=7, minute=0) +scheduler.start() diff --git a/misskaty/plugins/genss.py b/misskaty/plugins/genss.py new file mode 100644 index 00000000..5fed89f0 --- /dev/null +++ b/misskaty/plugins/genss.py @@ -0,0 +1,142 @@ +# the logging things +import logging + +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +import os, time, traceback +from asyncio import sleep, gather +from shutil import rmtree +from pyrogram import filters, enums +from pyrogram.errors import FloodWait +from misskaty import app +from misskaty.helper.ffmpeg_helper import take_ss, genss_link +from misskaty.vars import COMMAND_HANDLER +from misskaty.helper.pyro_progress import progress_for_pyrogram + +__MODULE__ = "MediaTool" +__HELP__ = """" +/genss [reply to video] - Generate Screenshot From Video. +/genss_link [link] - Generate Screenshot Video From URL. (Unstable) +/mediainfo [link/reply to TG Video] - Get Mediainfo From File. +""" + + +@app.on_message(filters.command(["genss"], COMMAND_HANDLER)) +async def genss(client, message): + if message.reply_to_message is not None: + process = await message.reply_text("`Processing, please wait..`") + c_time = time.time() + the_real_download_location = await client.download_media( + message=message.reply_to_message, + progress=progress_for_pyrogram, + progress_args=("Trying to download, please wait..", process, c_time), + ) + if the_real_download_location is not None: + try: + await client.edit_message_text( + text=f"File video berhasil didownload dengan path {the_real_download_location}.", + chat_id=message.chat.id, + message_id=process.id, + ) + await sleep(2) + images = await take_ss(the_real_download_location) + await client.edit_message_text( + text="Mencoba mengupload, hasil generate screenshot..", + chat_id=message.chat.id, + message_id=process.id, + ) + await client.send_chat_action( + chat_id=message.chat.id, action=enums.ChatAction.UPLOAD_PHOTO + ) + + try: + await gather( + *[ + message.reply_document( + images, reply_to_message_id=message.id + ), + message.reply_photo(images, reply_to_message_id=message.id), + ] + ) + except FloodWait as e: + await sleep(e.value) + await gather( + *[ + message.reply_document( + images, reply_to_message_id=message.id + ), + message.reply_photo(images, reply_to_message_id=message.id), + ] + ) + await message.reply( + f"ā˜‘ļø Uploaded [1] screenshoot.\n\n{message.from_user.first_name} ({message.from_user.id})\n#ļøāƒ£ #ssgen #id{message.from_user.id}\n\nSS Generate by @MissKatyRoBot", + reply_to_message_id=message.id, + ) + await process.delete() + try: + os.remove(images) + os.remove(the_real_download_location) + except: + pass + except Exception: + exc = traceback.format_exc() + await message.reply(f"Gagal generate screenshot.\n\n{exc}") + try: + os.remove(images) + os.remove(the_real_download_location) + except: + pass + else: + await message.reply("Reply to a Telegram media to get screenshots..") + + +@app.on_message(filters.command(["genss_link"], COMMAND_HANDLER)) +async def genss_link(client, message): + try: + link = message.text.split(" ")[1] + if link.startswith("https://file.yasirweb.my.id"): + link = link.replace( + "https://file.yasirweb.my.id", "https://file.yasiraris.workers.dev" + ) + if link.startswith("https://link.yasirweb.my.id"): + link = link.replace( + "https://link.yasirweb.my.id", "https://yasirrobot.herokuapp.com" + ) + process = await message.reply_text("`Processing, please wait..`") + tmp_directory_for_each_user = f"./MissKaty_Genss/{str(message.from_user.id)}" + if not os.path.isdir(tmp_directory_for_each_user): + os.makedirs(tmp_directory_for_each_user) + images = await genss_link(process, link, tmp_directory_for_each_user, 5, 8) + await sleep(2) + await client.edit_message_text( + text="Mencoba mengupload, hasil generate screenshot..", + chat_id=message.chat.id, + message_id=process.id, + ) + await client.send_chat_action( + chat_id=message.chat.id, action=enums.ChatAction.UPLOAD_PHOTO + ) + try: + await message.reply_media_group(images, reply_to_message_id=message.id) + except FloodWait as e: + await sleep(e.value) + await message.reply_media_group(images, reply_to_message_id=message.id) + await message.reply( + f"ā˜‘ļø Uploaded [8] screenshoot.\n\nGenerated by @MissKatyRoBot.", + reply_to_message_id=message.id, + ) + await process.delete() + try: + rmtree(tmp_directory_for_each_user) + except: + pass + except Exception: + exc = traceback.format_exc() + await message.reply(f"Gagal generate screenshot.\n\n{exc}") + try: + rmtree(tmp_directory_for_each_user) + except: + pass diff --git a/misskaty/plugins/grup_tools.py b/misskaty/plugins/grup_tools.py new file mode 100644 index 00000000..b0a9c028 --- /dev/null +++ b/misskaty/plugins/grup_tools.py @@ -0,0 +1,347 @@ +from datetime import datetime, timedelta +import time +import os +import logging +from misskaty.helper.http import http +from pyrogram import enums, filters +from pyrogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup +from pyrogram.errors import ChatSendMediaForbidden, MessageTooLong, RPCError, SlowmodeWait +from misskaty import app +from misskaty.core.decorator.errors import capture_err, asyncify +from PIL import Image, ImageChops, ImageDraw, ImageFont +import textwrap +from database.users_chats_db import db +from utils import temp +from pyrogram.errors import ChatAdminRequired +from misskaty.vars import SUDO, LOG_CHANNEL, SUPPORT_CHAT, COMMAND_HANDLER + +logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +LOGGER = logging.getLogger(__name__) + + +def circle(pfp, size=(215, 215)): + pfp = pfp.resize(size, Image.ANTIALIAS).convert("RGBA") + bigsize = (pfp.size[0] * 3, pfp.size[1] * 3) + mask = Image.new("L", bigsize, 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0) + bigsize, fill=255) + mask = mask.resize(pfp.size, Image.ANTIALIAS) + mask = ImageChops.darker(mask, pfp.split()[-1]) + pfp.putalpha(mask) + return pfp + + +def draw_multiple_line_text(image, text, font, text_start_height): + """ + From unutbu on [python PIL draw multiline text on image](https://stackoverflow.com/a/7698300/395857) + """ + draw = ImageDraw.Draw(image) + image_width, image_height = image.size + y_text = text_start_height + lines = textwrap.wrap(text, width=50) + for line in lines: + line_width, line_height = font.getsize(line) + draw.text(((image_width - line_width) / 2, y_text), line, font=font, fill="black") + y_text += line_height + + +@asyncify +def welcomepic(pic, user, chat, count, id): + background = Image.open("img/bg.png") # <- Background Image (Should be PNG) + background = background.resize((1024, 500), Image.ANTIALIAS) + pfp = Image.open(pic).convert("RGBA") + pfp = circle(pfp) + pfp = pfp.resize((265, 265)) # Resizes the Profilepicture so it fits perfectly in the circle + font = ImageFont.truetype("Calistoga-Regular.ttf", 37) # <- Text Font of the Member Count. Change the text size for your preference + member_text = f"User#{count}, Selamat Datang {user}" # <- Text under the Profilepicture with the Membercount + draw_multiple_line_text(background, member_text, font, 395) + draw_multiple_line_text(background, chat, font, 47) + ImageDraw.Draw(background).text((530, 460), "Generated by @MissKatyRoBot", font=ImageFont.truetype("Calistoga-Regular.ttf", 28), size=20, align="right") + background.paste(pfp, (379, 123), pfp) # Pastes the Profilepicture on the Background Image + background.save(f"downloads/welcome#{id}.png") # Saves the finished Image in the folder with the filename + return f"downloads/welcome#{id}.png" + + +@app.on_chat_member_updated(filters.group & filters.chat(-1001128045651)) +async def member_has_joined(c: app, member: ChatMemberUpdated): + if not member.new_chat_member or member.new_chat_member.status in {"banned", "left", "restricted"} or member.old_chat_member: + return + user = member.new_chat_member.user if member.new_chat_member else member.from_user + if user.id in SUDO: + await c.send_message( + member.chat.id, + "Waw, owner ku yang keren baru saja bergabung ke grup!", + ) + return + elif user.is_bot: + return # ignore bots + else: + if (temp.MELCOW).get(f"welcome-{member.chat.id}") is not None: + try: + await (temp.MELCOW[f"welcome-{member.chat.id}"]).delete() + except: + pass + mention = f"{user.first_name}" + joined_date = datetime.fromtimestamp(time.time()).strftime("%Y.%m.%d %H:%M:%S") + first_name = f"{user.first_name} {user.last_name}" if user.last_name else user.first_name + id = user.id + dc = user.dc_id or "Member tanpa PP" + count = await app.get_chat_members_count(member.chat.id) + try: + pic = await app.download_media(user.photo.big_file_id, file_name=f"pp{user.id}.png") + except AttributeError: + pic = "img/profilepic.png" + welcomeimg = await welcomepic(pic, user.first_name, member.chat.title, count, user.id) + temp.MELCOW[f"welcome-{member.chat.id}"] = await c.send_photo( + member.chat.id, + photo=welcomeimg, + caption=f"Hai {mention}, Selamat datang digrup {member.chat.title} harap baca rules di pinned message terlebih dahulu.\n\nNama : {first_name}\nID : {id}\nDC ID : {dc}\nTanggal Join : {joined_date}", + ) + userspammer = "" + # Spamwatch Detection + try: + headers = {"Authorization": "Bearer XvfzE4AUNXkzCy0DnIVpFDlxZi79lt6EnwKgBj8Quuzms0OSdHvf1k6zSeyzZ_lz"} + apispamwatch = (await http.get(f"https://api.spamwat.ch/banlist/{user.id}", headers=headers)).json() + if not apispamwatch.get("error"): + await app.ban_chat_member(member.chat.id, user.id, datetime.now() + timedelta(seconds=30)) + userspammer += f"#SpamWatch Federation Ban\nUser {mention} [{user.id}] has been kicked because {apispamwatch.get('reason')}.\n" + except Exception as err: + LOGGER.error(f"ERROR in Spamwatch Detection. {err}") + # Combot API Detection + try: + apicombot = (await http.get(f"https://api.cas.chat/check?user_id={user.id}")).json() + if apicombot.get("ok") == "true": + await app.ban_chat_member(member.chat.id, user.id, datetime.now() + timedelta(seconds=30)) + userspammer += f"#CAS Federation Ban\nUser {mention} [{user.id}] detected as spambot and has been kicked. Powered by Combot AntiSpam." + except Exception as err: + LOGGER.error(f"ERROR in Combot API Detection. {err}") + if userspammer != "": + await c.send_message(member.chat.id, userspammer) + try: + os.remove(f"downloads/welcome#{user.id}.png") + os.remove(f"downloads/pp{user.id}.png") + except Exception: + pass + + +@app.on_message(filters.new_chat_members & filters.group) +async def save_group(bot, message): + r_j_check = [u.id for u in message.new_chat_members] + if temp.ME in r_j_check: + if not await db.get_chat(message.chat.id): + total = await bot.get_chat_members_count(message.chat.id) + r_j = message.from_user.mention if message.from_user else "Anonymous" + await bot.send_message( + LOG_CHANNEL, + f"#NewGroup\nGroup = {message.chat.title}({message.chat.id})\nMembers Count = {total}\nAdded by - {r_j}", + ) + + await db.add_chat(message.chat.id, message.chat.title) + if message.chat.id in temp.BANNED_CHATS: + # Inspired from a boat of a banana tree + buttons = [[InlineKeyboardButton("Support", url=f"https://t.me/{SUPPORT_CHAT}")]] + reply_markup = InlineKeyboardMarkup(buttons) + k = await message.reply( + text="CHAT NOT ALLOWED šŸž\n\nMy admins has restricted me from working here ! If you want to know more about it contact support..", + reply_markup=reply_markup, + ) + + try: + await k.pin() + except: + pass + await bot.leave_chat(message.chat.id) + return + buttons = [[InlineKeyboardButton("ā„¹ļø Help", url=f"https://t.me/{temp.U_NAME}?start=help"), InlineKeyboardButton("šŸ“¢ Updates", url="https://t.me/YasirPediaChannel")]] + reply_markup = InlineKeyboardMarkup(buttons) + await message.reply_text(text=f"Terimakasih sudah menambahkan saya di {message.chat.title} ā£ļø\n\nJika ada kendala atau saran bisa kontak ke saya.", reply_markup=reply_markup) + else: + for u in message.new_chat_members: + count = await app.get_chat_members_count(message.chat.id) + try: + pic = await app.download_media(u.photo.big_file_id, file_name=f"pp{u.id}.png") + except AttributeError: + pic = "img/profilepic.png" + welcomeimg = await welcomepic(pic, u.first_name, message.chat.title, count, u.id) + if (temp.MELCOW).get(f"welcome-{message.chat.id}") is not None: + try: + await (temp.MELCOW[f"welcome-{message.chat.id}"]).delete() + except: + pass + try: + temp.MELCOW[f"welcome-{message.chat.id}"] = await app.send_photo( + message.chat.id, + photo=welcomeimg, + caption=f"Hai {u.mention}, Selamat datang digrup {message.chat.title}.", + ) + except (ChatSendMediaForbidden, SlowmodeWait): + await app.leave_chat(message.chat.id) + try: + os.remove(f"downloads/welcome#{u.id}.png") + os.remove(f"downloads/pp{u.id}.png") + except Exception: + pass + + +@app.on_message(filters.command("leave") & filters.user(SUDO)) +async def leave_a_chat(bot, message): + if len(message.command) == 1: + return await message.reply("Give me a chat id") + chat = message.command[1] + try: + chat = int(chat) + except: + chat = chat + try: + buttons = [[InlineKeyboardButton("Support", url=f"https://t.me/{SUPPORT_CHAT}")]] + reply_markup = InlineKeyboardMarkup(buttons) + await bot.send_message( + chat_id=chat, + text="Hai kawan, \nOwner aku bilang saya harus pergi! Jika kamu ingin menambahkan bot ini lagi silahkan kontak owner bot ini.", + reply_markup=reply_markup, + ) + await bot.leave_chat(chat) + except Exception as e: + await message.reply(f"Error - {e}") + await bot.leave_chat(chat) + + +@app.on_message(filters.command("disable") & filters.user(SUDO)) +async def disable_chat(bot, message): + if len(message.command) == 1: + return await message.reply("Give me a chat id") + r = message.text.split(None) + if len(r) > 2: + reason = message.text.split(None, 2)[2] + chat = message.text.split(None, 2)[1] + else: + chat = message.command[1] + reason = "No reason Provided" + try: + chat_ = int(chat) + except: + return await message.reply("Give Me A Valid Chat ID") + cha_t = await db.get_chat(chat_) + if not cha_t: + return await message.reply("Chat Not Found In DB") + if cha_t["is_disabled"]: + return await message.reply(f"This chat is already disabled:\nReason- {cha_t['reason']} ") + await db.disable_chat(chat_, reason) + temp.BANNED_CHATS.append(chat_) + await message.reply("Chat Succesfully Disabled") + try: + buttons = [[InlineKeyboardButton("Support", url=f"https://t.me/{SUPPORT_CHAT}")]] + reply_markup = InlineKeyboardMarkup(buttons) + await bot.send_message(chat_id=chat_, text=f"Hello Friends, \nMy admin has told me to leave from group so i go! If you wanna add me again contact my support group. \nReason : {reason}", reply_markup=reply_markup) + await bot.leave_chat(chat_) + except Exception as e: + await message.reply(f"Error - {e}") + + +@app.on_message(filters.command("enable") & filters.user(SUDO)) +async def re_enable_chat(bot, message): + if len(message.command) == 1: + return await message.reply("Give me a chat id") + chat = message.command[1] + try: + chat_ = int(chat) + except: + return await message.reply("Give Me A Valid Chat ID") + sts = await db.get_chat(int(chat)) + if not sts: + return await message.reply("Chat Not Found In DB !") + if not sts.get("is_disabled"): + return await message.reply("This chat is not yet disabled.") + await db.re_enable_chat(chat_) + temp.BANNED_CHATS.remove(chat_) + await message.reply("Chat Succesfully re-enabled") + + +# a function for trespassing into others groups, Inspired by a Vazha +# Not to be used , But Just to showcase his vazhatharam. +# @app.on_message(filters.command('invite') & filters.user(SUDO)) +async def gen_invite(bot, message): + if len(message.command) == 1: + return await message.reply("Give me a chat id") + chat = message.command[1] + try: + chat = int(chat) + except: + return await message.reply("Give Me A Valid Chat ID") + try: + link = await bot.create_chat_invite_link(chat) + except ChatAdminRequired: + return await message.reply("Invite Link Generation Failed, Iam Not Having Sufficient Rights") + except Exception as e: + return await message.reply(f"Error {e}") + await message.reply(f"Here is your Invite Link {link.invite_link}") + + +@app.on_message(filters.command(["adminlist", "adminlist@MissKatyRoBot"], COMMAND_HANDLER)) +@capture_err +async def adminlist(_, message): + if message.chat.type == enums.ChatType.PRIVATE: + return await message.reply("Perintah ini hanya untuk grup") + try: + administrators = [] + async for m in app.get_chat_members(message.chat.id, filter=enums.ChatMembersFilter.ADMINISTRATORS): + administrators.append(f"{m.user.first_name}") + + res = "".join(f"~ {i}\n" for i in administrators) + return await message.reply(f"Daftar Admin di {message.chat.title} ({message.chat.id}):\n~ {res}") + except Exception as e: + await message.reply(f"ERROR: {str(e)}") + + +@app.on_message(filters.command(["kickme"], COMMAND_HANDLER)) +@capture_err +async def kickme(_, message): + reason = None + if len(message.text.split()) >= 2: + reason = message.text.split(None, 1)[1] + try: + await message.ban_member(message.from_user.id) + txt = f"Pengguna {message.from_user.mention} menendang dirinya sendiri. Mungkin dia sedang frustasi šŸ˜•" + txt += f"\nAlasan: {reason}" if reason else "" + await message.reply_text(txt) + await message.unban_member(message.from_user.id) + except RPCError as ef: + await message.reply_text(f"Sepertinya ada error, silahkan report ke owner saya. \nERROR: {str(ef)}") + return + + +@app.on_message(filters.command("users") & filters.user(SUDO)) +async def list_users(bot, message): + # https://t.me/GetTGLink/4184 + raju = await message.reply("Getting List Of Users") + users = await db.get_all_users() + out = "Users Saved In DB Are:\n\n" + async for user in users: + out += f"{user['name']}" + if user["ban_status"]["is_banned"]: + out += "( Banned User )" + out += "\n" + try: + await raju.edit_text(out) + except MessageTooLong: + with open("users.txt", "w+") as outfile: + outfile.write(out) + await message.reply_document("users.txt", caption="List Of Users") + + +@app.on_message(filters.command("chats") & filters.user(SUDO)) +async def list_chats(bot, message): + raju = await message.reply("Getting List Of chats") + chats = await db.get_all_chats() + out = "Chats Saved In DB Are:\n\n" + async for chat in chats: + out += f"**Title:** `{chat['title']}`\n**- ID:** `{chat['id']}`" + if chat["chat_status"]["is_disabled"]: + out += "( Disabled Chat )" + out += "\n" + try: + await raju.edit_text(out) + except MessageTooLong: + with open("chats.txt", "w+") as outfile: + outfile.write(out) + await message.reply_document("chats.txt", caption="List Of Chats") diff --git a/misskaty/plugins/inkick_user.py b/misskaty/plugins/inkick_user.py new file mode 100644 index 00000000..ace0aa7c --- /dev/null +++ b/misskaty/plugins/inkick_user.py @@ -0,0 +1,233 @@ +import time +from asyncio import sleep +from misskaty import app +from misskaty.vars import COMMAND_HANDLER +from pyrogram import enums, filters +from pyrogram.errors import FloodWait +from pyrogram.errors.exceptions.forbidden_403 import ChatWriteForbidden +from pyrogram.errors.exceptions.bad_request_400 import ( + ChatAdminRequired, + UserAdminInvalid, +) + +__MODULE__ = "Inkick" +__HELP__ = """" +/instatus - View member status in group. +/dkick - Remove deleted account from group. +""" + + +@app.on_message( + filters.incoming & ~filters.private & filters.command(["inkick"], COMMAND_HANDLER) +) +async def inkick(_, message): + user = await app.get_chat_member(message.chat.id, message.from_user.id) + if user.status.value in ("administrator", "owner"): + if len(message.command) > 1: + input_str = message.command + sent_message = await message.reply_text( + "🚮**Sedang membersihkan user, mungkin butuh waktu beberapa saat...**" + ) + count = 0 + async for member in app.get_chat_members(message.chat.id): + if member.user.is_bot: + continue + if ( + member.user.status.value in input_str + and member.status.value not in ("administrator", "owner") + ): + try: + await message.chat.ban_member(member.user.id) + count += 1 + await sleep(1) + await message.chat.unban_member(member.user.id) + except (ChatAdminRequired, UserAdminInvalid): + await sent_message.edit( + "ā—**Oh tidaakk, saya bukan admin disini**\n__Saya pergi dari sini, tambahkan aku kembali dengan perijinan banned pengguna.__" + ) + await app.leave_chat(message.chat.id) + break + except FloodWait as e: + await sleep(e.value) + try: + await sent_message.edit( + f"āœ”ļø **Berhasil menendang {count} pengguna berdasarkan argumen.**" + ) + + except ChatWriteForbidden: + await app.leave_chat(message.chat.id) + else: + await message.reply_text( + "ā— **Arguments Required**\n__See /help in personal message for more information.__" + ) + else: + sent_message = await message.reply_text( + "ā— **You have to be the group creator to do that.**" + ) + await sleep(5) + await sent_message.delete() + + +# Kick User Without Username +@app.on_message( + filters.incoming & ~filters.private & filters.command(["uname"], COMMAND_HANDLER) +) +async def uname(_, message): + user = await app.get_chat_member(message.chat.id, message.from_user.id) + if user.status.value in ("administrator", "owner"): + sent_message = await message.reply_text( + "🚮**Sedang membersihkan user, mungkin butuh waktu beberapa saat...**" + ) + count = 0 + async for member in app.get_chat_members(message.chat.id): + if not member.user.username and member.status.value not in ( + "administrator", + "owner", + ): + try: + await message.chat.ban_member(member.user.id) + count += 1 + await sleep(1) + await message.chat.unban_member(member.user.id) + except (ChatAdminRequired, UserAdminInvalid): + await sent_message.edit( + "ā—**Oh tidaakk, saya bukan admin disini**\n__Saya pergi dari sini, tambahkan aku kembali dengan perijinan banned pengguna.__" + ) + await app.leave_chat(message.chat.id) + break + except FloodWait as e: + await sleep(e.value) + try: + await sent_message.edit( + f"āœ”ļø **Berhasil menendang {count} pengguna berdasarkan argumen.**" + ) + + except ChatWriteForbidden: + await app.leave_chat(message.chat.id) + else: + sent_message = await message.reply_text( + "ā— **You have to be the group creator to do that.**" + ) + await sleep(5) + await sent_message.delete() + + +@app.on_message( + filters.incoming & ~filters.private & filters.command(["dkick"], COMMAND_HANDLER) +) +async def dkick(client, message): + user = await app.get_chat_member(message.chat.id, message.from_user.id) + if user.status.value in ("administrator", "owner"): + sent_message = await message.reply_text( + "🚮**Sedang membersihkan user, mungkin butuh waktu beberapa saat...**" + ) + count = 0 + async for member in app.get_chat_members(message.chat.id): + if member.user.is_deleted and member.status.value not in ( + "administrator", + "owner", + ): + try: + await message.chat.ban_member(member.user.id) + count += 1 + await sleep(1) + await message.chat.unban_member(member.user.id) + except (ChatAdminRequired, UserAdminInvalid): + await sent_message.edit( + "ā—**Oh tidaakk, saya bukan admin disini**\n__Saya pergi dari sini, tambahkan aku kembali dengan perijinan banned pengguna.__" + ) + await app.leave_chat(message.chat.id) + break + except FloodWait as e: + await sleep(e.value) + try: + await sent_message.edit(f"āœ”ļø **Berhasil menendang {count} akun terhapus.**") + except ChatWriteForbidden: + await app.leave_chat(message.chat.id) + else: + sent_message = await message.reply_text( + "ā— **Kamu harus jadi admin atau owner grup untuk melakukan tindakan ini.**" + ) + await sleep(5) + await sent_message.delete() + + +@app.on_message( + filters.incoming & ~filters.private & filters.command(["instatus"], COMMAND_HANDLER) +) +async def instatus(client, message): + start_time = time.perf_counter() + user = await app.get_chat_member(message.chat.id, message.from_user.id) + count = await app.get_chat_members_count(message.chat.id) + if user.status in ( + enums.ChatMemberStatus.ADMINISTRATOR, + enums.ChatMemberStatus.OWNER, + ): + sent_message = await message.reply_text( + "**Sedang mengumpulkan informasi pengguna...**" + ) + recently = 0 + within_week = 0 + within_month = 0 + long_time_ago = 0 + deleted_acc = 0 + premium_acc = 0 + no_username = 0 + restricted = 0 + banned = 0 + uncached = 0 + bot = 0 + async for ban in app.get_chat_members( + message.chat.id, filter=enums.ChatMembersFilter.BANNED + ): + banned += 1 + async for restr in app.get_chat_members( + message.chat.id, filter=enums.ChatMembersFilter.RESTRICTED + ): + restricted += 1 + async for member in app.get_chat_members(message.chat.id): + user = member.user + if user.is_deleted: + deleted_acc += 1 + elif user.is_bot: + bot += 1 + elif user.is_premium: + premium_acc += 1 + elif not user.username: + no_username += 1 + elif user.status.value == "recently": + recently += 1 + elif user.status.value == "last_week": + within_week += 1 + elif user.status.value == "last_month": + within_month += 1 + elif user.status.value == "long_ago": + long_time_ago += 1 + else: + uncached += 1 + end_time = time.perf_counter() + timelog = "{:.2f}".format(end_time - start_time) + await sent_message.edit( + "šŸ’  {}\nšŸ‘„ {} Anggota\n——————\nšŸ‘ā€šŸ—Ø Informasi Status Anggota\n——————\nšŸ•’ recently: {}\nšŸ•’ last_week: {}\nšŸ•’ last_month: {}\nšŸ•’ long_ago: {}\nšŸ‰‘ Tanpa Username: {}\n🤐 Dibatasi: {}\n🚫 Diblokir: {}\nšŸ‘» Deleted Account (/dkick): {}\nšŸ¤– Bot: {}\nā­ļø Premium User: {}\nšŸ‘½ UnCached: {}\n\nā± Waktu eksekusi {} detik.".format( + message.chat.title, + count, + recently, + within_week, + within_month, + long_time_ago, + no_username, + restricted, + banned, + deleted_acc, + bot, + premium_acc, + uncached, + timelog, + ) + ) + else: + sent_message = await message.reply_text( + "ā— **Kamu harus jadi admin atau owner grup untuk melakukan tindakan ini.**" + ) + await sleep(5) + await sent_message.delete() diff --git a/misskaty/plugins/inline_search.py b/misskaty/plugins/inline_search.py new file mode 100644 index 00000000..36d6584b --- /dev/null +++ b/misskaty/plugins/inline_search.py @@ -0,0 +1,607 @@ +import json, traceback +from sys import version as pyver, platform +from misskaty import app, user +from motor import version as mongover +from misskaty.plugins.misc_tools import get_content +from pyrogram import __version__ as pyrover +from misskaty.helper.http import http +from misskaty.helper.tools import GENRES_EMOJI +from pyrogram import filters, enums +from bs4 import BeautifulSoup +from utils import demoji +from pykeyboard import InlineKeyboard +from deep_translator import GoogleTranslator +from pyrogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup, + InlineQuery, + InlineQueryResultArticle, + InputTextMessageContent, + InlineQueryResultPhoto, +) + +__MODULE__ = "InlineFeature" +__HELP__ = """ +To use this feature, just type bot username with following args below. +~ imdb [query] - Search movie details in IMDb.com. +~ pypi [query] - Search package from Pypi. +~ git [query] - Search in Git. +~ google [query] - Search in Google. +""" + +keywords_list = ["imdb", "pypi", "git", "google", "secretmsg"] + +PRVT_MSGS = {} + + +@app.on_inline_query() +async def inline_menu(_, inline_query: InlineQuery): + if inline_query.query.strip().lower().strip() == "": + buttons = InlineKeyboard(row_width=2) + buttons.add( + *[ + (InlineKeyboardButton(text=i, switch_inline_query_current_chat=i)) + for i in keywords_list + ] + ) + + btn = InlineKeyboard(row_width=2) + bot_state = "Alive" if await app.get_me() else "Dead" + ubot_state = "Alive" if await user.get_me() else "Dead" + btn.add( + InlineKeyboardButton("Stats", callback_data="stats_callback"), + InlineKeyboardButton("Go Inline!", switch_inline_query_current_chat=""), + ) + + msg = f""" +**[MissKaty✨](https://github.com/yasirarism):** +**MainBot:** `{bot_state}` +**UserBot:** `{ubot_state}` +**Python:** `{pyver.split()[0]}` +**Pyrogram:** `{pyrover}` +**MongoDB:** `{mongover}` +**Platform:** `{platform}` +**Profiles:** {(await app.get_me()).username} | {(await user.get_me()).first_name} + """ + answerss = [ + InlineQueryResultArticle( + title="Inline Commands", + description="Help Related To Inline Usage.", + input_message_content=InputTextMessageContent( + "Click A Button To Get Started." + ), + thumb_url="https://hamker.me/cy00x5x.png", + reply_markup=buttons, + ), + InlineQueryResultArticle( + title="Github Repo", + description="Github Repo of This Bot.", + input_message_content=InputTextMessageContent( + "Github Repo @MissKatyRoBot\n\nhttps://github.com/yasirarism/MissKatyPyro" + ), + thumb_url="https://hamker.me/gjc9fo3.png", + ), + InlineQueryResultArticle( + title="Alive", + description="Check Bot's Stats", + thumb_url="https://yt3.ggpht.com/ytc/AMLnZu-zbtIsllERaGYY8Aecww3uWUASPMjLUUEt7ecu=s900-c-k-c0x00ffffff-no-rj", + input_message_content=InputTextMessageContent( + msg, disable_web_page_preview=True + ), + reply_markup=btn, + ), + ] + await inline_query.answer(results=answerss) + elif inline_query.query.strip().lower().split()[0] == "google": + if len(inline_query.query.strip().lower().split()) < 2: + return await inline_query.answer( + results=[], + switch_pm_text="Google Search | google [QUERY]", + switch_pm_parameter="inline", + ) + judul = inline_query.query.split(None, 1)[1].strip() + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/61.0.3163.100 Safari/537.36" + } + search_results = await http.get( + f"https://www.google.com/search?q={judul}&num=20", headers=headers + ) + soup = BeautifulSoup(search_results.text, "lxml") + data = [] + for result in soup.select(".tF2Cxc"): + title = result.select_one(".DKV0Md").text + link = result.select_one(".yuRUbf a")["href"] + try: + snippet = result.select_one("#rso .lyLwlc").text + except: + snippet = "-" + message_text = f"{title}\n" + message_text += f"Deskription: {snippet}" + data.append( + InlineQueryResultArticle( + title=f"{title}", + input_message_content=InputTextMessageContent( + message_text=message_text, + parse_mode=enums.ParseMode.HTML, + disable_web_page_preview=False, + ), + url=link, + description=snippet, + thumb_url="https://te.legra.ph/file/ed8ea62ae636793000bb4.jpg", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton(text="Open Website", url=link)]] + ), + ) + ) + await inline_query.answer( + results=data, + is_gallery=False, + is_personal=False, + next_offset="", + switch_pm_text=f"Found {len(data)} results", + switch_pm_parameter="google", + ) + elif inline_query.query.strip().lower().split()[0] == "secretmsg": + if len(inline_query.query.strip().lower().split()) < 3: + return await inline_query.answer( + results=[], + switch_pm_text="SecretMsg | secretmsg [USERNAME/ID] [MESSAGE]", + switch_pm_parameter="inline", + ) + _id = inline_query.query.split()[1] + msg = inline_query.query.split(None, 2)[2].strip() + + if not msg or not msg.endswith(":"): + inline_query.stop_propagation() + + try: + penerima = await app.get_users(_id.strip()) + except Exception: # pylint: disable=broad-except + inline_query.stop_propagation() + return + + PRVT_MSGS[inline_query.id] = ( + penerima.id, + penerima.first_name, + inline_query.from_user.id, + msg.strip(": "), + ) + prvte_msg = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Show Message šŸ”", callback_data=f"prvtmsg({inline_query.id})" + ) + ], + [ + InlineKeyboardButton( + "Destroyā˜ ļø this msg", + callback_data=f"destroy({inline_query.id})", + ) + ], + ] + ) + mention = ( + f"@{penerima.username}" + if penerima.username + else f"{penerima.first_name}" + ) + + msg_c = ( + f"šŸ”’ A private message to {mention} [{penerima.id}], " + ) + msg_c += "Only he/she can open it." + results = [ + InlineQueryResultArticle( + title=f"A Private Msg to {penerima.first_name}", + input_message_content=InputTextMessageContent(msg_c), + description="Only he/she can open it", + thumb_url="https://te.legra.ph/file/16133ab3297b3f73c8da5.png", + reply_markup=prvte_msg, + ) + ] + await inline_query.answer(results=results, cache_time=3) + elif inline_query.query.strip().lower().split()[0] == "git": + if len(inline_query.query.strip().lower().split()) < 2: + return await inline_query.answer( + results=[], + switch_pm_text="Github Search | git [QUERY]", + switch_pm_parameter="inline", + ) + query = inline_query.query.split(None, 1)[1].strip() + search_results = await http.get( + f"https://api.github.com/search/repositories?q={query}" + ) + srch_results = json.loads(search_results.text) + item = srch_results.get("items") + data = [] + for sraeo in item: + title = sraeo.get("full_name") + link = sraeo.get("html_url") + deskripsi = sraeo.get("description") + lang = sraeo.get("language") + message_text = f"šŸ”—: {sraeo.get('html_url')}\n│\nā””ā”€šŸ“Forks: {sraeo.get('forks')} ā”ƒā”ƒ 🌟Stars: {sraeo.get('stargazers_count')}\n\n" + message_text += f"Description: {deskripsi}\n" + message_text += f"Language: {lang}" + data.append( + InlineQueryResultArticle( + title=f"{title}", + input_message_content=InputTextMessageContent( + message_text=message_text, + parse_mode=enums.ParseMode.HTML, + disable_web_page_preview=False, + ), + url=link, + description=deskripsi, + thumb_url="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton(text="Open Github Link", url=link)]] + ), + ) + ) + await inline_query.answer( + results=data, + is_gallery=False, + is_personal=False, + next_offset="", + switch_pm_text=f"Found {len(data)} results", + switch_pm_parameter="github", + ) + elif inline_query.query.strip().lower().split()[0] == "pypi": + if len(inline_query.query.strip().lower().split()) < 2: + return await inline_query.answer( + results=[], + switch_pm_text="Pypi Search | pypi [QUERY]", + switch_pm_parameter="inline", + ) + query = inline_query.query.split(None, 1)[1].strip() + search_results = await http.get( + f"https://api.hayo.my.id/api/pypi?package={query}" + ) + srch_results = json.loads(search_results.text) + data = [] + for sraeo in srch_results: + title = sraeo.get("title") + link = sraeo.get("link") + deskripsi = sraeo.get("desc") + version = sraeo.get("version") + message_text = f"{title} {version}\n" + message_text += f"Description: {deskripsi}\n" + data.append( + InlineQueryResultArticle( + title=f"{title}", + input_message_content=InputTextMessageContent( + message_text=message_text, + parse_mode=enums.ParseMode.HTML, + disable_web_page_preview=False, + ), + url=link, + description=deskripsi, + thumb_url="https://raw.githubusercontent.com/github/explore/666de02829613e0244e9441b114edb85781e972c/topics/pip/pip.png", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton(text="Open Link", url=link)]] + ), + ) + ) + await inline_query.answer( + results=data, + is_gallery=False, + is_personal=False, + next_offset="", + switch_pm_text=f"Found {len(data)} results", + switch_pm_parameter="pypi", + ) + elif inline_query.query.strip().lower().split()[0] == "yt": + if len(inline_query.query.strip().lower().split()) < 2: + return await inline_query.answer( + results=[], + switch_pm_text="YouTube Search | yt [QUERY]", + switch_pm_parameter="inline", + ) + judul = inline_query.query.split(None, 1)[1].strip() + search_results = await http.get( + f"https://api.abir-hasan.tk/youtube?query={judul}" + ) + srch_results = json.loads(search_results.text) + asroe = srch_results.get("results") + oorse = [] + for sraeo in asroe: + title = sraeo.get("title") + link = sraeo.get("link") + view = sraeo.get("viewCount").get("text") + thumb = sraeo.get("thumbnails")[0].get("url") + durasi = sraeo.get("accessibility").get("duration") + publishTime = sraeo.get("publishedTime") + try: + deskripsi = "".join( + f"{i['text']} " for i in sraeo.get("descriptionSnippet") + ) + except: + deskripsi = "-" + message_text = f"{title}\n" + message_text += f"Description: {deskripsi}\n" + message_text += f"Total View: {view}\n" + message_text += f"Duration: {durasi}\n" + message_text += f"Published Time: {publishTime}" + oorse.append( + InlineQueryResultArticle( + title=f"{title}", + input_message_content=InputTextMessageContent( + message_text=message_text, + parse_mode=enums.ParseMode.HTML, + disable_web_page_preview=False, + ), + url=link, + description=deskripsi, + thumb_url=thumb, + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton(text="Watch Video šŸ“¹", url=link)]] + ), + ) + ) + await inline_query.answer( + results=oorse, + is_gallery=False, + is_personal=False, + next_offset="", + switch_pm_text=f"Found {len(asroe)} results", + switch_pm_parameter="yt", + ) + elif inline_query.query.strip().lower().split()[0] == "imdb": + if len(inline_query.query.strip().lower().split()) < 2: + return await inline_query.answer( + results=[], + switch_pm_text="IMDB Search | imdb [QUERY]", + switch_pm_parameter="inline", + ) + movie_name = inline_query.query.split(None, 1)[1].strip() + search_results = await http.get( + f"https://yasirapi.eu.org/imdb-search?q={movie_name}" + ) + res = json.loads(search_results.text).get("result") + oorse = [] + for midb in res: + title = midb.get("l", "") + description = midb.get("q", "") + stars = midb.get("s", "") + imdb_url = f"https://imdb.com/title/{midb.get('id')}" + year = f"({midb.get('y')})" if midb.get("y") else "" + image_url = ( + midb.get("i").get("imageUrl").replace(".jpg", "._V1_UX360.jpg") + if midb.get("i") + else "https://te.legra.ph/file/e263d10ff4f4426a7c664.jpg" + ) + caption = f"šŸŽ¬" + caption += f"{title} {year}" + oorse.append( + InlineQueryResultPhoto( + title=f"{title} {year}", + caption=caption, + description=f" {description} | {stars}", + photo_url=image_url, + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text="Get IMDB details", + callback_data=f"imdbinl_{inline_query.from_user.id}_{midb.get('id')}", + ) + ] + ] + ), + ) + ) + resfo = json.loads(search_results.text).get("q") + await inline_query.answer( + results=oorse, + is_gallery=False, + is_personal=False, + next_offset="", + switch_pm_text=f"Found {len(oorse)} results for {resfo}", + switch_pm_parameter="imdb", + ) + + +@app.on_callback_query(filters.regex(r"prvtmsg\((.+)\)")) +async def prvt_msg(_, c_q): + msg_id = str(c_q.matches[0].group(1)) + + if msg_id not in PRVT_MSGS: + await c_q.answer("Message now outdated !", show_alert=True) + return + + user_id, flname, sender_id, msg = PRVT_MSGS[msg_id] + + if c_q.from_user.id in [user_id, sender_id]: + await c_q.answer(msg, show_alert=True) + else: + await c_q.answer(f"Only {flname} can see this Private Msg!", show_alert=True) + + +@app.on_callback_query(filters.regex(r"destroy\((.+)\)")) +async def destroy_msg(_, c_q): + msg_id = str(c_q.matches[0].group(1)) + + if msg_id not in PRVT_MSGS: + await c_q.answer("Message now outdated !", show_alert=True) + return + + user_id, flname, sender_id, msg = PRVT_MSGS[msg_id] + + if c_q.from_user.id in [user_id, sender_id]: + del PRVT_MSGS[msg_id] + by = "receiver" if c_q.from_user.id == user_id else "sender" + await c_q.edit_message_text(f"This secret message is ā˜ ļødestroyedā˜ ļø by msg {by}") + else: + await c_q.answer(f"Only {flname} can see this Private Msg!", show_alert=True) + + +@app.on_callback_query(filters.regex("^imdbinl_")) +async def imdb_inl(_, query): + i, user, movie = query.data.split("_") + if user == f"{query.from_user.id}": + await query.edit_message_caption("ā³ Permintaan kamu sedang diproses.. ") + try: + url = f"https://www.imdb.com/title/{movie}/" + resp = await get_content(url) + sop = BeautifulSoup(resp, "lxml") + r_json = json.loads( + sop.find("script", attrs={"type": "application/ld+json"}).contents[0] + ) + res_str = "" + type = f"{r_json['@type']}" if r_json.get("@type") else "" + if r_json.get("name"): + try: + tahun = ( + sop.select('ul[data-testid="hero-title-block__metadata"]')[0] + .find(class_="sc-8c396aa2-2 itZqyK") + .text + ) + except: + tahun = "-" + res_str += f"šŸ“¹ Judul: {r_json['name']} [{tahun}] ({type})\n" + if r_json.get("alternateName"): + res_str += ( + f"šŸ“¢ AKA: {r_json.get('alternateName')}\n\n" + ) + else: + res_str += "\n" + if sop.select('li[data-testid="title-techspec_runtime"]'): + durasi = ( + sop.select('li[data-testid="title-techspec_runtime"]')[0] + .find(class_="ipc-metadata-list-item__content-container") + .text + ) + res_str += f"Durasi: {GoogleTranslator('auto', 'id').translate(durasi)}\n" + if r_json.get("contentRating"): + res_str += f"Kategori: {r_json['contentRating']} \n" + if r_json.get("aggregateRating"): + res_str += f"Peringkat: {r_json['aggregateRating']['ratingValue']}ā­ļø dari {r_json['aggregateRating']['ratingCount']} pengguna \n" + if sop.select('li[data-testid="title-details-releasedate"]'): + rilis = ( + sop.select('li[data-testid="title-details-releasedate"]')[0] + .find( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + .text + ) + rilis_url = sop.select('li[data-testid="title-details-releasedate"]')[ + 0 + ].find( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + )[ + "href" + ] + res_str += f"Rilis: {rilis}\n" + if r_json.get("genre"): + genre = "".join( + f"{GENRES_EMOJI[i]} #{i.replace('-', '_').replace(' ', '_')}, " + if i in GENRES_EMOJI + else f"#{i.replace('-', '_').replace(' ', '_')}, " + for i in r_json["genre"] + ) + + genre = genre[:-2] + res_str += f"Genre: {genre}\n" + if sop.select('li[data-testid="title-details-origin"]'): + country = "".join( + f"{demoji(country.text)} #{country.text.replace(' ', '_').replace('-', '_')}, " + for country in sop.select('li[data-testid="title-details-origin"]')[ + 0 + ].findAll( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + ) + country = country[:-2] + res_str += f"Negara: {country}\n" + if sop.select('li[data-testid="title-details-languages"]'): + language = "".join( + f"#{lang.text.replace(' ', '_').replace('-', '_')}, " + for lang in sop.select('li[data-testid="title-details-languages"]')[ + 0 + ].findAll( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + ) + language = language[:-2] + res_str += f"Bahasa: {language}\n" + res_str += "\nšŸ™Ž Info Cast:\n" + if r_json.get("director"): + director = "" + for i in r_json["director"]: + name = i["name"] + url = i["url"] + director += f"{name}, " + director = director[:-2] + res_str += f"Sutradara: {director}\n" + if r_json.get("creator"): + creator = "" + for i in r_json["creator"]: + if i["@type"] == "Person": + name = i["name"] + url = i["url"] + creator += f"{name}, " + creator = creator[:-2] + res_str += f"Penulis: {creator}\n" + if r_json.get("actor"): + actors = "" + for i in r_json["actor"]: + name = i["name"] + url = i["url"] + actors += f"{name}, " + actors = actors[:-2] + res_str += f"Pemeran: {actors}\n\n" + if r_json.get("description"): + summary = GoogleTranslator("auto", "id").translate( + r_json.get("description") + ) + res_str += f"šŸ“œ Plot: {summary}\n\n" + if r_json.get("keywords"): + keywords = r_json["keywords"].split(",") + key_ = "" + for i in keywords: + i = i.replace(" ", "_").replace("-", "_") + key_ += f"#{i}, " + key_ = key_[:-2] + res_str += f"šŸ”„ Kata Kunci: {key_} \n" + if sop.select('li[data-testid="award_information"]'): + awards = ( + sop.select('li[data-testid="award_information"]')[0] + .find(class_="ipc-metadata-list-item__list-content-item") + .text + ) + res_str += f"šŸ† Penghargaan: {GoogleTranslator('auto', 'id').translate(awards)}\n\n" + else: + res_str += "\n" + res_str += "Ā©ļø IMDb by @MissKatyRoBot" + if r_json.get("trailer"): + trailer_url = r_json["trailer"]["url"] + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "šŸŽ¬ Open IMDB", + url=f"https://www.imdb.com{r_json['url']}", + ), + InlineKeyboardButton("ā–¶ļø Trailer", url=trailer_url), + ] + ] + ) + else: + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "šŸŽ¬ Open IMDB", + url=f"https://www.imdb.com{r_json['url']}", + ) + ] + ] + ) + await query.edit_message_caption(res_str, reply_markup=markup) + except Exception: + exc = traceback.format_exc() + await query.edit_message_caption(f"ERROR:\n{exc}") + else: + await query.answer("āš ļø Akses Ditolak!", True) diff --git a/misskaty/plugins/json.py b/misskaty/plugins/json.py new file mode 100644 index 00000000..43d791fa --- /dev/null +++ b/misskaty/plugins/json.py @@ -0,0 +1,25 @@ +import os +from pyrogram import filters +from misskaty import app +from misskaty.vars import COMMAND_HANDLER + + +# View Structure Telegram Message As JSON +@app.on_message(filters.command(["json"], COMMAND_HANDLER)) +async def jsonify(_, message): + the_real_message = None + reply_to_id = None + + the_real_message = message.reply_to_message or message + try: + await message.reply_text(f"{the_real_message}") + except Exception as e: + with open("json.text", "w+", encoding="utf8") as out_file: + out_file.write(str(the_real_message)) + await message.reply_document( + document="json.text", + caption=f"{str(e)}", + disable_notification=True, + reply_to_message_id=reply_to_id, + ) + os.remove("json.text") diff --git a/misskaty/plugins/karma.py b/misskaty/plugins/karma.py new file mode 100644 index 00000000..9c0bfcdf --- /dev/null +++ b/misskaty/plugins/karma.py @@ -0,0 +1,201 @@ +import re +from pyrogram import filters +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.permissions import adminsOnly +from misskaty.helper.functions import alpha_to_int, int_to_alpha +from database.karma_db import ( + get_karma, + get_karmas, + is_karma_on, + karma_off, + karma_on, + update_karma, +) + +__MODULE__ = "Karma" +__HELP__ = """ +Give reputation to other people in group. + +/karma_toggle [enable/disable] - Enable/Disable Karma. +/karma - View all karma from member group. +""" + +karma_positive_group = 3 +karma_negative_group = 4 + +regex_upvote = ( + r"^(\+|\+\+|\+1|thx|tnx|ty|thank you|thanx|thanks|pro|cool|good|makasih|šŸ‘|\+\+ .+)$" +) +regex_downvote = r"^(-|--|-1|šŸ‘Ž|-- .+)$" + +n = "\n" +w = " " + +bold = lambda x: f"**{x}:** " +bold_ul = lambda x: f"**--{x}:**-- " + +mono = lambda x: f"`{x}`{n}" + + +def section( + title: str, + body: dict, + indent: int = 2, + underline: bool = False, +) -> str: + text = (bold_ul(title) + n) if underline else bold(title) + n + + for key, value in body.items(): + text += ( + indent * w + + bold(key) + + ((value[0] + n) if isinstance(value, list) else mono(value)) + ) + return text + + +async def get_user_id_and_usernames(client) -> dict: + with client.storage.lock, client.storage.conn: + users = client.storage.conn.execute( + 'SELECT * FROM peers WHERE type in ("user", "bot") AND username NOT null' + ).fetchall() + return {user[0]: user[3] for user in users} + + +@app.on_message( + filters.text + & filters.group + & filters.incoming + & filters.reply + & filters.regex(regex_upvote, re.IGNORECASE) + & ~filters.via_bot + & ~filters.bot, + group=karma_positive_group, +) +@capture_err +async def upvote(_, message): + if not await is_karma_on(message.chat.id): + return + if not message.reply_to_message.from_user: + return + if not message.from_user: + return + if message.reply_to_message.from_user.id == message.from_user.id: + return + chat_id = message.chat.id + user_id = message.reply_to_message.from_user.id + user_mention = message.reply_to_message.from_user.mention + current_karma = await get_karma(chat_id, await int_to_alpha(user_id)) + if current_karma: + current_karma = current_karma["karma"] + karma = current_karma + 1 + else: + karma = 1 + new_karma = {"karma": karma} + await update_karma(chat_id, await int_to_alpha(user_id), new_karma) + await message.reply_text( + f"Incremented Karma of {user_mention} By 1 \nTotal Points: {karma}" + ) + + +@app.on_message( + filters.text + & filters.group + & filters.incoming + & filters.reply + & filters.regex(regex_downvote, re.IGNORECASE) + & ~filters.via_bot + & ~filters.bot, + group=karma_negative_group, +) +@capture_err +async def downvote(_, message): + if not await is_karma_on(message.chat.id): + return + if not message.reply_to_message.from_user: + return + if not message.from_user: + return + if message.reply_to_message.from_user.id == message.from_user.id: + return + + chat_id = message.chat.id + user_id = message.reply_to_message.from_user.id + user_mention = message.reply_to_message.from_user.mention + current_karma = await get_karma(chat_id, await int_to_alpha(user_id)) + if current_karma: + current_karma = current_karma["karma"] + karma = current_karma - 1 + else: + karma = 1 + new_karma = {"karma": karma} + await update_karma(chat_id, await int_to_alpha(user_id), new_karma) + await message.reply_text( + f"Decremented Karma Of {user_mention} By 1 \nTotal Points: {karma}" + ) + + +@app.on_message(filters.command("karma") & filters.group) +@capture_err +async def command_karma(_, message): + chat_id = message.chat.id + if not message.reply_to_message: + m = await message.reply_text("Analyzing Karma...") + karma = await get_karmas(chat_id) + if not karma: + return await m.edit("No karma in DB for this chat.") + msg = f"Karma list of {message.chat.title}" + limit = 0 + karma_dicc = {} + for i in karma: + user_id = await alpha_to_int(i) + user_karma = karma[i]["karma"] + karma_dicc[str(user_id)] = user_karma + karma_arranged = dict( + sorted( + karma_dicc.items(), + key=lambda item: item[1], + reverse=True, + ) + ) + if not karma_dicc: + return await m.edit("No karma in DB for this chat.") + userdb = await get_user_id_and_usernames(app) + karma = {} + for user_idd, karma_count in karma_arranged.items(): + if limit > 15: + break + if int(user_idd) not in list(userdb.keys()): + continue + username = userdb[int(user_idd)] + karma[f"@{username}"] = [f"**{str(karma_count)}**"] + limit += 1 + await m.edit(section(msg, karma)) + else: + if not message.reply_to_message.from_user: + return await message.reply("Anon user hash no karma.") + + user_id = message.reply_to_message.from_user.id + karma = await get_karma(chat_id, await int_to_alpha(user_id)) + karma = karma["karma"] if karma else 0 + await message.reply_text(f"**Total Points**: __{karma}__") + + +@app.on_message(filters.command("karma_toggle") & ~filters.private) +@adminsOnly +async def captcha_state(_, message): + usage = "**Usage:**\n/karma_toggle [ENABLE|DISABLE]" + if len(message.command) != 2: + return await message.reply_text(usage) + chat_id = message.chat.id + state = message.text.split(None, 1)[1].strip() + state = state.lower() + if state == "enable": + await karma_on(chat_id) + await message.reply_text("Enabled karma system.") + elif state == "disable": + await karma_off(chat_id) + await message.reply_text("Disabled karma system.") + else: + await message.reply_text(usage) diff --git a/misskaty/plugins/mediainfo.py b/misskaty/plugins/mediainfo.py new file mode 100644 index 00000000..7c172b8d --- /dev/null +++ b/misskaty/plugins/mediainfo.py @@ -0,0 +1,81 @@ +import io +from os import remove as osremove +import time +import subprocess +from pyrogram import filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from misskaty.vars import COMMAND_HANDLER +from utils import get_file_id +from misskaty import app +from misskaty.helper.media_helper import post_to_telegraph, runcmd +from misskaty.core.decorator.errors import capture_err +from misskaty.helper.pyro_progress import ( + progress_for_pyrogram, +) + + +@app.on_message(filters.command(["mediainfo", "mediainfo@MissKatyRoBot"], COMMAND_HANDLER)) +@capture_err +async def mediainfo(client, message): + if message.reply_to_message and message.reply_to_message.media: + process = await message.reply_text("`Sedang memproses, lama waktu tergantung ukuran file kamu...`", quote=True) + file_info = get_file_id(message.reply_to_message) + if file_info is None: + await process.edit_text("Balas ke format media yang valid") + return + c_time = time.time() + # file_path = safe_filename(await reply.download()) + file_path = await client.download_media( + message=message.reply_to_message, + progress=progress_for_pyrogram, + progress_args=("trying to download, sabar yakk..", process, c_time), + ) + output_ = await runcmd(f'mediainfo "{file_path}"') + out = output_[0] if len(output_) != 0 else None + body_text = f""" + +

JSON

+
{file_info}.type
+
+

DETAILS

+
{out or 'Not Supported'}
+ """ + title = "MissKaty Bot Mediainfo" + text_ = file_info.message_type + link = post_to_telegraph(title, body_text) + markup = InlineKeyboardMarkup([[InlineKeyboardButton(text=text_, url=link)]]) + await message.reply("ā„¹ļø MEDIA INFO", reply_markup=markup, quote=True) + await process.delete() + try: + osremove(file_path) + except Exception: + pass + else: + try: + link = message.text.split(" ", maxsplit=1)[1] + if link.startswith("https://file.yasirweb.my.id"): + link = link.replace("https://file.yasirweb.my.id", "https://file.yasiraris.workers.dev") + if link.startswith("https://link.yasirweb.my.id"): + link = link.replace("https://link.yasirweb.my.id", "https://yasirrobot.herokuapp.com") + process = await message.reply_text("`Mohon tunggu sejenak...`") + try: + output = subprocess.check_output(["mediainfo", f"{link}"]).decode("utf-8") + except Exception: + return await process.edit("Sepertinya link yang kamu kirim tidak valid, pastikan direct link dan bisa di download.") + title = "MissKaty Bot Mediainfo" + body_text = f""" + +
{output}
+ """ + tgraph = post_to_telegraph(title, body_text) + # siteurl = "https://spaceb.in/api/v1/documents/" + # response = await http.post(siteurl, data={"content": output, "extension": 'txt'} ) + # response = response.json() + # spacebin = "https://spaceb.in/"+response['payload']['id'] + markup = InlineKeyboardMarkup([[InlineKeyboardButton(text="šŸ’¬ Telegraph", url=tgraph)]]) + with io.BytesIO(str.encode(output)) as out_file: + out_file.name = "MissKaty_Mediainfo.txt" + await message.reply_document(out_file, caption=f"Hasil mediainfo anda..\n\nRequest by: {message.from_user.mention}", reply_markup=markup) + await process.delete() + except IndexError: + return await message.reply_text("Gunakan command /mediainfo [link], atau reply telegram media dengan /mediainfo.") diff --git a/misskaty/plugins/memify.py b/misskaty/plugins/memify.py new file mode 100644 index 00000000..3884d80d --- /dev/null +++ b/misskaty/plugins/memify.py @@ -0,0 +1,154 @@ +import textwrap +from os import remove as hapus +from misskaty.core.decorator.errors import capture_err +from misskaty import app +from pyrogram import filters +from misskaty.vars import COMMAND_HANDLER +from PIL import Image, ImageFont, ImageDraw + + +async def draw_meme_text(image_path, text): + img = Image.open(image_path) + hapus(image_path) + i_width, i_height = img.size + m_font = ImageFont.truetype("Calistoga-Regular.ttf", int((70 / 640) * i_width)) + if ";" in text: + upper_text, lower_text = text.split(";") + else: + upper_text = text + lower_text = "" + draw = ImageDraw.Draw(img) + current_h, pad = 10, 5 + if upper_text: + for u_text in textwrap.wrap(upper_text, width=15): + u_width, u_height = draw.textsize(u_text, font=m_font) + + draw.text( + xy=(((i_width - u_width) / 2) - 1, int((current_h / 640) * i_width)), + text=u_text, + font=m_font, + fill=(0, 0, 0), + stroke_width=3, + stroke_fill="black", + ) + draw.text( + xy=(((i_width - u_width) / 2) + 1, int((current_h / 640) * i_width)), + text=u_text, + font=m_font, + fill=(0, 0, 0), + stroke_width=3, + stroke_fill="black", + ) + draw.text( + xy=((i_width - u_width) / 2, int(((current_h / 640) * i_width)) - 1), + text=u_text, + font=m_font, + fill=(0, 0, 0), + stroke_width=3, + stroke_fill="black", + ) + draw.text( + xy=(((i_width - u_width) / 2), int(((current_h / 640) * i_width)) + 1), + text=u_text, + font=m_font, + fill=(0, 0, 0), + stroke_width=3, + stroke_fill="black", + ) + + draw.text( + xy=((i_width - u_width) / 2, int((current_h / 640) * i_width)), + text=u_text, + font=m_font, + fill=(255, 255, 255), + ) + current_h += u_height + pad + if lower_text: + for l_text in textwrap.wrap(lower_text, width=15): + u_width, u_height = draw.textsize(l_text, font=m_font) + + draw.text( + xy=( + ((i_width - u_width) / 2) - 1, + i_height - u_height - int((20 / 640) * i_width), + ), + text=l_text, + font=m_font, + fill=(0, 0, 0), + stroke_width=3, + stroke_fill="black", + ) + draw.text( + xy=( + ((i_width - u_width) / 2) + 1, + i_height - u_height - int((20 / 640) * i_width), + ), + text=l_text, + font=m_font, + fill=(0, 0, 0), + stroke_width=3, + stroke_fill="black", + ) + draw.text( + xy=( + (i_width - u_width) / 2, + (i_height - u_height - int((20 / 640) * i_width)) - 1, + ), + text=l_text, + font=m_font, + fill=(0, 0, 0), + stroke_width=3, + stroke_fill="black", + ) + draw.text( + xy=( + (i_width - u_width) / 2, + (i_height - u_height - int((20 / 640) * i_width)) + 1, + ), + text=l_text, + font=m_font, + fill=(0, 0, 0), + stroke_width=3, + stroke_fill="black", + ) + + draw.text( + xy=( + (i_width - u_width) / 2, + i_height - u_height - int((20 / 640) * i_width), + ), + text=l_text, + font=m_font, + fill=(255, 255, 255), + stroke_width=3, + stroke_fill="black", + ) + current_h += u_height + pad + + webp_file = "memify.webp" + img.save(webp_file, "WebP") + return webp_file + + +@app.on_message(filters.command(["mmf"], COMMAND_HANDLER)) +@capture_err +async def memify(client, message): + if message.reply_to_message and ( + message.reply_to_message.sticker or message.reply_to_message.photo + ): + try: + file = await message.reply_to_message.download() + res = await draw_meme_text(file, message.text.split(None, 1)[1].strip()) + await message.reply_sticker(res) + try: + hapus(res) + except: + pass + except: + await message.reply( + "Gunakan command /mmf dengan reply ke sticker, pisahkan dengan ; untuk membuat posisi text dibawah." + ) + else: + await message.reply( + "Gunakan command /mmf dengan reply ke sticker, pisahkan dengan ; untuk membuat posisi text dibawah." + ) diff --git a/misskaty/plugins/misc_tools.py b/misskaty/plugins/misc_tools.py new file mode 100644 index 00000000..fe80d9cc --- /dev/null +++ b/misskaty/plugins/misc_tools.py @@ -0,0 +1,645 @@ +import os, re +import aiohttp +from bs4 import BeautifulSoup +import json +import traceback +from pyrogram import Client, filters +from deep_translator import GoogleTranslator +from gtts import gTTS +from pyrogram.errors import MediaEmpty, MessageNotModified, PhotoInvalidDimensions, UserNotParticipant, WebpageMediaEmpty, MessageTooLong +from misskaty.vars import COMMAND_HANDLER +from utils import extract_user, get_file_id, demoji +import time +from datetime import datetime +from pykeyboard import InlineKeyboard +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery +from misskaty.core.decorator.errors import capture_err +from misskaty.helper.tools import rentry, GENRES_EMOJI +from misskaty.helper.http import http +from misskaty import app +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.ERROR) + +__MODULE__ = "Misc" +__HELP__ = """ +/sof [query] - Search your problem in StackOverflow. +/google [query] - Search using Google Search. +(/tr, /trans, /translate) [lang code] - Translate text using Google Translate. +/tts - Convert Text to Voice. +/imdb [query] - Find Movie Details From IMDB.com in Indonesian Language. +/imdb_en [query] - Find Movie Details From IMDB.com in English Language. +""" + + +def remove_html_tags(text): + """Remove html tags from a string""" + import re + + clean = re.compile("<.*?>") + return re.sub(clean, "", text) + + +@app.on_message(filters.command(["sof"], COMMAND_HANDLER)) +@capture_err +async def stackoverflow(client, message): + if len(message.command) == 1: + return await message.reply("Give a query to search in StackOverflow!") + r = (await http.get(f"https://api.stackexchange.com/2.3/search/excerpts?order=asc&sort=relevance&q={message.command[1]}&accepted=True&migrated=False¬ice=False&wiki=False&site=stackoverflow")).json() + hasil = "" + for count, data in enumerate(r["items"], start=1): + question = data["question_id"] + title = data["title"] + snippet = remove_html_tags(data["excerpt"])[:80].replace("\n", "").replace(" ", "") if len(remove_html_tags(data["excerpt"])) > 80 else remove_html_tags(data["excerpt"]).replace("\n", "").replace(" ", "") + hasil += f"{count}. {title}\n{snippet}\n" + try: + await message.reply(hasil) + except MessageTooLong: + url = await rentry(hasil) + await r.edit(f"Your text pasted to rentry because has long text:\n{url}") + except Exception as e: + await message.reply(e) + + +@app.on_message(filters.command(["google"], COMMAND_HANDLER)) +@capture_err +async def gsearch(client, message): + if len(message.command) == 1: + return await message.reply("Give a query to search in Google!") + query = message.text.split(" ", maxsplit=1)[1] + msg = await message.reply_text(f"**Googling** for `{query}` ...") + try: + headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/61.0.3163.100 Safari/537.36"} + html = await http.get(f"https://www.google.com/search?q={query}&gl=id&hl=id&num=17", headers=headers) + soup = BeautifulSoup(html.text, "lxml") + + # collect data + data = [] + + for result in soup.select(".tF2Cxc"): + title = result.select_one(".DKV0Md").text + link = result.select_one(".yuRUbf a")["href"] + try: + snippet = result.select_one("#rso .lyLwlc").text + except: + snippet = "-" + + # appending data to an array + data.append( + { + "title": title, + "link": link, + "snippet": snippet, + } + ) + arr = json.dumps(data, indent=2, ensure_ascii=False) + parse = json.loads(arr) + total = len(parse) + res = "".join(f"{i['title']}\n{i['snippet']}\n\n" for i in parse) + except Exception: + exc = traceback.format_exc() + return await msg.edit(exc) + await msg.edit(text=f"Ada {total} Hasil Pencarian dari {query}:\n{res}Scraped by @MissKatyRoBot", disable_web_page_preview=True) + + +@app.on_message(filters.command(["tr", "trans", "translate"], COMMAND_HANDLER)) +@capture_err +async def translate(client, message): + if message.reply_to_message and (message.reply_to_message.text or message.reply_to_message.caption): + target_lang = "id" if len(message.command) == 1 else message.text.split()[1] + text = message.reply_to_message.text or message.reply_to_message.caption + else: + if len(message.command) == 1: + return await message.reply_text( + "Berikan Kode bahasa yang valid.\n[Available options](https://telegra.ph/Lang-Codes-11-08).\nUsage: /tr en", + ) + target_lang = message.text.split(None, 2)[1] + text = message.text.split(None, 2)[2] + msg = await message.reply("Menerjemahkan...") + try: + tekstr = (await http.get(f"https://script.google.com/macros/s/AKfycbyhNk6uVgrtJLEFRUT6y5B2pxETQugCZ9pKvu01-bE1gKkDRsw/exec?q={text}&target={target_lang}")).json()["text"] + except Exception as err: + return await msg.edit(f"Error: {str(err)}") + try: + await msg.edit(f"{tekstr}") + except MessageTooLong: + url = await rentry(tekstr.text) + await msg.edit(f"Your translated text pasted to rentry because has long text:\n{url}") + + +@app.on_message(filters.command(["tts"], COMMAND_HANDLER)) +@capture_err +async def tts(_, message): + if message.reply_to_message and (message.reply_to_message.text or message.reply_to_message.caption): + if len(message.text.split()) == 1: + target_lang = "id" + else: + target_lang = message.text.split()[1] + text = message.reply_to_message.text or message.reply_to_message.caption + else: + if len(message.text.split()) <= 2: + await message.reply_text( + "Berikan Kode bahasa yang valid.\n[Available options](https://telegra.ph/Lang-Codes-11-08).\nUsage: /tts en ", + ) + return + target_lang = message.text.split(None, 2)[1] + text = message.text.split(None, 2)[2] + msg = await message.reply("Converting to voice...") + try: + tts = gTTS(text, lang=target_lang) + tts.save(f"tts_{message.from_user.id}.mp3") + except ValueError as err: + await msg.edit(f"Error: {str(err)}") + return + await msg.delete() + await msg.reply_audio(f"tts_{message.from_user.id}.mp3") + try: + os.remove(f"tts_{message.from_user.id}.mp3") + except: + pass + + +@app.on_message(filters.command(["tosticker", "tosticker@MissKatyRoBot"], COMMAND_HANDLER)) +@capture_err +async def tostick(client, message): + try: + if not message.reply_to_message or not message.reply_to_message.photo: + return await message.reply_text("Reply ke foto untuk mengubah ke sticker") + sticker = await client.download_media(message.reply_to_message.photo.file_id, f"tostick_{message.from_user.id}.webp") + await message.reply_sticker(sticker) + os.remove(sticker) + except Exception as e: + await message.reply_text(str(e)) + + +@app.on_message(filters.command(["toimage", "toimage@MissKatyRoBot"], COMMAND_HANDLER)) +@capture_err +async def topho(client, message): + try: + if not message.reply_to_message or not message.reply_to_message.sticker: + return await message.reply_text("Reply ke sticker untuk mengubah ke foto") + if message.reply_to_message.sticker.is_animated: + return await message.reply_text("Ini sticker animasi, command ini hanya untuk sticker biasa.") + photo = await client.download_media(message.reply_to_message.sticker.file_id, f"tostick_{message.from_user.id}.jpg") + await message.reply_photo(photo=photo, caption="Sticker -> Image\n@MissKatyRoBot") + + os.remove(photo) + except Exception as e: + await message.reply_text(str(e)) + + +@app.on_message(filters.command(["id", "id@MissKatyRoBot"], COMMAND_HANDLER)) +async def showid(client, message): + chat_type = message.chat.type + if chat_type == "private": + user_id = message.chat.id + first = message.from_user.first_name + last = message.from_user.last_name or "" + username = message.from_user.username + dc_id = message.from_user.dc_id or "" + await message.reply_text(f"āž² First Name: {first}\nāž² Last Name: {last}\nāž² Username: {username}\nāž² Telegram ID: {user_id}\nāž² Data Centre: {dc_id}", quote=True) + + elif chat_type in ["group", "supergroup"]: + _id = "" + _id += "āž² Chat ID: " f"{message.chat.id}\n" + if message.reply_to_message: + _id += ( + "āž² User ID: " + f"{message.from_user.id if message.from_user else 'Anonymous'}\n" + "āž² Replied User ID: " + f"{message.reply_to_message.from_user.id if message.reply_to_message.from_user else 'Anonymous'}\n" + ) + file_info = get_file_id(message.reply_to_message) + else: + _id += "āž² User ID: " f"{message.from_user.id if message.from_user else 'Anonymous'}\n" + file_info = get_file_id(message) + if file_info: + _id += f"{file_info.message_type}: " f"{file_info.file_id}\n" + await message.reply_text(_id, quote=True) + + +@app.on_message(filters.command(["info"], COMMAND_HANDLER)) +async def who_is(client, message): + # https://github.com/SpEcHiDe/PyroGramBot/blob/master/pyrobot/plugins/admemes/whois.py#L19 + status_message = await message.reply_text("`Fetching user info...`") + await status_message.edit("`Processing user info...`") + from_user = None + from_user_id, _ = extract_user(message) + try: + from_user = await client.get_users(from_user_id) + except Exception as error: + await status_message.edit(str(error)) + return + if from_user is None: + return await status_message.edit("no valid user_id / message specified") + message_out_str = "" + message_out_str += f"āž²First Name: {from_user.first_name}\n" + last_name = from_user.last_name or "None" + message_out_str += f"āž²Last Name: {last_name}\n" + message_out_str += f"āž²Telegram ID: {from_user.id}\n" + username = from_user.username or "None" + dc_id = from_user.dc_id or "[User Doesnt Have A Valid DP]" + message_out_str += f"āž²Data Centre: {dc_id}\n" + message_out_str += f"āž²User Name: @{username}\n" + message_out_str += f"āž²User š–«š—‚š—‡š—„: Click Here\n" + if message.chat.type in (("supergroup", "channel")): + try: + chat_member_p = await message.chat.get_member(from_user.id) + joined_date = datetime.fromtimestamp(chat_member_p.joined_date or time.time()).strftime("%Y.%m.%d %H:%M:%S") + message_out_str += "āž²Joined this Chat on: " f"{joined_date}" "\n" + except UserNotParticipant: + pass + if chat_photo := from_user.photo: + local_user_photo = await client.download_media(message=chat_photo.big_file_id) + buttons = [[InlineKeyboardButton("šŸ” Close", callback_data="close_data")]] + reply_markup = InlineKeyboardMarkup(buttons) + await message.reply_photo(photo=local_user_photo, quote=True, reply_markup=reply_markup, caption=message_out_str, disable_notification=True) + os.remove(local_user_photo) + else: + buttons = [[InlineKeyboardButton("šŸ” Close", callback_data="close_data")]] + reply_markup = InlineKeyboardMarkup(buttons) + await message.reply_text(text=message_out_str, reply_markup=reply_markup, quote=True, disable_notification=True) + await status_message.delete() + + +headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.1.17 (KHTML, like Gecko) Version/7.1 Safari/537.85.10"} + + +async def get_content(url): + async with aiohttp.ClientSession() as session: + r = await session.get(url, headers=headers) + return await r.read() + + +async def mdlapi(title): + link = f"https://kuryana.vercel.app/search/q/{title}" + async with aiohttp.ClientSession() as ses: + async with ses.get(link) as result: + return await result.json() + + +@app.on_message(filters.command(["mdl"], COMMAND_HANDLER)) +@capture_err +async def mdlsearch(client, message): + if " " in message.text: + r, title = message.text.split(None, 1) + k = await message.reply("Sedang mencari di Database MyDramaList.. 😓") + movies = await mdlapi(title) + res = movies["results"]["dramas"] + if not movies: + return await k.edit("Tidak ada hasil ditemukan.. šŸ˜•") + btn = [ + [ + InlineKeyboardButton( + text=f"{movie.get('title')} ({movie.get('year')})", + callback_data=f"mdls_{message.from_user.id}_{message.id}_{movie['slug']}", + ) + ] + for movie in res + ] + await k.edit(f"Ditemukan {len(movies)} query dari {title}", reply_markup=InlineKeyboardMarkup(btn)) + else: + await message.reply("Berikan aku nama drama yang ingin dicari. šŸ¤·šŸ»ā€ā™‚ļø") + + +@app.on_callback_query(filters.regex("^mdls")) +@capture_err +async def mdl_callback(bot: Client, query: CallbackQuery): + i, user, msg_id, slug = query.data.split("_") + if user == f"{query.from_user.id}": + await query.message.edit_text("Permintaan kamu sedang diproses.. ") + result = "" + try: + res = (await http.get(f"https://kuryana.vercel.app/id/{slug}")).json() + result += f"Title: {res['data']['title']}\n" + result += f"AKA: {res['data']['others']['also_known_as']}\n\n" + result += f"Rating: {res['data']['details']['score']}\n" + result += f"Content Rating: {res['data']['details']['content_rating']}\n" + result += f"Type: {res['data']['details']['type']}\n" + result += f"Country: {res['data']['details']['country']}\n" + if res["data"]["details"]["type"] == "Movie": + result += f"Release Date: {res['data']['details']['release_date']}\n" + elif res["data"]["details"]["type"] == "Drama": + result += f"Episode: {res['data']['details']['episodes']}\n" + result += f"Aired: {res['data']['details']['aired']}\n" + try: + result += f"Aired on: {res['data']['details']['aired_on']}\n" + except: + pass + try: + result += f"Original Network: {res['data']['details']['original_network']}\n" + except: + pass + result += f"Duration: {res['data']['details']['duration']}\n" + result += f"Genre: {res['data']['others']['genres']}\n\n" + result += f"Synopsis: {res['data']['synopsis']}\n" + result += f"Tags: {res['data']['others']['tags']}\n" + btn = InlineKeyboardMarkup([[InlineKeyboardButton("šŸŽ¬ Open MyDramaList", url=res["data"]["link"])]]) + await query.message.edit_text(result, reply_markup=btn) + except Exception as e: + await query.message.edit_text(f"ERROR:\n{e}") + else: + await query.answer("Tombol ini bukan untukmu", show_alert=True) + + +# IMDB Versi Indonesia v1 +@app.on_message(filters.command(["imdb"], COMMAND_HANDLER)) +@capture_err +async def imdb1_search(client, message): + BTN = [] + if message.sender_chat: + return await message.reply("Mohon maaf fitur tidak tersedia untuk akun channel, harap ganti ke akun biasa..") + if len(message.command) == 1: + return await message.reply("Berikan aku nama series atau movie yang ingin dicari. šŸ¤·šŸ»ā€ā™‚ļø", quote=True) + r, judul = message.text.split(None, 1) + k = await message.reply("šŸ”Ž Sedang mencari di Database IMDB..", quote=True) + msg = "" + buttons = InlineKeyboard(row_width=4) + try: + r = await get_content(f"https://yasirapi.eu.org/imdb-search?q={judul}") + res = json.loads(r).get("result") + if not res: + return await k.edit("Tidak ada hasil ditemukan.. šŸ˜•") + msg += f"Ditemukan {len(res)} query dari {judul} ~ {message.from_user.mention}\n\n" + for count, movie in enumerate(res, start=1): + title = movie.get("l") + year = f"({movie.get('y')})" if movie.get("y") else "" + type = movie.get("q").replace("feature", "movie").capitalize() + movieID = re.findall(r"tt(\d+)", movie.get("id"))[0] + msg += f"{count}. {title} {year} ~ {type}\n" + BTN.append(InlineKeyboardButton(text=count, callback_data=f"imdbid#{message.from_user.id}#{movieID}")) + buttons.add(*BTN) + await k.edit(msg, reply_markup=buttons) + except Exception as err: + await k.edit(f"Ooppss, gagal mendapatkan daftar judul di IMDb.\n\nERROR: {err}") + + +@app.on_callback_query(filters.regex("^imdbid")) +async def imdbcb_backup(bot: Client, query: CallbackQuery): + usr = query.message.reply_to_message + i, userid, movie = query.data.split("#") + if query.from_user.id != int(userid): + return await query.answer("āš ļø Akses Ditolak!", True) + try: + await query.message.edit_text("Permintaan kamu sedang diproses.. ") + url = f"https://www.imdb.com/title/tt{movie}/" + resp = await get_content(url) + sop = BeautifulSoup(resp, "lxml") + r_json = json.loads(sop.find("script", attrs={"type": "application/ld+json"}).contents[0]) + res_str = "" + type = f"{r_json['@type']}" if r_json.get("@type") else "" + if r_json.get("name"): + try: + tahun = sop.select('ul[data-testid="hero-title-block__metadata"]')[0].find(class_="sc-8c396aa2-2 itZqyK").text + except: + tahun = "-" + res_str += f"šŸ“¹ Judul: {r_json['name']} [{tahun}] ({type})\n" + if r_json.get("alternateName"): + res_str += f"šŸ“¢ AKA: {r_json.get('alternateName')}\n\n" + else: + res_str += "\n" + if sop.select('li[data-testid="title-techspec_runtime"]'): + durasi = sop.select('li[data-testid="title-techspec_runtime"]')[0].find(class_="ipc-metadata-list-item__content-container").text + res_str += f"Durasi: {GoogleTranslator('auto', 'id').translate(durasi)}\n" + if r_json.get("contentRating"): + res_str += f"Kategori: {r_json['contentRating']} \n" + if r_json.get("aggregateRating"): + res_str += f"Peringkat: {r_json['aggregateRating']['ratingValue']}ā­ļø dari {r_json['aggregateRating']['ratingCount']} pengguna \n" + if sop.select('li[data-testid="title-details-releasedate"]'): + rilis = sop.select('li[data-testid="title-details-releasedate"]')[0].find(class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link").text + rilis_url = sop.select('li[data-testid="title-details-releasedate"]')[0].find(class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link")["href"] + res_str += f"Rilis: {rilis}\n" + if r_json.get("genre"): + genre = "".join(f"{GENRES_EMOJI[i]} #{i.replace('-', '_').replace(' ', '_')}, " if i in GENRES_EMOJI else f"#{i.replace('-', '_').replace(' ', '_')}, " for i in r_json["genre"]) + + genre = genre[:-2] + res_str += f"Genre: {genre}\n" + if sop.select('li[data-testid="title-details-origin"]'): + country = "".join( + f"{demoji(country.text)} #{country.text.replace(' ', '_').replace('-', '_')}, " + for country in sop.select('li[data-testid="title-details-origin"]')[0].findAll(class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link") + ) + country = country[:-2] + res_str += f"Negara: {country}\n" + if sop.select('li[data-testid="title-details-languages"]'): + language = "".join( + f"#{lang.text.replace(' ', '_').replace('-', '_')}, " for lang in sop.select('li[data-testid="title-details-languages"]')[0].findAll(class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link") + ) + language = language[:-2] + res_str += f"Bahasa: {language}\n" + res_str += "\nšŸ™Ž Info Cast:\n" + if r_json.get("director"): + director = "" + for i in r_json["director"]: + name = i["name"] + url = i["url"] + director += f"{name}, " + director = director[:-2] + res_str += f"Sutradara: {director}\n" + if r_json.get("creator"): + creator = "" + for i in r_json["creator"]: + if i["@type"] == "Person": + name = i["name"] + url = i["url"] + creator += f"{name}, " + creator = creator[:-2] + res_str += f"Penulis: {creator}\n" + if r_json.get("actor"): + actors = "" + for i in r_json["actor"]: + name = i["name"] + url = i["url"] + actors += f"{name}, " + actors = actors[:-2] + res_str += f"Pemeran: {actors}\n\n" + if r_json.get("description"): + summary = GoogleTranslator("auto", "id").translate(r_json.get("description")) + res_str += f"šŸ“œ Plot: {summary}\n\n" + if r_json.get("keywords"): + keywords = r_json["keywords"].split(",") + key_ = "" + for i in keywords: + i = i.replace(" ", "_").replace("-", "_") + key_ += f"#{i}, " + key_ = key_[:-2] + res_str += f"šŸ”„ Kata Kunci: {key_} \n" + if sop.select('li[data-testid="award_information"]'): + awards = sop.select('li[data-testid="award_information"]')[0].find(class_="ipc-metadata-list-item__list-content-item").text + res_str += f"šŸ† Penghargaan: {GoogleTranslator('auto', 'id').translate(awards)}\n\n" + else: + res_str += "\n" + res_str += "Ā©ļø IMDb by @MissKatyRoBot" + if r_json.get("trailer"): + trailer_url = r_json["trailer"]["url"] + markup = InlineKeyboardMarkup([[InlineKeyboardButton("šŸŽ¬ Open IMDB", url=f"https://www.imdb.com{r_json['url']}"), InlineKeyboardButton("ā–¶ļø Trailer", url=trailer_url)]]) + else: + markup = InlineKeyboardMarkup([[InlineKeyboardButton("šŸŽ¬ Open IMDB", url=f"https://www.imdb.com{r_json['url']}")]]) + if thumb := r_json.get("image"): + try: + await query.message.reply_photo(photo=thumb, quote=True, caption=res_str, reply_to_message_id=usr.id, reply_markup=markup) + except (MediaEmpty, PhotoInvalidDimensions, WebpageMediaEmpty): + poster = thumb.replace(".jpg", "._V1_UX360.jpg") + await query.message.reply_photo(photo=poster, caption=res_str, reply_to_message_id=usr.id, reply_markup=markup) + except Exception: + await query.message.reply(res_str, reply_markup=markup, disable_web_page_preview=False, reply_to_message_id=usr.id) + await query.message.delete() + else: + await query.message.edit(res_str, reply_markup=markup, disable_web_page_preview=False) + await query.answer() + except MessageNotModified: + pass + except Exception: + exc = traceback.format_exc() + await query.message.edit_text(f"ERROR:\n{exc}") + + +# IMDB Versi English +@app.on_message(filters.command(["imdb_en"], COMMAND_HANDLER)) +@capture_err +async def imdb_en_search(client, message): + BTN = [] + if message.sender_chat: + return await message.reply("This feature not available for channel.") + if len(message.command) == 1: + return await message.reply("Give movie name or series. Ex: /imdb_en soul. šŸ¤·šŸ»ā€ā™‚ļø", quote=True) + r, title = message.text.split(None, 1) + k = await message.reply("Searching Movie/Series in IMDB Database.. 😓", quote=True) + msg = "" + buttons = InlineKeyboard(row_width=4) + try: + r = await get_content(f"https://yasirapi.eu.org/imdb-search?q={title}") + res = json.loads(r).get("result") + if not res: + return await k.edit("Sad, No Result.. šŸ˜•") + msg = f"Found {len(res)} result from {title} ~ {message.from_user.mention}\n\n" + for count, movie in enumerate(res, start=1): + titles = movie.get("l") + year = f"({movie.get('y')})" if movie.get("y") else "" + type = movie.get("qid").replace("feature", "movie").capitalize() + movieID = re.findall(r"tt(\d+)", movie.get("id"))[0] + msg += f"{count}. {titles} {year} ~ {type}\n" + BTN.append(InlineKeyboardButton(text=count, callback_data=f"imdben#{message.from_user.id}#{movieID}")) + buttons.add(*BTN) + await k.edit(msg, reply_markup=buttons) + except Exception as err: + await k.edit(f"Ooppss, failed get movie list from IMDb.\n\nERROR: {err}") + + +@app.on_callback_query(filters.regex("^imdben")) +@capture_err +async def imdb_en_callback(bot: Client, query: CallbackQuery): + usr = query.message.reply_to_message + i, userid, movie = query.data.split("#") + if query.from_user.id != int(userid): + return await query.answer("āš ļø Access Denied!", True) + await query.message.edit_text("ā³ Processing your request..") + try: + url = f"https://www.imdb.com/title/tt{movie}/" + resp = await get_content(url) + sop = BeautifulSoup(resp, "lxml") + r_json = json.loads(sop.find("script", attrs={"type": "application/ld+json"}).contents[0]) + res_str = "" + type = f"{r_json['@type']}" if r_json.get("@type") else "" + if r_json.get("name"): + try: + tahun = sop.select('ul[data-testid="hero-title-block__metadata"]')[0].find(class_="sc-8c396aa2-2 itZqyK").text + except: + tahun = "-" + res_str += f"šŸ“¹ Title: {r_json['name']} [{tahun}] ({type})\n" + if r_json.get("alternateName"): + res_str += f"šŸ“¢ AKA: {r_json.get('alternateName')}\n\n" + else: + res_str += "\n" + if sop.select('li[data-testid="title-techspec_runtime"]'): + durasi = sop.select('li[data-testid="title-techspec_runtime"]')[0].find(class_="ipc-metadata-list-item__content-container").text + res_str += f"Duration: {durasi}\n" + if r_json.get("contentRating"): + res_str += f"Category: {r_json['contentRating']} \n" + if r_json.get("aggregateRating"): + res_str += f"Rating: {r_json['aggregateRating']['ratingValue']}ā­ļø from {r_json['aggregateRating']['ratingCount']} user \n" + if sop.select('li[data-testid="title-details-releasedate"]'): + rilis = sop.select('li[data-testid="title-details-releasedate"]')[0].find(class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link").text + rilis_url = sop.select('li[data-testid="title-details-releasedate"]')[0].find(class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link")["href"] + res_str += f"Release Data: {rilis}\n" + if r_json.get("genre"): + genre = "".join(f"{GENRES_EMOJI[i]} #{i.replace('-', '_').replace(' ', '_')}, " if i in GENRES_EMOJI else f"#{i.replace('-', '_').replace(' ', '_')}, " for i in r_json["genre"]) + + genre = genre[:-2] + res_str += f"Genre: {genre}\n" + if sop.select('li[data-testid="title-details-origin"]'): + country = "".join( + f"{demoji(country.text)} #{country.text.replace(' ', '_').replace('-', '_')}, " + for country in sop.select('li[data-testid="title-details-origin"]')[0].findAll(class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link") + ) + country = country[:-2] + res_str += f"Country: {country}\n" + if sop.select('li[data-testid="title-details-languages"]'): + language = "".join( + f"#{lang.text.replace(' ', '_').replace('-', '_')}, " for lang in sop.select('li[data-testid="title-details-languages"]')[0].findAll(class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link") + ) + language = language[:-2] + res_str += f"Language: {language}\n" + res_str += "\nšŸ™Ž Cast Info:\n" + if r_json.get("director"): + director = "" + for i in r_json["director"]: + name = i["name"] + url = i["url"] + director += f"{name}, " + director = director[:-2] + res_str += f"Director: {director}\n" + if r_json.get("creator"): + creator = "" + for i in r_json["creator"]: + if i["@type"] == "Person": + name = i["name"] + url = i["url"] + creator += f"{name}, " + creator = creator[:-2] + res_str += f"Penulis: {creator}\n" + if r_json.get("actor"): + actors = "" + for i in r_json["actor"]: + name = i["name"] + url = i["url"] + actors += f"{name}, " + actors = actors[:-2] + res_str += f"Stars: {actors}\n\n" + if r_json.get("description"): + res_str += f"šŸ“œ Summary: {r_json['description'].replace(' ', ' ')}\n\n" + if r_json.get("keywords"): + keywords = r_json["keywords"].split(",") + key_ = "" + for i in keywords: + i = i.replace(" ", "_").replace("-", "_") + key_ += f"#{i}, " + key_ = key_[:-2] + res_str += f"šŸ”„ Keywords: {key_} \n" + if sop.select('li[data-testid="award_information"]'): + awards = sop.select('li[data-testid="award_information"]')[0].find(class_="ipc-metadata-list-item__list-content-item").text + res_str += f"šŸ† Awards: {awards}\n\n" + else: + res_str += "\n" + res_str += "Ā©ļø IMDb by @MissKatyRoBot" + if r_json.get("trailer"): + trailer_url = r_json["trailer"]["url"] + markup = InlineKeyboardMarkup([[InlineKeyboardButton("šŸŽ¬ Open IMDB", url=f"https://www.imdb.com{r_json['url']}"), InlineKeyboardButton("ā–¶ļø Trailer", url=trailer_url)]]) + else: + markup = InlineKeyboardMarkup([[InlineKeyboardButton("šŸŽ¬ Open IMDB", url=f"https://www.imdb.com{r_json['url']}")]]) + if thumb := r_json.get("image"): + try: + await query.message.reply_photo(photo=thumb, quote=True, caption=res_str, reply_to_message_id=usr.id, reply_markup=markup) + except (MediaEmpty, PhotoInvalidDimensions, WebpageMediaEmpty): + poster = thumb.replace(".jpg", "._V1_UX360.jpg") + await query.message.reply_photo(photo=poster, caption=res_str, reply_to_message_id=usr.id, reply_markup=markup) + except Exception: + await query.message.reply(res_str, reply_markup=markup, disable_web_page_preview=False, reply_to_message_id=usr.id) + await query.message.delete() + else: + await query.message.edit(res_str, reply_markup=markup, disable_web_page_preview=False) + await query.answer() + except Exception: + exc = traceback.format_exc() + await query.message.edit_text(f"ERROR:\n{exc}") diff --git a/misskaty/plugins/nightmode.py b/misskaty/plugins/nightmode.py new file mode 100644 index 00000000..9d07da5d --- /dev/null +++ b/misskaty/plugins/nightmode.py @@ -0,0 +1,213 @@ +# Auto Close and Open Group, I dont have time to add Database Support +from pyrogram.types import ChatPermissions +from pyrogram import Client, __version__, filters +from apscheduler.schedulers.asyncio import AsyncIOScheduler +import pytz +import traceback +from misskaty import app +from datetime import datetime +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery +from misskaty.vars import LOG_CHANNEL + + +# Check calculate how long it will take to Ramadhan +def puasa(): + now = datetime.now(pytz.timezone("Asia/Jakarta")) + tahun = now.strftime("%Y") + bulan = now.strftime("%m") + tgl = now.strftime("%d") + jam = now.strftime("%H") + menit = now.strftime("%M") + detik = now.strftime("%S") + x = datetime(int(tahun), int(bulan), int(tgl), int(jam), int(menit), int(detik)) + y = datetime(2022, 4, 2, 0, 0, 0) + return y - x + + +async def job_close(): + now = datetime.now(pytz.timezone("Asia/Jakarta")) + days = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"] + month = [ + "Unknown", + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember", + ] + tgl = now.strftime("%d") + tahun = now.strftime("%Y") + jam = now.strftime("%H:%M") + try: + # version = check_output(["git log -1 --date=format:v%y.%m%d.%H%M --pretty=format:%cd"], shell=True).decode() + reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="ā¤ļø", callback_data="nightmd")]] + ) + await app.set_chat_permissions( + -1001128045651, + ChatPermissions(can_send_messages=False, can_invite_users=True), + ) + await app.send_message( + -1001128045651, + f"šŸ“† {days[now.weekday()]}, {tgl} {month[now.month]} {tahun}\nā° Jam : {jam}\n\n**šŸŒ— Mode Malam Aktif**\n`Grup ditutup dan semua member tidak akan bisa mengirim pesan. Selamat beristirahat dan bermimpi indah !!`", + reply_markup=reply_markup, + ) + except Exception: + exc = traceback.format_exc() + await app.send_message(LOG_CHANNEL, f"ERROR:\n{exc}") + + +async def job_close_ymoviez(): + now = datetime.now(pytz.timezone("Asia/Jakarta")) + days = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"] + month = [ + "Unknown", + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember", + ] + tgl = now.strftime("%d") + tahun = now.strftime("%Y") + jam = now.strftime("%H:%M") + try: + # version = check_output(["git log -1 --date=format:v%y.%m%d.%H%M --pretty=format:%cd"], shell=True).decode() + reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="ā¤ļø", callback_data="nightmd")]] + ) + await app.set_chat_permissions( + -1001255283935, + ChatPermissions(can_send_messages=False, can_invite_users=True), + ) + await app.send_message( + -1001255283935, + f"šŸ“† {days[now.weekday()]}, {tgl} {month[now.month]} {tahun}\nā° Jam : {jam}\n\n**šŸŒ— Mode Malam Aktif**\n`Grup ditutup hingga jam 9 pagi. Selamat beristirahat.....`", + ) + except Exception: + exc = traceback.format_exc() + await app.send_message(LOG_CHANNEL, f"ERROR:\n{exc}") + + +async def job_open(): + now = datetime.now(pytz.timezone("Asia/Jakarta")) + days = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"] + month = [ + "Unknown", + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember", + ] + tgl = now.strftime("%d") + tahun = now.strftime("%Y") + jam = now.strftime("%H:%M") + try: + # version = check_output(["git log -1 --date=format:v%y.%m%d.%H%M --pretty=format:%cd"], shell=True).decode() + reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="ā¤ļø", callback_data="nightmd")]] + ) + await app.set_chat_permissions( + -1001128045651, + ChatPermissions( + can_send_messages=True, + can_send_media_messages=True, + can_invite_users=True, + can_add_web_page_previews=True, + can_send_other_messages=False, + ), + ) + await app.send_message( + -1001128045651, + f"šŸ“† {days[now.weekday()]}, {tgl} {month[now.month]} {tahun}\nā° {jam}`\n\nšŸŒ— Mode Malam Selesai\nSelamat pagi, grup kini telah dibuka semoga hari-harimu menyenangkan.`", + reply_markup=reply_markup, + ) + except Exception: + exc = traceback.format_exc() + await app.send_message(LOG_CHANNEL, f"ERROR:\n{exc}") + + +async def job_open_ymoviez(): + now = datetime.now(pytz.timezone("Asia/Jakarta")) + days = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"] + month = [ + "Unknown", + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember", + ] + tgl = now.strftime("%d") + tahun = now.strftime("%Y") + jam = now.strftime("%H:%M") + try: + # version = check_output(["git log -1 --date=format:v%y.%m%d.%H%M --pretty=format:%cd"], shell=True).decode() + reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="ā¤ļø", callback_data="nightmd")]] + ) + await app.set_chat_permissions( + -1001255283935, + ChatPermissions( + can_send_messages=True, + can_send_media_messages=True, + can_invite_users=True, + can_add_web_page_previews=True, + can_send_other_messages=True, + ), + ) + await app.send_message( + -1001255283935, + f"šŸ“† {days[now.weekday()]}, {tgl} {month[now.month]} {tahun}\nā° {jam}`\n\nšŸŒ— Mode Malam Selesai\nSelamat pagi, grup kini telah dibuka semoga hari-harimu menyenangkan.`", + reply_markup=reply_markup, + ) + except Exception: + exc = traceback.format_exc() + await app.send_message(LOG_CHANNEL, f"ERROR:\n{exc}") + + +@app.on_callback_query(filters.regex(r"^nightmd$")) +async def _callbackanightmd(c: Client, q: CallbackQuery): + # version = check_output(["git log -1 --date=format:v%y.%m%d.%H%M --pretty=format:%cd"], shell=True).decode() + await q.answer( + f"šŸ”– Hai, Aku MissKatyRoBot dibuat menggunakan Framework Pyrogram v{__version__} dan Python 3.10.\n\nMau buat bot seperti ini? Yuuk belajar di @botindonesia\nOwner: @YasirArisM", + show_alert=True, + cache_time=21600, + ) + + +scheduler = AsyncIOScheduler(timezone="Asia/Jakarta") +scheduler.add_job(job_close, trigger="cron", hour=22, minute=0) +scheduler.add_job(job_close_ymoviez, trigger="cron", hour=22, minute=0) +scheduler.add_job(job_open, trigger="cron", hour=6, minute=0) +scheduler.add_job(job_open_ymoviez, trigger="cron", hour=10, minute=0) +scheduler.start() diff --git a/misskaty/plugins/ocr.py b/misskaty/plugins/ocr.py new file mode 100644 index 00000000..893d3389 --- /dev/null +++ b/misskaty/plugins/ocr.py @@ -0,0 +1,38 @@ +import os +from pyrogram import filters +from telegraph import upload_file +from misskaty.vars import COMMAND_HANDLER +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.helper.http import http + +__MODULE__ = "OCR" +__HELP__ = "/ocr [reply to photo] - Read Text From Image" + + +@app.on_message(filters.command(["ocr"], COMMAND_HANDLER)) +@capture_err +async def ocr(_, message): + reply = message.reply_to_message + if not reply or not reply.photo and not reply.sticker: + return await message.reply_text( + f"Reply photo with /{message.command[0]} command" + ) + msg = await message.reply("Reading image...") + try: + file_path = await reply.download() + if reply.sticker: + file_path = await reply.download(f"ocr{message.from_user.id}.jpg") + response = upload_file(file_path) + url = f"https://telegra.ph{response[0]}" + req = ( + await http.get( + f"https://script.google.com/macros/s/AKfycbwURISN0wjazeJTMHTPAtxkrZTWTpsWIef5kxqVGoXqnrzdLdIQIfLO7jsR5OQ5GO16/exec?url={url}", + follow_redirects=True + ) + ).json() + await msg.edit(f"Hasil OCR:\n{req['text']}") + os.remove(file_path) + except Exception as e: + await msg.edit(str(e)) + os.remove(file_path) diff --git a/misskaty/plugins/paste.py b/misskaty/plugins/paste.py new file mode 100644 index 00000000..4385115a --- /dev/null +++ b/misskaty/plugins/paste.py @@ -0,0 +1,195 @@ +from os import remove +from re import compile as compiles +from misskaty.helper.http import http +from pyrogram import filters +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from json import loads as json_loads +from misskaty import app +from misskaty.vars import COMMAND_HANDLER +from misskaty.helper.tools import rentry + +__MODULE__ = "Paste" +__HELP__ = """ +/paste [Text/Reply To Message] - Post text to Rentry using markdown style. +/temp_paste [Text/Reply To Message] - Post text to tempaste.com using html style. +""" + +# Size Checker for Limit +def humanbytes(size: int): + """Convert Bytes To Bytes So That Human Can Read It""" + if not isinstance(command, int): + try: + size = size + except ValueError: + size = None + if not size: + return "0 B" + size = int(size) + power = 2**10 + raised_to_pow = 0 + dict_power_n = { + 0: "", + 1: "K", + 2: "M", + 3: "G", + 4: "T", + 5: "P", + 6: "E", + 7: "Z", + 8: "Y", + } + while size > power: + size /= power + raised_to_pow += 1 + try: + real_size = f"{str(round(size, 2))} {dict_power_n[raised_to_pow]}B" + except KeyError: + real_size = "Can't Define Real Size !" + return real_size + + +# Pattern if extension supported, PR if want to add more +pattern = compiles(r"^text/|json$|yaml$|xml$|toml$|x-sh$|x-shellscript$|x-subrip$") + + +@app.on_message(filters.command(["paste"], COMMAND_HANDLER)) +async def create(_, message): + reply = message.reply_to_message + target = str(message.command[0]).split("@", maxsplit=1)[0] + if not reply and len(message.command) < 2: + return await message.reply_text( + f"**Reply To A Message With /{target} or with command**" + ) + + msg = await message.reply_text("`Pasting to Rentry...`") + data = "" + limit = 1024 * 1024 + if reply and reply.document: + if reply.document.file_size > limit: + return await msg.edit( + f"**You can only paste files smaller than {humanbytes(limit)}.**" + ) + if not pattern.search(reply.document.mime_type): + return await msg.edit("**Only text files can be pasted.**") + file = await reply.download() + try: + with open(file, "r") as text: + data = text.read() + remove(file) + except UnicodeDecodeError: + try: + remove(file) + except: + pass + return await msg.edit("`File Not Supported !`") + elif reply and (reply.text or reply.caption): + data = reply.text.markdown or reply.caption.markdown + elif not reply and len(message.command) >= 2: + data = message.text.split(None, 1)[1] + + if message.from_user: + if message.from_user.username: + uname = f"@{message.from_user.username}" + else: + uname = ( + f"[{message.from_user.first_name}](tg://user?id={message.from_user.id})" + ) + else: + uname = message.sender_chat.title + + try: + url = await rentry(data) + except Exception as e: + await msg.edit(f"`{e}`") + return + + if not url: + return await msg.edit("Text Too Short Or File Problems") + button = [[InlineKeyboardButton("Open Link", url=url)]] + button.append( + [ + InlineKeyboardButton( + "Share Link", url=f"https://telegram.me/share/url?url={url}" + ) + ] + ) + + pasted = f"**Successfully pasted your data to Rentry.\n\nPaste by {uname}**" + await msg.edit(pasted, reply_markup=InlineKeyboardMarkup(button)) + + +@app.on_message(filters.command(["temp_paste"], COMMAND_HANDLER)) +async def create(_, message): + reply = message.reply_to_message + target = str(message.command[0]).split("@", maxsplit=1)[0] + if not reply and len(message.command) < 2: + return await message.reply_text( + f"**Reply To A Message With /{target} or with command**" + ) + + msg = await message.reply_text("`Pasting to TempPaste...`") + data = "" + limit = 1024 * 1024 + if reply and reply.document: + if reply.document.file_size > limit: + return await msg.edit( + f"**You can only paste files smaller than {humanbytes(limit)}.**" + ) + if not pattern.search(reply.document.mime_type): + return await msg.edit("**Only text files can be pasted.**") + file = await reply.download() + try: + with open(file, "r") as text: + data = text.read() + remove(file) + except UnicodeDecodeError: + try: + remove(file) + except: + pass + return await msg.edit("`File Not Supported !`") + elif reply and (reply.text or reply.caption): + data = reply.text.html or reply.caption.html + elif not reply and len(message.command) >= 2: + data = message.text.split(None, 1)[1] + + if message.from_user: + if message.from_user.username: + uname = f"@{message.from_user.username}" + else: + uname = ( + f"[{message.from_user.first_name}](tg://user?id={message.from_user.id})" + ) + else: + uname = message.sender_chat.title + + try: + req = await http.post( + "https://tempaste.com/api/v1/create-paste/", + data={ + "api_key": "xnwuzXubxk3kCUz9Q2pjMVR8xeTO4t", + "title": "MissKaty Paste", + "paste_content": data, + "visibility": "public", + "expiry_date_type": "months", + "expiry_date": 12, + }, + ) + url = f"https://tempaste.com/{json_loads(req.text)['url']}" + except Exception as e: + await msg.edit(f"`{e}`") + return + + if not url: + return await msg.edit("Text Too Short Or File Problems") + button = [[InlineKeyboardButton("Open Link", url=url)]] + button.append( + [ + InlineKeyboardButton( + "Share Link", url=f"https://telegram.me/share/url?url={url}" + ) + ] + ) + + pasted = f"**Successfully pasted your data to Tempaste.\n\nPaste by {uname}**" + await msg.edit(pasted, reply_markup=InlineKeyboardMarkup(button)) diff --git a/misskaty/plugins/ping.py b/misskaty/plugins/ping.py new file mode 100644 index 00000000..e2c8ff3e --- /dev/null +++ b/misskaty/plugins/ping.py @@ -0,0 +1,55 @@ +import time +from re import findall, MULTILINE +from asyncio import Lock +from pyrogram import filters +from misskaty.vars import COMMAND_HANDLER +from misskaty import app, botStartTime +from misskaty.helper.human_read import get_readable_time +from subprocess import run as srun + + +@app.on_message(filters.command(["ping"], COMMAND_HANDLER)) +async def ping(_, message): + currentTime = get_readable_time(time.time() - botStartTime) + start_t = time.time() + rm = await message.reply_text("🐱 Pong!!...") + end_t = time.time() + time_taken_s = round(end_t - start_t, 3) + try: + await rm.edit( + f"🐈 MissKatyBot online.\n\nPing: {time_taken_s} detik\nUptime: {currentTime}" + ) + except Exception: + pass + + +@app.on_message(filters.command(["ping_dc"], COMMAND_HANDLER)) +async def ping_handler(_, message): + m = await message.reply("Pinging datacenters...") + async with Lock(): + ips = { + "dc1": "149.154.175.53", + "dc2": "149.154.167.51", + "dc3": "149.154.175.100", + "dc4": "149.154.167.91", + "dc5": "91.108.56.130", + } + text = "**Pings:**\n" + + for dc, ip in ips.items(): + try: + shell = srun( + ["ping", "-c", "1", "-W", "2", ip], + text=True, + check=True, + capture_output=True, + ) + resp_time = findall(r"time=.+m?s", shell.stdout, MULTILINE)[0].replace( + "time=", "" + ) + + text += f" **{dc.upper()}:** {resp_time} āœ…\n" + except Exception: + # There's a cross emoji here, but it's invisible. + text += f" **{dc.upper}:** āŒ\n" + await m.edit(text) diff --git a/misskaty/plugins/quotly.py b/misskaty/plugins/quotly.py new file mode 100644 index 00000000..5b2c988e --- /dev/null +++ b/misskaty/plugins/quotly.py @@ -0,0 +1,280 @@ +from pyrogram import Client, filters +from pyrogram.types import Message +from misskaty import app +from misskaty.helper.http import http +from io import BytesIO + +__MODULE__ = "Fun" +__HELP__ = """ +/q [int] - Generate quotly from message +/memify [text] - Reply to sticker to give text on sticker. +""" + + +class QuotlyException(Exception): + pass + + +async def get_message_sender_id(m: Message): + if m.forward_date: + if m.forward_sender_name: + return 1 + elif m.forward_from: + return m.forward_from.id + elif m.forward_from_chat: + return m.forward_from_chat.id + else: + return 1 + elif m.from_user: + return m.from_user.id + elif m.sender_chat: + return m.sender_chat.id + else: + return 1 + + +async def get_message_sender_name(m: Message): + if m.forward_date: + if m.forward_sender_name: + return m.forward_sender_name + elif m.forward_from: + return ( + f"{m.forward_from.first_name} {m.forward_from.last_name}" + if m.forward_from.last_name + else m.forward_from.first_name + ) + + elif m.forward_from_chat: + return m.forward_from_chat.title + else: + return "" + elif m.from_user: + if m.from_user.last_name: + return f"{m.from_user.first_name} {m.from_user.last_name}" + else: + return m.from_user.first_name + elif m.sender_chat: + return m.sender_chat.title + else: + return "" + + +async def get_custom_emoji(m: Message): + if m.forward_date: + return ( + "" + if m.forward_sender_name + or not m.forward_from + and m.forward_from_chat + or not m.forward_from + else m.forward_from.emoji_status.custom_emoji_id + ) + + return m.from_user.emoji_status.custom_emoji_id if m.from_user else "" + + +async def get_message_sender_username(m: Message): + if m.forward_date: + if ( + not m.forward_sender_name + and not m.forward_from + and m.forward_from_chat + and m.forward_from_chat.username + ): + return m.forward_from_chat.username + elif ( + not m.forward_sender_name + and not m.forward_from + and m.forward_from_chat + or m.forward_sender_name + or not m.forward_from + ): + return "" + else: + return m.forward_from.username or "" + elif m.from_user and m.from_user.username: + return m.from_user.username + elif ( + m.from_user or m.sender_chat and not m.sender_chat.username or not m.sender_chat + ): + return "" + else: + return m.sender_chat.username + + +async def get_message_sender_photo(m: Message): + if m.forward_date: + if ( + not m.forward_sender_name + and not m.forward_from + and m.forward_from_chat + and m.forward_from_chat.photo + ): + return { + "small_file_id": m.forward_from_chat.photo.small_file_id, + "small_photo_unique_id": m.forward_from_chat.photo.small_photo_unique_id, + "big_file_id": m.forward_from_chat.photo.big_file_id, + "big_photo_unique_id": m.forward_from_chat.photo.big_photo_unique_id, + } + elif ( + not m.forward_sender_name + and not m.forward_from + and m.forward_from_chat + or m.forward_sender_name + or not m.forward_from + ): + return "" + else: + return ( + { + "small_file_id": m.forward_from.photo.small_file_id, + "small_photo_unique_id": m.forward_from.photo.small_photo_unique_id, + "big_file_id": m.forward_from.photo.big_file_id, + "big_photo_unique_id": m.forward_from.photo.big_photo_unique_id, + } + if m.forward_from.photo + else "" + ) + + elif m.from_user and m.from_user.photo: + return { + "small_file_id": m.from_user.photo.small_file_id, + "small_photo_unique_id": m.from_user.photo.small_photo_unique_id, + "big_file_id": m.from_user.photo.big_file_id, + "big_photo_unique_id": m.from_user.photo.big_photo_unique_id, + } + elif m.from_user or m.sender_chat and not m.sender_chat.photo or not m.sender_chat: + return "" + else: + return { + "small_file_id": m.sender_chat.photo.small_file_id, + "small_photo_unique_id": m.sender_chat.photo.small_photo_unique_id, + "big_file_id": m.sender_chat.photo.big_file_id, + "big_photo_unique_id": m.sender_chat.photo.big_photo_unique_id, + } + + +async def get_text_or_caption(m: Message): + if m.text: + return m.text + elif m.caption: + return m.caption + else: + return "" + + +async def pyrogram_to_quotly(messages): + if not isinstance(messages, list): + messages = [messages] + payload = { + "type": "quote", + "format": "png", + "backgroundColor": "#1b1429", + "messages": [], + } + + for message in messages: + the_message_dict_to_append = {} + if message.entities: + the_message_dict_to_append["entities"] = [ + { + "type": entity.type.name.lower(), + "offset": entity.offset, + "length": entity.length, + } + for entity in message.entities + ] + elif message.caption_entities: + the_message_dict_to_append["entities"] = [ + { + "type": entity.type.name.lower(), + "offset": entity.offset, + "length": entity.length, + } + for entity in message.caption_entities + ] + else: + the_message_dict_to_append["entities"] = [] + the_message_dict_to_append["chatId"] = await get_message_sender_id(message) + the_message_dict_to_append["text"] = await get_text_or_caption(message) + the_message_dict_to_append["avatar"] = True + the_message_dict_to_append["from"] = {} + the_message_dict_to_append["from"]["id"] = await get_message_sender_id(message) + the_message_dict_to_append["from"]["name"] = await get_message_sender_name( + message + ) + the_message_dict_to_append["from"][ + "username" + ] = await get_message_sender_username(message) + the_message_dict_to_append["from"]["type"] = message.chat.type.name.lower() + the_message_dict_to_append["from"]["photo"] = await get_message_sender_photo( + message + ) + if message.reply_to_message: + the_message_dict_to_append["replyMessage"] = { + "name": await get_message_sender_name(message.reply_to_message), + "text": await get_text_or_caption(message.reply_to_message), + "chatId": await get_message_sender_id(message.reply_to_message), + } + else: + the_message_dict_to_append["replyMessage"] = {} + payload["messages"].append(the_message_dict_to_append) + r = await http.post("https://bot.lyo.su/quote/generate.png", json=payload) + if not r.is_error: + return r.read() + else: + raise QuotlyException(r.json()) + + +def isArgInt(txt) -> list: + count = txt + try: + count = int(count) + return [True, count] + except ValueError: + return [False, 0] + + +@app.on_message(filters.command(["q"]) & filters.reply) +async def msg_quotly_cmd(c: Client, m: Message): + if len(m.text.split()) > 1: + check_arg = isArgInt(m.command[1]) + if check_arg[0]: + if check_arg[1] < 2 or check_arg[1] > 10: + return await m.reply_text("Invalid range") + try: + messages = [ + i + for i in await c.get_messages( + chat_id=m.chat.id, + message_ids=range( + m.reply_to_message.id, + m.reply_to_message.id + (check_arg[1] + 5), + ), + replies=-1, + ) + if not i.empty and not i.media + ] + except Exception: + return await m.reply_text("šŸ¤·šŸ»ā€ā™‚ļø") + try: + make_quotly = await pyrogram_to_quotly(messages) + bio_sticker = BytesIO(make_quotly) + bio_sticker.name = "biosticker.webp" + return await m.reply_sticker(bio_sticker) + except Exception: + return await m.reply_text("šŸ¤·šŸ»ā€ā™‚ļø") + try: + messages_one = await c.get_messages( + chat_id=m.chat.id, message_ids=m.reply_to_message.id, replies=-1 + ) + messages = [messages_one] + except Exception: + return await m.reply_text("šŸ¤·šŸ»ā€ā™‚ļø") + try: + make_quotly = await pyrogram_to_quotly(messages) + bio_sticker = BytesIO(make_quotly) + bio_sticker.name = "biosticker.webp" + return await m.reply_sticker(bio_sticker) + except Exception as e: + return await m.reply_text(f"ERROR: {e}") diff --git a/misskaty/plugins/scrapwebsite.py b/misskaty/plugins/scrapwebsite.py new file mode 100644 index 00000000..13bfff04 --- /dev/null +++ b/misskaty/plugins/scrapwebsite.py @@ -0,0 +1,417 @@ +# This plugin to scrape from melongmovie, and lk21 +from bs4 import BeautifulSoup +import aiohttp +import re +import traceback +from misskaty import app +from pyrogram import filters +from pyrogram.errors import MessageTooLong +from misskaty.vars import COMMAND_HANDLER +from misskaty.core.decorator.errors import capture_err +from misskaty.helper.tools import rentry +from misskaty.helper.http import http + +__MODULE__ = "WebScraper" +__HELP__ = """ +/melongmovie - Scrape website data from MelongMovie Web. If without query will give latest movie list. +/lk21 [query ] - Scrape website data from LayarKaca21. If without query will give latest movie list. +/terbit21 [query ] - Scrape website data from Terbit21. If without query will give latest movie list. +/savefilm21 [query ] - Scrape website data from Savefilm21. If without query will give latest movie list. +/movieku [query ] - Scrape website data from Movieku.cc +/gomov [query ] - Scrape website data from GoMov. If without query will give latest movie list. +""" + + +# Broken +@app.on_message(filters.command(["nodrakor"], COMMAND_HANDLER)) +@capture_err +async def nodrakor(_, message): + try: + judul = message.text.split(" ", maxsplit=1)[1] + except IndexError: + judul = "" + + msg = await message.reply("Sedang proses scrap, mohon tunggu..") + try: + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + html = await http.get(f"https://109.234.34.246/?s={judul}", headers=headers) + soup = BeautifulSoup(html.text, "lxml") + res = soup.find_all(class_="content-thumbnail text-center") + data = [] + for i in res: + link = i.find_all("a")[0]["href"] + judul = i.find_all("a")[0]["title"].split(": ")[1] + data.append({"judul": judul, "link": link}) + if not data: + return await msg.edit("Oops, data film tidak ditemukan.") + res = "".join(f"{i['judul']}\n{i['link']}\n\n" for i in data) + await msg.edit( + f"Hasil Pencarian di Nodrakor:\n{res}\nScraped by @MissKatyRoBot" + ) + except Exception as e: + await msg.edit(f"ERROR: {str(e)}") + + +# Broken +@app.on_message(filters.command(["ngefilm21"], COMMAND_HANDLER)) +@capture_err +async def ngefilm21(_, message): + if len(message.command) == 1: + return await message.reply("Masukkan query yang akan dicari..!!") + title = message.text.split(" ", maxsplit=1)[1] + + msg = await message.reply("Sedang proses scrap, mohon tunggu..") + try: + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + + html = await http.get( + f"http://185.237.253.209/search?q={title}", headers=headers + ) + soup = BeautifulSoup(html.text, "lxml") + res = soup.find_all("h2") + data = [] + for i in res: + a = i.find_all("a")[0] + judul = a.find_all(class_="r-snippetized") + b = i.find_all("a")[0]["href"] + data.append({"judul": judul[0].text, "link": b}) + # print(f"{judul[0].text}{b}\n") + if not data: + return await msg.edit("Oops, data film tidak ditemukan.") + res = "".join(f"{i['judul']}\n{i['link']}\n" for i in data) + await msg.edit(f"Hasil Scrap dari Ngefilm21:\n{res}") + except Exception as e: + await msg.edit(f"ERROR: {str(e)}") + + +# Scrape Web From Movieku.CC +@app.on_message(filters.command(["movieku"], COMMAND_HANDLER)) +@capture_err +async def movikucc(_, message): + if len(message.command) == 1: + return await message.reply("Masukkan query yang akan dicari..!!") + judul = message.text.split(" ", maxsplit=1)[1] + msg = await message.reply("Sedang proses scrap, mohon tunggu..") + try: + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + html = await http.get(f"https://107.152.39.187/?s={judul}", headers=headers) + soup = BeautifulSoup(html.text, "lxml") + data = soup.find_all(class_="bx") + res = "".join( + f"Judul: {i.find_all('a')[0]['title']}\nLink: {i.find_all('a')[0]['href']}\n\n" + for i in data + ) + await msg.edit( + f"Hasil Scrap di Movieku.cc:\n{res} āš ļø Gunakan command /movieku_scrap [link] untuk mengambil link download (hanya untuk movie)." + ) + except Exception as e: + await msg.edit(f"ERROR: {str(e)}") + + +@app.on_message(filters.command(["savefilm21"], COMMAND_HANDLER)) +@capture_err +async def savefilm21(_, message): + if len(message.command) == 1: + return await message.reply("Masukkan query yang akan dicari..!!") + judul = message.text.split(" ", maxsplit=1)[1] + msg = await message.reply("Sedang proses scrap, mohon tunggu..") + try: + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + + html = await http.get( + f"http://38.242.196.210/?s={judul}", headers=headers, allow_redirects=False + ) + soup = BeautifulSoup(html.text, "lxml") + res = soup.find_all(class_="entry-title") + data = [] + for i in res: + pas = i.find_all("a") + judul = pas[0].text + link = pas[0]["href"] + data.append({"judul": judul, "link": link}) + if not data: + return await msg.edit("Oops, data film tidak ditemukan") + res = "".join( + f"Judul: {i['judul']}\nLink: {i['link']}\n\n" for i in data + ) + await msg.edit( + f"Hasil Scrap {judul} dari Savefilm21:\n{res}\n\nāš ļø Gunakan /savefilm21_scrap [link] untuk mengambil link downloadnya." + ) + except Exception as e: + await msg.edit(f"ERROR: {str(e)}") + + +@app.on_message(filters.command(["melongmovie"], COMMAND_HANDLER)) +@capture_err +async def melongmovie(_, message): + try: + judul = message.text.split(" ", maxsplit=1)[1] + except IndexError: + judul = "" + + msg = await message.reply("Sedang proses scrap, mohon tunggu..") + try: + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + + html = await http.get(f"http://167.99.31.48/?s={judul}", headers=headers) + soup = BeautifulSoup(html.text, "lxml") + data = [] + for res in soup.select(".box"): + dd = res.select("a") + url = dd[0]["href"] + title = dd[0]["title"] + try: + kualitas = dd[0].find(class_="quality").text + except: + kualitas = "" + data.append({"judul": title, "link": url, "kualitas": kualitas}) + if not data: + return await msg.edit("Oops, data film tidak ditemukan di melongmovie") + res = "".join( + f"Judul: {i['judul']}\nKualitas: {i['kualitas']}\nLink: {i['link']}\n\n" + for i in data + ) + # return await message.reply(json.dumps(data, indent=2, ensure_ascii=False)) + return await msg.edit(res) + except Exception as e: + await msg.edit(f"ERROR: {str(e)}") + + +@app.on_message(filters.command(["terbit21"], COMMAND_HANDLER)) +@capture_err +async def terbit21_scrap(_, message): + if len(message.command) == 1: + r = await http.get("https://yasirapi.eu.org/terbit21") + res = r.json() + data = "".join( + f"**Judul: {i['judul']}**\n`{i['kategori']}`\n{i['link']}\n**Download:** [Klik Disini]({i['dl']})\n\n" + for i in res["result"] + ) + try: + return await message.reply( + f"**Daftar rilis movie terbaru di web Terbit21**:\n{data}", + disable_web_page_preview=True, + ) + except MessageTooLong: + msg = await rentry(data) + return await message.reply( + f"Karena hasil scrape terlalu panjang, maka hasil scrape di post ke rentry.\n\n{msg}" + ) + judul = message.text.split(" ", maxsplit=1)[1] + msg = await message.reply(f"Mencari film di Terbit21 dg keyword {judul}..") + r = await http.get(f"https://yasirapi.eu.org/terbit21?q={judul}") + res = r.json() + data = "".join( + f"**Judul: {i['judul']}**\n`{i['kategori']}`\n{i['link']}\n**Download:** [Klik Disini]({i['dl']})\n\n" + for i in res["result"] + ) + if not res["result"]: + return await msg.edit("Yahh, ga ada hasil ditemukan") + try: + await msg.edit( + f"Hasil pencarian query {judul} di lk21:\n{data}", + disable_web_page_preview=True, + ) + except MessageTooLong: + pesan = await rentry(data) + await msg.edit( + f"Karena hasil scrape terlalu panjang, maka hasil scrape di post ke rentry.\n\n{pesan}" + ) + + +@app.on_message(filters.command(["lk21"], COMMAND_HANDLER)) +@capture_err +async def lk21_scrap(_, message): + if len(message.command) == 1: + msg = await message.reply("Mendapatkan daftar post film terbaru di lk21") + r = await http.get("https://yasirapi.eu.org/lk21") + res = r.json() + if res.get("detail", None): + return await msg.edit(f"ERROR: {res['detail']}") + data = "".join( + f"**Judul: {i['judul']}**\n`{i['kategori']}`\n{i['link']}\n**Download:** [Klik Disini]({i['dl']})\n\n" + for i in res["result"] + ) + try: + return await msg.edit( + f"**Daftar rilis movie terbaru di web LK21**:\n{data}", + disable_web_page_preview=True, + ) + except MessageTooLong: + msg = await rentry(data) + await msg.edit( + f"Karena hasil scrape terlalu panjang, maka hasil scrape di post ke rentry.\n\n{msg}" + ) + judul = message.text.split(" ", maxsplit=1)[1] + msg = await message.reply(f"Mencari film di lk21 dg keyword {judul}..") + r = await http.get(f"https://yasirapi.eu.org/lk21?q={judul}") + res = r.json() + if res.get("detail", None): + return await msg.edit(f"ERROR: {res['detail']}") + data = "".join( + f"**Judul: {i['judul']}**\n`{i['kategori']}`\n{i['link']}\n**Download:** [Klik Disini]({i['dl']})\n\n" + for i in res["result"] + ) + if not res["result"]: + return await msg.edit("Yahh, ga ada hasil ditemukan") + try: + await msg.edit( + f"Hasil pencarian query {judul} di lk21:\n{data}", + disable_web_page_preview=True, + ) + except MessageTooLong: + pesan = await rentry(data) + return await msg.edit( + f"Karena hasil scrape terlalu panjang, maka hasil scrape di post ke rentry.\n\n{pesan}" + ) + + +@app.on_message(filters.command(["gomov", "gomov@MissKatyRoBot"], COMMAND_HANDLER)) +@capture_err +async def gomov_scrap(_, message): + try: + judul = message.text.split(" ", maxsplit=1)[1] + except IndexError: + judul = "" + + msg = await message.reply("Scraping GoMov Website..") + try: + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + + html = await http.get(f"https://185.173.38.216/?s={judul}", headers=headers) + soup = BeautifulSoup(html.text, "lxml") + entry = soup.find_all(class_="entry-title") + DATA = [] + for i in entry: + judul = i.find_all("a")[0].text + link = i.find_all("a")[0]["href"] + DATA.append({"judul": judul, "link": link}) + if not DATA: + return await msg.edit("Oops, data film tidak ditemukan di GoMov") + res = "".join(f"Judul: {i['judul']}\n{i['link']}\n\n" for i in DATA) + await msg.edit( + f"Hasil Pencarian di website GoMov:\n{res}\nScraped by @MissKatyRoBot" + ) + except Exception: + exc = traceback.format_exc() + await msg.edit(f"ERROR: {exc}") + + +@app.on_message( + filters.command( + ["savefilm21_scrap", "savefilm21_scrap@MissKatyRoBot"], COMMAND_HANDLER + ) +) +@capture_err +async def savefilm21_scrap(_, message): + try: + link = message.text.split(" ", maxsplit=1)[1] + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + + html = await http.get(link, headers=headers, follow_redirects=False) + soup = BeautifulSoup(html.text, "lxml") + res = soup.find_all(class_="button button-shadow") + res = "".join(f"{i.text}\n{i['href']}\n\n" for i in res) + await message.reply(f"Hasil Scrap dari {link}:\n\n{res}") + except IndexError: + return await message.reply( + "Gunakan command /savefilm21_scrap [link] untuk scrap link download" + ) + except Exception as e: + await message.reply(f"ERROR: {str(e)}") + + +@app.on_message( + filters.command(["nodrakor_scrap", "nodrakor_scrap@MissKatyRoBot"], COMMAND_HANDLER) +) +@capture_err +async def nodrakor_scrap(_, message): + try: + link = message.text.split(" ", maxsplit=1)[1] + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + + html = await http.get(link, headers=headers, follow_redirects=False) + soup = BeautifulSoup(html.text, "lxml") + hasil = soup.find_all(class_="gmr-download-wrap clearfix")[0] + await message.reply(f"Hasil Scrap dari {link}:\n{hasil}") + except IndexError: + return await message.reply( + "Gunakan command /nodrakor_scrap [link] untuk scrap link download" + ) + except Exception as e: + await message.reply(f"ERROR: {str(e)}") + + +# Scrape Link Download Movieku.CC +@app.on_message( + filters.command(["movieku_scrap", "movieku_scrap@MissKatyRoBot"], COMMAND_HANDLER) +) +@capture_err +async def muviku_scrap(_, message): + try: + link = message.text.split(" ", maxsplit=1)[1] + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + + html = await http.get(link, headers=headers) + soup = BeautifulSoup(html.text, "lxml") + res = soup.find_all(class_="smokeurl") + data = [] + for i in res: + for b in range(len(i.find_all("a"))): + link = i.find_all("a")[b]["href"] + kualitas = i.find_all("a")[b].text + # print(f"{kualitas}\n{link + data.append({"link": link, "kualitas": kualitas}) + if not data: + return await message.reply("Oops, data film tidak ditemukan.") + res = "".join(f"Host: {i['kualitas']}\n{i['link']}\n\n" for i in data) + await message.reply(res) + except IndexError: + return await message.reply( + "Gunakan command /movieku_scrap [link] untuk scrap link download" + ) + except Exception as e: + await message.reply(f"ERROR: {str(e)}") + + +@app.on_message( + filters.command(["melong", "melong@MissKatyRoBot"], COMMAND_HANDLER) + & filters.user([617426792, 1985689491, 1172699512, 2024984460]) +) +@capture_err +async def melong_scrap(_, message): + try: + link = message.text.split(" ", maxsplit=1)[1] + headers = { + "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582" + } + + html = await http.get(link, headers=headers) + soup = BeautifulSoup(html.text, "lxml") + for ep in soup.findAll(text=re.compile(r"(?i)episode\s+\d+|LINK DOWNLOAD")): + hardsub = ep.findPrevious("div") + softsub = ep.findNext("div") + rep = f"{hardsub}\n{softsub}" + await message.reply(rep) + except IndexError: + await message.reply( + "Gunakan command /melong [link] untuk scrap link download" + ) diff --git a/misskaty/plugins/sed.py b/misskaty/plugins/sed.py new file mode 100644 index 00000000..cc623873 --- /dev/null +++ b/misskaty/plugins/sed.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2018-2022 Amano Team + +import html + +import regex +from pyrogram import filters +from pyrogram.types import Message +from pyrogram.errors import MessageEmpty +from misskaty import app + + +@app.on_message(filters.regex(r"^s/(.+)?/(.+)?(/.+)?") & filters.reply) +async def sed(c: app, m: Message): + exp = regex.split(r"(? 3 else "" + + rflags = 0 + + count = 0 if "g" in flags else 1 + if "i" in flags and "s" in flags: + rflags = regex.I | regex.S + elif "i" in flags: + rflags = regex.I + elif "s" in flags: + rflags = regex.S + + text = m.reply_to_message.text or m.reply_to_message.caption + + if not text: + return + + try: + res = regex.sub( + pattern, replace_with, text, count=count, flags=rflags, timeout=1 + ) + except TimeoutError: + return await m.reply_text("Oops, your regex pattern has run for too long.") + except regex.error as e: + return await m.reply_text(str(e)) + else: + try: + await c.send_message( + m.chat.id, + f"
{html.escape(res)}
", + reply_to_message_id=m.reply_to_message.id, + ) + except MessageEmpty: + return await m.reply_text("Please reply message to use this feature.") + except Exception as e: + return await m.reply_text(f"ERROR: {str(e)}") diff --git a/misskaty/plugins/sub_extractor.py b/misskaty/plugins/sub_extractor.py new file mode 100644 index 00000000..fed0e65b --- /dev/null +++ b/misskaty/plugins/sub_extractor.py @@ -0,0 +1,150 @@ +from misskaty import app +from pyrogram import filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from misskaty.vars import COMMAND_HANDLER +from misskaty.core.decorator.errors import capture_err +from misskaty.plugins.dev import shell_exec +import json, os, traceback +from time import perf_counter +from re import split as ngesplit, I +from urllib.parse import unquote +from misskaty.helper.tools import get_random_string + +ARCH_EXT = [".mkv", ".avi", ".mp4", ".mov"] + +__MODULE__ = "MediaExtract" +__HELP__ = """ +/extractmedia [URL] - Extract subtitle or audio from video using link. +""" + + +def get_base_name(orig_path: str): + if ext := [ext for ext in ARCH_EXT if orig_path.lower().endswith(ext)]: + ext = ext[0] + return ngesplit(f"{ext}$", orig_path, maxsplit=1, flags=I)[0] + + +def get_subname(url, format): + fragment_removed = url.split("#")[0] # keep to left of first # + query_string_removed = fragment_removed.split("?")[0] + scheme_removed = query_string_removed.split("://")[-1].split(":")[-1] + if scheme_removed.find("/") == -1: + return f"MissKatySub_{get_random_string(4)}.{format}" + return f"{get_base_name(os.path.basename(unquote(scheme_removed)))}.{format}" + + +@app.on_message(filters.command(["ceksub", "extractmedia"], COMMAND_HANDLER)) +async def ceksub(_, m): + cmd = m.text.split(" ", 1) + if len(cmd) == 1: + return await m.reply( + f"Gunakan command /{m.command[0]} [link] untuk mengecek subtitle dan audio didalam video." + ) + link = cmd[1] + start_time = perf_counter() + pesan = await m.reply("Sedang memproses perintah..", quote=True) + try: + res = ( + await shell_exec( + f"ffprobe -loglevel 0 -print_format json -show_format -show_streams {link}" + ) + )[0] + details = json.loads(res) + buttons = [] + for stream in details["streams"]: + mapping = stream["index"] + try: + stream_name = stream["codec_name"] + except: + stream_name = "-" + stream_type = stream["codec_type"] + if stream_type not in ("audio", "subtitle"): + continue + try: + lang = stream["tags"]["language"] + except: + lang = mapping + buttons.append( + [ + InlineKeyboardButton( + f"0:{mapping}({lang}): {stream_type}: {stream_name}", + f"streamextract_{mapping}_{stream_name}", + ) + ] + ) + end_time = perf_counter() + timelog = "{:.2f}".format(end_time - start_time) + " second" + buttons.append([InlineKeyboardButton("Cancel", "cancel")]) + await pesan.edit( + f"Press the button below to extract subtitles/audio. Only support direct link at this time.\nProcessed in {timelog}", + reply_markup=InlineKeyboardMarkup(buttons), + ) + except Exception: + err = traceback.format_exc() + await pesan.edit(f"Gagal extract media data.\nLink: {link}\nERROR: {err}") + + +ALLOWED_USER = [978550890, 617426792, 2024984460, 1533008300, 1985689491] + + +@app.on_message(filters.command(["converttosrt"], COMMAND_HANDLER)) +@capture_err +async def convertsrt(_, m): + reply = m.reply_to_message + if not reply and reply.document: + return await m.reply( + f"Gunakan command /{m.command[0]} dengan mereply ke file ass untuk convert subtitle ke srt." + ) + msg = await m.reply("Sedang memproses perintah...") + dl = await reply.download() + (await shell_exec(f"mediaextract -i {dl} {os.path.basename(dl)}.srt"))[0] + await m.reply_document( + f"{os.path.basename(dl)}.srt", caption=f"{os.path.basename(dl)}.srt" + ) + await msg.delete() + try: + os.remove(dl) + os.remove(f"{os.path.basename(dl)}.srt") + except: + pass + + +@app.on_callback_query(filters.regex(r"^streamextract_")) +async def stream_extract(bot, update): + cb_data = update.data + usr = update.message.reply_to_message + if update.from_user.id != usr.from_user.id: + return await update.answer("āš ļø Access Denied!", True) + _, map, codec = cb_data.split("_") + link = update.message.reply_to_message.command[1] + await update.message.edit("Processing...") + if codec == "aac": + format = "aac" + elif codec == "mp3": + format = "mp3" + elif codec == "eac3": + format = "eac3" + elif codec == "subrip": + format = "srt" + else: + format == "ass" + try: + start_time = perf_counter() + namafile = get_subname(link, format) + extract = (await shell_exec(f"mediaextract -i {link} -map 0:{map} {namafile}"))[ + 0 + ] + end_time = perf_counter() + timelog = "{:.2f}".format(end_time - start_time) + " second" + await update.message.reply_document( + namafile, + caption=f"Filename: {namafile}\n\nExtracted by @MissKatyRoBot in {timelog}", + reply_to_message_id=usr.id, + ) + await update.message.delete() + try: + os.remove(namafile) + except: + pass + except Exception as e: + await update.message.edit(f"Failed extract sub. \n\nERROR: {e}") diff --git a/misskaty/plugins/tes_session.py b/misskaty/plugins/tes_session.py new file mode 100644 index 00000000..aa44d109 --- /dev/null +++ b/misskaty/plugins/tes_session.py @@ -0,0 +1,15 @@ +# This plugin to learn session using pyrogram +from misskaty import app +from misskaty.vars import COMMAND_HANDLER +from pyrogram import filters + + +@app.on_message(filters.command(["session"], COMMAND_HANDLER)) +async def session(_, message): + nama = await app.ask(message.chat.id, "Ketik nama kamu:") + umur = await app.ask(message.chat.id, "Ketik umur kamu") + alamat = await app.ask(message.chat.id, "Ketik alamat kamu:") + await app.send_message( + message.chat.id, + f"Nama Kamu Adalah: {nama.text}\nUmur Kamu Adalah: {umur.text}\nAlamat Kamu Adalah: {alamat.text}", + ) diff --git a/misskaty/plugins/ubot_plugin.py b/misskaty/plugins/ubot_plugin.py new file mode 100644 index 00000000..4a037a52 --- /dev/null +++ b/misskaty/plugins/ubot_plugin.py @@ -0,0 +1,180 @@ +import os +from pyrogram import enums, filters +from pyrogram.types import ( + Message, + InlineKeyboardMarkup, + InlineKeyboardButton, + ChatEventFilter, +) +from pyrogram.raw import functions +from misskaty import user, app +from datetime import datetime + +f = filters.chat([]) + + +@user.on_message(f) +async def auto_read(_, message: Message): + await user.read_history(message.chat.id) + await message.continue_propagation() + + +@user.on_message(filters.command("autoscroll", "!") & filters.me) +async def add_keep(_, message: Message): + if message.chat.id in f: + f.remove(message.chat.id) + await message.edit("Autoscroll dimatikan") + else: + f.add(message.chat.id) + await message.edit("Autoscroll diaktifkan, semua chat akan otomatis terbaca") + + +# @user.on_deleted_messages(filters.chat([-1001455886928, -1001255283935])) +async def del_msg(client, message): + async for a in user.get_chat_event_log( + message[0].chat.id, limit=1, filters=ChatEventFilter(deleted_messages=True) + ): + try: + ustat = ( + await user.get_chat_member( + message[0].chat.id, a.deleted_message.from_user.id + ) + ).status + except: + ustat = enums.ChatMemberStatus.MEMBER + if ( + ustat + in [enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER] + or a.deleted_message.from_user.is_bot + ): + return + if a.user.id == a.deleted_message.from_user.id: + if a.deleted_message.text: + await app.send_message( + a.deleted_message.chat.id, + f"#DELETED_MESSAGE\n\n{a.deleted_message.from_user.first_name} menghapus pesannya 🧐.\nPesan: {a.deleted_message.text}", + ) + elif a.deleted_message.video: + await app.send_message( + a.deleted_message.chat.id, + f"#DELETED_MESSAGE\n\n{a.deleted_message.from_user.first_name} menghapus pesannya 🧐.\nNama file: {a.deleted_message.video.file_name}", + ) + + +# @user.on_edited_message(filters.text & filters.chat(-1001455886928)) +async def edit_msg(client, message): + try: + ustat = ( + await user.get_chat_member(message.chat.id, message.from_user.id) + ).status + except: + ustat = enums.ChatMemberStatus.MEMBER + if message.from_user.is_bot or ustat in [ + enums.ChatMemberStatus.ADMINISTRATOR, + enums.ChatMemberStatus.OWNER, + ]: + return + async for a in user.get_chat_event_log( + message.chat.id, limit=1, filters=ChatEventFilter(edited_messages=True) + ): + if a.old_message.text.startswith( + ("/mirror", "/leech", "/unzipmirror", "/unzipleech") + ): + await app.send_message( + message.chat.id, + f"#EDITED_MESSAGE\n\n{a.user.first_name} mengedit pesannya 🧐.\nPesan: {a.old_message.text}", + ) + + +@user.on_message(filters.private & ~filters.bot & ~filters.me & filters.text) +async def message_pm(client, message): + await app.send_message( + 617426792, + f"Ada pesan baru dari {message.from_user.mention}\n\nPesan: {message.text}", + ) + + +@user.on_message(~filters.bot & filters.group & filters.mentioned) +async def mentioned(client, message): + if message.sender_chat: + return + cid = message.chat.id + pesan = message.text or message.caption + await app.send_message( + 617426792, + f"{message.from_user.mention} mention kamu di {message.chat.title}\n\nPesan: {pesan}", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text="šŸ’¬ Lihat Pesan", + url=f"https://t.me/c/{str(cid)[4:]}/{message.id}", + ) + ] + ] + ), + ) + + +@user.on_message(filters.command("joindate", "!") & filters.me) +async def join_date(app, message: Message): + members = [] + async for m in app.iter_chat_members(message.chat.id): + members.append( + ( + m.user.first_name, + m.joined_date or (await app.get_messages(message.chat.id, 1)).date, + ) + ) + members.sort(key=lambda member: member[1]) + + with open("joined_date.txt", "w", encoding="utf8") as f: + f.write("Join Date First Name\n") + for member in members: + f.write( + str(datetime.fromtimestamp(member[1]).strftime("%y-%m-%d %H:%M")) + + f" {member[0]}\n" + ) + + await user.send_document(message.chat.id, "joined_date.txt") + os.remove("joined_date.txt") + + +@user.on_message(filters.command("memberstats", "!") & filters.me) +async def memberstats(client, message): + people = {} + total = await user.get_chat_members_count(message.chat.id) + async for msg in user.iter_history(message.chat.id, limit=1000): + if msg.from_user and not msg.from_user.is_bot: + people[msg.from_user.id] = msg.from_user.first_name + await message.edit(f"{round(len(people) / total)}%") + + +@user.on_message(filters.command("recent_action", "!") & filters.me) +async def recent_act(client, message): + full_log = await user.invoke( + functions.channels.GetAdminLog( + channel=await user.resolve_peer(message.chat.id), + q="", + max_id=0, + min_id=0, + limit=0, + ) + ) + with open( + f"recent_actions_{message.chat.id}.txt", "w", encoding="utf8" + ) as log_file: + log_file.write(str(full_log)) + await message.reply_document(f"recent_actions_{message.chat.id}.txt") + + +@user.on_message(filters.command(["screenshot"], prefixes="!")) +async def take_a_screenshot(client, message): + await message.delete() + await user.invoke( + functions.messages.SendScreenshotNotification( + peer=await user.resolve_peer(message.chat.id), + reply_to_msg_id=0, + random_id=app.rnd_id(), + ) + ) diff --git a/misskaty/plugins/webss.py b/misskaty/plugins/webss.py new file mode 100644 index 00000000..8248d63b --- /dev/null +++ b/misskaty/plugins/webss.py @@ -0,0 +1,76 @@ +from asyncio import gather +from base64 import b64decode +from io import BytesIO +from pyrogram import filters +from misskaty import app +from misskaty.vars import COMMAND_HANDLER +from misskaty.core.decorator.errors import capture_err +from misskaty.helper.http import post + +__MODULE__ = "WebSS" +__HELP__ = """ +/webss [URL] [FULL_SIZE?, use (y|yes|true) to get full size image. (optional)] - Take A Screenshot Of A Webpage. +""" + + +async def take_screenshot(url: str, full: bool = False): + url = url if url.startswith("http") else f"https://{url}" + payload = { + "url": url, + "width": 1920, + "height": 1080, + "scale": 1, + "format": "jpeg", + } + if full: + payload["full"] = True + data = await post( + "https://webscreenshot.vercel.app/api", + data=payload, + ) + if "image" not in data: + return None + b = data["image"].replace("data:image/jpeg;base64,", "") + file = BytesIO(b64decode(b)) + file.name = "webss.jpg" + return file + + +@app.on_message(filters.command(["webss"], COMMAND_HANDLER)) +@capture_err +async def take_ss(_, message): + if len(message.command) < 2: + return await message.reply("Give A Url To Fetch Screenshot.") + + if len(message.command) == 2: + url = message.text.split(None, 1)[1] + full = False + elif len(message.command) == 3: + url = message.text.split(None, 2)[1] + full = message.text.split(None, 2)[2].lower().strip() in [ + "yes", + "y", + "1", + "true", + ] + else: + return await message.reply("Invalid Command.") + + m = await message.reply("Capturing screenshot...") + + try: + photo = await take_screenshot(url, full) + if not photo: + return await m.edit("Failed To Take Screenshot") + + m = await m.edit("Uploading...") + + if not full: + # Full size images have problem with reply_photo, that's why + # we need to only use reply_photo if we're not using full size + await gather(*[message.reply_document(photo), message.reply_photo(photo)]) + else: + await message.reply_document(photo) + await m.delete() + except Exception as e: + await m.edit(str(e)) diff --git a/misskaty/plugins/ytdl_download.py b/misskaty/plugins/ytdl_download.py new file mode 100644 index 00000000..04774454 --- /dev/null +++ b/misskaty/plugins/ytdl_download.py @@ -0,0 +1,386 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @lastModified 2022-12-01 09:32:31 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved + """ +import os, logging, json, shutil, asyncio, time +from misskaty import app +from PIL import Image +from pyrogram import filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from misskaty.vars import COMMAND_HANDLER +from datetime import datetime +from hachoir.metadata import extractMetadata +from hachoir.parser import createParser +from misskaty.helper.ytdl_helper import random_char, DownLoadFile +from misskaty.helper.human_read import get_readable_file_size +from misskaty.plugins.dev import shell_exec +from misskaty.core.decorator.errors import capture_err +from misskaty.helper.pyro_progress import progress_for_pyrogram + +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +LOGGER = logging.getLogger(__name__) + +user_time = {} + + +@app.on_message(filters.command(["ytdown"], COMMAND_HANDLER) & ~filters.channel) +@capture_err +async def ytdown(_, message): + if len(message.command) == 1: + return await message.reply( + f"Gunakan command /{message.command[0]} YT_LINK untuk download video dengan YT-DLP." + ) + userLastDownloadTime = user_time.get(message.chat.id) + try: + if userLastDownloadTime > datetime.now(): + wait_time = round( + (userLastDownloadTime - datetime.now()).total_seconds() / 60, 2 + ) + await message.reply_text(f"Wait {wait_time} Minutes before next request..") + return + except: + pass + + url = message.command[1] + command_to_exec = f"yt-dlp --no-warnings --youtube-skip-dash-manifest -j {url}" + t_response = (await shell_exec(command_to_exec))[0] + if "ERROR" in t_response: + await message.reply_text(t_response, quote=True, disable_web_page_preview=True) + return False + inline_keyboard = [] + if t_response: + x_reponse = t_response + if "\n" in x_reponse: + x_reponse, _ = x_reponse.split("\n") + response_json = json.loads(x_reponse) + randem = random_char(5) + if not os.path.exists("./YT_Down"): + os.makedirs("./YT_Down") + save_ytdl_json_path = f"YT_Down/{str(message.from_user.id)}{randem}.json" + with open(save_ytdl_json_path, "w", encoding="utf8") as outfile: + json.dump(response_json, outfile, ensure_ascii=False) + duration = None + if "duration" in response_json: + duration = response_json["duration"] + if "formats" in response_json: + for formats in response_json["formats"]: + format_id = formats.get("format_id") + format_string = formats.get("format_note") + if format_string in ["ultralow", "low", "medium"]: + continue + if format_string is None: + format_string = formats.get("format") + format_ext = formats.get("ext") + if format_ext == "mhtml": + continue + if formats.get("filesize"): + size = formats["filesize"] + elif formats.get("filesize_approx"): + size = formats["filesize_approx"] + else: + size = 0 + cb_string_video = f"ytdl|video|{format_id}|{format_ext}|{randem}" + cb_string_file = f"ytdl|file|{format_id}|{format_ext}|{randem}" + if format_string and "audio only" not in format_string: + ikeyboard = [ + InlineKeyboardButton( + f"šŸŽ¬ {format_string} {format_ext} {get_readable_file_size(size)} ", + callback_data=(cb_string_video).encode("UTF-8"), + ), + InlineKeyboardButton( + f"šŸ“„ {format_string} {format_ext} {get_readable_file_size(size)} ", + callback_data=(cb_string_file).encode("UTF-8"), + ), + ] + + else: + # special weird case :\ + ikeyboard = [ + InlineKeyboardButton( + "SVideo [" + "] ( " + get_readable_file_size(size) + " )", + callback_data=(cb_string_video).encode("UTF-8"), + ), + InlineKeyboardButton( + "DFile [" + "] ( " + get_readable_file_size(size) + " )", + callback_data=(cb_string_file).encode("UTF-8"), + ), + ] + inline_keyboard.append(ikeyboard) + if duration is not None: + cb_string_64 = f"ytdl|audio|64k|mp3|{randem}" + cb_string_128 = f"ytdl|audio|128k|mp3|{randem}" + cb_string = f"ytdl|audio|320k|mp3|{randem}" + inline_keyboard.extend( + ( + [ + InlineKeyboardButton( + "MP3 " + "(" + "64 kbps" + ")", + callback_data=cb_string_64.encode("UTF-8"), + ), + InlineKeyboardButton( + "MP3 " + "(" + "128 kbps" + ")", + callback_data=cb_string_128.encode("UTF-8"), + ), + ], + [ + InlineKeyboardButton( + "MP3 " + "(" + "320 kbps" + ")", + callback_data=cb_string.encode("UTF-8"), + ) + ], + ) + ) + + else: + format_id = response_json["format_id"] + format_ext = response_json["ext"] + cb_string_file = f"ytdl|file|{format_id}|{format_ext}|{randem}" + cb_string_video = f'ytdl|{"video"}|{format_id}|{format_ext}|{randem}' + inline_keyboard.append( + [ + InlineKeyboardButton( + "SVideo", callback_data=(cb_string_video).encode("UTF-8") + ), + InlineKeyboardButton( + "DFile", callback_data=(cb_string_file).encode("UTF-8") + ), + ] + ) + cb_string_file = f'{"file"}={format_id}={format_ext}' + cb_string_video = f'{"video"}={format_id}={format_ext}' + inline_keyboard.append( + [ + InlineKeyboardButton( + "video", callback_data=(cb_string_video).encode("UTF-8") + ), + InlineKeyboardButton( + "file", callback_data=(cb_string_file).encode("UTF-8") + ), + ] + ) + reply_markup = InlineKeyboardMarkup(inline_keyboard) + thumbnail = "https://uxwing.com/wp-content/themes/uxwing/download/signs-and-symbols/no-video-icon.png" + thumbnail_image = "https://uxwing.com/wp-content/themes/uxwing/download/signs-and-symbols/no-video-icon.png" + if "thumbnail" in response_json and response_json["thumbnail"] is not None: + thumbnail = response_json["thumbnail"] + thumbnail_image = response_json["thumbnail"] + thumb_image_path = DownLoadFile( + thumbnail_image, + f"YT_Down/{str(message.from_user.id)}{randem}.jpg", + 128, + None, + "Trying to download..", + message.id, + message.chat.id, + ) # bot, + await message.reply_photo( + photo=thumb_image_path, + quote=True, + caption=f"Select the desired format: file size might be approximate", + reply_markup=reply_markup, + ) + + else: + cb_string_file = f'{"file"}={"LFO"}={"NONE"}' + cb_string_video = f'{"video"}={"OFL"}={"ENON"}' + inline_keyboard.append( + [ + InlineKeyboardButton( + "SVideo", callback_data=(cb_string_video).encode("UTF-8") + ), + InlineKeyboardButton( + "DFile", callback_data=(cb_string_file).encode("UTF-8") + ), + ] + ) + reply_markup = InlineKeyboardMarkup(inline_keyboard) + await message.reply_photo( + photo="https://telegra.ph/file/ce37f8203e1903feed544.png", + quote=True, + caption=f"""Select the desired format: file size might be approximate""", + reply_markup=reply_markup, + reply_to_message_id=message.id, + ) + + +@app.on_callback_query(filters.regex(r"^ytdl|")) +async def youtube_dl_call_back(bot, update): + cb_data = update.data + usr = update.message.reply_to_message + if update.from_user.id != usr.from_user.id: + return await update.answer("āš ļø Akses Denied!", True) + # youtube_dl extractors + _, tg_send_type, youtube_dl_format, youtube_dl_ext, ranom = cb_data.split("|") + thumb_image_path = f"YT_Down/{str(update.from_user.id)}{ranom}.jpg" + save_ytdl_json_path = f"YT_Down/{str(update.from_user.id)}{ranom}.json" + try: + with open(save_ytdl_json_path, "r", encoding="utf8") as f: + response_json = json.load(f) + except FileNotFoundError: + await update.message.delete() + return False + + custom_file_name = ( + f"{str(response_json.get('title'))}_{youtube_dl_format}.{youtube_dl_ext}" + ) + custom_file_name = "%(title,fulltitle,alt_title)s %(height& |)s%(height|)s%(height&p|)s%(fps|)s%(fps&fps|)s%(tbr& |)s%(tbr|)d.%(ext)s" + youtube_dl_url = update.message.reply_to_message.text.split(" ", 1)[1] + await update.message.edit_caption("Trying to download media...") + tmp_directory_for_each_user = os.path.join( + f"downloads/{str(update.from_user.id)}{random_char(5)}" + ) + if not os.path.isdir(tmp_directory_for_each_user): + os.makedirs(tmp_directory_for_each_user) + download_directory = os.path.join(tmp_directory_for_each_user, custom_file_name) + if tg_send_type == "audio": + command_to_exec = f"yt-dlp -c --ffmpeg-location '/usr/bin/mediaextract' --max-filesize 2097152000 --prefer-ffmpeg --extract-audio --embed-metadata --audio-format {youtube_dl_ext} --audio-quality {youtube_dl_format} {youtube_dl_url} --output '{download_directory}'" + else: + minus_f_format = youtube_dl_format + if "youtu" in youtube_dl_url: + minus_f_format = f"{youtube_dl_format}+bestaudio[ext=m4a]" + command_to_exec = f"yt-dlp -c --ffmpeg-location '/usr/bin/mediaextract' --max-filesize 2097152000 --embed-subs --embed-metadata -f {minus_f_format} --hls-prefer-ffmpeg {youtube_dl_url} --output '{download_directory}'" + start = datetime.now() + t_response = (await shell_exec(command_to_exec))[0] + if t_response: + os.remove(save_ytdl_json_path) + end_one = datetime.now() + time_taken_for_download = (end_one - start).seconds + file_size = 2097152000 + 1 + download_directory_dirname = os.path.dirname(download_directory) + download_directory_contents = os.listdir(download_directory_dirname) + for download_directory_c in download_directory_contents: + current_file_name = os.path.join( + download_directory_dirname, download_directory_c + ) + file_size = os.stat(current_file_name).st_size + + if file_size == 0: + await update.message.edit(text="File Not found šŸ¤’") + asyncio.create_task(clendir(tmp_directory_for_each_user)) + return + + if file_size > 2097152000: + await update.message.edit_caption( + caption="I cannot upload files greater than 1.95GB due to Telegram API limitations. This file has size {}.".format( + get_readable_file_size(file_size) + ) + ) + asyncio.create_task(clendir(thumb_image_path)) + asyncio.create_task(clendir(tmp_directory_for_each_user)) + return + + else: + await update.message.edit_caption(caption="Trying to upload..") + # get the correct width, height, and duration + # for videos greater than 10MB + # ref: message from @BotSupport + width = 0 + height = 0 + duration = 0 + if tg_send_type != "file": + metadata = extractMetadata(createParser(current_file_name)) + if metadata is not None and metadata.has("duration"): + duration = metadata.get("duration").seconds + # get the correct width, height, and duration + # for videos greater than 10MB + if os.path.exists(thumb_image_path): + # https://stackoverflow.com/a/21669827/4723940 + Image.open(thumb_image_path).convert("RGB").save(thumb_image_path) + metadata = extractMetadata(createParser(thumb_image_path)) + if metadata.has("width"): + width = metadata.get("width") + if metadata.has("height"): + height = metadata.get("height") + if tg_send_type == "vm": + height = width + else: + thumb_image_path = None + start_time = time.time() + # try to upload file + if tg_send_type == "audio": + await update.message.reply_audio( + audio=current_file_name, + caption=f"{os.path.basename(current_file_name)}", + duration=duration, + thumb=thumb_image_path, + reply_to_message_id=usr.id, + progress=progress_for_pyrogram, + progress_args=( + "Trying to upload...", + update.message, + start_time, + ), + ) + elif tg_send_type == "file": + await update.message.reply_document( + document=current_file_name, + thumb=thumb_image_path, + caption=f"{os.path.basename(current_file_name)}", + reply_to_message_id=usr.id, + progress=progress_for_pyrogram, + progress_args=( + "Trying to upload...", + update.message, + start_time, + ), + ) + elif tg_send_type == "vm": + await update.message.reply_video_note( + video_note=current_file_name, + duration=duration, + length=width, + thumb=thumb_image_path, + reply_to_message_id=usr.id, + progress=progress_for_pyrogram, + progress_args=( + "Trying to upload...", + update.message, + start_time, + ), + ) + elif tg_send_type == "video": + await update.message.reply_video( + video=current_file_name, + caption=f"{os.path.basename(current_file_name)}", + duration=duration, + width=width, + height=height, + supports_streaming=True, + reply_to_message_id=usr.id, + thumb=thumb_image_path, + progress=progress_for_pyrogram, + progress_args=( + "Trying to upload...", + update.message, + start_time, + ), + ) + else: + LOGGER.info("Did this happen? :\\") + end_two = datetime.now() + time_taken_for_upload = (end_two - end_one).seconds + await update.message.edit_caption( + caption="Downloaded in {} seconds.\nUploaded in {} seconds.".format( + time_taken_for_download, time_taken_for_upload + ) + ) + asyncio.create_task(clendir(thumb_image_path)) + asyncio.create_task(clendir(tmp_directory_for_each_user)) + await asyncio.sleep(5) + await update.message.delete() + + +async def clendir(directory): + try: + shutil.rmtree(directory) + except: + pass + try: + os.remove(directory) + except: + pass diff --git a/misskaty/vars.py b/misskaty/vars.py new file mode 100644 index 00000000..319f7cd8 --- /dev/null +++ b/misskaty/vars.py @@ -0,0 +1,69 @@ +import logging +from os import environ +from dotenv import load_dotenv + +load_dotenv("config.env", override=True) + +LOGGER = logging.getLogger(__name__) + + +def getConfig(name: str): + try: + return environ[name] + except: + return "" + + +# Required ENV +try: + BOT_TOKEN = getConfig("BOT_TOKEN") + API_ID = getConfig("API_ID") + API_HASH = getConfig("API_HASH") + # MongoDB information + DATABASE_URI = getConfig("DATABASE_URI") + DATABASE_NAME = getConfig("DATABASE_NAME") + LOG_CHANNEL = int(getConfig("LOG_CHANNEL")) + USER_SESSION = getConfig("USER_SESSION") +except Exception as e: + LOGGER.error(f"One or more env variables missing! Exiting now.\n{e}") + exit(1) +COMMAND_HANDLER = environ.get("COMMAND_HANDLER", "! /").split() +SUDO = list( + { + int(x) + for x in environ.get( + "SUDO", + "617426792 2024984460", + ).split() + } +) +SUPPORT_CHAT = environ.get("SUPPORT_CHAT", "YasirPediaChannel") + +## Config For AUtoForwarder +# Forward From Chat ID +FORWARD_FROM_CHAT_ID = list( + { + int(x) + for x in environ.get( + "FORWARD_FROM_CHAT_ID", + "-1001128045651 -1001455886928 -1001686184174", + ).split() + } +) +# Forward To Chat ID +FORWARD_TO_CHAT_ID = list( + {int(x) for x in environ.get("FORWARD_TO_CHAT_ID", "-1001210537567").split()} +) +FORWARD_FILTERS = list(set(environ.get("FORWARD_FILTERS", "video document").split())) +BLOCK_FILES_WITHOUT_EXTENSIONS = bool( + environ.get("BLOCK_FILES_WITHOUT_EXTENSIONS", True) +) +BLOCKED_EXTENSIONS = list( + set( + environ.get( + "BLOCKED_EXTENSIONS", + "html htm json txt php gif png ink torrent url nfo xml xhtml jpg", + ).split() + ) +) +MINIMUM_FILE_SIZE = environ.get("MINIMUM_FILE_SIZE", None) diff --git a/utils.py b/utils.py new file mode 100644 index 00000000..62e85627 --- /dev/null +++ b/utils.py @@ -0,0 +1,100 @@ +import logging +from pyrogram.errors import FloodWait, InputUserDeactivated, PeerIdInvalid, UserIsBlocked +import asyncio +from pyrogram.types import Message +from typing import Union +import os +import emoji +from database.users_chats_db import db + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +BANNED = {} + + +# temp db for banned +class temp(object): + BANNED_USERS = [] + BANNED_CHATS = [] + ME = None + CURRENT = int(os.environ.get("SKIP", 2)) + CANCEL = False + MELCOW = {} + U_NAME = None + B_NAME = None + + +def demoji(teks): + return emoji.emojize(f":{teks.replace(' ', '_').replace('-', '_')}:") + + +async def broadcast_messages(user_id, message): + try: + await message.copy(chat_id=user_id) + return True, "Succes" + except FloodWait as e: + await asyncio.sleep(e.x) + return await broadcast_messages(user_id, message) + except InputUserDeactivated: + await db.delete_user(int(user_id)) + logging.info(f"{user_id}-Removed from Database, since deleted account.") + return False, "Deleted" + except UserIsBlocked: + logging.info(f"{user_id} -Blocked the bot.") + return False, "Blocked" + except PeerIdInvalid: + await db.delete_user(int(user_id)) + logging.info(f"{user_id} - PeerIdInvalid") + return False, "Error" + except Exception: + return False, "Error" + + +def get_size(size): + """Get size in readable format""" + + units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB"] + size = float(size) + i = 0 + while size >= 1024.0 and i < len(units): + i += 1 + size /= 1024.0 + return "%.2f %s" % (size, units[i]) + + +def get_file_id(msg: Message): + if msg.media: + for message_type in ("photo", "animation", "audio", "document", "video", "video_note", "voice", "sticker"): + if obj := getattr(msg, message_type): + setattr(obj, "message_type", message_type) + return obj + + +def extract_user(message: Message) -> Union[int, str]: + """extracts the user from a message""" + # https://github.com/SpEcHiDe/PyroGramBot/blob/f30e2cca12002121bad1982f68cd0ff9814ce027/pyrobot/helper_functions/extract_user.py#L7 + user_id = None + user_first_name = None + if message.reply_to_message: + user_id = message.reply_to_message.from_user.id + user_first_name = message.reply_to_message.from_user.first_name + + elif len(message.command) > 1: + if len(message.entities) > 1 and message.entities[1].type == "text_mention": + + required_entity = message.entities[1] + user_id = required_entity.user.id + user_first_name = required_entity.user.first_name + else: + user_id = message.command[1] + # don't want to make a request -_- + user_first_name = user_id + try: + user_id = int(user_id) + except ValueError: + pass + else: + user_id = message.from_user.id + user_first_name = message.from_user.first_name + return (user_id, user_first_name)