commit e960a69126cc53ab4bae7c86c2b6a5dd2ba1c1e1 Author: yasir Date: Thu Aug 3 11:01:15 2023 +0700 Rest Commit AUgs 2023 diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 00000000..75b76df3 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,13 @@ +version = 1 + +[[analyzers]] +name = "python" + + [analyzers.meta] + runtime_version = "3.x.x" + +[[transformers]] +name = "isort" + +[[transformers]] +name = "black" \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..b50a6b2d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: [yasirarism] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +other: ['https://yasirpedia.eu.org/images/my-qris.jpg', 'https://paypal.me/yasirarism'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] \ No newline at end of file diff --git a/.github/workflows/h3r0ku.yml b/.github/workflows/h3r0ku.yml new file mode 100644 index 00000000..b931ecd5 --- /dev/null +++ b/.github/workflows/h3r0ku.yml @@ -0,0 +1,36 @@ +name: H3r0ku Deployer + +on: workflow_dispatch + +env: + IMAGE_NAME: h3r0ku + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} + HEROKU_APP: ${{ secrets.HEROKU_APP }} + +jobs: + build_and_push: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Build the image + run: docker build . --file Dockerfile --tag "${IMAGE_NAME}" + + - name: Login into Heroku Container registry + run: heroku container:login + + - name: Push the image to Heroku + run: heroku container:push "${IMAGE_NAME}" -a "${HEROKU_APP}" + + - name: Release image to Heroku + run: heroku container:release "${IMAGE_NAME}" -a "${HEROKU_APP}" + + - name: Check ENV + run: | + API_KEY="$(heroku config:get HEROKU_API_KEY -a ${HEROKU_APP})" + APP_NAME="$(heroku config:get HEROKU_APP -a ${HEROKU_APP})" + if [[ -z "${API_KEY}" && -z "${APP_NAME}" ]]; then + heroku config:set HEROKU_API_KEY="${HEROKU_API_KEY}" \ + HEROKU_APP="${HEROKU_APP}" + fi \ No newline at end of file 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/.gitignore b/.gitignore new file mode 100644 index 00000000..4edd7505 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +__pycache__ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1d82e93d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# * @author Yasir Aris M +# * @date 2022-12-01 09:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved + +# Base Docker Using Debian 12 (Bookworm), Python 3.11.4 and Built In Pip +## With Built in Pip Package +FROM yasirarism/misskaty-docker:latest +## Without Built in Pip Package +# FROM yasirarism/misskaty-docker:free + +# Set Hostname +ENV HOSTNAME yasir-server +# Copy Files +COPY . . +# Instal pip package +# RUN pip3 install --no-cache-dir -r requirements.txt +# Set CMD Bot +CMD ["bash", "start.sh"] 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.id.md b/README.id.md new file mode 100644 index 00000000..b83897df --- /dev/null +++ b/README.id.md @@ -0,0 +1,194 @@ +# MissKatyPyro + + +![MIT License][license-shield] ![Repository Size][repository-size-shield] ![Issue Closed][issue-closed-shield] + + +

+ +

+ + + [![Readme in Indonesian][readme-ko-shield]][readme-ko-url] [![View Demo][view-demo-shield]][view-demo-url] [![Report bug][report-bug-shield]][report-bug-url] [![Request feature][request-feature-shield]][request-feature-url] + + +# Table of Contents +- [[1] About MissKaty](#1-about-misskaty) +- [[2] Framework Tools And Server That Used To Build This Bot](#2-framework-tools-and-server-that-used-to-build-this-bot) +- [[3] Donation](#3-donation) +- [[4] Notes](#4-notes) +- [[5] Features](#5-features) +- [[6] Variables](#6-variables) +- [[7] Deploying Tutorial](#7-deploy-recommended-using-dockerdocker-compose) + - [Build And Run Using Legacy Method](#build-and-run-using-legacy-method) + - [Build And Run Using Docker](#build-and-run-using-docker) + - [Build And Run The Docker Image Using docker-compose](#build-and-run-the-docker-image-using-docker-compose) +- [[8] Credits](#8-thanks-to) +- [[9] Disclaimer](#8-disclaimer) + +# [1] About MissKaty +*MissKaty* adalah Bot Telegram yang dibuat menggunakan Python dan library Pyrogram. Banyak fitur yang berguna untuk kita gunakan. Saya berharap suatu saat jika project ini dihentikan, ada yang melanjutkan atau mengembangkannya lagi. Saya memberi nama MissKaty karena saya suka kucing, hewan lucu yang suka bermain dan bersahabat dengan manusia. + +## [2] Framework Tools And Server That Used To Build This Bot + 🌱 PyroFork v2.x.x (Fork Pyrogram dengan Dukungan Topik dan Beberapa Patch)
+ 🌱 Dukungan Python 3.11
+ 🌱 MongoDB sebagai Database
+ 🌱 PyKeyboard for Building Pagination
+ 🌱 VS Code
+ 🌱 VPS/Server With Docker Support (Recommended)
+ +## [3] Donation +*Khusus Indonesia Saja:*
+ 🌱 [QRIS][qris-url]
+ +*Untuk Semua Negara:*
+ 🌱 [Paypal][paypal-url]
+ +## [4] Notes +Jika Anda ingin membantu saya memperbaiki beberapa kesalahan di bot saya, Anda dapat membuat PR ke repo ini. Saya sangat senang jika Anda dapat membantu saya. Anda juga dapat memberikan dukungan kepada saya untuk membeli server. + +## [5] Features + +| FEATURE MY BOT |🌱| +| ------------- | ------------- | +| Basic Admin Feature |✔️| +| AFK Feature |✔️| +| Downloader FB, TikTok and YT-DLP Support |✔️| +| MultiLanguage Support (Still Beta) |⚠️| +| NightMode |✔️| +| ChatBot based on OpenAI |✔️| +| MissKaty Mata |✔️| +| Inline Search |✔️| +| Sticker Tools |✔️| +| PasteBin Tools |✔️| +| WebScraper (Pahe, MelongMovie, LK21, Terbit21, Kusonime, etc) |✔️| +| IMDB Search With Multi Language Per User |✔️| +| GenSS From Media and MediaInfo Generator |✔️| +| And Many More.. |✔️| + +## [6] Variables + +### Variabel yang Diperlukan +* `BOT_TOKEN`: Buat bot menggunakan [@BotFather](https://t.me/BotFather), dan dapatkan token Telegram API. +* `API_ID`: Dapatkan value ini dari [telegram.org](https://my.telegram.org/apps) +* `API_HASH`: Dapatkan value ini dari [telegram.org](https://my.telegram.org/apps) +* `DATABASE_URI`: [mongoDB](https://www.mongodb.com) URI. Dapatkan value ini dari [mongoDB](https://www.mongodb.com). Untuk bantuan lebih lanjut, tonton [video] ini(https://youtu.be/1G1XwEOnxxo) +* `LOG_CHANNEL` : Channel untuk mencatat aktivitas bot. Pastikan bot adalah admin di channel. + +### Variabel Opsional +* `USER_SESSION` : String session untuk Userbot. +* `DATABASE_NAME`: Nama database di MongoDB +* `COMMAND_HANDLER`: Daftar perintah handler bot dipisahkan dengan spasi. Contoh: `. !` > jadi bot akan merespon dengan `.cmd` atau `!cmd` +* `SUDO`: User ID yang memiliki akses ke bot, dipisahkan dengan spasi +* `OPENAI_API`: Dapatkan dari Web OpenAI +* `CURRENCY_API`: Dapatkan API Key di https://app.exchangerate-api.com/sign-up + +## [7] Tutorial Deploy (Recommended using Docker/Docker Compose) + +#### Bangun Dan Jalankan Menggunakan Metode Lama +- Pastikan versi python minimum adalah 3.8 untuk mencegah beberapa error. Periksa dengan perintah ini: +``` +python3 --version +``` +- Instal semua dependensi yang membutuhkan bot untuk dijalankan. *(memerlukan akses root, Anda dapat melewati ini jika server Anda tidak memiliki akses root tetapi beberapa plugin tidak berfungsi)* +``` +apt update -y & apt install libjpeg-dev zlib1g-dev libwebp-dev python3-pip python3-lxml git wget curl lokal ffmpeg tzdata neofetch mediainfo speedtest-cli -y +``` +- Instal requirements.txt, jika menggunakan python 3.11, Anda harus menggunakan opsi venv saat menginstal.
+*Python < 3.10* +``` +pip3 install -r requirements.txt +``` +*Python 3.11* +``` +Install venv dari terminal server kamu +pip3 install -r requirements.txt +``` +- Atur config environment saat menjalankan bot dan jangan lupa isi semua value yang wajib di isi. +- Jalankan Bot +``` +bash start.sh +``` + +#### Build And Run Using Docker + +- Mulai daemon Docker (Lewati jika sudah berjalan): +``` +sudo dockerd +``` +- Build Docker image: +``` +sudo docker build . -t misskaty +``` +- Jalankan Docker image: +``` +sudo docker run misskaty +``` +- Untuk Menghentikan image: +``` +sudo docker ps +sudo docker stop +``` + +#### Build And Run The Docker Image Using docker-compose + +- Install docker-compose +``` +sudo apt install docker-compose +``` +- Build and run Docker image or to view current running image: +``` +sudo docker-compose up +``` +- After editing files with nano for example (nano start.sh): +``` +sudo docker-compose up --build +``` +- To stop the running image: +``` +sudo docker ps +``` +``` +sudo docker-compose stop +``` + +---- + + +## [8] Thanks to + - Terima kasih Kepada Allah Swt. + - Terima kasih Kepada Dan [Pyrogram Library](https://github.com/pyrogram/pyrogram). + - Terima kasih Kepada [The Hamker Cat](https://github.com/TheHamkerCat) Untuk Kode WilliamButcher. + - Terima kasih Kepada [Team Yukki](https://github.com/TeamYukki) Untuk Kode AFK Bot. + - Terima kasih Kepada [Wrench](https://github.com/EverythingSuckz) Untuk Beberapa Kode. + - Terima kasih Kepada [AmanoTeam](https://github.com/AmanoTeam) Untuk Template MultiBahasa. + - Dan Semua Orang Yang Membantuku Dalam Hidupku... +Jika kode Anda digunakan dalam repo ini dan ingin memberikan kredit, silakan buka masalah.. + +## [9] 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) +Dilisensikan di bawah [GNU AGPL 2.0.](https://github.com/yasirarism/MissKatyPyro/blob/master/LICENSE) +PERINGATAN: Menjual Kode Kepada Orang Lain Demi Uang *Dilarang Keras*. Tuhan selalu melihatmu dimanapun kamu berada. + + +[license-shield]: https://img.shields.io/github/license/yasirarism/MissKatyPyro?labelColor=D8D8D8&color=04B4AE +[repository-size-shield]: https://img.shields.io/github/repo-size/yasirarism/MissKatyPyro?labelColor=D8D8D8&color=BE81F7 +[issue-closed-shield]: https://img.shields.io/github/issues-closed/yasirarism/MissKatyPyro?labelColor=D8D8D8&color=FE9A2E + + +[readme-ko-shield]: https://img.shields.io/badge/-readme%20in%20Indonesian-2E2E2E?style=for-the-badge +[view-demo-shield]: https://img.shields.io/badge/-%F0%9F%98%8E%20view%20demo-F3F781?style=for-the-badge +[view-demo-url]: https://t.me/MissKatyPyro +[report-bug-shield]: https://img.shields.io/badge/-%F0%9F%90%9E%20report%20bug-F5A9A9?style=for-the-badge +[report-bug-url]: https://github.com/yasirarism/MissKatyPyro/issues +[request-feature-shield]: https://img.shields.io/badge/-%E2%9C%A8%20request%20feature-A9D0F5?style=for-the-badge +[request-feature-url]: https://github.com/yasirarism/MissKatyPyro/issues + + +[readme-ko-url]: README.md +[kofi-url]: https://ko-fi.com/yasirarism +[paypal-url]: https://paypal.me/yasirarism +[qris-url]: https://telegra.ph/file/2acf7698f300ef3d9138f.jpg +[sociabuzz-url]: https://sociabuzz.com/yasirarism/tribe +[saweria-url]: https://saweria.co/yasirarism +[trakteer-url]: https://trakteer.id/yasir-aris-sp7cn \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..f0b670ff --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +# MissKatyPyro + + +![MIT License][license-shield] ![Repository Size][repository-size-shield] ![Issue Closed][issue-closed-shield] + + +

+ +

+ + + [![Readme in Indonesian][readme-ko-shield]][readme-ko-url] [![View Demo][view-demo-shield]][view-demo-url] [![Report bug][report-bug-shield]][report-bug-url] [![Request feature][request-feature-shield]][request-feature-url] + + +# Table of Contents +- [[1] About MissKaty](#1-about-misskaty) +- [[2] Framework Tools And Server That Used To Build This Bot](#2-framework-tools-and-server-that-used-to-build-this-bot) +- [[3] Donation](#3-donation) +- [[4] Notes](#4-notes) +- [[5] Features](#5-features) +- [[6] Variables](#6-variables) +- [[7] Deploying Tutorial](#7-deploy-recommended-using-dockerdocker-compose) + - [Build And Run Using Legacy Method](#build-and-run-using-legacy-method) + - [Build And Run Using Docker](#build-and-run-using-docker) + - [Build And Run The Docker Image Using docker-compose](#build-and-run-the-docker-image-using-docker-compose) +- [[8] Credits](#8-thanks-to) +- [[9] Disclaimer](#8-disclaimer) + +# [1] About MissKaty +*MissKaty* is a Telegram Bot built using Python and the Pyrogram library. Many useful features for us to use. I hope that one day this project will be discontinued, someone will continue or develop it again. I gave the name MissKaty because I like cats, a cute animal that likes to be played with and friendly with humans. + +## [2] Framework Tools And Server That Used To Build This Bot + 🌱 PyroFork v2.x.x (Fork of Pyrogram with Topics Support and Some Patch)
+ 🌱 Python 3.11 Support
+ 🌱 MongoDB as Database
+ 🌱 PyKeyboard for Building Pagination
+ 🌱 VS Code
+ 🌱 VPS/Server With Docker Support (Recommended)
+ +## [3] Donation and Support +*For Indonesian Only and some supported country:*
+ 🌱 [QRIS][qris-url]
+ +*For International Payment:*
+ 🌱 [Paypal][paypal-url]
+ +## [4] Notes +If you want help me fixing some error in my bot, you can make pull request to this repo. I'm very glad if you can help me. You can also give support to me for buying server. + +## [5] Features + +| FEATURE MY BOT |🌱| +| ------------- | ------------- | +| Basic Admin Feature |✔️| +| AFK Feature |✔️| +| Downloader FB, TikTok and YT-DLP Support |✔️| +| MultiLanguage Support (Unfinished) |⚠️| +| NightMode |✔️| +| ChatBot based on OpenAI |✔️| +| MissKaty Mata |✔️| +| Inline Search |✔️| +| Sticker Tools |✔️| +| PasteBin Tools |✔️| +| WebScraper (Pahe, MelongMovie, LK21, Terbit21, Kusonime, etc) |✔️| +| IMDB Search With Multi Language Per User |✔️| +| GenSS From Media and MediaInfo Generator |✔️| +| And Many More.. |✔️| + +## [6] Variables + +### Required Variables +* `BOT_TOKEN`: Create a bot using [@BotFather](https://t.me/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) +* `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) +* `LOG_CHANNEL` : A channel to log the activities of bot. Make sure bot is an admin in the channel. + +### Optional Variables +* `USER_SESSION` : Session string for Userbot. +* `DATABASE_NAME`: Name of the database in MongoDB +* `COMMAND_HANDLER`: List of handler bot command splitted by space. Ex: `. !` > so bot will respond with `.cmd` or `!cmd` +* `SUDO`: User ID that have access to bot, split by space +* `OPENAI_API`: Get it from OpenAI Web +* `CURRENCY_API`: Get API Key from https://app.exchangerate-api.com/sign-up + +## [7] Tutorial Deploy (Recommended using Docker/Docker Compose) + +#### Build And Run Using Legacy Method +- Make sure minimum python version is 3.8 to prevent some errors. Check it with this command: +``` +python3 --version +``` +- Install all dependency that needed bot to run. *(need root access, you can skip this if your server didn't have root access but some plugins will not work)* +``` +apt update -y & apt install libjpeg-dev zlib1g-dev libwebp-dev python3-pip python3-lxml git wget curl ffmpeg locales tzdata neofetch mediainfo speedtest-cli -y +``` +- Install requirements.txt, if using python 3.11, you need use venv when install pip package.
+*Python < 3.10* +``` +pip3 install -r requirements.txt +``` +*Python 3.11* +``` +Install venv from your terminal and activate it +pip3 install -r requirements.txt +``` +- Setting your config.env or via environment and dont forget fill all required value. +- Run Bot +``` +bash start.sh +``` + +#### Build And Run Using Docker + +- 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 +``` + +#### Build And Run The Docker Image Using docker-compose + +- Install docker-compose +``` +sudo apt install docker-compose +``` +- Build and run Docker image or to view current running image: +``` +sudo docker-compose up +``` +- After editing files with nano for example (nano start.sh): +``` +sudo docker-compose up --build +``` +- To stop the running image: +``` +sudo docker ps +``` +``` +sudo docker-compose stop +``` + +---- + + +## [8] 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 WilliamButcher Code. + - Thanks To [Team Yukki](https://github.com/TeamYukki) For AFK Bot Code. + - Thanks To [Wrench](https://github.com/EverythingSuckz) For Some Code. + - Thanks To [AmanoTeam](https://github.com/AmanoTeam) For MultiLanguage Template. + - And All People Who Help Me In My Life... + If your code used in this repo and want to give credit please open issue.. + +## [9] 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) +WARNING: Selling The Codes To Other People For Money Is *Strictly Prohibited*. God always sees you. + + +[license-shield]: https://img.shields.io/github/license/yasirarism/MissKatyPyro?labelColor=D8D8D8&color=04B4AE +[repository-size-shield]: https://img.shields.io/github/repo-size/yasirarism/MissKatyPyro?labelColor=D8D8D8&color=BE81F7 +[issue-closed-shield]: https://img.shields.io/github/issues-closed/yasirarism/MissKatyPyro?labelColor=D8D8D8&color=FE9A2E + + +[readme-ko-shield]: https://img.shields.io/badge/-readme%20in%20Indonesian-2E2E2E?style=for-the-badge +[view-demo-shield]: https://img.shields.io/badge/-%F0%9F%98%8E%20view%20demo-F3F781?style=for-the-badge +[view-demo-url]: https://t.me/MissKatyPyro +[report-bug-shield]: https://img.shields.io/badge/-%F0%9F%90%9E%20report%20bug-F5A9A9?style=for-the-badge +[report-bug-url]: https://github.com/yasirarism/MissKatyPyro/issues +[request-feature-shield]: https://img.shields.io/badge/-%E2%9C%A8%20request%20feature-A9D0F5?style=for-the-badge +[request-feature-url]: https://github.com/yasirarism/MissKatyPyro/issues + + +[readme-ko-url]: README.id.md +[kofi-url]: https://ko-fi.com/yasirarism +[paypal-url]: https://paypal.me/yasirarism +[qris-url]: https://telegra.ph/file/9427d61d6968b8ee4fb2f.jpg +[sociabuzz-url]: https://sociabuzz.com/yasirarism/tribe +[saweria-url]: https://saweria.co/yasirarism +[trakteer-url]: https://trakteer.id/yasir-aris-sp7cn \ No newline at end of file diff --git a/assets/Calistoga-Regular.ttf b/assets/Calistoga-Regular.ttf new file mode 100644 index 00000000..6ba77fb3 Binary files /dev/null and b/assets/Calistoga-Regular.ttf differ diff --git a/assets/DejaVuSans-Bold.ttf b/assets/DejaVuSans-Bold.ttf new file mode 100644 index 00000000..0f4d5e9d Binary files /dev/null and b/assets/DejaVuSans-Bold.ttf differ diff --git a/assets/DejaVuSans.ttf b/assets/DejaVuSans.ttf new file mode 100644 index 00000000..e5f7eecc Binary files /dev/null and b/assets/DejaVuSans.ttf differ diff --git a/assets/IronFont.otf b/assets/IronFont.otf new file mode 100644 index 00000000..563fb0ea Binary files /dev/null and b/assets/IronFont.otf differ diff --git a/assets/MutantAcademyStyle.ttf b/assets/MutantAcademyStyle.ttf new file mode 100644 index 00000000..00c56493 Binary files /dev/null and b/assets/MutantAcademyStyle.ttf differ diff --git a/assets/assfont.ttf b/assets/assfont.ttf new file mode 100644 index 00000000..1070aacd Binary files /dev/null and b/assets/assfont.ttf differ diff --git a/assets/bg.png b/assets/bg.png new file mode 100644 index 00000000..39bec0ef Binary files /dev/null and b/assets/bg.png differ diff --git a/assets/kertas.jpg b/assets/kertas.jpg new file mode 100644 index 00000000..7b884031 Binary files /dev/null and b/assets/kertas.jpg differ diff --git a/assets/profilepic.png b/assets/profilepic.png new file mode 100644 index 00000000..5f6674f6 Binary files /dev/null and b/assets/profilepic.png differ diff --git a/assets/statsbg.jpg b/assets/statsbg.jpg new file mode 100644 index 00000000..eff8e935 Binary files /dev/null and b/assets/statsbg.jpg differ diff --git a/assets/thumb.jpg b/assets/thumb.jpg new file mode 100644 index 00000000..9ec67071 Binary files /dev/null and b/assets/thumb.jpg differ diff --git a/config.env.sample b/config.env.sample new file mode 100644 index 00000000..3f1ed593 --- /dev/null +++ b/config.env.sample @@ -0,0 +1,15 @@ +# Required Vars +API_HASH= +API_ID= +BOT_TOKEN= +DATABASE_URI=mongodb+srv:// +LOG_CHANNEL= + +# Optional Vars +SUDO=617426792 +DATABASE_NAME=MissKatyDB +SUPPORT_CHAT=YasirPediaChannel +COMMAND_HANDLER= +USER_SESSION= +OPENAI_API= +CURRENCY_API= \ No newline at end of file diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 00000000..0da3654d --- /dev/null +++ b/database/__init__.py @@ -0,0 +1,12 @@ +""" + * @author yasir + * @date 2022-09-06 10:12:09 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +from async_pymongo import AsyncClient + +from misskaty.vars import DATABASE_NAME, DATABASE_URI + +mongo = AsyncClient(DATABASE_URI) +dbname = mongo[DATABASE_NAME] diff --git a/database/afk_db.py b/database/afk_db.py new file mode 100644 index 00000000..a60217cd --- /dev/null +++ b/database/afk_db.py @@ -0,0 +1,63 @@ +# +# Copyright (C) 2021-2022 by TeamYukki@Github, < https://github.com/TeamYukki >. +# +# This file is part of < https://github.com/TeamYukki/YukkiAFKBot > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/TeamYukki/YukkiAFKBot/blob/master/LICENSE > +# +# All rights reserved. +# + +from database import dbname + +usersdb = dbname["users"] +cleandb = dbname["cleanmode"] +cleanmode = {} + + +async def is_cleanmode_on(chat_id: int) -> bool: + mode = cleanmode.get(chat_id) + if not mode: + user = await cleandb.find_one({"chat_id": chat_id}) + if not user: + cleanmode[chat_id] = True + return True + cleanmode[chat_id] = False + return False + return mode + + +async def cleanmode_on(chat_id: int): + cleanmode[chat_id] = True + user = await cleandb.find_one({"chat_id": chat_id}) + if user: + return await cleandb.delete_one({"chat_id": chat_id}) + + +async def cleanmode_off(chat_id: int): + cleanmode[chat_id] = False + user = await cleandb.find_one({"chat_id": chat_id}) + if not user: + return await cleandb.insert_one({"chat_id": chat_id}) + + +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}}) + return list(await users.to_list(length=1000000000)) if users else [] diff --git a/database/blacklist_db.py b/database/blacklist_db.py new file mode 100644 index 00000000..d05ec9cc --- /dev/null +++ b/database/blacklist_db.py @@ -0,0 +1,37 @@ +from typing import List + +from database import dbname + +blacklist_filtersdb = dbname["blacklistFilters"] + + +async def get_blacklisted_words(chat_id: int) -> List[str]: + _filters = await blacklist_filtersdb.find_one({"chat_id": chat_id}) + if not _filters: + return [] + return _filters["filters"] + + +async def save_blacklist_filter(chat_id: int, word: str): + word = word.lower().strip() + _filters = await get_blacklisted_words(chat_id) + _filters.append(word) + await blacklist_filtersdb.update_one( + {"chat_id": chat_id}, + {"$set": {"filters": _filters}}, + upsert=True, + ) + + +async def delete_blacklist_filter(chat_id: int, word: str) -> bool: + filtersd = await get_blacklisted_words(chat_id) + word = word.lower().strip() + if word in filtersd: + filtersd.remove(word) + await blacklist_filtersdb.update_one( + {"chat_id": chat_id}, + {"$set": {"filters": filtersd}}, + upsert=True, + ) + return True + return False diff --git a/database/filters_db.py b/database/filters_db.py new file mode 100644 index 00000000..66ac2004 --- /dev/null +++ b/database/filters_db.py @@ -0,0 +1,45 @@ +from typing import Dict, List, Union + +from database import dbname + +filtersdb = dbname["filters"] + + +async def _get_filters(chat_id: int) -> Dict[str, int]: + _filters = await filtersdb.find_one({"chat_id": chat_id}) + return _filters["filters"] if _filters else {} + + +async def delete_filter(chat_id: int, name: str) -> bool: + filtersd = await _get_filters(chat_id) + name = name.lower().strip() + if name in filtersd: + del filtersd[name] + await filtersdb.update_one( + {"chat_id": chat_id}, + {"$set": {"filters": filtersd}}, + upsert=True, + ) + return True + return False + + +async def get_filter(chat_id: int, name: str) -> Union[bool, dict]: + name = name.lower().strip() + _filters = await _get_filters(chat_id) + return _filters[name] if name in _filters else False + + +async def get_filters_names(chat_id: int) -> List[str]: + return list(await _get_filters(chat_id)) + + +async def save_filter(chat_id: int, name: str, _filter: dict): + name = name.lower().strip() + _filters = await _get_filters(chat_id) + _filters[name] = _filter + await filtersdb.update_one( + {"chat_id": chat_id}, + {"$set": {"filters": _filters}}, + upsert=True, + ) diff --git a/database/gban_db.py b/database/gban_db.py new file mode 100644 index 00000000..6f6d032a --- /dev/null +++ b/database/gban_db.py @@ -0,0 +1,22 @@ +from database import dbname + +gbansdb = dbname["gban"] + + +async def is_gbanned_user(user_id: int) -> bool: + user = await gbansdb.find_one({"user_id": user_id}) + return bool(user) + + +async def add_gban_user(user_id: int): + is_gbanned = await is_gbanned_user(user_id) + if is_gbanned: + return + return await gbansdb.insert_one({"user_id": user_id}) + + +async def remove_gban_user(user_id: int): + is_gbanned = await is_gbanned_user(user_id) + if not is_gbanned: + return + return await gbansdb.delete_one({"user_id": user_id}) diff --git a/database/imdb_db.py b/database/imdb_db.py new file mode 100644 index 00000000..5d3c16e5 --- /dev/null +++ b/database/imdb_db.py @@ -0,0 +1,20 @@ +from database import dbname + +imbd_db = dbname["imdb"] + + +async def is_imdbset(user_id: int) -> bool: + user = await imbd_db.find_one({"user_id": user_id}) + return (True, user["lang"]) if user else (False, {}) + + +async def add_imdbset(user_id: int, lang): + await imbd_db.update_one( + {"user_id": user_id}, {"$set": {"lang": lang}}, upsert=True + ) + + +async def remove_imdbset(user_id: int): + user = await imbd_db.find_one({"user_id": user_id}) + if user: + return await imbd_db.delete_one({"user_id": user_id}) diff --git a/database/karma_db.py b/database/karma_db.py new file mode 100644 index 00000000..0ce0b9cb --- /dev/null +++ b/database/karma_db.py @@ -0,0 +1,67 @@ +from typing import Dict, Union + +from database import dbname +from misskaty.helper.functions import int_to_alpha + +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 not 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/locale_db.py b/database/locale_db.py new file mode 100644 index 00000000..d6823b54 --- /dev/null +++ b/database/locale_db.py @@ -0,0 +1,22 @@ +from typing import Iterable + +from pyrogram.enums import ChatType + +from database import dbname + +localesdb = dbname["locale"] # DB for localization + +group_types: Iterable[ChatType] = (ChatType.GROUP, ChatType.SUPERGROUP) + + +async def set_db_lang(chat_id: int, chat_type: str, lang_code: str): + await localesdb.update_one( + {"chat_id": chat_id}, + {"$set": {"lang": lang_code, "chat_type": chat_type.value}}, + upsert=True, + ) + + +async def get_db_lang(chat_id: int) -> str: + ul = await localesdb.find_one({"chat_id": chat_id}) + return ul["lang"] if ul else {} diff --git a/database/notes_db.py b/database/notes_db.py new file mode 100644 index 00000000..f4a3d1a4 --- /dev/null +++ b/database/notes_db.py @@ -0,0 +1,44 @@ +from typing import Dict, List, Union + +from database import dbname + +notesdb = dbname["notes"] + + +async def _get_notes(chat_id: int) -> Dict[str, int]: + _notes = await notesdb.find_one({"chat_id": chat_id}) + return _notes["notes"] if _notes else {} + + +async def delete_note(chat_id: int, name: str) -> bool: + notesd = await _get_notes(chat_id) + name = name.lower().strip() + if name in notesd: + del notesd[name] + await notesdb.update_one( + {"chat_id": chat_id}, + {"$set": {"notes": notesd}}, + upsert=True, + ) + return True + return False + + +async def get_note(chat_id: int, name: str) -> Union[bool, dict]: + name = name.lower().strip() + _notes = await _get_notes(chat_id) + return _notes[name] if name in _notes else False + + +async def get_note_names(chat_id: int) -> List[str]: + return list(await _get_notes(chat_id)) + + +async def save_note(chat_id: int, name: str, note: dict): + name = name.lower().strip() + _notes = await _get_notes(chat_id) + _notes[name] = note + + await notesdb.update_one( + {"chat_id": chat_id}, {"$set": {"notes": _notes}}, upsert=True + ) diff --git a/database/sangmata_db.py b/database/sangmata_db.py new file mode 100644 index 00000000..813355e0 --- /dev/null +++ b/database/sangmata_db.py @@ -0,0 +1,42 @@ +from database import dbname + +matadb = dbname["sangmata"] + + +# Get Data User +async def cek_userdata(user_id: int) -> bool: + user = await matadb.find_one({"user_id": user_id}) + return bool(user) + + +async def get_userdata(user_id: int) -> bool: + user = await matadb.find_one({"user_id": user_id}) + return user["username"], user["first_name"], user["last_name"] + + +async def add_userdata(user_id: int, username, first_name, last_name): + await matadb.update_one( + {"user_id": user_id}, + { + "$set": { + "username": username, + "first_name": first_name, + "last_name": last_name, + } + }, + upsert=True, + ) + + +# Enable Mata MissKaty in Selected Chat +async def is_sangmata_on(chat_id: int) -> bool: + chat = await matadb.find_one({"chat_id_toggle": chat_id}) + return bool(chat) + + +async def sangmata_on(chat_id: int) -> bool: + await matadb.insert_one({"chat_id_toggle": chat_id}) + + +async def sangmata_off(chat_id: int): + await matadb.delete_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..a0ac45e7 --- /dev/null +++ b/database/users_chats_db.py @@ -0,0 +1,112 @@ +from async_pymongo import AsyncClient + +from misskaty.vars import DATABASE_NAME, DATABASE_URI + + +class UsersData: + def __init__(self, uri, database_name): + self._client = AsyncClient(uri) + self.db = self._client[database_name] + self.col = self.db["users"] + self.grp = self.db["groups"] + + @staticmethod + def new_user(id, name): + return dict( + id=id, + name=name, + ban_status=dict( + is_banned=False, + ban_reason="", + ), + ) + + @staticmethod + def new_group(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 is_chat_exist(self, id): + user = await self.grp.find_one({"id": int(id)}) + return bool(user) + + 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 = UsersData(DATABASE_URI, DATABASE_NAME) diff --git a/database/warn_db.py b/database/warn_db.py new file mode 100644 index 00000000..5ca68da8 --- /dev/null +++ b/database/warn_db.py @@ -0,0 +1,51 @@ +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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..15aeac97 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.3" + +services: + app: + build: + context: . + dockerfile: Dockerfile + command: bash start.sh + restart: on-failure diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 00000000..7b13190d --- /dev/null +++ b/heroku.yml @@ -0,0 +1,3 @@ +build: + docker: + worker: Dockerfile diff --git a/locales/__init__.py b/locales/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/locales/en-US/admin.json b/locales/en-US/admin.json new file mode 100644 index 00000000..91e65073 --- /dev/null +++ b/locales/en-US/admin.json @@ -0,0 +1,68 @@ +{ + "no_admin_error": "You must be an administrator to use this command.", + "no_permission_error": "I'm sorry but you don't have the required permissions to run this command. Missing permissions: {permissions}", + "private_not_allowed": "This command can't be used in a private chat. If you need any help, please use the /help command.", + "purge_no_reply": "Reply to a message to purge from.", + "delete_no_reply": "Reply To A Message To Delete It", + "pin_no_reply": "Reply to a message to pin/unpin it.", + "report_no_reply": "Reply To A Message To Report That User.", + "no_delete_perm": "Please give me delete message permission.", + "purge_success": "Successfully deleted {del_total} messages..", + "user_not_found": "I can't find that user.", + "invalid_id_uname": "⚠️ Invalid userid/username", + "kick_self_err": "I can't kick myself, i can leave if you want.", + "ban_self_err": "I can't ban myself, i can leave if you want.", + "report_self_err": "Why are you reporting yourself ?", + "demote_self_err": "I can't demote myself.", + "warn_self_err": "I can't warn myself.", + "mute_self_err": "I can't mute myself.", + "kick_sudo_err": "Wow, you wanna kick my owner?", + "ban_sudo_err": "Wow, you wanna try ban my owner?", + "demote_sudo_err": "Wow, you wanna try demote my owner?", + "warn_sudo_err": "Wow, you wanna try give warning to my owner?", + "mute_sudo_err": "Wow, you wanna try give mute to my owner?", + "kick_admin_err": "Lol, it's crazy if i can kick an admin.", + "ban_admin_err": "Lol, it's crazy if i can banned an admin.", + "mute_admin_err": "Lol, it's crazy if i can mute an admin.", + "warn_admin_err": "Lol, it's crazy if i can warn an admin.", + "kick_msg": "**Kicked User:** {mention} [`{id}`]\n**Kicked By:** {kicker}\n**Reason:** {reasonmsg}", + "ban_msg": "**Banned User:** {mention} [`{id}`]\n**Banned By:** {banner}\n", + "unban_msg": "__Banned removed by {mention}__", + "no_ban_permission": "Please give me ban permission to ban user in this group.", + "no_more_99": "You can't use more than 99", + "banned_time": "**Banned For:** {val}\n", + "muted_time": "**Muted For:** {val}\n", + "banned_reason": "**Reason:** {reas}", + "unban_channel_err": "You cannot unban a channel", + "give_unban_user": "Provide a username or reply to a user's message to unban.", + "unban_success": "Successfully unbanned {umention}!", + "give_idban_with_msg_link": "Provide a userid/username along with message link and reason to list-ban", + "give_idunban_with_msg_link": "Provide a userid/username along with message link and reason to list-unban", + "give_reason_list_ban": "You must provide a reason to list-ban", + "Invalid_tg_link": "Invalid message link provided", + "multiple_ban_progress": "`Banning User from multiple groups. This may take some time`", + "multiple_unban_progress": "`Unbanning User from multiple groups. This may take some time`", + "failed_get_uname": "Could not get group usernames", + "listban_msg": "**List-Banned User:** {mention}\n**Banned User ID:** `{uid}`\n**Admin:** {frus}\n**Affected chats:** `{ct}`\n**Reason:** {reas}", + "listunban_msg": "**List-Unbanned User:** {mention}\n**Unbanned User ID:** `{uid}`\n**Admin:** {frus}\n**Affected chats:** `{ct}`\n**Reason:** {reas}", + "promote_self_err": "I can't promote myself.", + "no_promote_perm": "Sadly, I don't permission to promote users.", + "full_promote": "Fully Promoted {umention}!", + "normal_promote": "Promoted {umention}!", + "pin_success": "**Pinned [this]({link}) message.**", + "unpin_success": "**UnPinned [this]({link}) message.**", + "pin_no_perm": "Please give me pin permission to use this command!.", + "report_msg": "Reported {user_mention} to admins!", + "reported_is_admin": "Do you know that the user you are replying is an admin ?", + "user_no_warn": "User {mention} has no warnings.", + "ch_warn_msg": "User {mention} has {warns}/3 warnings.", + "warn_msg": "**Warned User:** {mention}\n**Warned By:** {warner}\n**Reason:** {reas}\n**Warns:** {twarn}/3", + "rmwarn_msg": "Removed warnings of {mention}.", + "unwarn_msg": "Removed warnings by {mention}.", + "rmmute_msg": "__Mute removed by {mention}__", + "unmute_msg": "Unmuted! {umention}", + "reply_to_rm_warn": "Reply to a message to remove a user's warnings.", + "exceed_warn_msg": "Number of warns of {mention} exceeded, BANNED!", + "mute_msg": "**Muted User:** {mention}\n**Muted By:** {muter}\n", + "rm_warn_btn": "🚨 Remove Warn 🚨" +} diff --git a/locales/en-US/afk.json b/locales/en-US/afk.json new file mode 100644 index 00000000..bdd7f0f7 --- /dev/null +++ b/locales/en-US/afk.json @@ -0,0 +1,13 @@ +{ + "no_channel": "This feature not supported for channel.", + "on_afk_msg_no_r": "**{usr}** [{id}] is back online and was away for {tm}\n\n", + "on_afk_msg_with_r": "**{usr}** [{id}] is back online and was away for {tm}\n\n**Reason:** `{reas}`\n\n", + "is_afk_msg_no_r": "**{usr}** [{id}] is AFK since {tm} ago.\n\n", + "is_afk_msg_with_r": "**{usr}** [{id}] is AFK since {tm} ago.\n\n**Reason:** {reas}\n\n", + "is_online": "**{usr}** [{id}] is back online", + "now_afk": "{usr} [{id}] is now AFK!.", + "afkdel_help": "**Usage:**\n/{cmd} [ENABLE|DISABLE] to enable or disable auto delete message.", + "afkdel_disable": "Disabled auto delete AFK message.", + "afkdel_enable": "Enabled auto delete AFK message in this chat.", + "is_afk": "{usr} [{id}] is AFK!." +} \ No newline at end of file diff --git a/locales/en-US/chatbot_ai.json b/locales/en-US/chatbot_ai.json new file mode 100644 index 00000000..7ed24ad5 --- /dev/null +++ b/locales/en-US/chatbot_ai.json @@ -0,0 +1,6 @@ +{ + "no_question": "Please use command /{cmd} [question] to ask your question with AI.", + "find_answers_str": "Wait a moment looking for your answer..", + "dont_spam": "Don't spam please, please wait {tm} second or i will ban you from this bot.", + "answers_too_long": "Question for your answer has exceeded TG text limit, check this link to view.\n\n{answerlink}" +} \ No newline at end of file diff --git a/locales/en-US/dev.json b/locales/en-US/dev.json new file mode 100644 index 00000000..111ef725 --- /dev/null +++ b/locales/en-US/dev.json @@ -0,0 +1,11 @@ +{ + "already_up": "Its already up-to date!", + "up_and_rest": "Updated with default branch, restarting now.", + "cl_btn": "❌ Close", + "no_eval": "__No evaluate message!__", + "run_eval": "Processing eval pyrogram..", + "run_exec": "Processing exec pyrogram..", + "no_cmd": "No command to execute was given.", + "success": "Success", + "no_reply": "No Reply" +} \ No newline at end of file diff --git a/locales/en-US/fun.json b/locales/en-US/fun.json new file mode 100644 index 00000000..c4c886a5 --- /dev/null +++ b/locales/en-US/fun.json @@ -0,0 +1,3 @@ +{ + "result": "🎲 The dice stopped at the number: {number}" +} \ No newline at end of file diff --git a/locales/en-US/general.json b/locales/en-US/general.json new file mode 100644 index 00000000..23599eed --- /dev/null +++ b/locales/en-US/general.json @@ -0,0 +1,6 @@ +{ + "back_btn": "« Go back", + "no_results": "No Results.", + "unknown_id": "Sorry I can't recognize this user. Maybe I've never met him.", + "exp_task": "😶‍🌫️ Timeout. Task has been cancelled!" + } \ No newline at end of file diff --git a/locales/en-US/genss.json b/locales/en-US/genss.json new file mode 100644 index 00000000..964ea1c1 --- /dev/null +++ b/locales/en-US/genss.json @@ -0,0 +1,13 @@ +{ + "wait_msg": "Give me some time to process your request!! 😴", + "wait_dl": "Processing, please wait..", + "dl_progress": "Trying to download, please wait..", + "up_progress": "Trying to upload...", + "success_dl_msg": "File has been downloaded to {path}.", + "fail_open": "😟 Sorry! I cannot open the file.", + "limit_dl": "Sorry, download limited to 2GB to reduce flood. You can convert your files to link.", + "err_ssgen": "Failed screenshoot generation.\n\n{exc}", + "up_msg": "☑️ Generation screenshot successfully.\n\n{namma} ({id})\n#️⃣ #ssgen #id{id}\n\nSS Generate by @{bot_uname}", + "no_reply": "Reply to a Telegram video or document or use direct link after command to generate screenshoot from media!", + "choose_no_ss": "Now choose how many result for screenshot? 🥳.\n\nTotal duration: `{td}` (`{dur} second`)" +} \ No newline at end of file diff --git a/locales/en-US/grup_tools.json b/locales/en-US/grup_tools.json new file mode 100644 index 00000000..5a58ec0d --- /dev/null +++ b/locales/en-US/grup_tools.json @@ -0,0 +1,13 @@ +{ + "sudo_join_msg": "Wow, my cool owner just joined the group!", + "log_bot_added": "#New Group\nGroup = {ttl}({cid})\nNumber of Members = {tot}\nAdded by - {r_j}", + "support_btn": "Support", + "help_btn": "ℹ️ Help", + "update_btn": "📢 Updates", + "chat_not_allowed": "CHAT NOT ALLOWED 🐞\n\nMy owner has banned me from working here! You can contact the owner of this bot..", + "welcome_thanks": "Thank you for adding me to {ttl} ❣️\n\nIf you have any problems or suggestions, you can contact me.", + "capt_welc": "Hi {umention} [{uid}], Welcome to the {ttl} group.", + "combot_msg": "#CAS Federation Ban\nUser {umention} [{uid}] has been detected as a spambot and has been banned. Powered by Combot AntiSpam.", + "spamwatch_msg": "#SpamWatch Federation Ban\nUser {umention} [{uid}] has been banned for {reas}.\n" , + "welcpic_msg": "Welcome {userr} [{id}]" +} \ No newline at end of file diff --git a/locales/en-US/help_menu.json b/locales/en-US/help_menu.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/locales/en-US/help_menu.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/en-US/lang_setting.json b/locales/en-US/lang_setting.json new file mode 100644 index 00000000..06585c9c --- /dev/null +++ b/locales/en-US/lang_setting.json @@ -0,0 +1,5 @@ +{ + "language_changed_successfully": "The language has been changed successfully.", + "language_changer_chat": "Here you can change the language used for the bot throughout the chat.\nIf your language is not listed here and you would like to contribute, you can open issue in my github repo.", + "language_changer_private": "Here you can change the language used for the bot in this private chat.\n\nIf you want to change the language of your group, please run the command /setchatlang on it.\nIf your language is not listed here and you would like to contribute, you can open issue in my github repo." + } \ No newline at end of file diff --git a/locales/en-US/main.json b/locales/en-US/main.json new file mode 100644 index 00000000..1a34be34 --- /dev/null +++ b/locales/en-US/main.json @@ -0,0 +1,4 @@ +{ + "language_name": "English", + "language_flag": "🇬🇧" + } \ No newline at end of file diff --git a/locales/en-US/media_extractor.json b/locales/en-US/media_extractor.json new file mode 100644 index 00000000..29c21fb1 --- /dev/null +++ b/locales/en-US/media_extractor.json @@ -0,0 +1,15 @@ +{ + "sub_extr_help": "Please use command /{cmd} [link] to check subtitles or audio in video file.", + "conv_sub_help": "Use command /{cmd} by reply to .ass or .vtt file, to convert subtitle from .ass or .vtt to srt.", + "progress_str": "⏳ Processing your request..", + "convert_str": "⏳ Converting...", + "unauth_cb": "⚠️ Access Denied!", + "cancel_btn": "❌ Cancel", + "invalid_cb": "⚠️ DONT DELETE YOUR MESSAGE!", + "up_str": "Uploading files..", + "press_btn_msg": "Press the button below to extract subtitles/audio. Only support direct link at this time.\nProcessed in {timelog}", + "fail_extr_media": "Failed extract media, make sure your link is not protected by WAF or maybe inaccessible for bot.", + "fail_extr_sub": "Failed extract sub, Maybe unsupported format..\n\nLink: {link}\nERR: {e}", + "capt_extr_sub": "Filename: {nf}\n\nExtracted by @{bot} in {timelog}", + "capt_conv_sub": "{nf}.srt\n\nConverted by @{bot}" +} diff --git a/locales/en-US/mediainfo.json b/locales/en-US/mediainfo.json new file mode 100644 index 00000000..154ef509 --- /dev/null +++ b/locales/en-US/mediainfo.json @@ -0,0 +1,11 @@ +{ + "processing_text": "`Processing, total time is based on the size of your files...`", + "wait_msg": "`Please wait a moment...`", + "err_link": "It looks like the link you sent is invalid, make sure it's a direct link and can be downloaded.", + "media_invalid": "Please reply to valid media.", + "dl_limit_exceeded": "Sorry, download limited to 2GB to reduce flood. You can convert your files to link.", + "dl_args_text": "Trying to download..", + "mediainfo_help": "Use the command /{cmd} [link], or reply to telegram media with /{cmd}.", + "capt_media": "ℹ️ Your mediainfo results..\n\n**Request By:** {ment}", + "viweb": "💬 Open on Web" +} \ No newline at end of file diff --git a/locales/en-US/nightmodev2.json b/locales/en-US/nightmodev2.json new file mode 100644 index 00000000..439d417b --- /dev/null +++ b/locales/en-US/nightmodev2.json @@ -0,0 +1,17 @@ +{ + "nmd_disabled": "Nightmode disabled.", + "nmd_not_enabled": "Nightmode isn't enabled in this chat.", + "invalid_time_format": "Invalid time format. Use HH:MM format.", + "invalid_lockdur": "Invalid time duration. Use proper format.\nExample: 6h (for 6 hours), 10m for 10 minutes.", + "schedule_already_on": "Already a schedule is running in this chat. Disable it using `-d` flag.", + "nmd_enable_success": "Successfully enabled nightmode in this chat.\nGroup will be locked at {st} and will be opened after {lockdur} everyday.", + "nmd_cb": "🔖 Hai, Aku {bname} dibuat menggunakan Framework Pyrogram v{ver} dan Python v{pyver}.\n\nMau buat bot seperti ini? Yuuk belajar di @botindonesia\nOwner: @YasirArisM", + "nmd_off_not_admin": "#NIGHTMODE_FAIL\nFailed to turn off nightmode at `{chat_id}`, since {bname} is not an admin in chat `{chat_id}`", + "nmd_off_not_present": "#NIGHTMODE_FAIL\nFailed to turn off nightmode at `{chat_id}`, since {bname} is not present in chat `{chat_id}`. Removed group from list.", + "nmd_off_err": "#NIGHTMODE_FAIL\nFailed to turn off nightmode at `{chat_id}`\nERROR: `{e}`", + "nmd_off_success": "#NIGHTMODE_HANDLER\n📆 {dt}\n\n☀️ Group is Opening.\nWill be closed at {close_at}", + "nmd_on_not_admin": "#NIGHTMODE_FAIL\nFailed to enable nightmode at `{chat_id}`, since {bname} is not an admin in chat `{chat_id}`", + "nmd_on_not_present": "#NIGHTMODE_FAIL\nFailed to enable nightmode at `{chat_id}`, since {bname} is not present in chat `{chat_id}`. Removed group from list.", + "nmd_on_err": "#NIGHTMODE_FAIL\nFailed to enable nightmode at `{chat_id}`\nERROR: `{e}`", + "nmd_on_success": "#NIGHTMODE_HANDLER\n📆 {dt}\n\n🌗 Group is closing.\nWill be opened at {open_at}" +} \ No newline at end of file diff --git a/locales/en-US/ocr.json b/locales/en-US/ocr.json new file mode 100644 index 00000000..d5ce6786 --- /dev/null +++ b/locales/en-US/ocr.json @@ -0,0 +1,6 @@ +{ + "no_photo": "Reply photo with /{cmd} command to scan text from images.", + "read_ocr": "Scanning your images..", + "result_ocr": "Hasil OCR:\n{result}", + "ocr_helper": "/ocr [reply to photo] - Read Text From Image" +} \ No newline at end of file diff --git a/locales/en-US/sangmata.json b/locales/en-US/sangmata.json new file mode 100644 index 00000000..9d3bf82b --- /dev/null +++ b/locales/en-US/sangmata.json @@ -0,0 +1,13 @@ +{ + "no_uname": "No Username", + "no_last_name": "No Last Name", + "uname_change_msg": "✨ Changed username from {bef} ➡️ {aft}.\n", + "lastname_change_msg": "✨ Changed last name from {bef} ➡️ {aft}.\n", + "firstname_change_msg": "✨ Changed first name from {bef} ➡️ {aft}.\n", + "set_sangmata_help": "Use /{cmd} on, to enable sangmata. If you want disable, you can use off parameter.", + "sangmata_already_on": "SangMata already enabled in your groups.", + "sangmata_enabled": "Sangmata enabled in your groups.", + "sangmata_already_off": "SangMata already disabled in your groups.", + "sangmata_disabled": "Sangmata disabled in your groups.", + "wrong_param": "Unknown parameter, use only on/off parameter." +} \ No newline at end of file diff --git a/locales/en-US/start_help.json b/locales/en-US/start_help.json new file mode 100644 index 00000000..adf6384d --- /dev/null +++ b/locales/en-US/start_help.json @@ -0,0 +1,11 @@ +{ + "newgroup_log": "#NewGroup\nGroup = {jdl}({id})\nMembers Count = {c}", + "newuser_log": "#NewUser\nID - {id}\nName - {nm}", + "help_name": "Here is the help for **{mod}**:\n", + "help_txt": "Hello {kamuh}, My name is {bot}.\nI'm a pyrogram bot that developed by kind owner with some useful features.\nYou can look by clicking a button below.\n\nGeneral command are:\n - /start: Start the bot\n - /help: Give this message\n - /setlang: Change bot language [BETA]", + "click_me": "Click Me", + "back_btn": "Back", + "click_btn": "Click on the below button to get help about {nm}", + "pm_detail": "PM Me For More Details.", + "start_msg": "Hi {kamuh}, PM me to know about all my features. You can change bot language in bot using /setlang command but it's still in beta stage." +} \ No newline at end of file diff --git a/locales/en-US/stickers.json b/locales/en-US/stickers.json new file mode 100644 index 00000000..35cc1175 --- /dev/null +++ b/locales/en-US/stickers.json @@ -0,0 +1,19 @@ +{ + "no_anim_stick": "Animated sticker is not supported!", + "not_sticker": "This is not a sticker!", + "unkang_msg": "Trying to remove from pack..", + "unkang_success": "Sticker has been removed from your pack", + "unkang_error": "Failed remove sticker from your pack.\n\nERR: {e}", + "unkang_help": "Please reply sticker that created by {c} to remove sticker from your pack.", + "anon_warn": "You are anon admin, kang stickers in my pm.", + "kang_msg": "Trying to steal your sticker...", + "stick_no_name": "The sticker has no name.", + "kang_help": "Want me to guess the sticker? Please tag a sticker.", + "exist_pack": "Using existing sticker pack...", + "new_packs": "Creating a new sticker pack...", + "please_start_msg": "It looks like you've never interacted with me in private chat, you need to do that first..", + "click_me": "Click Me", + "pack_full": "Your Sticker Pack is full if your pack is not in v1 Type /kang 1, if it is not in v2 Type /kang 2 and so on.", + "viewpack": "👀 View Your Pack", + "kang_success": "Sticker successfully stolen!\nEmoji: {emot}" +} \ No newline at end of file diff --git a/locales/en-US/web_scraper.json b/locales/en-US/web_scraper.json new file mode 100644 index 00000000..a02d74b4 --- /dev/null +++ b/locales/en-US/web_scraper.json @@ -0,0 +1,20 @@ +{ + "no_result": "Sorry, i couldn't find any results!", + "no_result_w_query": "Sorry, i could not find query: {kueri}", + "get_data": "⏳ Please wait, getting data from web..", + "cl_btn": "❌ Close", + "back_btn": "↩️ Back", + "dl_text": "⬇️ Download", + "cat_text": "Category", + "quality": "Quality", + "ex_data": "👇 Extract Data ", + "unauth": "This button is not for you..", + "invalid_cb": "Invalid callback data, please send command again..", + "res_scrape": "Scrape result from {link}:\n\n{kl}", + "header_with_query": "#{web} Results For: {kueri}\n\n", + "header_no_query": "#{web} Latest:\n🌀 Use /{cmd} [title] to start search with title.\n\n", + "invalid_cmd_scrape": "Use command /{cmd} [link] to scrape download link.", + "err_getweb": "ERROR: Failed getting data from web because {err}.", + "err_getapi": "ERROR: Failed getting data from API", + "unsupport_dl_btn": "Some result will not appear in extract button because unsupported link." +} \ No newline at end of file diff --git a/locales/en-US/webss.json b/locales/en-US/webss.json new file mode 100644 index 00000000..bd676c20 --- /dev/null +++ b/locales/en-US/webss.json @@ -0,0 +1,6 @@ +{ + "no_url": "Give A Url To Fetch Screenshot.", + "wait_str": "Capturing screenshot...", + "str_credit": "🌞 Screenshot generated using Puppeteer", + "ss_failed_str": "Failed To Take Screenshot. {err}" +} \ No newline at end of file diff --git a/locales/en-US/ytdl_plugins.json b/locales/en-US/ytdl_plugins.json new file mode 100644 index 00000000..332e5474 --- /dev/null +++ b/locales/en-US/ytdl_plugins.json @@ -0,0 +1,14 @@ +{ + "no_channel": "This feature not supported for channel or anonymous user.", + "no_query": "Please input a query..!", + "no_res": "No result found for `{kweri}`", + "dl_btn": "Download", + "back": "Back", + "yts_msg": "Published {pub}\n\n❯ Duration: {dur}\n❯ Views: {vi}\n❯ Uploader: {cname}\n\n", + "invalid_link": "Please input a valid YT-DLP Supported URL", + "err_parse": "Failed parse URL, check logs..", + "wait": "Please wait..", + "unauth": "Not Your Task..", + "endlist": "That's the end of list", + "vip-btn": "Because some user abuse and my server cannot handle it, best video now only for bot owner." +} \ No newline at end of file diff --git a/locales/id-ID/admin.json b/locales/id-ID/admin.json new file mode 100644 index 00000000..c6d83a83 --- /dev/null +++ b/locales/id-ID/admin.json @@ -0,0 +1,68 @@ +{ + "no_admin_error": "Anda harus menjadi administrator untuk menggunakan perintah ini.", + "no_permission_error": "Maaf, tapi anda tidak memiliki izin yang diperlukan untuk menjalankan perintah ini. Izin yang hilang: {permissions}", + "private_not_allowed": "Perintah ini tidak dapat digunakan dalam obrolan pribadi, Jika anda membutuhkan bantuan, mohon gunakan perintah /help.", + "purge_no_reply": "Balas ke pesan yang ingin dihapus.", + "delete_no_reply": "Balas Pesan Untuk Menghapusnya", + "pin_no_reply": "Balas pesan untuk menyematkan/melepas pin.", + "report_no_reply": "Balas Pesan Untuk Melaporkan Pengguna Itu.", + "no_delete_perm": "Tolong beri saya izin untuk menghapus pesan.", + "purge_success": "Berhasil menghapus {del_total} pesan..", + "user_not_found": "Saya tidak dapat menemukan pengguna itu.", + "invalid_id_uname": "⚠️ ID pengguna/nama pengguna salah", + "kick_self_err": "Saya tidak dapat menendang diri sendiri, saya dapat pergi jika Anda mau.", + "ban_self_err": "Saya tidak dapat melarang diri saya sendiri, saya dapat pergi jika Anda mau.", + "report_self_err": "Mengapa Anda melaporkan diri sendiri?", + "demote_self_err": "Saya tidak dapat menurunkan keanggotaan diri saya sendiri.", + "warn_self_err": "Saya tidak dapat memperingatkan diri sendiri.", + "mute_self_err": "Saya tidak dapat membisukan diri sendiri.", + "kick_sudo_err": "Wow, Anda ingin menendang pemilik saya yang spesial?", + "ban_sudo_err": "Wow, Anda ingin mencoba membanned pemilik saya?", + "demote_sudo_err": "Wow, Anda ingin mencoba mendemote pemilik saya?", + "warn_sudo_err": "Wow, Anda ingin mencoba memberi peringatan kepada pemilik saya?", + "mute_sudo_err": "Wow, Anda ingin mencoba membisukan pemilik saya?", + "kick_admin_err": "Hah, sungguh gila jika saya bisa menendang seorang admin.", + "ban_admin_err": "Hah, sungguh gila jika saya bisa melarang seorang admin.", + "mute_admin_err": "Hah, sungguh gila jika saya bisa membisukan admin.", + "warn_admin_err": "Hah, sungguh gila jika saya bisa memperingatkan seorang admin.", + "kick_msg": "**Pengguna yang Ditendang:** {mention} [`{id}`]\n**Ditendang Oleh:** {kicker}\n**Alasan:** {reasonmsg}", + "ban_msg": "**Pengguna yang Dibanned:** {mention} [`{id}`]\n**Dibanned Oleh:** {banner}\n", + "unban_msg": "__Banned dihapus oleh {mention}__", + "no_ban_permission": "Tolong beri saya izin banned untuk membanned pengguna di grup ini.", + "no_more_99": "Anda tidak dapat menggunakan lebih dari 99", + "banned_time": "**Dilarang Untuk:** {val}\n", + "muted_time": "**Dimute Untuk:** {val}\n", + "banned_reason": "**Alasan:** {reas}", + "unban_channel_err": "Anda tidak dapat membatalkan pemblokiran saluran", + "give_unban_user": "Berikan nama pengguna atau balas pesan pengguna untuk membatalkan banned.", + "unban_success": "Berhasil membatalkan pemblokiran {umention}!", + "give_idban_with_msg_link": "Berikan userid/nama pengguna beserta tautan pesan dan alasan pelarangan daftar", + "give_idunban_with_msg_link": "Berikan userid/nama pengguna beserta tautan pesan dan alasan untuk membatalkan larangan", + "give_reason_list_ban": "Anda harus memberikan alasan untuk melarang daftar", + "Invalid_tg_link": "Tautan pesan yang diberikan tidak valid", + "multiple_ban_progress": "`Melarang Pengguna dari banyak grup. Ini mungkin membutuhkan waktu`", + "multiple_unban_progress": "`Membatalkan pencekalan Pengguna dari banyak grup. Ini mungkin membutuhkan waktu`", + "failed_get_uname": "Tidak dapat memperoleh nama pengguna grup", + "listban_msg": "**Pengguna yang Dilarang Daftar:** {mention}\n**ID Pengguna yang Dilarang:** `{uid}`\n**Admin:** {frus}\n**Obrolan yang terpengaruh: ** `{ct}`\n**Alasan:** {reas}", + "listunban_msg": "**Pengguna yang Tidak Dilarang Daftar:** {mention}\n**ID Pengguna yang Tidak Dilarang:** `{uid}`\n**Admin:** {frus}\n**Obrolan yang terpengaruh: ** `{ct}`\n**Alasan:** {reas}", + "promote_self_err": "Saya tidak dapat mempromosikan diri saya sendiri.", + "no_promote_perm": "Sayangnya, saya tidak mengizinkan untuk mempromosikan pengguna.", + "full_promote": "Dipromosikan Sepenuhnya {umention}!", + "normal_promote": "Dipromosikan {umention}!", + "pin_success": "**Pesan [ini]({link}) berhasil disematkan.**", + "unpin_success": "**Pesan sematan [ini]({link}) berhasil dilepas.**", + "pin_no_perm": "Tolong beri saya izin pin untuk menggunakan perintah ini!.", + "report_msg": "Melaporkan {user_mention} ke admin!", + "reported_is_admin": "Apakah Anda tahu bahwa pengguna yang Anda balas adalah seorang admin?", + "user_no_warn": "Pengguna {mention} tidak memiliki peringatan.", + "ch_warn_msg": "Pengguna {mention} memiliki {warns}/3 peringatan.", + "warn_msg": "**Pengguna yang Diperingatkan:** {mention}\n**Diperingatkan Oleh:** {warner}\n**Alasan:** {reas}\n**Peringatan:** {twarn}/ 3", + "rmwarn_msg": "Peringatan {mention} telah dihapus.", + "unwarn_msg": "Peringatan dihapus oleh {mention}.", + "rmmute_msg": "__Bisukan dihapus oleh {mention}__", + "unmute_msg": "Disuarakan! {umention}", + "reply_to_rm_warn": "Balas pesan untuk menghapus peringatan pengguna.", + "exceed_warn_msg": "Jumlah peringatan dari {mention} terlampaui, DILARANG!", + "mute_msg": "**Pengguna yang Dimute:** {mention}\n**Dimute Oleh:** {muter}\n", + "rm_warn_btn": "🚨 Hapus Peringatan 🚨" +} diff --git a/locales/id-ID/afk.json b/locales/id-ID/afk.json new file mode 100644 index 00000000..545faac3 --- /dev/null +++ b/locales/id-ID/afk.json @@ -0,0 +1,13 @@ +{ + "no_channel": "Fitur ini tidak didukung untuk channel.", + "on_afk_msg_no_r": "**{usr}** [{id}] kembali online dan telah AFK selama {tm}\n\n", + "on_afk_msg_with_r": "**{usr}** [{id}] kembali online dan telah AFK selama {tm}\n\n**Alasan:** `{reas}`\n\n", + "is_afk_msg_no_r": "**{usr}** [{id}] telah AFK sejak {tm} yang lalu.\n\n", + "is_afk_msg_with_r": "**{usr}** [{id}] telah AFK sejak {tm} yang lalu.\n\n**Alasan:** {reas}\n\n" , + "is_online": "**{usr}** [{id}] kembali online", + "now_afk": "{usr} [{id}] sekarang AFK!.", + "afkdel_help": "**Penggunaan:**\n/{cmd} [ENABLE|DISABLE] untuk mengaktifkan atau menonaktifkan hapus pesan AFK secara otomatis.", + "afkdel_disable": "Penghapusan otomatis pesan AFK dinonaktifkan.", + "afkdel_enable": "Penghapusan otomatis pesan AFK di obrolan ini diaktifkan.", + "is_afk": "{usr} [{id}] sedang AFK!." +} diff --git a/locales/id-ID/chatbot_ai.json b/locales/id-ID/chatbot_ai.json new file mode 100644 index 00000000..d8b8e07d --- /dev/null +++ b/locales/id-ID/chatbot_ai.json @@ -0,0 +1,6 @@ +{ + "no_question": "Harap gunakan perintah /{cmd} [question] untuk mengajukan pertanyaan Anda menggunakan AI.", + "find_answers_str": "Sedang mencari jawaban terbaik buat Anda..", + "dont_spam": "Tolong jangan melakukan spam, harap tunggu {tm} detik atau saya akan membanned Anda dari bot ini.", + "answers_too_long": "Pertanyaan untuk jawaban Anda telah melampaui batas teks TG, periksa tautan ini untuk melihatnya.\n\n{answerlink}" +} \ No newline at end of file diff --git a/locales/id-ID/dev.json b/locales/id-ID/dev.json new file mode 100644 index 00000000..f2e855e6 --- /dev/null +++ b/locales/id-ID/dev.json @@ -0,0 +1,11 @@ +{ + "already_up": "Sudah paling update!", + "up_and_rest": "Diperbarui dengan branch default, dimulai ulang sekarang.", + "cl_btn": "❌ Tutup", + "no_eval": "__Tidak ada pesan eval!__", + "run_eval": "Memproses pyrogram eval..", + "run_exec": "Memproses pyrogram eksekutif..", + "no_cmd": "Tidak ada perintah untuk dieksekusi.", + "sukses": "Sukses", + "no_reply": "Tidak ada balasan" +} \ No newline at end of file diff --git a/locales/id-ID/fun.json b/locales/id-ID/fun.json new file mode 100644 index 00000000..2fb02a46 --- /dev/null +++ b/locales/id-ID/fun.json @@ -0,0 +1,3 @@ +{ + "result": "🎲 Dadu berhenti di angka: {number}" +} \ No newline at end of file diff --git a/locales/id-ID/general.json b/locales/id-ID/general.json new file mode 100644 index 00000000..5a66cba8 --- /dev/null +++ b/locales/id-ID/general.json @@ -0,0 +1,6 @@ +{ + "back_btn": "« Kembali", + "unknown_id": "Maaf saya tidak bisa mengenali pengguna ini. Mungkin saya belum pernah bertemu dengannya.", + "no_results": "Tidak ada hasil yang ditemukan.", + "exp_task": "😶‍🌫️ Waktu Habis. Tugas Telah Dibatalkan!" +} \ No newline at end of file diff --git a/locales/id-ID/genss.json b/locales/id-ID/genss.json new file mode 100644 index 00000000..980b0522 --- /dev/null +++ b/locales/id-ID/genss.json @@ -0,0 +1,13 @@ +{ + "wait_msg": "Beri saya waktu untuk memproses permintaan Anda!! 😴", + "wait_dl": "Sedang diproses, harap tunggu..", + "dl_progress": "Mencoba mengunduh, harap tunggu..", + "up_progress": "Mencoba mengunggah...", + "success_dl_msg": "Berkas telah diunduh ke {path}.", + "fail_open": "😟 Maaf! Saya tidak dapat membuka file.", + "limit_dl": "Maaf, unduh terbatas hingga 2GB untuk mengurangi flood. Anda dapat mengkonversi file Anda menjadi tautan.", + "err_ssgen": "Pembuatan screenshot gagal.\n\n{exc}", + "up_msg": "☑️ Pembuatan screenshot berhasil.\n\n{namma} ({id})\n#️⃣ #ssgen #id{id}\n\nSS Dibuat oleh @{bot_uname} ", + "no_reply": "Balas video atau dokumen Telegram atau gunakan tautan langsung setelah perintah untuk membuat screenshot dari media!", + "choose_no_ss": "Sekarang pilih berapa hasil screenshot? 🥳.\n\nTotal durasi: `{td}` (`{dur} detik`)" +} \ No newline at end of file diff --git a/locales/id-ID/grup_tools.json b/locales/id-ID/grup_tools.json new file mode 100644 index 00000000..84effeb3 --- /dev/null +++ b/locales/id-ID/grup_tools.json @@ -0,0 +1,13 @@ +{ + "sudo_join_msg": "Waw, Pemilikku yang keren baru saja bergabung ke grup!", + "log_bot_added": "#GrupBaru\nGrup = {ttl}({cid})\nJumlah Anggota = {tot}\nDitambahkan oleh - {r_j}", + "support_btn": "Dukungan", + "help_btn": "ℹ️ Bantuan", + "update_btn": "📢 Updates", + "chat_not_allowed": "CHAT TIDAK DIIJINKAN 🐞\n\nPemilik saya sudah melarang saya untuk bekerja disini! Kamu bisa menghubungi pemilik bot ini..", + "welcome_thanks": "Terimakasih sudah menambahkan saya di {ttl} ❣️\n\nJika ada kendala atau saran bisa kontak ke saya.", + "capt_welc": "Hai {umention} [{uid}], Selamat datang digrup {ttl}.", + "combot_msg": "#CAS Federation Ban\nPengguna {umention} [{uid}] terdeteksi sebagai spambot dan telah dikeluarkan. Powered by Combot AntiSpam.", + "spamwatch_msg": "#SpamWatch Federation Ban\nPengguna {umention} [{uid}] telah dikeluarkan karena {reas}.\n", + "welcpic_msg": "Selamat Datang {userr} [{id}]" +} \ No newline at end of file diff --git a/locales/id-ID/help_menu.json b/locales/id-ID/help_menu.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/locales/id-ID/help_menu.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/id-ID/lang_setting.json b/locales/id-ID/lang_setting.json new file mode 100644 index 00000000..bae662bf --- /dev/null +++ b/locales/id-ID/lang_setting.json @@ -0,0 +1,5 @@ +{ + "language_changed_successfully": "Bahasa telah berhasil diubah.", + "language_changer_chat": "Di sini Anda dapat mengubah bahasa yang digunakan bot selama obrolan.\nJika bahasa Anda tidak ada di sini dan Anda ingin berkontribusi, Anda dapat membuka masalah di github saya. .", + "language_changer_private": "Di sini Anda dapat mengubah bahasa yang digunakan oleh bot dalam obrolan pribadi ini.\n\nJika Anda ingin mengubah bahasa grup Anda, buka perintah /setchatlang. \nJika bahasa Anda tidak ada di sini dan Anda ingin berkontribusi, buka repo misskaty di github." +} \ No newline at end of file diff --git a/locales/id-ID/main.json b/locales/id-ID/main.json new file mode 100644 index 00000000..9e014f6e --- /dev/null +++ b/locales/id-ID/main.json @@ -0,0 +1,4 @@ +{ + "language_name": "Indonesia", + "language_flag": "🇮🇩" + } \ No newline at end of file diff --git a/locales/id-ID/media_extractor.json b/locales/id-ID/media_extractor.json new file mode 100644 index 00000000..4e159236 --- /dev/null +++ b/locales/id-ID/media_extractor.json @@ -0,0 +1,15 @@ +{ + "sub_extr_help": "Harap gunakan perintah /{cmd} [tautan] untuk memeriksa teks film atau audio dalam berkas video.", + "conv_sub_help": "Gunakan perintah /{cmd} dengan membalas berkas .ass atau .vtt, untuk mengonversi teks film dari .ass atau .vtt ke srt.", + "progress_str": "⏳ Memproses permintaan Anda..", + "convert_str": "⏳ Mengkonversi...", + "unauth_cb": "⚠️ Akses Ditolak!", + "batal_btn": "❌ Batal", + "invalid_cb": "⚠️ JANGAN HAPUS PESAN ANDA!", + "up_str": "Mengunggah berkas..", + "press_btn_msg": "Tekan tombol di bawah untuk mengekstrak teks/audio. Saat ini hanya mendukung tautan langsung.\nDiproses dalam {timelog}", + "fail_extr_media": "Gagal mengekstrak media, pastikan tautan Anda tidak dilindungi oleh WAF atau mungkin tidak dapat diakses oleh bot.", + "fail_extr_sub": "Sub ekstrak gagal, Mungkin formatnya tidak didukung..\n\nTautan: {link}\nERR: {e}", + "capt_extr_sub": "Nama file: {nf}\n\nDiekstrak oleh @{bot} dalam {timelog}", + "capt_conv_sub": "{nf}.srt\n\nDikonversi oleh @{bot}" +} diff --git a/locales/id-ID/mediainfo.json b/locales/id-ID/mediainfo.json new file mode 100644 index 00000000..6c719fec --- /dev/null +++ b/locales/id-ID/mediainfo.json @@ -0,0 +1,11 @@ +{ + "processing_text": "`Memproses, total waktu didasarkan pada ukuran file Anda...`", + "wait_msg": "`Mohon tunggu sementara...`", + "err_link": "Sepertinya tautan yang Anda kirim tidak valid, pastikan tautan langsung dan bisa di unduh.", + "media_invalid": "Silakan balas ke media yang valid.", + "dl_limit_exceeded": "Maaf, unduh dibatasi hingga 2GB untuk mengurangi flood. Anda dapat mengonversi berkas Anda menjadi tautan.", + "dl_args_text": "Mencoba mengunduh..", + "mediainfo_help": "Gunakan perintah /{cmd} [tautan], atau balas telegram media dengan /{cmd}.", + "capt_media": "ℹ️ Hasil mediainfo anda..\n\n**Diminta Oleh:** {ment}", + "viweb": "💬 Buka di Web" +} \ No newline at end of file diff --git a/locales/id-ID/nightmodev2.json b/locales/id-ID/nightmodev2.json new file mode 100644 index 00000000..98f858dd --- /dev/null +++ b/locales/id-ID/nightmodev2.json @@ -0,0 +1,17 @@ +{ + "nmd_disabled": "Mode malam dinonaktifkan.", + "nmd_not_enabled": "Mode malam tidak diaktifkan di obrolan ini.", + "invalid_time_format": "Format waktu tidak sah. Gunakan format HH:MM.", + "invalid_lockdur": "Durasi waktu tidak valid. Gunakan format yang tepat.\nContoh: 6j (selama 6 jam), 10m selama 10 menit.", + "schedule_already_on": "Sudah ada jadwal yang berjalan di obrolan ini. Nonaktifkan menggunakan tanda `-d`.", + "nmd_enable_success": "Berhasil mengaktifkan mode malam dalam obrolan ini.\nGrup akan dikunci pada {st} dan akan dibuka setelah {lockdur} setiap hari.", + "nmd_cb": "🔖 Hai, Saya {bname} dibuat menggunakan Framework Pyrogram v{ver} dan Python v{pyver}.\n\nMau buat bot seperti ini? Yuuk belajar di @botindonesia\nOwner: @YasirArisM", + "nmd_off_not_admin": "#NIGHTMODE_FAIL\nGagal mematikan mode malam di `{chat_id}`, karena {bname} bukan admin di obrolan `{chat_id}`", + "nmd_off_not_present": "#NIGHTMODE_FAIL\nGagal mematikan mode malam di `{chat_id}`, karena {bname} tidak ada di obrolan `{chat_id}`. Menghapus grup dari daftar.", + "nmd_off_err": "#NIGHTMODE_FAIL\nGagal mematikan mode malam di `{chat_id}`\nERROR: `{e}`", + "nmd_off_success": "#NIGHTMODE_HANDLER\n📆 {dt}\n\n☀️ Grup sedang dibuka.\nAkan ditutup pada {close_at}", + "nmd_on_not_admin": "#NIGHTMODE_FAIL\nGagal mengaktifkan mode malam di `{chat_id}`, karena {bname} bukan admin di obrolan `{chat_id}`", + "nmd_on_not_present": "#NIGHTMODE_FAIL\nGagal mengaktifkan mode malam di `{chat_id}`, karena {bname} tidak ada di obrolan `{chat_id}`. Grup dihapus dari daftar.", + "nmd_on_err": "#NIGHTMODE_FAIL\nGagal mengaktifkan mode malam di `{chat_id}`\nERROR: `{e}`", + "nmd_on_success": "#NIGHTMODE_HANDLER\n📆 {dt}\n\n🌗 Grup ditutup.\nAkan dibuka pada {open_at}" +} \ No newline at end of file diff --git a/locales/id-ID/ocr.json b/locales/id-ID/ocr.json new file mode 100644 index 00000000..a9043c73 --- /dev/null +++ b/locales/id-ID/ocr.json @@ -0,0 +1,6 @@ +{ + "no_photo": "Balas foto dengan perintah /{cmd} untuk memindai teks dari gambar.", + "read_ocr": "Memindai gambar Anda..", + "result_ocr": "Hasil OCR:\n{result}", + "ocr_helper": "/ocr [balas ke foto] - Baca Teks Dari Gambar" +} \ No newline at end of file diff --git a/locales/id-ID/sangmata.json b/locales/id-ID/sangmata.json new file mode 100644 index 00000000..52417d10 --- /dev/null +++ b/locales/id-ID/sangmata.json @@ -0,0 +1,13 @@ +{ + "no_uname": "Tanpa Username", + "no_last_name": "Tanpa Nama Belakang", + "uname_change_msg": "✨ Mengubah username dari {bef} ➡️ {aft}.\n", + "lastname_change_msg": "✨ Mengubah nama belakang dari {bef} ➡️ {aft}.\n", + "firstname_change_msg": "✨ Mengubah nama depan dari {bef} ➡️ {aft}.\n", + "set_sangmata_help": "Gunakan /{cmd} on, untuk mengaktifkan sangmata. Jika Anda ingin menonaktifkan, Anda dapat menggunakan parameter off.", + "sangmata_already_on": "SangMata telah diaktifkan di grup Anda.", + "sangmata_enabled": "Sangmata diaktifkan di grup Anda.", + "sangmata_already_off": "SangMata telah dinonaktifkan di grup Anda.", + "sangmata_disabled": "Sangmata dinonaktifkan di grup Anda.", + "wrong_param": "Parameter tidak diketahui, gunakan hanya parameter hidup/mati." +} \ No newline at end of file diff --git a/locales/id-ID/start_help.json b/locales/id-ID/start_help.json new file mode 100644 index 00000000..63b6ba3a --- /dev/null +++ b/locales/id-ID/start_help.json @@ -0,0 +1,11 @@ +{ + "newgroup_log": "#NewGroup\nGroup = {jdl}({id})\nJumlah Anggota = {c}", + "newuser_log": "#PenggunaBaru\nID - {id}\nNama - {nm}", + "help_name": "Ini bantuan untuk **{mod}**:\n", + "help_txt": "Halo {kamuh}, Nama saya {bot}.\nSaya adalah bot pyrogram yang dikembangkan oleh pemilik yang baik hati dengan beberapa fitur berguna.\nAnda dapat melihat dengan mengeklik tombol di bawah.\n\nPerintah umum adalah :\n - /start: Mulai bot\n - /help: Berikan pesan ini\n - /setlang: Ubah bahasa bot [BETA]", + "click_me": "Klik Saya", + "back_btn": "Kembali", + "click_btn": "Klik tombol di bawah untuk mendapatkan bantuan tentang {nm}", + "pm_detail": "PM Saya Untuk Detail Lebih Lanjut.", + "start_msg": "Hai {kamuh}, PM saya untuk mengetahui semua fitur saya. Anda dapat mengubah bahasa bot di bot menggunakan perintah /setlang tetapi ini masih dalam tahap beta." +} diff --git a/locales/id-ID/stickers.json b/locales/id-ID/stickers.json new file mode 100644 index 00000000..cd69b711 --- /dev/null +++ b/locales/id-ID/stickers.json @@ -0,0 +1,19 @@ +{ + "no_anim_stick": "Stiker animasi tidak didukung!", + "not_sticker": "Ini bukan stiker!", + "unkang_msg": "Mencoba menghapus dari paket..", + "unkang_success": "Stiker telah dihapus dari paket Anda", + "unkang_error": "Gagal menghapus stiker dari paket Anda.\n\nERR: {e}", + "unkang_help": "Tolong balas stiker yang dibuat oleh {c} untuk menghapus stiker dari paket Anda.", + "anon_warn": "Anda adalah admin anon, stiker kang ada di pm saya.", + "kang_msg": "Mencoba mencuri stiker Anda...", + "stick_no_name": "Stiker tidak memiliki nama.", + "kang_help": "Ingin saya menebak stikernya? Harap tandai stiker.", + "exist_pack": "Menggunakan paket stiker yang ada...", + "new_packs": "Membuat paket stiker baru...", + "please_start_msg": "Tampaknya Anda belum pernah berinteraksi dengan saya dalam obrolan pribadi, Anda harus melakukannya dulu..", + "click_me": "Klik Saya", + "pack_full": "Paket Stiker Anda penuh jika paket Anda tidak dalam Tipe v1 /kang 1, jika tidak dalam Tipe v2 /kang 2 dan seterusnya.", + "viewpack": "👀 Lihat Paket", + "kang_success": "Stiker berhasil dicuri!\nEmoji: {emot}" +} \ No newline at end of file diff --git a/locales/id-ID/web_scraper.json b/locales/id-ID/web_scraper.json new file mode 100644 index 00000000..36d458eb --- /dev/null +++ b/locales/id-ID/web_scraper.json @@ -0,0 +1,20 @@ +{ + "no_result": "Maaf, saya tidak dapat menemukan hasil apa pun!", + "no_result_w_query": "Maaf, saya tidak dapat menemukan kueri: {kueri}", + "get_data": "⏳ Mohon tunggu, mengambil data dari web..", + "cl_btn": "❌ Tutup", + "back_btn": "↩️ Kembali", + "dl_text": "⬇️ Unduh", + "cat_text": "Kategori", + "kualitas": "Kualitas", + "ex_data": "👇 Ekstrak Data", + "unauth": "Tombol ini bukan untuk Anda..", + "invalid_cb": "Data callback tidak valid, silakan kirim perintah lagi..", + "res_scrape": "Hasil Scrape dari {link}:\n\n{kl}", + "header_with_query": "Hasil Pencarian #{web} Untuk: {kueri}\n\n", + "header_no_query": "#{web} Terbaru:\n🌀 Gunakan /{cmd} [judul] untuk memulai pencarian dengan judul.\n\n", + "invalid_cmd_scrape": "Gunakan perintah /{cmd} [link] untuk mengambil link unduhan.", + "err_getweb": "ERROR: Failed getting data from web because {err}.", + "err_getapi": "ERROR: Failed getting data from API", + "unsupport_dl_btn": "Beberapa hasil tidak akan muncul di tombol ekstrak karena tautan tidak didukung." +} \ No newline at end of file diff --git a/locales/id-ID/webss.json b/locales/id-ID/webss.json new file mode 100644 index 00000000..fa9df737 --- /dev/null +++ b/locales/id-ID/webss.json @@ -0,0 +1,6 @@ +{ + "no_url": "Berikan url untuk mengambil tangkapan layar.", + "wait_str": "Mengambil tangkapan layar...", + "str_credit": "🌞 Screenshot dibuat dengan Puppeteer", + "ss_failed_str": "Gagal Mengambil Tangkapan Layar. ERROR: {err}" +} \ No newline at end of file diff --git a/locales/id-ID/ytdl_plugins.json b/locales/id-ID/ytdl_plugins.json new file mode 100644 index 00000000..e6e7ac38 --- /dev/null +++ b/locales/id-ID/ytdl_plugins.json @@ -0,0 +1,14 @@ +{ + "no_channel": "Fitur ini tidak didukung untuk channel atau pengguna anonim.", + "no_query": "Silakan kirim link yang mau diunduh..!", + "no_res": "Tidak ada hasil yang ditemukan untuk `{kweri}`", + "dl_btn": "Unduh", + "kembali": "kembali", + "yts_msg": "Dipublikasikan {pub}\n\n❯ Durasi: {dur}\n❯ Penayangan: {vi}\n❯ Pengunggah: {cname}\n\n", + "invalid_link": "Masukkan URL yang Didukung YT-DLP yang valid", + "err_parse": "Gagal menguraikan URL, periksa log..", + "tunggu": "Harap tunggu..", + "unauth": "Bukan Tugas Anda..", + "endlist": "Itu adalah akhir dari daftar", + "vip-btn": "Karena beberapa penyalahgunaan pengguna, video terbaik sekarang hanya untuk pemilik bot." +} \ No newline at end of file diff --git a/locales/id-JW/admin.json b/locales/id-JW/admin.json new file mode 100644 index 00000000..d2e4df0d --- /dev/null +++ b/locales/id-JW/admin.json @@ -0,0 +1,68 @@ +{ + "no_admin_error": "Sampeyan kudu dadi administrator kanggo nggunakake perintah iki.", + "no_permission_error": "Ngapunten, nanging sampeyan ora duwe izin sing dibutuhake kanggo nindakake perintah iki. Izin sing ilang: {permissions}", + "private_not_allowed": "Perintah iki ora bisa digunakake ing obrolan pribadi, Yen sampeyan butuh bantuan, mohon nggunakake perintah /help.", + "purge_no_reply": "Bales pesen sing arep dibusak.", + "delete_no_reply": "Bales Pesen Kanggo Mbusak", + "pin_no_reply": "Bales pesen sing arep disematke/copot.", + "report_no_reply": "Bales Pesen Kanggo Nglaporake Panganggo.", + "no_delete_perm": "Tulung aku kei izin mbusak pesen.", + "purge_success": "Kasil mbusak {del_total} pesen..", + "user_not_found": "Aku ora bisa nemokake panganggo kuwi.", + "invalid_id_uname": "⚠️ panganggo/jeneng panganggo ora sah", + "kick_self_err": "Aku ora bisa nyepak awakku dhewe, aku bisa lunga yen sampeyan pengin.", + "ban_self_err": "Aku ora bisa nglarang aku, aku bisa lunga yen sampeyan pengin.", + "report_self_err": "Kenging menapa panjenengan lapor piyambak?", + "demote_self_err": "Aku ora bisa mudhun dhewe.", + "warn_self_err": "Aku ora bisa ngelekake awakku dhewe.", + "mute_self_err": "Aku ora bisa bisu awakku dhewe.", + "kick_sudo_err": "Wah, kowe arep nyepak juraganku?", + "ban_sudo_err": "Wah, sampeyan pengin nyoba nglarang pemilikku?", + "demote_sudo_err": "Wah, sampeyan pengin nyoba nurunake pemilikku?", + "warn_sudo_err": "Wah, sampeyan pengin nyoba menehi peringatan marang pemilikku?", + "mute_sudo_err": "Wah, sampeyan pengin nyoba menehi bisu marang pemilikku?", + "kick_admin_err": "Lol, edan yen aku bisa nyepak admin.", + "ban_admin_err": "Lol, edan yen aku bisa nglarang admin.", + "mute_admin_err": "Lol, edan yen aku bisa bisu admin.", + "warn_admin_err": "Lol, edan yen aku bisa ngelingake admin.", + "kick_msg": "**Panganggo Ditendhang:** {mention} [`{id}`]\n**Ditendhang dening:** {kicker}\n**Alasan:** {reasonmsg}", + "ban_msg": "**Panganggo sing Dicekal:** {mention} [`{id}`]\n**Dicekal Dening:** {banner}\n", + "unban_msg": "__Dicekal dibusak kanthi {mention}__", + "no_ban_permission": "Mangga kula nyuwun idin nglarang panganggo ing grup punika.", + "no_more_99": "Sampeyan ora bisa nggunakake luwih saka 99", + "banned_time": "**Dicekal Kanggo:** {val}\n", + "muted_time": "**Diam Kanggo:** {val}\n", + "banned_reason": "**Alesan:** {reas}", + "unban_channel_err": "Sampeyan ora bisa mbatalake saluran", + "give_unban_user": "Nyedhiyani jeneng panganggo utawa mbales pesen panganggo kanggo mbatalake larangan.", + "unban_success": "Kasil mbatalake {umention}!", + "give_idban_with_msg_link": "Nyedhiyakake id panganggo/jeneng panganggo bebarengan karo pranala pesen lan alasan kanggo larangan daftar", + "give_idunban_with_msg_link": "Nyedhiyani id panganggo/jeneng panganggo bebarengan karo pranala pesen lan alasan kanggo daftar-unban", + "give_reason_list_ban": "Sampeyan kudu menehi alesan kanggo nglarang daftar", + "Invalid_tg_link": "Link pesen ora sah diwenehake", + "multiple_ban_progress": "`Nglarang Panganggo saka macem-macem grup. Iki mbutuhake sawetara wektu`", + "multiple_unban_progress": "`Mbusak Larangan Panganggo saka pirang-pirang grup. Iki mbutuhake sawetara wektu`", + "failed_get_uname": "Ora bisa njupuk jeneng panganggo grup", + "listban_msg": "**Panganggo sing Dilarang Dhaptar:** {mention}\n**ID Panganggo sing Dicekal:** `{uid}`\n**Admin:** {frus}\n**Obrolan sing kena pengaruh: ** `{ct}`\n**Alasan:** {reas}", + "listunban_msg": "**Daftar Panganggo sing Ora Dilarang:** {mention}\n**ID Panganggo sing Ora Dilarang:** `{uid}`\n**Admin:** {frus}\n**Obrolan sing kena pengaruh: ** `{ct}`\n**Alasan:** {reas}", + "promote_self_err": "Aku ora bisa promosi dhewe.", + "no_promote_perm": "Sedhih, aku ora ijin kanggo promosi pangguna.", + "full_promote": "{umention} dipromosekake kanthi lengkap!", + "normal_promote": "Dipromosikke {umention}!", + "pin_success": "**Sukses semat pesen iki [iki]({link}).**", + "unpin_success": "**Pesen sematan [iki]({link}) kasil dicopot.**", + "pin_no_perm": "Tulung wenehi pin izin kanggo nggunakake printah iki!.", + "report_msg": "Kacarita {user_mention} menyang admin!", + "reported_is_admin": "Apa sampeyan ngerti yen pangguna sing sampeyan bales iku admin?", + "user_no_warn": "Panganggo {mention} ora ana bebaya.", + "ch_warn_msg": "{mention} duwe {warning}/3 bebaya.", + "warn_msg": "**Panganggo sing Dielingake:** {mention}\n**Dielingake Dening:** {warner}\n**Alesan:** {reas}\n**Ngelingake:** {twarn}/ 3", + "rmwarn_msg": "Pènget saka {mention} wis dibusak.", + "unwarn_msg": "Pènget sing dibusak kanthi {mention}.", + "rmmute_msg": "__Bisu dibusak kanthi {mention}__", + "unmute_msg": "Ora bisu! {umention}", + "reply_to_rm_warn": "Bales pesen kanggo mbusak bebaya pangguna.", + "exceed_warn_msg": "Jumlah peringatan babagan {mention} ngluwihi, DIBRANG!", + "mute_msg": "**Panganggo Bisu:** {mention}\n**Dibunyikan Dening:** {muter}\n", + "rm_warn_btn": "🚨 Copot Warn 🚨" +} diff --git a/locales/id-JW/afk.json b/locales/id-JW/afk.json new file mode 100644 index 00000000..732ad35d --- /dev/null +++ b/locales/id-JW/afk.json @@ -0,0 +1,13 @@ +{ + "no_channel": "Fitur iki ora didhukung kanggo channel.", + "on_afk_msg_no_r": "**{usr}** [{id}] wis online maneh lan ora ana suwene {tm}\n\n", + "on_afk_msg_with_r": "**{usr}** [{id}] wis online maneh lan ora ana suwene {tm}\n\n**Alesan:** `{reas}`\n\n", + "is_afk_msg_no_r": "**{usr}** [{id}] iku AFK wiwit {tm} kepungkur.\n\n", + "is_afk_msg_with_r": "**{usr}** [{id}] iku AFK wiwit {tm} kepungkur.\n\n**Alesan:** {reas}\n\n" , + "is_online": "**{usr}** [{id}] wis online maneh", + "now_afk": "{usr} [{id}] saiki dadi AFK!.", + "afkdel_help": "**Panganggone:**\n/{cmd} [ENABLE|DISABLE] kanggo ngaktifake utawa mateni pesen otomatis mbusak.", + "afkdel_disable": "Mbusak pesen AFK otomatis dipateni.", + "afkdel_enable": "Mbusak pesen AFK otomatis ing obrolan iki diaktifake.", + "is_afk": "{usr} [{id}] iku AFK!." +} diff --git a/locales/id-JW/chatbot_ai.json b/locales/id-JW/chatbot_ai.json new file mode 100644 index 00000000..34693a3b --- /dev/null +++ b/locales/id-JW/chatbot_ai.json @@ -0,0 +1,6 @@ +{ + "no_question": "Tulung gunakake printah /{cmd} [pitakon] kanggo takon nganggo fitur AI.", + "find_answers_str": "Lagi goleki jawaban paling apik kanggo sampeyan..", + "dont_spam": "Aja spam, mangga ngenteni {tm} detik utawa aku bakal nglarang sampeyan saka bot iki.", + "answers_too_long": "Pitakonan kanggo jawaban sampeyan wis ngluwihi wates teks TG, priksa pranala iki kanggo ndeleng.\n\n{answerlink}" +} \ No newline at end of file diff --git a/locales/id-JW/dev.json b/locales/id-JW/dev.json new file mode 100644 index 00000000..490736f9 --- /dev/null +++ b/locales/id-JW/dev.json @@ -0,0 +1,11 @@ +{ + "already_up": "Wis up-to-date!", + "up_and_rest": "Dianyari nganggo branch standar, diwiwiti maneh saiki.", + "cl_btn": "❌ Tutup", + "no_eval": "__Ora ana pesen eval!__", + "run_eval": "Ngolah pyrogram eval..", + "run_exec": "Ngolah pyrogram exec..", + "no_cmd": "Ora ana prentah kanggo nglakokaké.", + "success": "Sukses", + "no_reply": "Ora Wangsulan" +} \ No newline at end of file diff --git a/locales/id-JW/fun.json b/locales/id-JW/fun.json new file mode 100644 index 00000000..4cf5eac5 --- /dev/null +++ b/locales/id-JW/fun.json @@ -0,0 +1,3 @@ +{ + "result": "🎲 Dadu mandheg ing nomer: {number}" +} \ No newline at end of file diff --git a/locales/id-JW/general.json b/locales/id-JW/general.json new file mode 100644 index 00000000..bd978128 --- /dev/null +++ b/locales/id-JW/general.json @@ -0,0 +1,6 @@ +{ + "back_btn": "« Balik Maneh", + "unknown_id": "Nganpunten, aku ora ngerti pengguna iki. Mungkin aku durung pernah ketemu mbi dek'e.", + "no_results": "Ora ana asil sing ditemokake.", + "exp_task": "😶‍🌫️ Wektu wis entek. Tugas wis dibatalake!" + } \ No newline at end of file diff --git a/locales/id-JW/genss.json b/locales/id-JW/genss.json new file mode 100644 index 00000000..7730eb22 --- /dev/null +++ b/locales/id-JW/genss.json @@ -0,0 +1,13 @@ +{ + "wait_msg": "Wenehi wektu kanggo ngolah panjalukmu!! 😴", + "wait_dl": "Lagi diproses, mangga ngenteni..", + "dl_progress": "Nyoba ngundhuh, mangga ngenteni..", + "up_progress": "Nyoba ngunggah...", + "success_dl_msg": "Berkas wis diundhuh menyang {path}.", + "fail_open": "😟 Nuwun sewu! Aku ora bisa mbukak file.", + "limit_dl": "Nuwun sewu, download dibatesi nganti 2GB kanggo nyuda flood. Sampeyan bisa ngowahi file dadi link luwih dhisik.", + "err_ssgen": "Gagal nggawe screenshoot.\n\n{exc}", + "up_msg": "☑️ Screenshot generasi kasil.\n\n{namma} ({id})\n#️⃣ #ssgen #id{id}\n\nSS Digawe dening @{bot_uname} ", + "no_reply": "Bales menyang video utawa dokumen Telegram utawa gunakake link langsung sawise printah kanggo ngasilake screenshot saka media!", + "choose_no_ss": "Saiki pilih pira asil kanggo screenshot? 🥳.\n\nTotal durasi: `{td}` (`{dur} detik`)" +} \ No newline at end of file diff --git a/locales/id-JW/grup_tools.json b/locales/id-JW/grup_tools.json new file mode 100644 index 00000000..aa0ef5d2 --- /dev/null +++ b/locales/id-JW/grup_tools.json @@ -0,0 +1,13 @@ +{ + "sudo_join_msg": "Waw, Pemilikku sing ganteng gabung menyang grup!", + "log_bot_added": "#GrupBaru\nGrup = {ttl}({cid})\nJumlah Anggota = {tot}\nDitambahake dening - {r_j}", + "support_btn": "Dukungan", + "help_btn": "ℹ️ Pitulungan", + "update_btn": "📢 Nganyari", + "chat_not_allowed": "CHAT ORA DIIJINKE 🐞\n\nPemilik ku wes nglarang aku kerja neng kene! Sampeyan bisa hubungi pemilik bot iki..", + "welcome_thanks": "Terimakasih wis nambahi saya ing {ttl} ❣️\n\nYen ana kendal utawa saran bisa hubungi saya.", + "capt_welc": "Hai {umention} [{uid}], sugeng rawuh ing grup {ttl}.", + "combot_msg": "Larangan Federasi #CAS\nPengguna {umention} [{uid}] dideteksi minangka spambot lan wis dibusak. Powered by Combot AntiSpam.", + "spamwatch_msg": "Larangan Federasi #SpamWatch\nPengguna {umention} [{uid}] wis dibusak amarga {reas}.\n" , + "welcpic_msg": "Sugeng Rawuh {userr} [{id}]" +} \ No newline at end of file diff --git a/locales/id-JW/help_menu.json b/locales/id-JW/help_menu.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/locales/id-JW/help_menu.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/id-JW/lang_setting.json b/locales/id-JW/lang_setting.json new file mode 100644 index 00000000..0b32a39d --- /dev/null +++ b/locales/id-JW/lang_setting.json @@ -0,0 +1,5 @@ +{ + "language_changed_successfully": "Basa wis kasil diganti.", + "language_changer_chat": "Ing kene sampeyan bisa ngganti basa sing digunakake kanggo bot sajrone obrolan.\nYen basa sampeyan ora ana ing kene lan sampeyan pengin nyumbang, sampeyan bisa mbukak masalah ing githubku. .", + "language_changer_private": "Ing kene sampeyan bisa ngganti basa sing digunakake kanggo bot ing obrolan pribadi iki.\n\nYen sampeyan pengin ngganti basa grup sampeyan, bukak printah /setchatlang. \nYen basa sampeyan ora ana ing kene lan sampeyan pengin nyumbang, bukak repo misskaty." +} \ No newline at end of file diff --git a/locales/id-JW/main.json b/locales/id-JW/main.json new file mode 100644 index 00000000..4e8ba7ed --- /dev/null +++ b/locales/id-JW/main.json @@ -0,0 +1,4 @@ +{ + "language_name": "Jawa", + "language_flag": "💫​" + } \ No newline at end of file diff --git a/locales/id-JW/media_extractor.json b/locales/id-JW/media_extractor.json new file mode 100644 index 00000000..12d2beac --- /dev/null +++ b/locales/id-JW/media_extractor.json @@ -0,0 +1,15 @@ +{ + "sub_extr_help": "Gunakake printah /{cmd} [link] kanggo mriksa subtitle utawa audio ing file video.", + "conv_sub_help": "Gunakake printah /{cmd} kanthi mbales berkas .ass utawa .vtt, kanggo ngowahi subtitle saka .ass utawa .vtt dadi srt.", + "progress_str": "⏳ Ngolah panjalukmu..", + "convert_str": "⏳ Ngonversi...", + "unauth_cb": "⚠️ Akses Ditolak!", + "cancel_btn": "❌ Rasido", + "invalid_cb": "⚠️ AJA Mbusak PESAN!", + "up_str": "Ngunggahaké berkas..", + "press_btn_msg": "Pencet tombol ing ngisor iki kanggo ngekstrak subtitle/audio. Mung ndhukung link langsung saiki.\nDiproses ing {timelog}", + "fail_extr_media": "Media ekstrak sing gagal, priksa manawa pranala sampeyan ora direksa WAF utawa ora bisa diakses kanggo bot.", + "fail_extr_sub": "Gagal ekstrak sub, Mungkin format ora didhukung..\n\nLink: {link}\nERR: {e}", + "capt_extr_sub": "Jeneng berkas: {nf}\n\nDiekstrak saka @{bot} ing {timelog}", + "capt_conv_sub": "{nf}.srt\n\nDiowahi dening @{bot}" +} diff --git a/locales/id-JW/mediainfo.json b/locales/id-JW/mediainfo.json new file mode 100644 index 00000000..00372a45 --- /dev/null +++ b/locales/id-JW/mediainfo.json @@ -0,0 +1,11 @@ +{ + "processing_text": "`Pengolahan, wektu total adhedhasar ukuran berkas panjenengan...`", + "wait_msg": "`Tulung enteni sedhela...`", + "err_link": "Kayak'e pranala sing dikirim ora valid, priksa link langsung lan bisa diundhuh.", + "media_invalid": "Mangga mbales media sing bener.", + "dl_limit_exceeded": "Nuwun sewu, download diwatesi 2GB kanggo nyuda flood. Sampeyan bisa ngowahi file dadi link.", + "dl_args_text": "Nyoba ngundhuh..", + "mediainfo_help": "Gunakake printah /{cmd} [link], utawa bales media telegram nganggo /{cmd}.", + "capt_media": "ℹ️ Hasil mediainfo sampeyan..\n\n**Panjalukan Saka:** {ment}", + "viweb": "💬 Bukak Web" +} \ No newline at end of file diff --git a/locales/id-JW/nightmodev2.json b/locales/id-JW/nightmodev2.json new file mode 100644 index 00000000..273e1307 --- /dev/null +++ b/locales/id-JW/nightmodev2.json @@ -0,0 +1,17 @@ +{ + "nmd_disabled": "Mode wengi dipateni.", + "nmd_not_enabled": "Mode wengi ora diaktifake ing obrolan iki.", + "invalid_time_format": "Format wektu ora valid. Gunakake format HH:MM.", + "invalid_lockdur": "Suwene wektu ora bener. Gunakake format sing bener.\nConto: 6h (6 jam), 10m kanggo 10 menit.", + "schedule_already_on": "Wis ana jadwal ing obrolan iki. Pateni nganggo gendéra `-d`.", + "nmd_enable_success": "Kasil ngaktifake mode wengi ing obrolan iki.\nGrup bakal dikunci ing {st} lan bakal dibukak sawise {lockdur} saben dina.", + "nmd_cb": "🔖 Hai, Aku {bname} digawe nganggo Framework Pyrogram v{ver} lan Python v{pyver}.\n\nMau gawe bot kaya iki? Yuuk belajar di @botindonesia\nOwner: @YasirArisM", + "nmd_off_not_admin": "#NIGHTMODE_FAIL\nGagal mateni nightmode ing `{chat_id}`, amarga {bname} dudu admin ing chat `{chat_id}`", + "nmd_off_not_present": "#NIGHTMODE_FAIL\nGagal mateni nightmode ing `{chat_id}`, amarga {bname} ora ana ing chat `{chat_id}`. Dibusak grup saka dhaptar.", + "nmd_off_err": "#NIGHTMODE_FAIL\nGagal mateni nightmode ing `{chat_id}`\nERROR: `{e}`", + "nmd_off_success": "#NIGHTMODE_HANDLER\n📆 {dt}\n\n☀️ Grup dibukak.\nBakal ditutup jam {close_at}", + "nmd_on_not_admin": "#NIGHTMODE_FAIL\nGagal ngaktifake mode wengi ing `{chat_id}`, amarga {bname} dudu admin ing chat `{chat_id}`", + "nmd_on_not_present": "#NIGHTMODE_FAIL\nGagal ngaktifake mode wengi ing `{chat_id}`, amarga {bname} ora ana ing obrolan `{chat_id}`. Grup dibusak saka dhaptar.", + "nmd_on_err": "#NIGHTMODE_FAIL\nGagal ngaktifake mode wengi ing `{chat_id}`\nERROR: `{e}`", + "nmd_on_success": "#NIGHTMODE_HANDLER\n📆 {dt}\n\n🌗 Grup ditutup.\nBakal dibukak ing {open_at}" +} \ No newline at end of file diff --git a/locales/id-JW/ocr.json b/locales/id-JW/ocr.json new file mode 100644 index 00000000..e083aa8c --- /dev/null +++ b/locales/id-JW/ocr.json @@ -0,0 +1,6 @@ +{ + "no_photo": "Bales foto nganggo printah /{cmd} kanggo mindhai teks saka gambar.", + "read_ocr": "Mindai gambar sampeyan..", + "result_ocr": "Hasil OCR:\n{result}", + "ocr_helper": "/ocr [reply to photo] - Waca Teks Saka Gambar" +} \ No newline at end of file diff --git a/locales/id-JW/sangmata.json b/locales/id-JW/sangmata.json new file mode 100644 index 00000000..c40ea3dd --- /dev/null +++ b/locales/id-JW/sangmata.json @@ -0,0 +1,13 @@ +{ + "no_uname": "Ora Username", + "no_last_name": "Ora Ana Jeneng mburi", + "uname_change_msg": "✨ Username diganti saka {bef} ➡️ {aft}.\n", + "lastname_change_msg": "✨ Ganti jeneng mburi saka {bef} ➡️ {aft}.\n", + "firstname_change_msg": "✨ Ganti jeneng ngarep saka {bef} ➡️ {aft}.\n", + "set_sangmata_help": "Gunakake /{cmd} on, kanggo ngaktifake sangmata. Yen sampeyan pengin mateni, sampeyan bisa nggunakake parameter mati.", + "sangmata_already_on": "SangMata wis diaktifake ing grup sampeyan.", + "sangmata_enabled": "Sangmata diaktifake ing grup sampeyan.", + "sangmata_already_off": "SangMata wis dipateni ing grup sampeyan.", + "sangmata_disabled": "Sangmata dipateni ing grup sampeyan.", + "wrong_param": "Parameter sing ora dingerteni, gunakake mung parameter aktif/mati." +} \ No newline at end of file diff --git a/locales/id-JW/start_help.json b/locales/id-JW/start_help.json new file mode 100644 index 00000000..afc95879 --- /dev/null +++ b/locales/id-JW/start_help.json @@ -0,0 +1,11 @@ +{ + "newgroup_log": "#NewGroup\nGroup = {jdl}({id})\nJumlah Anggota = {c}", + "newuser_log": "#NewUser\nID - {id}\nJeneng - {nm}", + "help_name": "Iki bantuan kanggo **{mod}**:\n", + "help_txt": "Halo {kamuh}, jenengku {bot}.\nAku bot pyrogram sing dikembangake dening pemilik apik karo sawetara fitur migunani.\nSampeyan bisa ndeleng kanthi ngeklik tombol ing ngisor iki.\n\nPrentah umum yaiku :\n - /start: Miwiti bot\n - /help: Wenehi pesen iki\n - /setlang: Ganti basa bot [BETA]", + "click_me": "Klik Aku", + "back_btn": "Mbalik", + "click_btn": "Klik tombol ing ngisor iki kanggo njaluk bantuan babagan {nm}", + "pm_detail": "PM Aku Kanggo Rincian Liyane.", + "start_msg": "Halo {kamuh}, PM aku kanggo ngerti kabeh fiturku. Sampeyan bisa ngganti basa bot ing bot nggunakake perintah /setlang nanging isih ing tahap beta." +} \ No newline at end of file diff --git a/locales/id-JW/stickers.json b/locales/id-JW/stickers.json new file mode 100644 index 00000000..19e38a7e --- /dev/null +++ b/locales/id-JW/stickers.json @@ -0,0 +1,19 @@ +{ + "no_anim_stick": "Stiker animasi ora didhukung!", + "not_sticker": "Iki dudu stiker!", + "unkang_msg": "Nyoba nyopot saka pack..", + "unkang_success": "Stiker wis dibusak saka pack panjenengan", + "unkang_error": "Gagal mbusak stiker saka paket sampeyan.\n\nERR: {e}", + "unkang_help": "Mangga wangsulana stiker sing digawe {c} kanggo mbusak stiker saka paket sampeyan.", + "anon_warn": "Sampeyan anon admin, kang stiker ing pmku.", + "kang_msg": "Nyolong stiker sampeyan...", + "stick_no_name": "Stiker ora ana jeneng.", + "kang_help": "Arep tak tebak stikere? Mangga tag stiker.", + "exist_pack": "Nganggo paket stiker sing ana...", + "new_packs": "Nggawe pak stiker anyar...", + "please_start_msg": "Koyone sampeyan ora tau sesambungan karo aku ing obrolan pribadi, sampeyan kudu nglakoni dhisik..", + "click_me": "Klik Aku", + "pack_full": "Paket Stiker sampeyan kebak yen paket sampeyan ora ana ing tipe v1 /kang 1, yen ora ana ing Tipe v2 /kang 2 lan sapiturute.", + "viewpack": "👀 Deleng Paket", + "kang_success": "Stiker kasil dicolong!\nEmoji: {emot}" +} \ No newline at end of file diff --git a/locales/id-JW/web_scraper.json b/locales/id-JW/web_scraper.json new file mode 100644 index 00000000..741b8894 --- /dev/null +++ b/locales/id-JW/web_scraper.json @@ -0,0 +1,20 @@ +{ + "no_result": "Nuwun sewu, kula mboten nemu kasil!", + "no_result_w_query": "Nuwun sewu, kula mboten saged manggihaken pitakon: {kueri}", + "get_data": "⏳ Mangga dienteni, lagi njupuk data saka web..", + "cl_btn": "❌ Tutup", + "back_btn": "↩️ Mbalik", + "dl_text": "⬇️ Unduh", + "cat_text": "💠 Kategori", + "kualitas": "Kwalitas", + "ex_data": "👇 Ekstrak Data ", + "unauth": "Tombol iki dudu kanggo sampeyan..", + "invalid_cb": "Data callback ora valid, kirim printah maneh..", + "res_scrape": "Asil scrape saka {link}:\n\n{kl}", + "header_with_query": "Asil Nggoleki #{web} Kanggo: {kueri}\n\n", + "header_no_query": "#{web} Paling anyar:\n🌀 Gunakake /{cmd} [judhul] kanggo miwiti nggoleki kanthi judhul.\n\n", + "invalid_cmd_scrape": "Gunakake prentah /{cmd} [link] kanggo ngunduh pranala kethokan", + "err_getweb": "ERROR: Gagal njupuk data saka web amarga {err}.", + "err_getapi": "ERROR: Gagal njupuk data saka API", + "unsupport_dl_btn": "Sawetara asil ora bakal katon ing tombol ekstrak amarga pranala ora didhukung." +} \ No newline at end of file diff --git a/locales/id-JW/webss.json b/locales/id-JW/webss.json new file mode 100644 index 00000000..0fe23ca1 --- /dev/null +++ b/locales/id-JW/webss.json @@ -0,0 +1,6 @@ +{ + "no_url": "Wenehana url kanggo njupuk screenshot.", + "wait_str": "Lagi njupuk gambar layar...", + "str_credit": "🌞 Screenshot digawe nganggo Puppeteer", + "ss_failed_str": "Gagal njupuk gambar. ERROR: {err}" +} \ No newline at end of file diff --git a/locales/id-JW/ytdl_plugins.json b/locales/id-JW/ytdl_plugins.json new file mode 100644 index 00000000..9b630867 --- /dev/null +++ b/locales/id-JW/ytdl_plugins.json @@ -0,0 +1,14 @@ +{ + "no_channel": "Fitur iki ora didhukung kanggo channel utawa pangguna anonim.", + "no_query": "Mangga lebokna url sing kepengen mbok unduh..!", + "no_res": "Ora ana asil kanggo `{kweri}`", + "dl_btn": "Ngunduh", + "back": "Mbalik", + "yts_msg": "Diterbitake {pub}\n\n❯ Duration: {dur}\n❯ Ndeleng: {vi}\n❯ Uploader: {cname}\n\n", + "invalid_link": "Mangga lebokna URL Dhukungan YT-DLP sing valid", + "err_parse": "Gagal ngurai URL, mriksa log..", + "ngenteni": "Tulung ngenteni..", + "unauth": "Ora Tugasmu..", + "endlist": "Iku pungkasan dhaptar", + "vip-btn": "Amarga sawetara pangguna penyalahgunaan, video paling apik saiki mung sing nduwe bot iki." +} \ No newline at end of file diff --git a/misskaty/__init__.py b/misskaty/__init__.py new file mode 100644 index 00000000..53852a03 --- /dev/null +++ b/misskaty/__init__.py @@ -0,0 +1,79 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import time +from logging import ERROR, INFO, StreamHandler, basicConfig, getLogger, handlers + +from apscheduler.jobstores.mongodb import MongoDBJobStore +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from async_pymongo import AsyncClient +from pymongo import MongoClient +from pyrogram import Client + +from misskaty.core import misskaty_patch +from misskaty.vars import ( + API_HASH, + API_ID, + BOT_TOKEN, + DATABASE_NAME, + DATABASE_URI, + TZ, + USER_SESSION, +) + +basicConfig( + level=INFO, + format="[%(asctime)s - %(levelname)s] - %(name)s.%(funcName)s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", + handlers=[ + handlers.RotatingFileHandler("MissKatyLogs.txt", mode="w+", maxBytes=1000000), + StreamHandler(), + ], +) +getLogger("pyrogram").setLevel(ERROR) +getLogger("openai").setLevel(ERROR) +getLogger("httpx").setLevel(ERROR) + +MOD_LOAD = [] +MOD_NOLOAD = ["subscene_dl"] +HELPABLE = {} +cleanmode = {} +botStartTime = time.time() +misskaty_version = "v2.10.13 - Stable" + +# Pyrogram Bot Client +app = Client( + "MissKatyBot", + api_id=API_ID, + api_hash=API_HASH, + bot_token=BOT_TOKEN, + mongodb=dict(connection=AsyncClient(DATABASE_URI), remove_peers=False), +) + +# Pyrogram UserBot Client +user = Client( + "YasirUBot", + session_string=USER_SESSION, +) + +jobstores = { + "default": MongoDBJobStore( + client=MongoClient(DATABASE_URI), database=DATABASE_NAME, collection="nightmode" + ) +} +scheduler = AsyncIOScheduler(jobstores=jobstores, timezone=TZ) + +app.start() +BOT_ID = app.me.id +BOT_NAME = app.me.first_name +BOT_USERNAME = app.me.username +if USER_SESSION: + user.start() + UBOT_ID = user.me.id + UBOT_NAME = user.me.first_name + UBOT_USERNAME = user.me.username +else: + UBOT_ID = None + UBOT_NAME = None + UBOT_USERNAME = None diff --git a/misskaty/__main__.py b/misskaty/__main__.py new file mode 100644 index 00000000..f174e681 --- /dev/null +++ b/misskaty/__main__.py @@ -0,0 +1,97 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved + """ +import asyncio +import importlib +import os +import pickle +import traceback +from logging import getLogger + +from pyrogram import __version__, idle +from pyrogram.raw.all import layer + +from database import dbname +from misskaty import BOT_NAME, BOT_USERNAME, HELPABLE, UBOT_NAME, app, scheduler +from misskaty.plugins import ALL_MODULES +from misskaty.plugins.web_scraper import web +from misskaty.vars import SUDO, USER_SESSION +from utils import auto_clean + +LOGGER = getLogger(__name__) +loop = asyncio.get_event_loop() + + +# Run Bot +async def start_bot(): + 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 + LOGGER.info("+===============================================================+") + LOGGER.info("| MissKatyPyro |") + LOGGER.info("+===============+===============+===============+===============+") + LOGGER.info(bot_modules) + LOGGER.info("+===============+===============+===============+===============+") + LOGGER.info("[INFO]: BOT STARTED AS @%s!", BOT_USERNAME) + + try: + LOGGER.info("[INFO]: SENDING ONLINE STATUS") + for i in SUDO: + if USER_SESSION: + await app.send_message( + i, + f"USERBOT AND BOT STARTED with Pyrogram v{__version__}..\nUserBot: {UBOT_NAME}\nBot: {BOT_NAME}\n\nwith Pyrogram v{__version__} (Layer {layer}) started on @{BOT_USERNAME}.\n\n{bot_modules}", + ) + else: + await app.send_message( + i, + f"BOT STARTED with Pyrogram v{__version__} as {BOT_NAME}\n\nwith Pyrogram v{__version__} (Layer {layer}) started on @{BOT_USERNAME}.\n\n{bot_modules}", + ) + except Exception as e: + LOGGER.error(str(e)) + scheduler.start() + if "web" not in await dbname.list_collection_names(): + webdb = dbname.web + for key, value in web.items(): + await webdb.insert_one({key: value}) + if os.path.exists("restart.pickle"): + with open("restart.pickle", "rb") as status: + chat_id, message_id = pickle.load(status) + os.remove("restart.pickle") + await app.edit_message_text( + chat_id=chat_id, + message_id=message_id, + text="Bot restarted successfully!", + ) + asyncio.create_task(auto_clean()) + await idle() + + +if __name__ == "__main__": + try: + loop.run_until_complete(start_bot()) + except KeyboardInterrupt: + pass + except Exception: + err = traceback.format_exc() + LOGGER.info(err) + finally: + loop.stop() + LOGGER.info( + "------------------------ Stopped Services ------------------------" + ) diff --git a/misskaty/core/decorator/__init__.py b/misskaty/core/decorator/__init__.py new file mode 100644 index 00000000..852264de --- /dev/null +++ b/misskaty/core/decorator/__init__.py @@ -0,0 +1,13 @@ +from .errors import capture_err +from .misc import asyncify, new_task +from .permissions import adminsOnly, require_admin +from .ratelimiter import ratelimiter + +__all__ = [ + "capture_err", + "asyncify", + "new_task", + "adminsOnly", + "require_admin", + "ratelimiter", +] diff --git a/misskaty/core/decorator/errors.py b/misskaty/core/decorator/errors.py new file mode 100644 index 00000000..9a587841 --- /dev/null +++ b/misskaty/core/decorator/errors.py @@ -0,0 +1,55 @@ +import os +import traceback +from datetime import datetime +from functools import wraps + +from pyrogram.errors.exceptions.forbidden_403 import ChatWriteForbidden +from pyrogram.types import CallbackQuery + +from misskaty.vars import LOG_CHANNEL + + +def capture_err(func): + @wraps(func) + async def capture(client, message, *args, **kwargs): + if isinstance(message, CallbackQuery): + sender = message.message.reply + chat = message.message.chat + msg = message.message.text or message.message.caption + else: + sender = message.reply + chat = message.chat + msg = message.text or message.caption + try: + return await func(client, message, *args, **kwargs) + except ChatWriteForbidden: + return await client.leave_chat(message.chat.id) + except Exception as err: + exc = traceback.format_exc() + error_feedback = "ERROR | {} | {}\n\n{}\n\n{}\n".format( + message.from_user.id if message.from_user else 0, + chat.id if chat else 0, + msg, + exc, + ) + day = datetime.now() + tgl_now = datetime.now() + + cap_day = f"{day.strftime('%A')}, {tgl_now.strftime('%d %B %Y %H:%M:%S')}" + await sender( + "😭 An Internal Error Occurred while processing your Command, the Logs have been sent to the Owners of this Bot. Sorry for Inconvenience..." + ) + with open( + f"crash_{tgl_now.strftime('%d %B %Y')}.txt", "w+", encoding="utf-8" + ) as log: + log.write(error_feedback) + log.close() + await client.send_document( + LOG_CHANNEL, + f"crash_{tgl_now.strftime('%d %B %Y')}.txt", + caption=f"Crash Report of this Bot\n{cap_day}", + ) + os.remove(f"crash_{tgl_now.strftime('%d %B %Y')}.txt") + raise err + + return capture diff --git a/misskaty/core/decorator/misc.py b/misskaty/core/decorator/misc.py new file mode 100644 index 00000000..12e4be50 --- /dev/null +++ b/misskaty/core/decorator/misc.py @@ -0,0 +1,28 @@ +import asyncio +import logging +from functools import wraps + +LOGGER = logging.getLogger(__name__) + + +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 new_task(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + loop = asyncio.get_running_loop() + return loop.create_task(func(*args, **kwargs)) + except Exception as e: + LOGGER.error( + f"Failed to create task for {func.__name__} : {e}" + ) # skipcq: PYL-E0602 + + return wrapper diff --git a/misskaty/core/decorator/permissions.py b/misskaty/core/decorator/permissions.py new file mode 100644 index 00000000..87718924 --- /dev/null +++ b/misskaty/core/decorator/permissions.py @@ -0,0 +1,221 @@ +from functools import partial, wraps +from time import time +from traceback import format_exc as err +from typing import Optional, Union + +from pyrogram import Client, enums +from pyrogram.errors import ChannelPrivate, ChatAdminRequired, ChatWriteForbidden +from pyrogram.types import CallbackQuery, Message + +from misskaty import app +from misskaty.helper import Cache +from misskaty.vars import SUDO + +from ...helper.localization import ( + default_language, + get_lang, + get_locale_string, + langdict, +) + + +async def member_permissions(chat_id: int, user_id: int, client): + perms = [] + try: + member = (await client.get_chat_member(chat_id, user_id)).privileges + if member.can_post_messages: + perms.append("can_post_messages") + if member.can_edit_messages: + perms.append("can_edit_messages") + if member.can_delete_messages: + perms.append("can_delete_messages") + if member.can_restrict_members: + perms.append("can_restrict_members") + if member.can_promote_members: + perms.append("can_promote_members") + if member.can_change_info: + perms.append("can_change_info") + if member.can_invite_users: + perms.append("can_invite_users") + if member.can_pin_messages: + perms.append("can_pin_messages") + if member.can_manage_video_chats: + perms.append("can_manage_video_chats") + return perms + except: + return [] + + +async def check_perms( + message: Union[CallbackQuery, Message], + permissions: Optional[Union[list, str]], + complain_missing_perms: bool, + strings, +) -> bool: + if isinstance(message, CallbackQuery): + sender = partial(message.answer, show_alert=True) + chat = message.message.chat + else: + sender = message.reply_text + chat = message.chat + if not message.from_user: + return bool(message.sender_chat and message.sender_chat.id == message.chat.id) + try: + user = await chat.get_member(message.from_user.id) + except ChatAdminRequired: + return False + if user.status == enums.ChatMemberStatus.OWNER: + return True + + # No permissions specified, accept being an admin. + if not permissions and user.status == enums.ChatMemberStatus.ADMINISTRATOR: + return True + if user.status != enums.ChatMemberStatus.ADMINISTRATOR: + if complain_missing_perms: + await sender(strings("no_admin_error")) + return False + + if isinstance(permissions, str): + permissions = [permissions] + + missing_perms = [ + permission + for permission in permissions + if not getattr(user.privileges, permission) + ] + + if not missing_perms: + return True + if complain_missing_perms: + await sender( + strings("no_permission_error").format(permissions=", ".join(missing_perms)) + ) + return False + + +admins_in_chat = Cache(filename="admin_cache.db", path="cache", in_memory=False) + + +async def list_admins(chat_id: int): + 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"] + + try: + admins_in_chat.add( + chat_id, + { + "last_updated_at": time(), + "data": [ + member.user.id + async for member in app.get_chat_members( + chat_id, filter=enums.ChatMembersFilter.ADMINISTRATORS + ) + ], + }, + timeout=6 * 60 * 60, + ) + return admins_in_chat[chat_id]["data"] + except ChannelPrivate: + return + + +async def authorised(func, subFunc2, client, message, *args, **kwargs): + try: + await func(client, message, *args, **kwargs) + except ChatWriteForbidden: + await message.chat.leave() + 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): + text = f"You don't have the required permission to perform this action.\n**Permission:** __{permission}__" + try: + await message.reply_text(text) + except ChatWriteForbidden: + await message.chat.leave() + 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, client) + 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 + + +def require_admin( + permissions: Union[list, str] = None, + allow_in_private: bool = False, + complain_missing_perms: bool = True, +): + def decorator(func): + @wraps(func) + async def wrapper( + client: Client, message: Union[CallbackQuery, Message], *args, **kwargs + ): + lang = await get_lang(message) + strings = partial( + get_locale_string, + langdict[lang].get("admin", langdict[default_language]["admin"]), + lang, + "admin", + ) + + if isinstance(message, CallbackQuery): + sender = partial(message.answer, show_alert=True) + msg = message.message + elif isinstance(message, Message): + sender = message.reply_text + msg = message + else: + raise NotImplementedError( + f"require_admin can't process updates with the type '{message.__name__}' yet." + ) + + # We don't actually check private and channel chats. + if msg.chat.type == enums.ChatType.PRIVATE: + if allow_in_private: + return await func(client, message, *args, *kwargs) + return await sender(strings("private_not_allowed")) + if msg.chat.type == enums.ChatType.CHANNEL: + return await func(client, message, *args, *kwargs) + has_perms = await check_perms( + message, permissions, complain_missing_perms, strings + ) + if has_perms: + return await func(client, message, *args, *kwargs) + + return wrapper + + return decorator diff --git a/misskaty/core/decorator/ratelimiter.py b/misskaty/core/decorator/ratelimiter.py new file mode 100644 index 00000000..bc1e69ab --- /dev/null +++ b/misskaty/core/decorator/ratelimiter.py @@ -0,0 +1,48 @@ +from functools import wraps +from typing import Callable, Union + +from cachetools import TTLCache +from pyrogram import Client +from pyrogram.errors import QueryIdInvalid +from pyrogram.types import CallbackQuery, Message + +from ..ratelimiter_func import RateLimiter + +ratelimit = RateLimiter() +# storing spammy user in cache for 1minute before allowing them to use commands again. +warned_users = TTLCache(maxsize=128, ttl=60) +warning_message = "Spam detected! ignoring your all requests for few minutes." + + +def ratelimiter(func: Callable) -> Callable: + """ + Restricts user's from spamming commands or pressing buttons multiple times + using leaky bucket algorithm and pyrate_limiter. + """ + + @wraps(func) + async def decorator(client: Client, update: Union[Message, CallbackQuery]): + userid = update.from_user.id if update.from_user else update.sender_chat.id + is_limited = await ratelimit.acquire(userid) + + if is_limited and userid not in warned_users: + if isinstance(update, Message): + await update.reply_text(warning_message) + warned_users[userid] = 1 + return + + elif isinstance(update, CallbackQuery): + try: + await update.answer(warning_message, show_alert=True) + except QueryIdInvalid: + warned_users[userid] = 1 + return + warned_users[userid] = 1 + return + + elif is_limited and userid in warned_users: + pass + else: + return await func(client, update) + + return decorator diff --git a/misskaty/core/keyboard.py b/misskaty/core/keyboard.py new file mode 100644 index 00000000..e80a1cc6 --- /dev/null +++ b/misskaty/core/keyboard.py @@ -0,0 +1,31 @@ +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/core/misskaty_patch/__init__.py b/misskaty/core/misskaty_patch/__init__.py new file mode 100644 index 00000000..26a3e98a --- /dev/null +++ b/misskaty/core/misskaty_patch/__init__.py @@ -0,0 +1 @@ +from . import bound, decorators, listen, methods diff --git a/misskaty/core/misskaty_patch/bound/__init__.py b/misskaty/core/misskaty_patch/bound/__init__.py new file mode 100644 index 00000000..416f4b66 --- /dev/null +++ b/misskaty/core/misskaty_patch/bound/__init__.py @@ -0,0 +1,2 @@ +# skipcq +from .message import Message diff --git a/misskaty/core/misskaty_patch/bound/message.py b/misskaty/core/misskaty_patch/bound/message.py new file mode 100644 index 00000000..b21bc12d --- /dev/null +++ b/misskaty/core/misskaty_patch/bound/message.py @@ -0,0 +1,324 @@ +import html +import io +from asyncio import get_event_loop +from asyncio import sleep as asleep +from logging import getLogger +from typing import Union + +from pyrogram.errors import ( + ChatAdminRequired, + ChatWriteForbidden, + FloodWait, + MessageAuthorRequired, + MessageDeleteForbidden, + MessageIdInvalid, + MessageNotModified, + MessageTooLong, + TopicClosed, + ChatSendPlainForbidden +) +from pyrogram.types import Message + +LOGGER = getLogger(__name__) + +Message.input = property( + lambda m: m.text[m.text.find(m.command[0]) + len(m.command[0]) + 1 :] + if len(m.command) > 1 + else None +) + + +async def reply_text( + self: Message, text: str, as_raw: bool = False, del_in: int = 0, *args, **kwargs +) -> Union["Message", bool]: + """\nExample: + message.reply_msg("hello") + Parameters: + text (``str``): + Text of the message to be sent. + del_in (``int``): + Time in Seconds for delete that message. + quote (``bool``, *optional*): + If ``True``, the message will be sent as + a reply to this message. + If *reply_to_message_id* is passed, + this parameter will be ignored. + Defaults to ``True`` in group chats + and ``False`` in private chats. + parse_mode (:obj:`enums.ParseMode`, *optional*): + By default, texts are parsed using both + Markdown and HTML styles. + You can combine both syntaxes together. + Pass "markdown" or "md" to enable + Markdown-style parsing only. + Pass "html" to enable HTML-style parsing only. + Pass None to completely disable style parsing. + disable_web_page_preview (``bool``, *optional*): + Disables link previews for links in this message. + disable_notification (``bool``, *optional*): + Sends the message silently. + Users will receive a notification with no sound. + reply_to_message_id (``int``, *optional*): + If the message is a reply, ID of the original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. Unix time. + protect_content (``bool``, *optional*): + Protects the contents of the sent message from forwarding and saving. + reply_markup (:obj:`InlineKeyboardMarkup` + | :obj:`ReplyKeyboardMarkup` | :obj:`ReplyKeyboardRemove` + | :obj:`ForceReply`, *optional*): + Additional interface options. An object for an inline keyboard, + custom reply keyboard, + instructions to remove reply keyboard or to + force a reply from the user. + Returns: + On success, the sent Message or True is returned. + Raises: + RPCError: In case of a Telegram RPC error. + """ + try: + if as_raw: + msg = await self.reply_text( + text=f"{html.escape(text.html)}", *args, **kwargs + ) + else: + msg = await self.reply_text(text=text, *args, **kwargs) + if del_in == 0: + return msg + await asleep(del_in) + return bool(await msg.delete_msg()) + except FloodWait as e: + await asleep(e.value) + return await reply_text(self, text, *args, **kwargs) + except TopicClosed: + return + except (ChatWriteForbidden, ChatAdminRequired, ChatSendPlainForbidden): + LOGGER.info( + f"Leaving from {self.chat.title} [{self.chat.id}] because doesn't have enough permission." + ) + return await self.chat.leave() + + +async def edit_text( + self, text: str, del_in: int = 0, *args, **kwargs +) -> Union["Message", bool]: + """\nExample: + message.edit_msg("hello") + Parameters: + text (``str``): + New text of the message. + del_in (``int``): + Time in Seconds for delete that message. + parse_mode (:obj:`enums.ParseMode`, *optional*): + By default, texts are parsed using + both Markdown and HTML styles. + You can combine both syntaxes together. + Pass "markdown" or "md" to enable + Markdown-style parsing only. + Pass "html" to enable HTML-style parsing only. + Pass None to completely disable style parsing. + disable_web_page_preview (``bool``, *optional*): + Disables link previews for links in this message. + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + Returns: + On success, the edited + :obj:`Message` or True is returned. + Raises: + RPCError: In case of a Telegram RPC error. + """ + try: + msg = await self.edit_text(text, *args, **kwargs) + if del_in == 0: + return msg + await asleep(del_in) + return bool(await msg.delete_msg()) + except FloodWait as e: + LOGGER.warning(str(e)) + await asleep(e.value) + return await edit_text(self, text, *args, **kwargs) + except MessageNotModified: + return False + except (ChatWriteForbidden, ChatAdminRequired): + LOGGER.info( + f"Leaving from {self.chat.title} [{self.chat.id}] because doesn't have admin permission." + ) + return await self.chat.leave() + except (MessageAuthorRequired, MessageIdInvalid): + return await reply_text(self, text=text, *args, **kwargs) + + +async def edit_or_send_as_file( + self, text: str, del_in: int = 0, as_raw: bool = False, *args, **kwargs +) -> Union["Message", bool]: + """\nThis will first try to message.edit. + If it raises MessageTooLong error, + run message.send_as_file. + Example: + message.edit_or_send_as_file("some huge text") + Parameters: + text (``str``): + New text of the message. + del_in (``int``): + Time in Seconds for delete that message. + log (``bool`` | ``str``, *optional*): + If ``True``, the message will be forwarded + to the log channel. + If ``str``, the logger name will be updated. + sudo (``bool``, *optional*): + If ``True``, sudo users supported. + as_raw (``bool``, *optional*): + If ``False``, the message will be escaped with current parse mode. + default to ``False``. + parse_mode (:obj:`enums.ParseMode`, *optional*): + By default, texts are parsed using both + Markdown and HTML styles. + You can combine both syntaxes together. + Pass "markdown" or "md" to enable + Markdown-style parsing only. + Pass "html" to enable HTML-style parsing only. + Pass None to completely disable style parsing. + disable_web_page_preview (``bool``, *optional*): + Disables link previews for links in this message. + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + **kwargs (for message.send_as_file) + Returns: + On success, the edited + :obj:`Message` or True is returned. + Raises: + RPCError: In case of a Telegram RPC error. + """ + text = html.escape(text.html) if as_raw else text + try: + msg = await edit_text(self, text=text, *args, **kwargs) + if del_in == 0: + return msg + await asleep(del_in) + return bool(await msg.delete_msg()) + except (MessageTooLong, OSError): + return await reply_as_file(self, text=text, *args, **kwargs) + + +async def reply_or_send_as_file( + self, text: str, as_raw: bool = False, del_in: int = 0, *args, **kwargs +) -> Union["Message", bool]: + """\nThis will first try to message.reply. + If it raise MessageTooLong error, + run message.send_as_file. + Example: + message.reply_or_send_as_file("some huge text") + Parameters: + text (``str``): + Text of the message to be sent. + del_in (``int``): + Time in Seconds for delete that message. + quote (``bool``, *optional*): + If ``True``, the message will be sent + as a reply to this message. + If *reply_to_message_id* is passed, + this parameter will be ignored. + Defaults to ``True`` in group chats + and ``False`` in private chats. + parse_mode (:obj:`enums.ParseMode`, *optional*): + By default, texts are parsed using + both Markdown and HTML styles. + You can combine both syntaxes together. + Pass "markdown" or "md" to enable + Markdown-style parsing only. + Pass "html" to enable HTML-style parsing only. + Pass None to completely disable style parsing. + disable_web_page_preview (``bool``, *optional*): + Disables link previews for links in this message. + disable_notification (``bool``, *optional*): + Sends the message silently. + Users will receive a notification with no sound. + reply_to_message_id (``int``, *optional*): + If the message is a reply, ID of the + original message. + reply_markup (:obj:`InlineKeyboardMarkup` + | :obj:`ReplyKeyboardMarkup` | :obj:`ReplyKeyboardRemove` + | :obj:`ForceReply`, *optional*): + Additional interface options. An object for an + inline keyboard, custom reply keyboard, + instructions to remove reply keyboard + or to force a reply from the user. + **kwargs (for message.send_as_file) + Returns: + On success, the sent Message or True is returned. + Raises: + RPCError: In case of a Telegram RPC error. + """ + text = html.escape(text.html) if as_raw else text + try: + return await reply_text(self, text=text, del_in=del_in, *args, **kwargs) + except MessageTooLong: + return await reply_as_file(self, text=text, **kwargs) + + +async def reply_as_file( + self, + text: str, + filename: str = "output.txt", + caption: str = "", + delete_message: bool = True, +): + """\nYou can send large outputs as file + Example: + message.reply_as_file(text="hello") + Parameters: + text (``str``): + Text of the message to be sent. + filename (``str``, *optional*): + file_name for output file. + caption (``str``, *optional*): + caption for output file. + delete_message (``bool``, *optional*): + If ``True``, the message will be deleted + after sending the file. + Returns: + On success, the sent Message is returned. + """ + reply_to_id = self.reply_to_message.id if self.reply_to_message else self.id + if delete_message: + get_event_loop().create_task(self.delete()) + doc = io.BytesIO(text.encode()) + doc.name = filename + return await self.reply_document( + document=doc, + caption=caption[:1024], + disable_notification=True, + reply_to_message_id=reply_to_id, + ) + + +async def delete(self, revoke: bool = True) -> bool: + """\nThis will first try to delete and ignore + it if it raises MessageDeleteForbidden + Parameters: + revoke (``bool``, *optional*): + Deletes messages on both parts. + This is only for private cloud chats and normal groups, messages on + channels and supergroups are always revoked (i.e.: deleted for everyone). + Defaults to True. + Returns: + True on success, False otherwise. + """ + try: + return bool(await self.delete(revoke=revoke)) + except FloodWait as e: + LOGGER.warning(str(e)) + await asleep(e.value) + return await delete(self, revoke) + except MessageDeleteForbidden: + return False + except Exception as e: + LOGGER.warning(str(e)) + + +Message.reply_msg = reply_text +Message.edit_msg = edit_text +Message.edit_or_send_as_file = edit_or_send_as_file +Message.reply_or_send_as_file = reply_or_send_as_file +Message.reply_as_file = reply_as_file +Message.delete_msg = delete diff --git a/misskaty/core/misskaty_patch/decorators/__init__.py b/misskaty/core/misskaty_patch/decorators/__init__.py new file mode 100644 index 00000000..a676061f --- /dev/null +++ b/misskaty/core/misskaty_patch/decorators/__init__.py @@ -0,0 +1,5 @@ +from .adminsOnly import adminsOnly +from .callback import callback +from .command import command + +__all__ = ["callback", "command", "adminsOnly"] diff --git a/misskaty/core/misskaty_patch/decorators/adminsOnly.py b/misskaty/core/misskaty_patch/decorators/adminsOnly.py new file mode 100644 index 00000000..02360502 --- /dev/null +++ b/misskaty/core/misskaty_patch/decorators/adminsOnly.py @@ -0,0 +1,199 @@ +# tgEasy - Easy for a brighter Shine. A monkey pather add-on for Pyrogram +# Copyright (C) 2021 - 2022 Jayant Hegde Kageri + +# This file is part of tgEasy. + +# tgEasy is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# tgEasy 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 Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public License +# along with tgEasy. If not, see . + +import contextlib +import typing + +import pyrogram +from cachetools import TTLCache +from pyrogram.methods import Decorators + +from ..utils import check_rights, handle_error, is_admin + +ANON = TTLCache(maxsize=250, ttl=30) + + +async def anonymous_admin(m: pyrogram.types.Message): + """ + Helper function for Anonymous Admin Verification + """ + keyboard = pyrogram.types.InlineKeyboardMarkup( + [ + [ + pyrogram.types.InlineKeyboardButton( + text="Verify!", + callback_data=f"anon.{m.id}", + ), + ] + ] + ) + return await m.reply_text( + "Click here to prove you are admin with the required rights to perform this action!", + reply_markup=keyboard, + ) + + +async def anonymous_admin_verification( + self, CallbackQuery: pyrogram.types.CallbackQuery +): + if int( + f"{CallbackQuery.message.chat.id}{CallbackQuery.data.split('.')[1]}" + ) not in set(ANON.keys()): + try: + await CallbackQuery.message.edit_text("Button has been Expired.") + except pyrogram.types.RPCError: + with contextlib.suppress(pyrogram.types.RPCError): + await CallbackQuery.message.delete() + return + cb = ANON.pop( + int(f"{CallbackQuery.message.chat.id}{CallbackQuery.data.split('.')[1]}") + ) + member = await CallbackQuery.message.chat.get_member(CallbackQuery.from_user.id) + if member.status not in ( + pyrogram.enums.ChatMemberStatus.OWNER, + pyrogram.enums.ChatMemberStatus.ADMINISTRATOR, + ): + return await CallbackQuery.answer("You need to be an admin to do this.") + permission = cb[2] + + if isinstance(permission, str) and not await check_rights( + CallbackQuery.message.chat.id, + CallbackQuery.from_user.id, + permission, + client=self, + ): + return await CallbackQuery.message.edit_text( + f"You are Missing the following Rights to use this Command:\n{permission}", + ) + if isinstance(permission, list): + permissions = "" + for perm in permission: + if not await check_rights( + CallbackQuery.message.chat.id, + CallbackQuery.from_user.id, + perm, + client=self, + ): + permissions += f"\n{perm}" + if permissions != "": + return await CallbackQuery.message.edit_text( + f"You are Missing the following Rights to use this Command:{permissions}", + ) + try: + await CallbackQuery.message.delete() + await cb[1](self, cb[0]) + except pyrogram.errors.exceptions.forbidden_403.ChatAdminRequired: + return await CallbackQuery.message.edit_text( + "I must be admin to execute this Command", + ) + except BaseException as e: + return await handle_error(e, CallbackQuery) + + +def adminsOnly( + self, + permission: typing.Union[str, list], + TRUST_ANON_ADMINS: typing.Union[bool, bool] = False, +): + """ + # `tgEasy.tgClient.adminsOnly` + - A decorater for running the function only if the admin have the specified Rights. + - If the admin is Anonymous Admin, it also checks his rights by making a Callback. + - Parameters: + - permission (str): + - Permission which the User must have to use the Functions + + - TRUST_ANON_ADMIN (bool) **optional**: + - If the user is an Anonymous Admin, then it bypasses his right check. + + # Example + .. code-block:: python + from tgEasy import tgClient + import pyrogram + + app = tgClient(pyrogram.Client()) + + @app.command("start") + @app.adminsOnly("can_change_info") + async def start(client, message): + await message.reply_text(f"Hello Admin {message.from_user.mention}") + """ + + def wrapper(func): + async def decorator(client, message): + permissions = "" + if message.chat.type != pyrogram.enums.ChatType.SUPERGROUP: + return await message.reply_text( + "This command can be used in supergroups only.", + ) + if message.sender_chat and not TRUST_ANON_ADMINS: + ANON[int(f"{message.chat.id}{message.id}")] = ( + message, + func, + permission, + ) + return await anonymous_admin(message) + if not await is_admin( + message.chat.id, + message.from_user.id, + client=client, + ): + return await message.reply_text( + "Only admins can execute this Command!", + ) + if isinstance(permission, str) and not await check_rights( + message.chat.id, + message.from_user.id, + permission, + client=client, + ): + return await message.reply_text( + f"You are Missing the following Rights to use this Command:\n{permission}", + ) + if isinstance(permission, list): + for perm in permission: + if not await check_rights( + message.chat.id, + message.from_user.id, + perm, + client=client, + ): + permissions += f"\n{perm}" + if permissions != "": + return await message.reply_text( + f"You are Missing the following Rights to use this Command:{permissions}", + ) + try: + await func(client, message) + except pyrogram.errors.exceptions.forbidden_403.ChatWriteForbidden: + await client.leave_chat(message.chat.id) + except BaseException as exception: + await handle_error(exception, message) + + self.add_handler( + pyrogram.handlers.CallbackQueryHandler( + anonymous_admin_verification, + pyrogram.filters.regex("^anon."), + ), + ) + return decorator + + return wrapper + + +Decorators.adminsOnly = adminsOnly diff --git a/misskaty/core/misskaty_patch/decorators/callback.py b/misskaty/core/misskaty_patch/decorators/callback.py new file mode 100644 index 00000000..7f336c1d --- /dev/null +++ b/misskaty/core/misskaty_patch/decorators/callback.py @@ -0,0 +1,102 @@ +# tgEasy - Easy for a brighter Shine. A monkey pather add-on for Pyrogram +# Copyright (C) 2021 - 2022 Jayant Hegde Kageri + +# This file is part of tgEasy. + +# tgEasy is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# tgEasy 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 Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public License +# along with tgEasy. If not, see . + +import typing + +import pyrogram +from pyrogram.methods import Decorators + +from ..utils import handle_error + + +def callback( + self, + data: typing.Union[str, list], + self_admin: typing.Union[bool, bool] = False, + filtercb: typing.Union[pyrogram.filters.Filter] = None, + *args, + **kwargs, +): + """ + ### `Client.callback` + + - A decorater to Register Callback Quiries in simple way and manage errors in that Function itself, alternative for `@pyrogram.Client.on_callback_query(pyrogram.filters.regex('^data.*'))` + - Parameters: + - data (str || list): + - The callback query to be handled for a function + + - self_admin (bool) **optional**: + - If True, the command will only executeed if the Bot is Admin in the Chat, By Default False + + - filter (`~pyrogram.filters`) **optional**: + - Pyrogram Filters, hope you know about this, for Advaced usage. Use `and` for seaperating filters. + + #### Example + .. code-block:: python + import pyrogram + + app = pyrogram.Client() + + @app.command("start") + async def start(client, message): + await message.reply_text( + f"Hello {message.from_user.mention}", + reply_markup=pyrogram.types.InlineKeyboardMarkup([[ + pyrogram.types.InlineKeyboardButton( + "Click Here", + "data" + ) + ]]) + ) + + @app.callback("data") + async def data(client, CallbackQuery): + await CallbackQuery.answer("Hello :)", show_alert=True) + """ + if filtercb: + filtercb = pyrogram.filters.regex(f"^{data}.*") & args["filter"] + else: + filtercb = pyrogram.filters.regex(f"^{data}.*") + + def wrapper(func): + async def decorator(client, CallbackQuery: pyrogram.types.CallbackQuery): + if self_admin: + me = await client.get_chat_member( + CallbackQuery.message.chat.id, (await client.get_me()).id + ) + if me.status not in ( + pyrogram.enums.ChatMemberStatus.OWNER, + pyrogram.enums.ChatMemberStatus.ADMINISTRATOR, + ): + return await CallbackQuery.message.edit_text( + "I must be admin to execute this Command" + ) + try: + await func(client, CallbackQuery) + except pyrogram.errors.exceptions.forbidden_403.ChatAdminRequired: + pass + except BaseException as e: + return await handle_error(e, CallbackQuery) + + self.add_handler(pyrogram.handlers.CallbackQueryHandler(decorator, filtercb)) + return decorator + + return wrapper + + +Decorators.on_cb = callback diff --git a/misskaty/core/misskaty_patch/decorators/command.py b/misskaty/core/misskaty_patch/decorators/command.py new file mode 100644 index 00000000..46198467 --- /dev/null +++ b/misskaty/core/misskaty_patch/decorators/command.py @@ -0,0 +1,133 @@ +import typing + +import pyrogram +from pyrogram.methods import Decorators + +from misskaty.core import pyro_cooldown +from misskaty.vars import COMMAND_HANDLER + +from ..utils import handle_error + + +def command( + self, + cmd: typing.Union[str, list], + is_disabled: typing.Union[bool, bool] = False, + pm_only: typing.Union[bool, bool] = False, + group_only: typing.Union[bool, bool] = False, + self_admin: typing.Union[bool, bool] = False, + self_only: typing.Union[bool, bool] = False, + no_channel: typing.Union[bool, bool] = False, + handler: typing.Optional[list] = None, + filtercmd: typing.Union[pyrogram.filters.Filter] = None, + *args, + **kwargs +): + """ + ### `tgClient.command` + - A decorater to Register Commands in simple way and manage errors in that Function itself, alternative for `@pyrogram.Client.on_message(pyrogram.filters.command('command'))` + - Parameters: + - cmd (str || list): + - The command to be handled for a function + + - group_only (bool) **optional**: + - If True, the command will only executed in Groups only, By Default False. + + - pm_only (bool) **optional**: + - If True, the command will only executed in Private Messages only, By Default False. + + - self_only (bool) **optional**: + - If True, the command will only excute if used by Self only, By Default False. + + - handler (list) **optional**: + - If set, the command will be handled by the specified Handler, By Default `Config.HANDLERS`. + + - self_admin (bool) **optional**: + - If True, the command will only executeed if the Bot is Admin in the Chat, By Default False + + - filtercmd (`~pyrogram.filters`) **optional**: + - Pyrogram Filters, hope you know about this, for Advaced usage. Use `and` for seaperating filters. + + #### Example + .. code-block:: python + import pyrogram + + app = pyrogram.Client() + + @app.on_cmd("start", is_disabled=False, group_only=False, pm_only=False, self_admin=False, self_only=False, pyrogram.filters.chat("777000") and pyrogram.filters.text) + async def start(client, message): + await message.reply_text(f"Hello {message.from_user.mention}") + """ + if handler is None: + handler = COMMAND_HANDLER + if filtercmd: + if self_only: + filtercmd = ( + pyrogram.filters.command(cmd, prefixes=handler) + & filtercmd + & pyrogram.filters.me + ) + else: + filtercmd = ( + pyrogram.filters.command(cmd, prefixes=handler) + & filtercmd + & pyrogram.filters.me + & pyro_cooldown.wait(7) + ) + else: + if self_only: + filtercmd = ( + pyrogram.filters.command(cmd, prefixes=handler) & pyrogram.filters.me + ) + else: + filtercmd = pyrogram.filters.command( + cmd, prefixes=handler + ) & pyro_cooldown.wait(7) + + def wrapper(func): + async def decorator(client, message: pyrogram.types.Message): + if is_disabled: + return await message.reply_text( + "Sorry, this command has been disabled by owner." + ) + if not message.from_user and no_channel: + return await message.reply_text( + "I'm cannot identify user. Use my command in private chat." + ) + if self_admin and message.chat.type != pyrogram.enums.ChatType.SUPERGROUP: + return await message.reply_text( + "This command can be used in supergroups only." + ) + if self_admin: + me = await client.get_chat_member( + message.chat.id, (await client.get_me()).id + ) + if me.status not in ( + pyrogram.enums.ChatMemberStatus.OWNER, + pyrogram.enums.ChatMemberStatus.ADMINISTRATOR, + ): + return await message.reply_text( + "I must be admin to execute this Command" + ) + if group_only and message.chat.type != pyrogram.enums.ChatType.SUPERGROUP: + return await message.reply_text( + "This command can be used in supergroups only." + ) + if pm_only and message.chat.type != pyrogram.enums.ChatType.PRIVATE: + return await message.reply_text("This command can be used in PMs only.") + try: + await func(client, message) + except pyrogram.errors.exceptions.forbidden_403.ChatWriteForbidden: + await client.leave_chat(message.chat.id) + except BaseException as exception: + return await handle_error(exception, message) + + self.add_handler( + pyrogram.handlers.MessageHandler(callback=decorator, filters=filtercmd) + ) + return decorator + + return wrapper + + +Decorators.on_cmd = command diff --git a/misskaty/core/misskaty_patch/listen/__init__.py b/misskaty/core/misskaty_patch/listen/__init__.py new file mode 100644 index 00000000..1657d965 --- /dev/null +++ b/misskaty/core/misskaty_patch/listen/__init__.py @@ -0,0 +1,2 @@ +# skipcq +from .listen import Chat, Client, MessageHandler, User diff --git a/misskaty/core/misskaty_patch/listen/listen.py b/misskaty/core/misskaty_patch/listen/listen.py new file mode 100644 index 00000000..5453e9aa --- /dev/null +++ b/misskaty/core/misskaty_patch/listen/listen.py @@ -0,0 +1,386 @@ +""" +pyromod - A monkeypatcher add-on for Pyrogram +Copyright (C) 2020 Cezar H. + +This file is part of pyromod. + +pyromod 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 3 of the License, or +(at your option) any later version. + +pyromod 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 pyromod. If not, see . +""" + +import asyncio +import logging +from enum import Enum +from inspect import iscoroutinefunction +from typing import Callable, Optional, Union + +import pyrogram +from pyrogram.errors import MessageIdInvalid + +from ..utils import PyromodConfig, patch, patchable + +logger = logging.getLogger(__name__) + + +class ListenerStopped(Exception): + pass + + +class ListenerTimeout(Exception): + pass + + +class ListenerTypes(Enum): + MESSAGE = "message" + CALLBACK_QUERY = "callback_query" + + +@patch(pyrogram.client.Client) +class Client: + @patchable() + def __init__(self, *args, **kwargs): + self.listeners = {listener_type: {} for listener_type in ListenerTypes} + self.old__init__(*args, **kwargs) + + @patchable() + async def listen( + self, + identifier: tuple, + filters=None, + listener_type=ListenerTypes.MESSAGE, + timeout=None, + unallowed_click_alert=True, + ): + if type(listener_type) != ListenerTypes: + raise TypeError( + "Parameter listener_type should be a" + " value from pyromod.listen.ListenerTypes" + ) + + future = self.loop.create_future() + future.add_done_callback( + lambda f: self.stop_listening(identifier, listener_type) + ) + + listener_data = { + "future": future, + "filters": filters, + "unallowed_click_alert": unallowed_click_alert, + } + + self.listeners[listener_type].update({identifier: listener_data}) + + try: + return await asyncio.wait_for(future, timeout) + except asyncio.exceptions.TimeoutError: + if callable(PyromodConfig.timeout_handler): + PyromodConfig.timeout_handler(identifier, listener_data, timeout) + elif PyromodConfig.throw_exceptions: + raise ListenerTimeout(timeout) + + @patchable() + async def ask( + self, + text, + identifier: tuple, + filters=None, + listener_type=ListenerTypes.MESSAGE, + timeout=None, + *args, + **kwargs, + ): + request = await self.send_message(identifier[0], text, *args, **kwargs) + response = await self.listen(identifier, filters, listener_type, timeout) + if response: + response.request = request + + return response + + # needed for matching when message_id or user_id is null, and to take precedence + @patchable() + def match_listener( + self, + data: Optional[tuple] = None, + listener_type: ListenerTypes = ListenerTypes.MESSAGE, + identifier_pattern: Optional[tuple] = None, + ) -> tuple: + if data: + listeners = self.listeners[listener_type] + # case with 3 args on identifier + # most probably waiting for a specific user + # to click a button in a specific message + if data in listeners: + return listeners[data], data + + # cases with 2 args on identifier + # (None, user, message) does not make + # sense since the message_id is not unique + elif (data[0], data[1], None) in listeners: + matched = (data[0], data[1], None) + elif (data[0], None, data[2]) in listeners: + matched = (data[0], None, data[2]) + + # cases with 1 arg on identifier + # (None, None, message) does not make sense as well + elif (data[0], None, None) in listeners: + matched = (data[0], None, None) + elif (None, data[1], None) in listeners: + matched = (None, data[1], None) + else: + return None, None + + return listeners[matched], matched + elif identifier_pattern: + + def match_identifier(pattern, identifier): + comparison = ( + pattern[0] in (identifier[0], None), + pattern[1] in (identifier[1], None), + pattern[2] in (identifier[2], None), + ) + return comparison == (True, True, True) + + for identifier, listener in self.listeners[listener_type].items(): + if match_identifier(identifier_pattern, identifier): + return listener, identifier + return None, None + + @patchable() + def stop_listening( + self, + data: Optional[tuple] = None, + listener_type: ListenerTypes = ListenerTypes.MESSAGE, + identifier_pattern: Optional[tuple] = None, + ): + listener, identifier = self.match_listener( + data, listener_type, identifier_pattern + ) + + if not listener: + return + elif listener["future"].done(): + del self.listeners[listener_type][identifier] + return + + if callable(PyromodConfig.stopped_handler): + PyromodConfig.stopped_handler(identifier, listener) + elif PyromodConfig.throw_exceptions: + listener["future"].set_exception(ListenerStopped()) + + del self.listeners[listener_type][identifier] + + +@patch(pyrogram.handlers.message_handler.MessageHandler) +class MessageHandler: + @patchable() + def __init__(self, callback: Callable, filters=None): + self.registered_handler = callback + self.old__init__(self.resolve_future, filters) + + @patchable() + async def check(self, client, message): + if user := getattr(message, "from_user", None): + user = user.id + try: + listener = client.match_listener( + (message.chat.id, user, message.id), + ListenerTypes.MESSAGE, + )[0] + except AttributeError as err: + logger.warning(f"Get : {err}\n\n{message}") + raise err + + listener_does_match = handler_does_match = False + + if listener: + filters = listener["filters"] + if callable(filters): + if iscoroutinefunction(filters.__call__): + listener_does_match = await filters(client, message) + else: + listener_does_match = await client.loop.run_in_executor( + None, filters, client, message + ) + else: + listener_does_match = True + + if callable(self.filters): + if iscoroutinefunction(self.filters.__call__): + handler_does_match = await self.filters(client, message) + else: + handler_does_match = await client.loop.run_in_executor( + None, self.filters, client, message + ) + else: + handler_does_match = True + + # let handler get the chance to handle if listener + # exists but its filters doesn't match + return listener_does_match or handler_does_match + + @patchable() + async def resolve_future(self, client, message, *args): + listener_type = ListenerTypes.MESSAGE + if user := getattr(message, "from_user", None): + user = user.id + listener, identifier = client.match_listener( + (message.chat.id, user, message.id), + listener_type, + ) + listener_does_match = False + if listener: + filters = listener["filters"] + if callable(filters): + if iscoroutinefunction(filters.__call__): + listener_does_match = await filters(client, message) + else: + listener_does_match = await client.loop.run_in_executor( + None, filters, client, message + ) + else: + listener_does_match = True + + if listener_does_match: + if not listener["future"].done(): + listener["future"].set_result(message) + del client.listeners[listener_type][identifier] + raise pyrogram.StopPropagation + else: + await self.registered_handler(client, message, *args) + + +@patch(pyrogram.handlers.callback_query_handler.CallbackQueryHandler) +class CallbackQueryHandler: + @patchable() + def __init__(self, callback: Callable, filters=None): + self.registered_handler = callback + self.old__init__(self.resolve_future, filters) + + @patchable() + async def check(self, client, query): + chatID, mID = None, None + if message := getattr(query, "message", None): + chatID, mID = message.chat.id, message.id + try: + listener = client.match_listener( + (chatID, query.from_user.id, mID), + ListenerTypes.CALLBACK_QUERY, + )[0] + except AttributeError as err: + logger.warning(f"Get : {err}\n\n{message}") + raise err + + # managing unallowed user clicks + if PyromodConfig.unallowed_click_alert: + permissive_listener = client.match_listener( + identifier_pattern=( + chatID, + None, + mID, + ), + listener_type=ListenerTypes.CALLBACK_QUERY, + )[0] + + if (permissive_listener and not listener) and permissive_listener[ + "unallowed_click_alert" + ]: + alert = ( + permissive_listener["unallowed_click_alert"] + if type(permissive_listener["unallowed_click_alert"]) is str + else PyromodConfig.unallowed_click_alert_text + ) + try: + await query.answer(alert) + except MessageIdInvalid: + pass + return False + + filters = listener["filters"] if listener else self.filters + + if callable(filters): + if iscoroutinefunction(filters.__call__): + return await filters(client, query) + else: + return await client.loop.run_in_executor(None, filters, client, query) + else: + return True + + @patchable() + async def resolve_future(self, client, query, *args): + listener_type = ListenerTypes.CALLBACK_QUERY + chatID, mID = None, None + if message := getattr(query, "message", None): + chatID, mID = message.chat.id, message.id + listener, identifier = client.match_listener( + (chatID, query.from_user.id, mID), + listener_type, + ) + + if listener and not listener["future"].done(): + listener["future"].set_result(query) + del client.listeners[listener_type][identifier] + else: + await self.registered_handler(client, query, *args) + + +@patch(pyrogram.types.messages_and_media.message.Message) +class Message(pyrogram.types.messages_and_media.message.Message): + @patchable() + async def wait_for_click( + self, + from_user_id: Optional[int] = None, + timeout: Optional[int] = None, + filters=None, + alert: Union[str, bool] = True, + ): + return await self._client.listen( + (self.chat.id, from_user_id, self.id), + listener_type=ListenerTypes.CALLBACK_QUERY, + timeout=timeout, + filters=filters, + unallowed_click_alert=alert, + ) + + +@patch(pyrogram.types.user_and_chats.chat.Chat) +class Chat(pyrogram.types.Chat): + @patchable() + def listen(self, *args, **kwargs): + return self._client.listen((self.id, None, None), *args, **kwargs) + + @patchable() + def ask(self, text, *args, **kwargs): + return self._client.ask(text, (self.id, None, None), *args, **kwargs) + + @patchable() + def stop_listening(self, *args, **kwargs): + return self._client.stop_listening( + *args, identifier_pattern=(self.id, None, None), **kwargs + ) + + +@patch(pyrogram.types.user_and_chats.user.User) +class User(pyrogram.types.User): + @patchable() + def listen(self, *args, **kwargs): + return self._client.listen((None, self.id, None), *args, **kwargs) + + @patchable() + def ask(self, text, *args, **kwargs): + return self._client.ask(text, (self.id, self.id, None), *args, **kwargs) + + @patchable() + def stop_listening(self, *args, **kwargs): + return self._client.stop_listening( + *args, identifier_pattern=(None, self.id, None), **kwargs + ) diff --git a/misskaty/core/misskaty_patch/methods/__init__.py b/misskaty/core/misskaty_patch/methods/__init__.py new file mode 100644 index 00000000..3a8c831f --- /dev/null +++ b/misskaty/core/misskaty_patch/methods/__init__.py @@ -0,0 +1,4 @@ +# skipcq +from .edit_message_text import edit_message_text +from .send_as_file import send_as_file +from .send_message import send_message diff --git a/misskaty/core/misskaty_patch/methods/edit_message_text.py b/misskaty/core/misskaty_patch/methods/edit_message_text.py new file mode 100644 index 00000000..019a8344 --- /dev/null +++ b/misskaty/core/misskaty_patch/methods/edit_message_text.py @@ -0,0 +1,61 @@ +import asyncio +from typing import Union + +from pyrogram import Client + + +async def edit_message_text( + self, + chat_id: Union[int, str], + message_id: int, + text: str, + del_in: int = 0, + *args, + **kwargs +) -> Union["Message", bool]: + """\nExample: + message.edit_text("hello") + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) + you can simply use "me" or "self". + For a contact that exists in your Telegram address book + you can use his phone number (str). + message_id (``int``): + Message identifier in the chat specified in chat_id. + text (``str``): + New text of the message. + del_in (``int``): + Time in Seconds for delete that message. + parse_mode (:obj:`enums.ParseMode`, *optional*): + By default, texts are parsed using + both Markdown and HTML styles. + You can combine both syntaxes together. + Pass "markdown" or "md" to enable + Markdown-style parsing only. + Pass "html" to enable HTML-style parsing only. + Pass None to completely disable style parsing. + entities (List of :obj:`~pyrogram.types.MessageEntity`): + List of special entities that appear in message text, + which can be specified instead of *parse_mode*. + disable_web_page_preview (``bool``, *optional*): + Disables link previews for links in this message. + reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): + An InlineKeyboardMarkup object. + Returns: + On success, the edited + :obj:`Message` or True is returned. + Raises: + RPCError: In case of a Telegram RPC error. + """ + msg = await self.edit_message_text( + chat_id=chat_id, message_id=message_id, text=text, *args, **kwargs + ) + if del_in == 0: + return msg + await asyncio.sleep(del_in) + return bool(await msg.delete()) + + +Client.edit_msg_text = edit_message_text diff --git a/misskaty/core/misskaty_patch/methods/send_as_file.py b/misskaty/core/misskaty_patch/methods/send_as_file.py new file mode 100644 index 00000000..b5722140 --- /dev/null +++ b/misskaty/core/misskaty_patch/methods/send_as_file.py @@ -0,0 +1,48 @@ +import io +from typing import Optional, Union + +from pyrogram import Client + + +async def send_as_file( + self, + chat_id: Union[int, str], + text: str, + filename: str = "output.txt", + caption: str = "", + reply_to_message_id: Optional[int] = None, +) -> "Message": + """\nYou can send large outputs as file + Example: + @userge.send_as_file(chat_id=12345, text="hello") + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) + you can simply use "me" or "self". + For a contact that exists in your Telegram address book + you can use his phone number (str). + text (``str``): + Text of the message to be sent. + filename (``str``, *optional*): + file_name for output file. + caption (``str``, *optional*): + caption for output file. + reply_to_message_id (``int``, *optional*): + If the message is a reply, ID of the original message. + Returns: + On success, the sent Message is returned. + """ + doc = io.BytesIO(text.encode()) + doc.name = filename + + return await self.send_document( + chat_id=chat_id, + document=doc, + caption=caption[:1024], + disable_notification=True, + reply_to_message_id=reply_to_message_id, + ) + + +Client.send_as_file = send_as_file diff --git a/misskaty/core/misskaty_patch/methods/send_message.py b/misskaty/core/misskaty_patch/methods/send_message.py new file mode 100644 index 00000000..2a2f4194 --- /dev/null +++ b/misskaty/core/misskaty_patch/methods/send_message.py @@ -0,0 +1,63 @@ +import asyncio +from typing import Union + +from pyrogram import Client + + +async def send_message( + self, chat_id: Union[int, str], text: str, del_in: int = 0, *args, **kwargs +) -> Union["Message", bool]: + """\nSend text messages. + Example: + @userge.send_message(chat_id=12345, text='test') + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) + you can simply use "me" or "self". + For a contact that exists in your Telegram address book + you can use his phone number (str). + text (``str``): + Text of the message to be sent. + del_in (``int``): + Time in Seconds for delete that message. + log (``bool`` | ``str``, *optional*): + If ``True``, the message will be forwarded to the log channel. + If ``str``, the logger name will be updated. + parse_mode (:obj:`enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + Pass "markdown" or "md" to enable Markdown-style parsing only. + Pass "html" to enable HTML-style parsing only. + Pass None to completely disable style parsing. + entities (List of :obj:`~pyrogram.types.MessageEntity`): + List of special entities that appear in message text, + which can be specified instead of *parse_mode*. + disable_web_page_preview (``bool``, *optional*): + Disables link previews for links in this message. + disable_notification (``bool``, *optional*): + Sends the message silently. + Users will receive a notification with no sound. + reply_to_message_id (``int``, *optional*): + If the message is a reply, ID of the original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. Unix time. + protect_content (``bool``, *optional*): + Protects the contents of the sent message from forwarding and saving. + reply_markup (:obj:`InlineKeyboardMarkup` | :obj:`ReplyKeyboardMarkup` + | :obj:`ReplyKeyboardRemove` | :obj:`ForceReply`, *optional*): + Additional interface options. An object for an inline keyboard, + custom reply keyboard, instructions to remove + reply keyboard or to force a reply from the user. + Returns: + :obj:`Message`: On success, the sent text message or True is returned. + """ + msg = await self.send_message(chat_id=chat_id, text=text, *args, **kwargs) + if del_in == 0: + return msg + if del_in > 0: + await asyncio.sleep(del_in) + return bool(await msg.delete()) + + +Client.send_msg = send_message diff --git a/misskaty/core/misskaty_patch/utils/__init__.py b/misskaty/core/misskaty_patch/utils/__init__.py new file mode 100644 index 00000000..4cf62927 --- /dev/null +++ b/misskaty/core/misskaty_patch/utils/__init__.py @@ -0,0 +1,5 @@ +# skipcq +from .admin_utils import check_rights, is_admin +from .get_user import get_user +from .handler_error import handle_error +from .utils import PyromodConfig, patch, patchable diff --git a/misskaty/core/misskaty_patch/utils/admin_utils.py b/misskaty/core/misskaty_patch/utils/admin_utils.py new file mode 100644 index 00000000..d2ca8434 --- /dev/null +++ b/misskaty/core/misskaty_patch/utils/admin_utils.py @@ -0,0 +1,158 @@ +# tgEasy - Easy for a brighter Shine. A monkey pather add-on for Pyrogram +# Copyright (C) 2021 - 2022 Jayant Hegde Kageri + +# This file is part of tgEasy. + +# tgEasy is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# tgEasy 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 Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public License +# along with tgEasy. If not, see . + +import typing + +import pyrogram + + +async def check_rights( + chat_id: typing.Union[int, int], + user_id: typing.Union[int, int], + rights: typing.Union[str, list], + client, +) -> bool: + """ + ### `check_rights` + - Checks the Rights of an User + - This is an Helper Function for `adminsOnly` + + - Parameters: + - chat_id (int): + - The Chat ID of Which Chat have to check the Rights. + + - user_id (int): + - The User ID of Whose Rights have to Check. + + - rights (str): + - The Rights have to Check. + + - client (`pyrogram.Client`): + - From which Client to Check the Rights. + + - Returns: + - `True` if the User have the Right. + - `False` if the User don't have the Right. + + #### Example + .. code-block:: python + import pyrogram + + app = pyrogram.Client() + + @app.command("ban", group_only=True, self_admin=True) + async def ban(client, message): + if not await check_rights(message.chat.id, message.from_user.id, "can_restrict_members"): + return await message.reply_text("You don't have necessary rights to use this Command.") + user = await get_user(message) + await message.chat.kick_member(user.id) + """ + try: + user = await client.get_chat_member(chat_id, user_id) + except Exception: + return False + if user.status == "user": + return False + if user.status in ( + pyrogram.enums.ChatMemberStatus.OWNER, + pyrogram.enums.ChatMemberStatus.ADMINISTRATOR, + ): + permission = [] + if user.privileges.can_manage_chat: + permission.append("can_manage_chat") + + if user.privileges.can_delete_messages: + permission.append("can_delete_messages") + + if user.privileges.can_manage_video_chats: + permission.append("can_manage_video_chats") + + if user.privileges.can_restrict_members: + permission.append("can_restrict_members") + + if user.privileges.can_promote_members: + permission.append("can_promote_members") + + if user.privileges.can_change_info: + permission.append("can_change_info") + + if user.privileges.can_post_messages: + permission.append("can_post_messages") + + if user.privileges.can_edit_messages: + permission.append("can_edit_messages") + + if user.privileges.can_invite_users: + permission.append("can_invite_users") + + if user.privileges.can_pin_messages: + permission.append("can_pin_messages") + + if user.privileges.is_anonymous: + permission.append("is_anonymous") + + if isinstance(rights, str): + return rights in permission + if isinstance(rights, list): + for right in rights: + return right in permission + return False + + +async def is_admin( + chat_id: typing.Union[int, str], user_id: typing.Union[int, str], client +) -> bool: + """ + ### `is_admin` + - A Functions to Check if the User is Admin or not + + - Parameters: + - chat_id (int): + - The Chat ID of Which Chat have to check the Admin Status. + + - user_id (int): + - The User ID of Whose Admin Status have to Check. + + - client (`pyrogram.Client`): + - From which Client to Check the Admin Status. + + - Returns: + - `True` if the User is Admin. + - `False` if the User is't Admin. + #### Example + .. code-block:: python + import pyrogram + + app = pyrogram.Client() + + @app.command("ban", group_only=True, self_admin=True) + @app.adminsOnly("can_restrict_members") + async def ban(client, message): + if await is_admin(message.chat.id, (await get_user(mesasge)).id): + return await message.reply_text("You can't Ban Admins.") + await message.chat.kick_member((await get_user(message)).id) + await message.reply_text("User has been Banned.") + """ + try: + user = await client.get_chat_member(chat_id, user_id) + except Exception: + return False + return user.status in ( + pyrogram.enums.ChatMemberStatus.OWNER, + pyrogram.enums.ChatMemberStatus.ADMINISTRATOR, + ) diff --git a/misskaty/core/misskaty_patch/utils/get_user.py b/misskaty/core/misskaty_patch/utils/get_user.py new file mode 100644 index 00000000..0cf6f0e5 --- /dev/null +++ b/misskaty/core/misskaty_patch/utils/get_user.py @@ -0,0 +1,122 @@ +# tgEasy - Easy for a brighter Shine. A monkey pather add-on for Pyrogram +# Copyright (C) 2021 - 2022 Jayant Hegde Kageri + +# This file is part of tgEasy. + +# tgEasy is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# tgEasy 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 Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public License +# along with tgEasy. If not, see . + +import contextlib +import typing + +import pyrogram + + +async def get_user( + m: typing.Union[pyrogram.types.Message, pyrogram.types.CallbackQuery] +) -> pyrogram.types.User or bool: + """ + ### `tgEasy.get_user` + - Gets a User from Message/RepliedMessage/CallbackQuery + - Parameters: + - m (`~pyrogram.types.Message` || `~pyrogram.types.CallbackQuery`) + - Returns: + - `pyrogram.types.User` on Success + - `False` on Error + + #### Example + .. code-block:: python + from tgEasy import get_user, command, adminsOnly + + @command("ban", group_only=True, self_admin=True) + @adminsOnly("can_restrict_members") + async def ban(client, message): + user = await get_user(message) + await message.chat.kick_member(user.id) + """ + if isinstance(m, pyrogram.types.Message): + message = m + client = m._client + if isinstance(m, pyrogram.types.CallbackQuery): + message = m.message + client = message._client + if message.reply_to_message: + if message.reply_to_message.sender_chat: + return False + return await client.get_users(message.reply_to_message.from_user.id) + + command = message.command[1] if len(message.command) > 1 else None + if command and (command.startswith("@") or command.isdigit()): + with contextlib.suppress( + pyrogram.errors.exceptions.bad_request_400.UsernameNotOccupied, + pyrogram.errors.exceptions.bad_request_400.UsernameInvalid, + pyrogram.errors.exceptions.bad_request_400.PeerIdInvalid, + IndexError, + ): + return await client.get_users(message.command[1]) + if message.entities: + for mention in message.entities: + if mention.type == "text_mention": + user = mention.user.id + break + with contextlib.suppress(Exception): + return await client.get_users(user) + return False + + +async def get_user_adv( + m: typing.Union[pyrogram.types.Message, pyrogram.types.CallbackQuery] +) -> pyrogram.types.User or bool: + """ + ### `tgEasy.get_user_adv` + - A Function to Get the User from the Message/CallbackQuery, If there is None arguments, returns the From User. + - Parameters: + - m (`pyrogram.types.Message` || `pyrogram.types.CallbackQuery`): + - Message or Callbackquery. + - Returns: + - `pyrogram.types.User` on Success + - `False` on Error + + #### Example + .. code-block:: python + from tgEasy import command, get_user_adv + + @command("id") + async def id(client, message): + user = await get_user_adv(message) + await message.reply_text(f"Your ID is `{user.id}`") + """ + if isinstance(m, pyrogram.types.Message): + message = m + if isinstance(m, pyrogram.types.CallbackQuery): + message = m.message + if message.sender_chat: + return False + with contextlib.suppress(IndexError, AttributeError): + if len(message.command) > 1: + if message.command[1].startswith("@"): + return await get_user(message) + if message.command[1].isdigit(): + return await get_user(message) + if "text_mention" in message.entities: + return await get_user(message) + if "from_user" in str(message.reply_to_message): + return await get_user(message) + with contextlib.suppress(Exception): + if "sender_chat" in str(message.reply_to_message): + return False + if "from_user" in str(message.reply_to_message): + return await message._client.get_users( + message.reply_to_message.from_user.id + ) + return await message._client.get_users(message.from_user.id) diff --git a/misskaty/core/misskaty_patch/utils/handler_error.py b/misskaty/core/misskaty_patch/utils/handler_error.py new file mode 100644 index 00000000..89c7f97e --- /dev/null +++ b/misskaty/core/misskaty_patch/utils/handler_error.py @@ -0,0 +1,93 @@ +# tgEasy - Easy for a brighter Shine. A monkey pather add-on for Pyrogram +# Copyright (C) 2021 - 2022 Jayant Hegde Kageri + +# This file is part of tgEasy. + +# tgEasy is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# tgEasy 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 Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public License +# along with tgEasy. If not, see . +import contextlib +import logging +import os +import traceback +import typing +from datetime import datetime + +import pyrogram + +from misskaty.vars import LOG_CHANNEL + +LOGGER = logging.getLogger(__name__) + + +async def handle_error( + _, m: typing.Union[pyrogram.types.Message, pyrogram.types.CallbackQuery] +): + """ + ### `handle_error` + - A Function to Handle the Errors in Functions. + - This Sends the Error Log to the Log Group and Replies Sorry Message for the Users. + - This is Helper for all of the functions for handling the Errors. + + - Parameters: + - error: + - The Exceptation. + + - m (`pyrogram.types.Message` or `pyrogram.types.CallbackQuery`): + - The Message or Callback Query where the Error occurred. + + """ + + day = datetime.now() + tgl_now = datetime.now() + cap_day = f"{day.strftime('%A')}, {tgl_now.strftime('%d %B %Y %H:%M:%S')}" + f_errname = f"crash_{tgl_now.strftime('%d %B %Y')}.txt" + LOGGER.error(traceback.format_exc()) + with open(f_errname, "w+", encoding="utf-8") as log: + log.write(traceback.format_exc()) + log.close() + if isinstance(m, pyrogram.types.Message): + with contextlib.suppress(Exception): + try: + await m.reply_photo( + "https://telegra.ph/file/3c9162b242567ae25d5af.jpg", + caption="An Internal Error Occurred while Processing your Command, the Logs have been sent to the Owners of this Bot. Sorry for Inconvenience", + ) + except: + await m.reply_msg( + "An Internal Error Occurred while Processing your Command, the Logs have been sent to the Owners of this Bot. Sorry for Inconvenience" + ) + await m._client.send_document( + LOG_CHANNEL, + f_errname, + caption=f"Crash Report of this Bot\n{cap_day}", + ) + if isinstance(m, pyrogram.types.CallbackQuery): + with contextlib.suppress(Exception): + await m.message.delete() + try: + await m.reply_photo( + "https://telegra.ph/file/3c9162b242567ae25d5af.jpg", + caption="An Internal Error Occurred while Processing your Command, the Logs have been sent to the Owners of this Bot. Sorry for Inconvenience", + ) + except: + await m.message.reply_msg( + "An Internal Error Occurred while Processing your Command, the Logs have been sent to the Owners of this Bot. Sorry for Inconvenience" + ) + await m.message._client.send_document( + LOG_CHANNEL, + f_errname, + caption=f"Crash Report of this Bot\n{cap_day}", + ) + if os.path.exists(f_errname): + os.remove(f_errname) + return True diff --git a/misskaty/core/misskaty_patch/utils/utils.py b/misskaty/core/misskaty_patch/utils/utils.py new file mode 100644 index 00000000..90571767 --- /dev/null +++ b/misskaty/core/misskaty_patch/utils/utils.py @@ -0,0 +1,123 @@ +""" +pyromod - A monkeypatcher add-on for Pyrogram +Copyright (C) 2020 Cezar H. + +This file is part of pyromod. + +pyromod 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 3 of the License, or +(at your option) any later version. + +pyromod 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 pyromod. If not, see . +""" +from contextlib import asynccontextmanager, contextmanager +from inspect import iscoroutinefunction +from logging import getLogger +from typing import Callable + +from pyrogram.sync import async_to_sync + +logger = getLogger(__name__) + + +class PyromodConfig: + timeout_handler = None + stopped_handler = None + throw_exceptions = True + unallowed_click_alert = True + unallowed_click_alert_text = "[pyromod] You're not expected to click this button." + + +def patch(obj): + def is_patchable(item): + return getattr(item[1], "patchable", False) + + def wrapper(container): + for name, func in filter(is_patchable, container.__dict__.items()): + old = getattr(obj, name, None) + if old is not None: # Not adding 'old' to new func + setattr(obj, f"old{name}", old) + + # Worse Code + tempConf = { + i: getattr(func, i, False) + for i in ["is_property", "is_static", "is_context"] + } + + async_to_sync(container, name) + func = getattr(container, name) + + for tKey, tValue in tempConf.items(): + setattr(func, tKey, tValue) + + if func.is_property: + func = property(func) + elif func.is_static: + func = staticmethod(func) + elif func.is_context: + if iscoroutinefunction(func.__call__): + func = asynccontextmanager(func) + else: + func = contextmanager(func) + + logger.info( + f"Patch Attribute To {obj.__name__} From {container.__name__} : {name}" + ) + setattr(obj, name, func) + return container + + return wrapper + + +def patchable( + is_property: bool = False, is_static: bool = False, is_context: bool = False +) -> Callable: + """ + A decorator that marks a function as patchable. + + Usage: + + @patchable(is_property=True) + def my_property(): + ... + + @patchable(is_static=True) + def my_static_method(): + ... + + @patchable(is_context=True) + def my_context_manager(): + ... + + @patchable(is_property=False, is_static=False, is_context=False) + def my_function(): + ... + + @patchable() + def default_usage(): + ... + + Parameters: + - is_property (bool): whether the function is a property. Default is False. + - is_static (bool): whether the function is a static method. Default is False. + - is_context (bool): whether the function is a context manager. Default is False. + + Returns: + - A callable object that marks the function as patchable. + """ + + def wrapper(func: Callable) -> Callable: + func.patchable = True + func.is_property = is_property + func.is_static = is_static + func.is_context = is_context + return func + + return wrapper diff --git a/misskaty/core/pyro_cooldown.py b/misskaty/core/pyro_cooldown.py new file mode 100644 index 00000000..04a43937 --- /dev/null +++ b/misskaty/core/pyro_cooldown.py @@ -0,0 +1,51 @@ +import asyncio + +from pyrogram import filters +from pyrogram.errors import MessageDeleteForbidden + +from misskaty.vars import SUDO + +data = {} + + +async def task(msg, warn=False, sec=None): + if warn: + user = msg.from_user or msg.sender_chat + ids = await msg.reply_msg( + f"Sorry {user.mention if msg.from_user else msg.sender_chat.title} [{user.id}], you must wait for {sec}s before using this feature again.." + ) + try: + await msg.delete_msg() + except MessageDeleteForbidden: + pass + await asyncio.sleep(sec) + await ids.edit_msg( + f"Alright {user.mention if msg.from_user else msg.sender_chat.title} [{user.id}], your cooldown is over you can command again.", + del_in=3, + ) + + +def wait(sec): + async def ___(flt, _, msg): + user_id = msg.from_user.id if msg.from_user else msg.sender_chat.id + if user_id in SUDO: + return True + if user_id in data: + if msg.date.timestamp() >= data[user_id]["timestamp"] + flt.data: + data[user_id] = {"timestamp": msg.date.timestamp(), "warned": False} + return True + else: + if not data[user_id]["warned"]: + data[user_id]["warned"] = True + asyncio.ensure_future( + task(msg, True, flt.data) + ) # for super accuracy use (future - time.time()) + return False # cause we dont need delete again + + asyncio.ensure_future(task(msg)) + return False + else: + data.update({user_id: {"timestamp": msg.date.timestamp(), "warned": False}}) + return True + + return filters.create(___, data=sec) diff --git a/misskaty/core/ratelimiter_func.py b/misskaty/core/ratelimiter_func.py new file mode 100644 index 00000000..3ea75873 --- /dev/null +++ b/misskaty/core/ratelimiter_func.py @@ -0,0 +1,49 @@ +from typing import Union + +from pyrate_limiter import ( + BucketFullException, + Duration, + Limiter, + MemoryListBucket, + RequestRate, +) + + +class RateLimiter: + """ + Implement rate limit logic using leaky bucket + algorithm, via pyrate_limiter. + (https://pypi.org/project/pyrate-limiter/) + """ + + def __init__(self) -> None: + # 1 requests per seconds + self.second_rate = RequestRate(1, Duration.SECOND) + + # 15 requests per minute. + self.minute_rate = RequestRate(15, Duration.MINUTE) + + # 100 requests per hour + self.hourly_rate = RequestRate(100, Duration.HOUR) + + # 500 requests per day + self.daily_rate = RequestRate(500, Duration.DAY) + + self.limiter = Limiter( + self.minute_rate, + self.hourly_rate, + self.daily_rate, + bucket_class=MemoryListBucket, + ) + + async def acquire(self, userid: Union[int, str]) -> bool: + """ + Acquire rate limit per userid and return True / False + based on userid ratelimit status. + """ + + try: + self.limiter.try_acquire(userid) + return False + except BucketFullException: + return True diff --git a/misskaty/helper/__init__.py b/misskaty/helper/__init__.py new file mode 100644 index 00000000..ba04e3ad --- /dev/null +++ b/misskaty/helper/__init__.py @@ -0,0 +1,15 @@ +from .ffmpeg_helper import * +from .files import * +from .functions import * +from .http import * +from .human_read import * +from .kuso_utils import * +from .media_helper import * +from .misc import * +from .pyro_progress import * +from .stickerset import * +from .subscene_helper import * +from .time_gap import * +from .tools import * +from .ytdl_helper import * +from .sqlite_helper import Cache \ No newline at end of file diff --git a/misskaty/helper/eval_helper.py b/misskaty/helper/eval_helper.py new file mode 100644 index 00000000..86f1c49c --- /dev/null +++ b/misskaty/helper/eval_helper.py @@ -0,0 +1,159 @@ +import ast +import os +import traceback +from typing import List, Optional + + +# We dont modify locals VVVV ; this lets us keep the message available to the user-provided function +async def meval(code, globs, **kwargs): + # This function is released in the public domain. Feel free to kang it (although I like credit) + # Note to self: please don't set globals here as they will be lost. + # Don't clutter locals + locs = {} + # Restore globals latertypes + globs = globs.copy() + # This code saves __name__ and __package into a kwarg passed to the function. + # It is set before the users code runs to make sure relative imports work + global_args = "_globs" + while global_args in globs.keys(): + # Make sure there's no name collision, just keep prepending _s + global_args = f"_{global_args}" + kwargs[global_args] = {} + for glob in ["__name__", "__package__"]: + # Copy data to args we are sending + kwargs[global_args][glob] = globs[glob] + + root = ast.parse(code, "exec") + code = root.body + + ret_name = "_ret" + ok = False + while True: + if ret_name in globs.keys(): + ret_name = f"_{ret_name}" + continue + for node in ast.walk(root): + if isinstance(node, ast.Name) and node.id == ret_name: + ret_name = f"_{ret_name}" + break + ok = True + if ok: + break + + if not code: + return None + + if not any(isinstance(node, ast.Return) for node in code): + for i in range(len(code)): + if isinstance(code[i], ast.Expr) and ( + i == len(code) - 1 or not isinstance(code[i].value, ast.Call) + ): + code[i] = ast.copy_location( + ast.Expr( + ast.Call( + func=ast.Attribute( + value=ast.Name(id=ret_name, ctx=ast.Load()), + attr="append", + ctx=ast.Load(), + ), + args=[code[i].value], + keywords=[], + ) + ), + code[-1], + ) + else: + for node in code: + if isinstance(node, ast.Return): + node.value = ast.List(elts=[node.value], ctx=ast.Load()) + + code.append( + ast.copy_location( + ast.Return(value=ast.Name(id=ret_name, ctx=ast.Load())), code[-1] + ) + ) + + # globals().update(**) + glob_copy = ast.Expr( + ast.Call( + func=ast.Attribute( + value=ast.Call( + func=ast.Name(id="globals", ctx=ast.Load()), args=[], keywords=[] + ), + attr="update", + ctx=ast.Load(), + ), + args=[], + keywords=[ + ast.keyword(arg=None, value=ast.Name(id=global_args, ctx=ast.Load())) + ], + ) + ) + ast.fix_missing_locations(glob_copy) + code.insert(0, glob_copy) + ret_decl = ast.Assign( + targets=[ast.Name(id=ret_name, ctx=ast.Store())], + value=ast.List(elts=[], ctx=ast.Load()), + ) + ast.fix_missing_locations(ret_decl) + code.insert(1, ret_decl) + args = [] + for a in list(map(lambda x: ast.arg(x, None), kwargs.keys())): + ast.fix_missing_locations(a) + args += [a] + args = ast.arguments( + args=[], + vararg=None, + kwonlyargs=args, + kwarg=None, + defaults=[], + kw_defaults=[None for _ in range(len(args))], + ) + args.posonlyargs = [] + fun = ast.AsyncFunctionDef( + name="tmp", args=args, body=code, decorator_list=[], returns=None + ) + ast.fix_missing_locations(fun) + mod = ast.parse("") + mod.body = [fun] + comp = compile(mod, "", "exec") + + exec(comp, {}, locs) + + r = await locs["tmp"](**kwargs) + for i in range(len(r)): + if hasattr(r[i], "__await__"): + r[i] = await r[i] # workaround for 3.5 + i = 0 + while i < len(r) - 1: + if r[i] is None: + del r[i] + else: + i += 1 + if len(r) == 1: + [r] = r + elif not r: + r = None + return r + + +def format_exception( + exp: BaseException, tb: Optional[List[traceback.FrameSummary]] = None +) -> str: + """Formats an exception traceback as a string, similar to the Python interpreter.""" + + if tb is None: + tb = traceback.extract_tb(exp.__traceback__) + + # Replace absolute paths with relative paths + cwd = os.getcwd() + for frame in tb: + if cwd in frame.filename: + frame.filename = os.path.relpath(frame.filename) + + stack = "".join(traceback.format_list(tb)) + msg = str(exp) + if msg: + msg = f": {msg}" + + return f"Traceback (most recent call last):\n{stack}{type(exp).__name__}{msg}" diff --git a/misskaty/helper/ffmpeg_helper.py b/misskaty/helper/ffmpeg_helper.py new file mode 100644 index 00000000..f9dfdf7a --- /dev/null +++ b/misskaty/helper/ffmpeg_helper.py @@ -0,0 +1,77 @@ +import asyncio +import os +import time + +from pyrogram.errors import FloodWait +from pyrogram.types import InputMediaPhoto + +from misskaty.plugins.dev import shell_exec + + +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"vcsi '{video_file}' -t -w 1340 -g 4x4 --timestamp-font assets/DejaVuSans.ttf --metadata-font assets/DejaVuSans-Bold.ttf --template misskaty/helper/ssgen_template.html --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 = [ + "ffmpeg", + "-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..d8f8079f --- /dev/null +++ b/misskaty/helper/files.py @@ -0,0 +1,88 @@ +""" +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..3b5d2c8b --- /dev/null +++ b/misskaty/helper/functions.py @@ -0,0 +1,135 @@ +from datetime import datetime, timedelta +from re import findall +from re import sub as re_sub +from string import ascii_lowercase + +from pyrogram import enums + +from misskaty import app + + +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 + 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)) + + +def extract_text_and_keyb(ikb, text: str, row_width: int = 2): + keyboard = {} + try: + text = text.strip() + text = text.removeprefix("`") + text = text.removesuffix("`") + text, keyb = text.split("~") + + keyb = findall(r"\[.+\,.+\]", keyb) + for btn_str in keyb: + btn_str = re_sub(r"[\[\]]", "", btn_str) + btn_str = btn_str.split(",") + btn_txt, btn_url = btn_str[0], btn_str[1].strip() + + if not get_urls_from_text(btn_url): + continue + keyboard[btn_txt] = btn_url + keyboard = ikb(keyboard, row_width) + except Exception: + return + return text, keyboard diff --git a/misskaty/helper/http.py b/misskaty/helper/http.py new file mode 100644 index 00000000..3345a068 --- /dev/null +++ b/misskaty/helper/http.py @@ -0,0 +1,61 @@ +from asyncio import gather + +import httpx +from aiohttp import ClientSession + +# Aiohttp Async Client +session = ClientSession() + +# HTTPx Async Client +http = httpx.AsyncClient( + http2=True, + verify=False, + 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..93105efa --- /dev/null +++ b/misskaty/helper/human_read.py @@ -0,0 +1,62 @@ +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}d " + (hours, remainder) = divmod(remainder, 3600) + hours = int(hours) + if hours != 0: + result += f"{hours}h " + (minutes, seconds) = divmod(remainder, 60) + minutes = int(minutes) + if minutes != 0: + result += f"{minutes}m " + seconds = int(seconds) + result += f"{seconds}s " + return result + + +def get_readable_bitrate(bitrate_kbps): + return ( + f"{str(round(bitrate_kbps / 1000, 2))} Mb/s" + if bitrate_kbps > 10000 + else f"{str(round(bitrate_kbps, 2))} kb/s" + ) + + +def get_readable_time2(seconds: int) -> str: + count = 0 + ping_time = "" + time_list = [] + time_suffix_list = ["s", "m", "h", "d", "w", "m", "y"] + 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/kuso_utils.py b/misskaty/helper/kuso_utils.py new file mode 100644 index 00000000..f1104e88 --- /dev/null +++ b/misskaty/helper/kuso_utils.py @@ -0,0 +1,197 @@ +import logging +import traceback +from html import escape + +import chevron +from bs4 import BeautifulSoup +from telegraph.aio import Telegraph + +from misskaty import BOT_USERNAME +from misskaty.helper.http import http + +LOGGER = logging.getLogger(__name__) + +headers = { + "Accept": "*/*", + "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", +} + + +async def kusonimeBypass(url: str, slug=None): + result = {} + _url = url + if slug: + noslug_url = "https://kusonime.com/{slug}" + _url = noslug_url.format({"slug": slug}) + try: + page = await http.get(_url, headers=headers) + soup = BeautifulSoup(page.text, "lxml") + thumb = soup.find("div", {"class": "post-thumb"}).find("img").get("src") + data = [] + try: + title = soup.find("h1", {"class": "jdlz"}).text # fix title njing haha + season = ( + soup.select( + "#venkonten > div.vezone > div.venser > div.venutama > div.lexot > div.info > p:nth-child(3)" + )[0] + .text.split(":") + .pop() + .strip() + ) + tipe = ( + soup.select( + "#venkonten > div.vezone > div.venser > div.venutama > div.lexot > div.info > p:nth-child(5)" + )[0] + .text.split(":") + .pop() + .strip() + ) + status_anime = ( + soup.select( + "#venkonten > div.vezone > div.venser > div.venutama > div.lexot > div.info > p:nth-child(6)" + )[0] + .text.split(":") + .pop() + .strip() + ) + ep = ( + soup.select( + "#venkonten > div.vezone > div.venser > div.venutama > div.lexot > div.info > p:nth-child(7)" + )[0] + .text.split(":") + .pop() + .strip() + ) + score = ( + soup.select( + "#venkonten > div.vezone > div.venser > div.venutama > div.lexot > div.info > p:nth-child(8)" + )[0] + .text.split(":") + .pop() + .strip() + ) + duration = ( + soup.select( + "#venkonten > div.vezone > div.venser > div.venutama > div.lexot > div.info > p:nth-child(9)" + )[0] + .text.split(":") + .pop() + .strip() + ) + rilis = ( + soup.select( + "#venkonten > div.vezone > div.venser > div.venutama > div.lexot > div.info > p:nth-child(10)" + )[0] + .text.split(":") + .pop() + .strip() + ) + except Exception: + e = traceback.format_exc() + LOGGER.error(e) + title, season, tipe, status_anime, ep, score, duration, rilis = ( + "None", + "None", + "None", + "None", + 0, + 0, + 0, + "None", + ) + genre = [] + for _genre in soup.select( + "#venkonten > div.vezone > div.venser > div.venutama > div.lexot > div.info > p:nth-child(2)" + ): + gen = _genre.text.split(":").pop().strip().split(", ") + genre = gen + for _, smokedl in enumerate( + soup.find("div", {"class": "dlbodz"}).find_all( + "div", {"class": "smokeddlrh"} + ), + start=1, + ): + mendata = {"name": title, "links": []} + for smokeurl in smokedl.find_all("div", {"class": "smokeurlrh"}): + quality = smokeurl.find("strong").text + links = [] + for link in smokeurl.find_all("a"): + url = link.get("href") + client = link.text + links.append({"client": client, "url": url}) + mendata["links"].append({"quality": quality, "link_download": links}) + data.append(mendata) + result |= { + "error": False, + "title": title, + "thumb": thumb, + "genre": genre, + "genre_string": ", ".join(genre), + "status_anime": status_anime, + "season": season, + "tipe": tipe, + "ep": ep, + "score": score, + "duration": duration, + "rilis": rilis, + "data": data, + } + except Exception: + err = traceback.format_exc() + LOGGER.error(err) + result |= {"error": True, "error_message": err} + await http.delete(_url) + return result + + +async def byPassPh(url: str, name: str): + kusonime = await kusonimeBypass(url) + results = {"error": True, "error_message": kusonime} + if not kusonime["error"]: + template = """ + + +

Title : {{title}}

+

Genre : {{genre_string}}

+

Season : {{season}}

+

Type : {{tipe}}

+

Status : {{status_anime}}

+

Total Episode : {{ep}}

+

Score : {{score}}

+

Duration : {{duration}}

+

Released on : {{rilis}}

+

+{{#data}} +

{{name}}

+ {{#links}} +

Resolution: {{quality}}

+ {{#link_download}} +

{{client}}

+ {{/link_download}} + {{/links}} +
+{{/data}} +""".strip() + html = chevron.render(template, kusonime) + telegraph = Telegraph() + if not telegraph.get_access_token(): + await telegraph.create_account(short_name=BOT_USERNAME) + page = await telegraph.create_page( + f"{kusonime.get('title')} By {escape(name)}", html_content=html + ) + results |= {"error": False, "url": f'https://telegra.ph/{page["path"]}'} + del results["error_message"] + return results + + +class Kusonime: + def __init__(self): # skipcq + pass + + @staticmethod + async def byPass(url): + return await kusonimeBypass(url) + + @staticmethod + async def telegraph(url, name): + return await byPassPh(url, name) diff --git a/misskaty/helper/localization.py b/misskaty/helper/localization.py new file mode 100644 index 00000000..9168d35b --- /dev/null +++ b/misskaty/helper/localization.py @@ -0,0 +1,106 @@ +import inspect +import json +import os.path +from functools import partial, wraps +from glob import glob +from typing import Dict, List + +from pyrogram.enums import ChatType +from pyrogram.types import CallbackQuery, InlineQuery, Message + +from database.locale_db import get_db_lang + +enabled_locales: List[str] = [ + # "en-GB", # English (United Kingdom) + "en-US", # English (United States) + "id-ID", # Indonesian + "id-JW", # Javanese +] + +default_language: str = "en-US" + + +def cache_localizations(files: List[str]) -> Dict[str, Dict[str, Dict[str, str]]]: + ldict = {lang: {} for lang in enabled_locales} + for file in files: + _, lname, pname = file.split(os.path.sep) + pname = pname.split(".")[0] + dic = json.load(open(file, encoding="utf-8")) + dic.update(ldict[lname].get(pname, {})) + ldict[lname][pname] = dic + return ldict + + +jsons: List[str] = [] + +for locale in enabled_locales: + jsons += glob(os.path.join("locales", locale, "*.json")) + +langdict = cache_localizations(jsons) + + +def get_locale_string( + dic: dict, language: str, default_context: str, key: str, context: str = None +) -> str: + if context: + default_context = context + dic = langdict[language].get(context, langdict[default_language][context]) + res: str = ( + dic.get(key) or langdict[default_language][default_context].get(key) or key + ) + return res + + +async def get_lang(message) -> str: + if isinstance(message, CallbackQuery): + chat = message.message.chat + elif isinstance(message, Message): + chat = message.chat + elif isinstance(message, InlineQuery): + chat, chat.type = message.from_user, ChatType.PRIVATE + else: + raise TypeError(f"Update type '{message.__name__}' is not supported.") + + lang = await get_db_lang(chat.id) + + if chat.type == ChatType.PRIVATE: + lang = lang or message.from_user.language_code or default_language + else: + lang = lang or default_language + # User has a language_code without hyphen + if len(lang.split("-")) == 1: + # Try to find a language that starts with the provided language_code + for locale_ in enabled_locales: + if locale_.startswith(lang): + lang = locale_ + elif lang.split("-")[1].islower(): + lang = lang.split("-") + lang[1] = lang[1].upper() + lang = "-".join(lang) + return lang if lang in enabled_locales else default_language + + +def use_chat_lang(context: str = None): + if not context: + cwd = os.getcwd() + frame = inspect.stack()[1] + + fname = frame.filename + + if fname.startswith(cwd): + fname = fname[len(cwd) + 1 :] + context = fname.split(os.path.sep)[2].split(".")[0] + + def decorator(func): + @wraps(func) + async def wrapper(client, message): + lang = await get_lang(message) + + dic = langdict.get(lang, langdict[default_language]) + + lfunc = partial(get_locale_string, dic.get(context, {}), lang, context) + return await func(client, message, lfunc) + + return wrapper + + return decorator diff --git a/misskaty/helper/media_helper.py b/misskaty/helper/media_helper.py new file mode 100644 index 00000000..1efb8564 --- /dev/null +++ b/misskaty/helper/media_helper.py @@ -0,0 +1,78 @@ +import asyncio +import os +import shlex +from typing import Tuple + +from telegraph.aio import Telegraph + +from misskaty import BOT_USERNAME + + +async def post_to_telegraph(is_media: bool, title=None, content=None, media=None): + telegraph = Telegraph() + if telegraph.get_access_token() is None: + await telegraph.create_account(short_name=BOT_USERNAME) + if is_media: + # Create a Telegram Post Foto/Video + response = await telegraph.upload_file(media) + return f"https://telegra.ph{response[0]['src']}" + # Create a Telegram Post using HTML Content + response = await telegraph.create_page( + title, + html_content=content, + author_url=f"https://t.me/{BOT_USERNAME}", + author_name=BOT_USERNAME, + ) + return response["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, _ = 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/mediainfo_paste.py b/misskaty/helper/mediainfo_paste.py new file mode 100644 index 00000000..d65956ac --- /dev/null +++ b/misskaty/helper/mediainfo_paste.py @@ -0,0 +1,226 @@ +css = """ + + +""" + +import json +import re + +from .http import http + + +def html_builder(title: str, text: str) -> str: + """ + Make proper html with css from given content. + """ + + heading = "{content}" + subheading = "{content}" + infobox = "" + subtitlebox = "" + icon = "" + html_msg = f"{heading.format(content=title)}" + + for line in text.splitlines(): + if ":" not in line and bool(line): + if "Text #" in line: + if bool(re.search("Text #1$", line)): + subtitle_count = len(re.findall("Text #", text)) + html_msg += icon.format( + icon_url="https://te.legra.ph/file/9d4a676445544d0f2d6db.png" + ) + html_msg += subheading.format( + content=f"Subtitles ({subtitle_count} subtitle)" + ) + html_msg += "" + + elif "General" in line: + html_msg += icon.format( + icon_url="https://te.legra.ph/file/638fb0416f2600e7c5aa3.png" + ) + html_msg += subheading.format(content="General") + + elif "Video" in line: + html_msg += icon.format( + icon_url="https://te.legra.ph/file/fbc30d71cf71c9a54e59d.png" + ) + html_msg += subheading.format(content="Video") + + elif "Audio" in line: + html_msg += icon.format( + icon_url="https://te.legra.ph/file/a3c431be457fedbae2286.png" + ) + html_msg += subheading.format(content=f"{line.strip()}") + + elif "Menu" in line: + html_msg += "" + html_msg += icon.format( + icon_url="https://te.legra.ph/file/3023b0c2bc202ec9d6d0d.png" + ) + html_msg += subheading.format(content="Chapters") + + else: + html_msg += subheading.format(content=f"{line.strip()}") + html_msg += subtitlebox if "Text #" in line else infobox + + elif ":" in line: + if "Attachments" not in line and "ErrorDetectionType" not in line: + html_msg += f"
{line.strip()}
" + + else: + html_msg += "
" + + html_msg += "
" + return css + html_msg + + +async def mediainfo_paste(text: str, title: str) -> str: + html_content = html_builder(title, text) + URL = "https://mediainfo-1-y5870653.deta.app/api" + response = await http.post(URL, json={"content": html_content}) + return ( + f"https://mediainfo-1-y5870653.deta.app/{json.loads(response.content)['key']}" + ) diff --git a/misskaty/helper/misc.py b/misskaty/helper/misc.py new file mode 100644 index 00000000..3c0919f5 --- /dev/null +++ b/misskaty/helper/misc.py @@ -0,0 +1,83 @@ +from math import ceil + +from pyrogram.types import InlineKeyboardButton + +from misskaty import MOD_LOAD, MOD_NOLOAD + + +# skipcq: PYL-W1641 +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..23e2aa4f --- /dev/null +++ b/misskaty/helper/pyro_progress.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# (c) Shrimadhav U K + +import asyncio +import math +import time + +from pyrogram.errors import FloodWait, MessageIdInvalid, MessageNotModified + + +async def progress_for_pyrogram(current, total, ud_type, message, start, dc_id): + """generic progress display for Telegram Upload / Download status""" + now = time.time() + diff = now - start + if round(diff % 10.00) == 0 or current == total: + 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\nDC ID: {3}\nETA: {4}\n".format( + humanbytes(current), + humanbytes(total), + humanbytes(speed), + dc_id, + 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, MessageIdInvalid): + 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/sqlite_helper.py b/misskaty/helper/sqlite_helper.py new file mode 100644 index 00000000..3285a625 --- /dev/null +++ b/misskaty/helper/sqlite_helper.py @@ -0,0 +1,565 @@ +import os +import pickle +import sqlite3 +from contextlib import suppress +from datetime import datetime, timedelta, timezone +from functools import wraps +from pathlib import Path +from threading import local +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple + +__all__ = ["Cache"] + + +class Cache: + """Simple SQLite Cache.""" + + PICKLE_PROTOCOL = pickle.HIGHEST_PROTOCOL + DEFAULT_TIMEOUT = 300 + DEFAULT_PRAGMA = { + "mmap_size": 2**26, # https://www.sqlite.org/pragma.html#pragma_mmap_size + "cache_size": 8192, # https://www.sqlite.org/pragma.html#pragma_cache_size + "wal_autocheckpoint": 1000, # https://www.sqlite.org/pragma.html#pragma_wal_autocheckpoint + "auto_vacuum": "none", # https://www.sqlite.org/pragma.html#pragma_auto_vacuum + "synchronous": "off", # https://www.sqlite.org/pragma.html#pragma_synchronous + "journal_mode": "wal", # https://www.sqlite.org/pragma.html#pragma_journal_mode + "temp_store": "file", # https://www.sqlite.org/pragma.html#pragma_temp_store + } + + _transaction_sql = "BEGIN EXCLUSIVE TRANSACTION; {} COMMIT TRANSACTION;" + + _create_sql = "CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value BLOB, exp FLOAT);" + _create_index_sql = "CREATE UNIQUE INDEX IF NOT EXISTS cache_key ON cache(key);" + _set_pragma = "PRAGMA {};" + _set_pragma_equal = "PRAGMA {}={};" + + _add_sql = ( + "INSERT INTO cache (key, value, exp) VALUES (:key, :value, :exp) " + "ON CONFLICT(key) DO UPDATE SET value = :value, exp = :exp " + "WHERE (exp <> -1.0 AND DATETIME(exp, 'unixepoch') <= DATETIME('now'));" + ) + _get_sql = "SELECT value, exp FROM cache WHERE key = :key;" + _set_sql = ( + "INSERT INTO cache (key, value, exp) VALUES (:key, :value, :exp) " + "ON CONFLICT(key) DO UPDATE SET value = :value, exp = :exp;" + ) + _check_sql = ( + "SELECT value, exp FROM cache WHERE key = :key " + "AND (exp = -1.0 OR DATETIME(exp, 'unixepoch') > DATETIME('now'));" + ) + _update_sql = ( + "UPDATE cache SET value = :value WHERE key = :key " + "AND (exp = -1.0 OR DATETIME(exp, 'unixepoch') > DATETIME('now'));" + ) + + # TODO: add 'RETURNING COUNT(*)!=0' to these when sqlite3 version >=3.35.0 + _delete_sql = "DELETE FROM cache WHERE key = :key;" + _touch_sql = ( + "UPDATE cache SET exp = :exp WHERE key = :key " + "AND (exp = -1.0 OR DATETIME(exp, 'unixepoch') > DATETIME('now'));" + ) + _clear_sql = "DELETE FROM cache;" + + _add_many_sql = ( + "INSERT INTO cache (key, value, exp) VALUES {}" + "ON CONFLICT(key) DO UPDATE SET value = excluded.value, exp = excluded.exp " + "WHERE (exp <> -1.0 AND DATETIME(exp, 'unixepoch') <= DATETIME('now'));" + ) + _get_many_sql = "SELECT key, value, exp FROM cache WHERE key IN ({});" + _set_many_sql = ( + "INSERT INTO cache (key, value, exp) VALUES {}" + "ON CONFLICT(key) DO UPDATE SET value = excluded.value, exp = excluded.exp;" + ) + _delete_many_sql = "DELETE FROM cache WHERE key IN ({});" + + def __init__( + self, + *, + filename: str = ".cache", + path: str = None, + in_memory: bool = True, + timeout: int = 5, + isolation_level: Optional[ + Literal["DEFERRED", "IMMEDIATE", "EXCLUSIVE"] + ] = "DEFERRED", + **kwargs, + ): + """Create a cache using sqlite3. + + :param filename: Cache file name. + :param path: Path string to the wanted db location. If None, use current directory. + :param in_memory: Create database in-memory only. A file is still created, but nothing is stored in it. + :param timeout: Cache connection timeout. + :param isolation_level: Controls the transaction handling performed by sqlite3. + If set to None, transactions are never implicitly opened. + https://www.sqlite.org/lang_transaction.html + :param kwargs: Pragma settings. https://www.sqlite.org/pragma.html + """ + + filepath = filename if path is None else str(Path(path) / filename) + suffix = ":?mode=memory&cache=shared" if in_memory else "" + self.connection_string = f"{filepath}{suffix}" + self.pragma = {**kwargs, **self.DEFAULT_PRAGMA} + self.timeout = timeout + self.path = path + self.isolation_level = isolation_level + self.local = local() + self.local.instances = getattr(self.local, "instances", 0) + 1 + + self._con.execute(self._create_sql) + self._con.execute(self._create_index_sql) + self._con.commit() + + @property + def _con(self) -> sqlite3.Connection: + if not os.path.exists(self.path): + os.mkdir(self.path) + try: + return self.local.con + except AttributeError: + self.local.con = sqlite3.connect( + self.connection_string, + timeout=self.timeout, + isolation_level=self.isolation_level, + ) + self._apply_pragma() + return self.local.con + + def __getitem__(self, item: str) -> Any: + value = self.get(item) + if value is None: + raise KeyError("Key not in cache.") + return value + + def __setitem__(self, item: str, value: Any) -> None: + self.set(item, value) + + def __delitem__(self, key): + self.delete(key) + + def __contains__(self, key): + return self._con.execute(self._check_sql, {"key": key}).fetchone() is not None + + def __enter__(self): + self._con # noqa pylint: disable=W0104 + return self + + def __exit__(self, *args): + self.close() + + def __del__(self): + self.local.instances = getattr(self.local, "instances", 0) - 1 + if self.local.instances <= 0: + self.close() + + def close(self) -> None: + """Closes the cache.""" + self._con.execute( + self._set_pragma.format("optimize") + ) # https://www.sqlite.org/pragma.html#pragma_optimize + self._con.close() + with suppress(AttributeError): + delattr(self.local, "con") + + def _apply_pragma(self): + for key, value in self.pragma.items(): + self._con.execute(self._set_pragma_equal.format(key, value)) + + @staticmethod + def _exp_timestamp(timeout: int = DEFAULT_TIMEOUT) -> float: + if timeout < 0: + return -1.0 + return (datetime.now(tz=timezone.utc) + timedelta(seconds=timeout)).timestamp() + + @staticmethod + def _exp_datetime(exp: float) -> Optional[datetime]: + if exp == -1.0: + return None + return datetime.utcfromtimestamp(exp) + + def _stream(self, value: Any) -> bytes: + return pickle.dumps(value, protocol=self.PICKLE_PROTOCOL) + + def _unstream(self, value: bytes) -> Any: + return pickle.loads(value) # noqa: S301 + + def add(self, key: str, value: Any, timeout: int = DEFAULT_TIMEOUT) -> None: + """Set the value to the cache only if the key is not already in the cache, + or the found value has expired. + + :param key: Cache key. + :param value: Picklable object to store. + :param timeout: How long the value is valid in the cache. + Negative numbers will keep the key in cache until manually removed. + """ + data = { + "key": key, + "value": self._stream(value), + "exp": self._exp_timestamp(timeout), + } + self._con.execute(self._add_sql, data) + self._con.commit() + + def get(self, key: str, default: Any = None) -> Any: + """Get the value under some key. Return `default` if key not in the cache or expired. + + :param key: Cache key. + :param default: Value to return if key not in the cache. + """ + result: Optional[Tuple[bytes, float]] = self._con.execute( + self._get_sql, {"key": key} + ).fetchone() + + if result is None: + return default + + exp = self._exp_datetime(result[1]) + if exp is not None and datetime.utcnow() >= exp: + self._con.execute(self._delete_sql, {"key": key}) + self._con.commit() + return default + + return self._unstream(result[0]) + + def set(self, key: str, value: Any, timeout: int = DEFAULT_TIMEOUT) -> None: + """Set a value in cache under some key. + + :param key: Cache key. + :param value: Picklable object to store. + :param timeout: How long the value is valid in the cache. + Negative numbers will keep the key in cache until manually removed. + """ + data = { + "key": key, + "value": self._stream(value), + "exp": self._exp_timestamp(timeout), + } + self._con.execute(self._set_sql, data) + self._con.commit() + + def update(self, key: str, value: Any) -> None: + """Update value in the cache. Does nothing if key not in the cache or expired. + + :param key: Cache key. + :param value: Picklable object to store. + """ + data = {"key": key, "value": self._stream(value)} + self._con.execute(self._update_sql, data) + self._con.commit() + + def touch(self, key: str, timeout: int = DEFAULT_TIMEOUT) -> None: + """Extend the lifetime of an object in cache. Does nothing if key is not in the cache or is expired. + + :param key: Cache key. + :param timeout: How long the value is valid in the cache. + Negative numbers will keep the key in cache until manually removed. + """ + data = {"exp": self._exp_timestamp(timeout), "key": key} + self._con.execute(self._touch_sql, data) + self._con.commit() + + def delete(self, key: str) -> None: + """Remove the value under the given key from the cache. Does nothing if key is not in the cache. + + :param key: Cache key. + """ + self._con.execute(self._delete_sql, {"key": key}) + self._con.commit() + + def add_many(self, dict_: Dict[str, Any], timeout: int = DEFAULT_TIMEOUT) -> None: + """For all keys in the given dict, add the value to the cache only if the key is not + already in the cache, or the found value has expired. + + :param dict_: Cache keys with values to add. + :param timeout: How long the value is valid in the cache. + Negative numbers will keep the key in cache until manually removed. + """ + command = self._add_many_sql.format( + ", ".join([f"(:key{n}, :value{n}, :exp{n})" for n in range(len(dict_))]) + ) + + data = {} + exp = self._exp_timestamp(timeout) + for i, (key, value) in enumerate(dict_.items()): + data[f"key{i}"] = key + data[f"value{i}"] = self._stream(value) + data[f"exp{i}"] = exp + + self._con.execute(command, data) + self._con.commit() + + def get_many(self, keys: List[str]) -> Dict[str, Any]: + """Get all values that exist and aren't expired from the given cache keys, and return a dict. + + :param keys: List of cache keys. + """ + seq = ", ".join([f"'{value}'" for value in keys]) + fetched: List[Tuple[str, Any, float]] = self._con.execute( + self._get_many_sql.format(seq) + ).fetchall() + + if not fetched: + return {} + + results: Dict[str, Any] = {} + to_delete: List[str] = [] + for key, value, exp in fetched: + exp = self._exp_datetime(exp) + if exp is not None and datetime.utcnow() >= exp: + to_delete.append(key) + continue + + results[key] = self._unstream(value) + + if to_delete: + self._con.execute( + self._delete_many_sql.format( + ", ".join([f"'{value}'" for value in to_delete]) + ) + ) + self._con.commit() + + return results + + def set_many(self, dict_: Dict[str, Any], timeout: int = DEFAULT_TIMEOUT) -> None: + """Set values to the cache for all keys in the given dict. + + :param dict_: Cache keys with values to set. + :param timeout: How long the value is valid in the cache. + Negative numbers will keep the key in cache until manually removed. + """ + command = self._set_many_sql.format( + ", ".join([f"(:key{n}, :value{n}, :exp{n})" for n in range(len(dict_))]) + ) + + data = {} + exp = self._exp_timestamp(timeout) + for i, (key, value) in enumerate(dict_.items()): + data[f"key{i}"] = key + data[f"value{i}"] = self._stream(value) + data[f"exp{i}"] = exp + + self._con.execute(command, data) + self._con.commit() + + def update_many(self, dict_: Dict[str, Any]) -> None: + """Update values to the cache for all keys in the given dict. Does nothing if key not in cache or expired. + + :param dict_:Cache keys with values to update to. + """ + seq = [ + {"key": key, "value": self._stream(value)} for key, value in dict_.items() + ] + self._con.executemany(self._update_sql, seq) + self._con.commit() + + def touch_many(self, keys: List[str], timeout: int = DEFAULT_TIMEOUT) -> None: + """Extend the lifetime for all objects under the given keys in cache. + Does nothing if a key is not in the cache or is expired. + + :param keys: List of cache keys. + :param timeout: How long the value is valid in the cache. + Negative numbers will keep the key in cache until manually removed. + """ + exp = self._exp_timestamp(timeout) + seq = [{"key": key, "exp": exp} for key in keys] + self._con.executemany(self._touch_sql, seq) + self._con.commit() + + def delete_many(self, keys: List[str]) -> None: + """Remove all the values under the given keys from the cache. + + :param keys: List of cache keys. + """ + self._con.execute( + self._delete_many_sql.format(", ".join([f"'{value}'" for value in keys])) + ) + self._con.commit() + + def get_or_set(self, key: str, default: Any, timeout: int = DEFAULT_TIMEOUT) -> Any: + """Get a value under some key, or set the default if key is not in cache. + + :param key: Cache key. + :param default: Picklable object to store if key is not in cache. + :param timeout: How long the value is valid in the cache. + Negative numbers will keep the key in cache until manually removed. + """ + result: Optional[Tuple[bytes, float]] = self._con.execute( + self._get_sql, {"key": key} + ).fetchone() + + if result is not None: + exp = self._exp_datetime(result[1]) + if exp is not None and datetime.utcnow() >= exp: + self._con.execute(self._delete_sql, {"key": key}) + else: + return self._unstream(result[0]) + + data = { + "key": key, + "value": self._stream(default), + "exp": self._exp_timestamp(timeout), + } + self._con.execute(self._set_sql, data) + self._con.commit() + return default + + def get_all(self) -> Dict[str, Any]: + """Get all key-value pairs from the cache.""" + all_data = self._con.execute("SELECT key, value FROM cache;").fetchall() + results = {} + for key, value in all_data: + results[key] = self._unstream(value) + return results + + def clear(self) -> None: + """Clear the cache from all values.""" + self._con.execute(self._clear_sql) + self._con.commit() + + def incr(self, key: str, delta: int = 1) -> int: + """Increment the value in cache by the given delta. + Note that this is not an atomic transaction! + + :param key: Cache key. + :param delta: How much to increment. + :raises ValueError: Value cannot be incremented. + """ + result: Optional[Tuple[bytes, float]] = self._con.execute( + self._check_sql, {"key": key} + ).fetchone() + + if result is None: + raise ValueError("Nonexistent or expired cache key.") + + value = self._unstream(result[0]) + if not isinstance(value, int): + raise ValueError("Value is not a number.") + + new_value = value + delta + self._con.execute( + self._update_sql, {"key": key, "value": self._stream(new_value)} + ) + self._con.commit() + return new_value + + def decr(self, key: str, delta: int = 1) -> int: + """Decrement the value in cache by the given delta. + Note that this is not an atomic transaction! + + :param key: Cache key. + :param delta: How much to decrement. + :raises ValueError: Value cannot be decremented. + """ + result: Optional[Tuple[bytes, float]] = self._con.execute( + self._check_sql, {"key": key} + ).fetchone() + + if result is None: + raise ValueError("Nonexistent or expired cache key.") + + value = self._unstream(result[0]) + if not isinstance(value, int): + raise ValueError("Value is not a number.") + + new_value = value - delta + self._con.execute( + self._update_sql, {"key": key, "value": self._stream(new_value)} + ) + self._con.commit() + return new_value + + def memoize( + self, timeout: int = DEFAULT_TIMEOUT + ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """Save the result of the decorated function in cache. Calls with different + arguments are saved under different keys. + + :param timeout: How long the value is valid in the cache. + Negative numbers will keep the key in cache until manually removed. + """ + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + @wraps(func) + def wrapper(*args, **kwargs) -> Callable[..., Any]: + result = self.get(f"{func}-{args}-{kwargs}", obj) + if result == obj: + result = func(*args, **kwargs) + self.set(f"{func}-{args}-{kwargs}", result, timeout) + return result + + return wrapper + + obj = object() + return decorator + + memorize = memoize # for backwards compatibility + + def ttl(self, key: str) -> int: + """How long the key is still valid in the cache in seconds. + Returns `-1` if the value for the key does not expire. + Returns `-2` if the value for the key has expired, or has not been set. + + :param key: Cache key. + """ + result: Optional[Tuple[bytes, float]] = self._con.execute( + self._get_sql, {"key": key} + ).fetchone() + + if result is None: + return -2 + + exp = self._exp_datetime(result[1]) + if exp is None: + return -1 + + ttl = int((exp - datetime.utcnow()).total_seconds()) + if ttl <= 0: + self._con.execute(self._delete_sql, {"key": key}) + self._con.commit() + return -2 + + return ttl + + def ttl_many(self, keys: List[str]) -> Dict[str, int]: + """How long the given keys are still valid in the cache in seconds. + Returns `-1` if a value for the key does not expire. + Returns `-2` if a value for the key has expired, or has not been set. + + :param keys: List of cache keys. + """ + seq = ", ".join([f"'{value}'" for value in keys]) + fetched: List[Tuple[str, Any, float]] = self._con.execute( + self._get_many_sql.format(seq) + ).fetchall() + exp_by_key: Dict[str, float] = {key: exp for key, _, exp in fetched} + + results: Dict[str, int] = {} + to_delete: List[str] = [] + for key in keys: + exp_ = exp_by_key.get(key) + if exp_ is None: + results[key] = -2 + continue + + exp = self._exp_datetime(exp_) + if exp is None: + results[key] = -1 + continue + + if datetime.utcnow() >= exp: + to_delete.append(key) + results[key] = -2 + continue + + results[key] = int((exp - datetime.utcnow()).total_seconds()) + + if to_delete: + self._con.execute( + self._delete_many_sql.format( + ", ".join([f"'{value}'" for value in to_delete]) + ) + ) + self._con.commit() + + return results diff --git a/misskaty/helper/ssgen_template.html b/misskaty/helper/ssgen_template.html new file mode 100644 index 00000000..1517debf --- /dev/null +++ b/misskaty/helper/ssgen_template.html @@ -0,0 +1,6 @@ +Filename: {{filename}} +File size: {{size}} +Duration: {{duration}} +Frame Rate: {{frame_rate}} fps +Resolution: {{sample_width}}x{{sample_height}} +Screenshot by @MissKatyRoBot diff --git a/misskaty/helper/stickerset.py b/misskaty/helper/stickerset.py new file mode 100644 index 00000000..7b42f3d3 --- /dev/null +++ b/misskaty/helper/stickerset.py @@ -0,0 +1,79 @@ +""" +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/subscene_helper.py b/misskaty/helper/subscene_helper.py new file mode 100644 index 00000000..c6266084 --- /dev/null +++ b/misskaty/helper/subscene_helper.py @@ -0,0 +1,63 @@ +import cloudscraper +from bs4 import BeautifulSoup + + +async def down_page(url): + f = cloudscraper.create_scraper() + resp = f.get(url).text + soup = BeautifulSoup(resp, "lxml") + maindiv = soup.body.find("div", class_="subtitle").find("div", class_="top left") + title = maindiv.find("div", class_="header").h1.span.text.strip() + try: + imdb = maindiv.find("div", class_="header").h1.a["href"] + except TypeError: + imdb = "" + try: + poster = maindiv.find("div", class_="poster").a["href"] + except: + poster = "" + try: + author_name = ( + maindiv.find("div", class_="header") + .ul.find("li", class_="author") + .a.text.strip() + ) + author_link = f"https://subscene.com{maindiv.find('div', class_='header').ul.find('li', class_='author').a['href']}" + except: + author_link = "" + author_name = "Anonymous" + + download_url = f"https://subscene.com{maindiv.find('div', class_='header').ul.find('li', class_='clearfix').find('div', class_='download').a['href']}" + + try: + comments = ( + maindiv.find("div", class_="header") + .ul.find("li", class_="comment-wrapper") + .find("div", class_="comment") + .text + ) + except: + comments = "" + try: + release = ( + maindiv.find("div", class_="header") + .ul.find("li", class_="release") + .find_all("div") + ) + releases = "" + for i in range(2): + r = release[i].text.strip() + releases = f"{releases}\n{r}" + except Exception: + releases = "" + + return { + "title": title, + "imdb": imdb, + "poster": poster, + "author_name": author_name, + "author_url": author_link, + "download_url": download_url, + "comments": comments, + "releases": releases, + } 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..03545f5b --- /dev/null +++ b/misskaty/helper/tools.py @@ -0,0 +1,153 @@ +import logging +import os +import random +import string +import time +from http.cookies import SimpleCookie +from re import match as re_match +from urllib.parse import urlparse + +import psutil + +from misskaty import BOT_NAME, UBOT_NAME, botStartTime +from misskaty.helper.http import http +from misskaty.helper.human_read import get_readable_time +from misskaty.plugins import ALL_MODULES + +LOGGER = logging.getLogger(__name__) +URL_REGEX = r"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])" +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(["🥶", "🔪", "🤯"]), +} + + +def is_url(url): + url = re_match(URL_REGEX, url) + return bool(url) + + +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""" +{UBOT_NAME}@{BOT_NAME} +------------------ +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 remove_N(seq): + i = 1 + while i < len(seq): + if seq[i] == seq[i - 1]: + del seq[i] + i -= 1 + else: + i += 1 + + +def get_random_string(length: int = 5): + text_str = "".join( + random.SystemRandom().choice(string.ascii_letters + string.digits) + for _ in range(length) + ) + return text_str.upper() + + +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") + ) + + +def get_provider(url): + def pretty(names): + name = names[1] + if names[0] == "play": + name = "Google Play Movies" + elif names[0] == "hbogoasia": + name = "HBO Go Asia" + elif names[0] == "maxstream": + name = "Max Stream" + elif names[0] == "klikfilm": + name = "Klik Film" + return name.title() + + netloc = urlparse(url).netloc + return pretty(netloc.split(".")) + + +async def search_jw(movie_name: str, locale: str): + m_t_ = "" + try: + response = ( + await http.get( + f"https://yasirapi.eu.org/justwatch?q={movie_name}&locale={locale}" + ) + ).json() + except: + return m_t_ + if not response.get("results"): + LOGGER.error("JustWatch API Error or got Rate Limited.") + return m_t_ + for item in response.get("results")["items"]: + if movie_name == item.get("title", ""): + offers = item.get("offers", []) + t_m_ = [] + for offer in offers: + url = offer.get("urls").get("standard_web") + if url not in t_m_: + p_o = get_provider(url) + m_t_ += f"{p_o} | " + t_m_.append(url) + if m_t_ != "": + m_t_ = m_t_[:-2].strip() + break + return m_t_ diff --git a/misskaty/helper/ytdl_helper.py b/misskaty/helper/ytdl_helper.py new file mode 100644 index 00000000..51f8d973 --- /dev/null +++ b/misskaty/helper/ytdl_helper.py @@ -0,0 +1,44 @@ +import os +import random +import string +import time + +import requests + +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..086516c4 --- /dev/null +++ b/misskaty/plugins/__init__.py @@ -0,0 +1,55 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright ©YasirPedia All rights reserved + """ +import glob +import importlib +import sys +from logging import getLogger +from os.path import basename, dirname, isfile + +from misskaty import MOD_LOAD, MOD_NOLOAD + +LOGGER = getLogger(__name__) + + +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 + + +LOGGER.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..9d89fc7f --- /dev/null +++ b/misskaty/plugins/admin.py @@ -0,0 +1,834 @@ +""" +MIT License + +Copyright (c) 2023 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 asyncio +import os +import re +from logging import getLogger +from time import time + +from pyrogram import Client, enums, filters +from pyrogram.errors import ChatAdminRequired, FloodWait, PeerIdInvalid +from pyrogram.types import ChatPermissions, ChatPrivileges, Message + +from database.warn_db import add_warn, get_warn, remove_warns +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.permissions import ( + admins_in_chat, + list_admins, + member_permissions, +) +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.core.keyboard import ikb +from misskaty.helper.functions import ( + extract_user, + extract_user_and_reason, + int_to_alpha, + time_converter, +) +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER, SUDO + +LOGGER = getLogger(__name__) + +__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. +/set_chat_title - Change The Name Of A Group/Channel. +/set_chat_photo - Change The PFP Of A Group/Channel. +/set_user_title - Change The Administrator Title Of An Admin. +""" + + +# 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: + try: + 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 + ) + ], + } + LOGGER.info(f"Updated admin cache for {cmu.chat.id} [{cmu.chat.title}]") + except: + pass + + +# Purge CMD +@app.on_cmd("purge") +@app.adminsOnly("can_delete_messages") +@ratelimiter +@use_chat_lang() +async def purge(_, ctx: Message, strings): + try: + repliedmsg = ctx.reply_to_message + await ctx.delete_msg() + + if not repliedmsg: + return await ctx.reply_msg(strings("purge_no_reply")) + + cmd = ctx.command + if len(cmd) > 1 and cmd[1].isdigit(): + purge_to = repliedmsg.id + int(cmd[1]) + purge_to = min(purge_to, ctx.id) + else: + purge_to = ctx.id + + chat_id = ctx.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 ctx.reply_msg(strings("purge_success").format(del_total=del_total)) + except Exception as err: + await ctx.reply_msg(f"ERROR: {err}") + + +# Kick members +@app.on_cmd(["kick", "dkick"], self_admin=True, group_only=True) +@app.adminsOnly("can_restrict_members") +@ratelimiter +@use_chat_lang() +async def kickFunc(client: Client, ctx: Message, strings) -> "Message": + user_id, reason = await extract_user_and_reason(ctx) + if not user_id: + return await ctx.reply_msg(strings("user_not_found")) + if user_id == client.me.id: + return await ctx.reply_msg(strings("kick_self_err")) + if user_id in SUDO: + return await ctx.reply_msg(strings("kick_sudo_err")) + if user_id in (await list_admins(ctx.chat.id)): + return await ctx.reply_msg(strings("kick_admin_err")) + user = await app.get_users(user_id) + msg = strings("kick_msg").format( + mention=user.mention, + id=user.id, + kicker=ctx.from_user.mention if ctx.from_user else "Anon Admin", + reasonmsg=reason or "-", + ) + if ctx.command[0][0] == "d": + await ctx.reply_to_message.delete_msg() + try: + await ctx.chat.ban_member(user_id) + await ctx.reply_msg(msg) + await asyncio.sleep(1) + await ctx.chat.unban_member(user_id) + except ChatAdminRequired: + await ctx.reply_msg(strings("no_ban_permission")) + + +# Ban/DBan/TBan User +@app.on_cmd(["ban", "dban", "tban"], self_admin=True, group_only=True) +@app.adminsOnly("can_restrict_members") +@ratelimiter +@use_chat_lang() +async def banFunc(client, message, strings): + user_id, reason = await extract_user_and_reason(message, sender_chat=True) + + if not user_id: + return await message.reply_text(strings("user_not_found")) + if user_id == client.me.id: + return await message.reply_text(strings("ban_self_err")) + if user_id in SUDO: + return await message.reply_text(strings("ban_sudo_err")) + if user_id in (await list_admins(message.chat.id)): + return await message.reply_text(strings("ban_admin_err")) + + 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 = strings("ban_msg").format( + mention=mention, + id=user_id, + banner=message.from_user.mention if message.from_user else "Anon", + ) + 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 += strings("banner_time").format(val=time_value) + if temp_reason: + msg += strings("banned_reason").format(reas=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(strings("no_more_99")) + except AttributeError: + pass + return + if reason: + msg += strings("banned_reason").format(reas=reason) + keyboard = ikb({"🚨 Unban 🚨": f"unban_{user_id}"}) + try: + await message.chat.ban_member(user_id) + await message.reply_text(msg, reply_markup=keyboard) + except Exception as err: + await message.reply(f"ERROR: {err}") + + +# Unban members +@app.on_cmd("unban", self_admin=True, group_only=True) +@app.adminsOnly("can_restrict_members") +@ratelimiter +@use_chat_lang() +async def unban_func(_, message, strings): + # 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(strings("unban_channel_err")) + + if len(message.command) == 2: + user = message.text.split(None, 1)[1] + if not user.startswith("@"): + user = int(user) + elif len(message.command) == 1 and reply: + user = message.reply_to_message.from_user.id + else: + return await message.reply_msg(strings("give_unban_user")) + try: + await message.chat.unban_member(user) + umention = (await app.get_users(user)).mention + await message.reply_msg(strings("unban_success").format(umention=umention)) + except PeerIdInvalid: + await message.reply_msg(strings("unknown_id", context="general")) + + +# Ban users listed in a message +@app.on_message( + filters.user(SUDO) & filters.command("listban", COMMAND_HANDLER) & filters.group +) +@ratelimiter +@use_chat_lang() +async def list_ban_(c, message, strings): + userid, msglink_reason = await extract_user_and_reason(message) + if not userid or not msglink_reason: + return await message.reply_text(strings("give_idban_with_msg_link")) + if len(msglink_reason.split(" ")) == 1: # message link included with the reason + return await message.reply_text(strings("give_reason_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(strings("invalid_tg_link")) + + if userid == c.me.id: + return await message.reply_text(strings("ban_self_err")) + if userid in SUDO: + return await message.reply_text(strings("ban_sudo_err")) + splitted = messagelink.split("/") + uname, mid = splitted[-2], int(splitted[-1]) + m = await message.reply_text(strings("multiple_ban_progress")) + try: + msgtext = (await app.get_messages(uname, mid)).text + gusernames = re.findall(r"@\w+", msgtext) + except: + return await m.edit_text(strings("failed_get_uname")) + 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 = strings("listban_msg").format( + mention=mention, + uid=userid, + frus=message.from_user.mention, + ct=count, + reas=reason, + ) + await m.edit_text(msg) + + +# Unban users listed in a message +@app.on_message( + filters.user(SUDO) & filters.command("listunban", COMMAND_HANDLER) & filters.group +) +@ratelimiter +@use_chat_lang() +async def list_unban(_, message, strings): + userid, msglink = await extract_user_and_reason(message) + if not userid or not msglink: + return await message.reply_text(strings("give_idunban_with_msg_link")) + + if not re.search(r"(https?://)?t(elegram)?\.me/\w+/\d+", msglink): # validate link + return await message.reply_text(strings("invalid_tg_link")) + + splitted = msglink.split("/") + uname, mid = splitted[-2], int(splitted[-1]) + m = await message.reply_text(strings("multiple_unban_progress")) + try: + msgtext = (await app.get_messages(uname, mid)).text + gusernames = re.findall(r"@\w+", msgtext) + except: + return await m.edit_text(strings("failed_get_uname")) + 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 = strings("listunban_msg").format( + mention=mention, uid=userid, frus=message.from_user.mention, ct=count + ) + await m.edit_text(msg) + + +# Delete messages +@app.on_cmd("del", group_only=True) +@app.adminsOnly("can_delete_messages") +@ratelimiter +@use_chat_lang() +async def deleteFunc(_, message, strings): + if not message.reply_to_message: + return await message.reply_text(strings("delete_no_reply")) + try: + await message.reply_to_message.delete() + await message.delete() + except: + await message.reply(strings("no_delete_perm")) + + +# Promote Members +@app.on_cmd(["promote", "fullpromote"], self_admin=True, group_only=True) +@app.adminsOnly("can_promote_members") +@ratelimiter +@use_chat_lang() +async def promoteFunc(client, message, strings): + try: + user_id = await extract_user(message) + umention = (await app.get_users(user_id)).mention + except: + return await message.reply(strings("invalid_id_uname")) + if not user_id: + return await message.reply_text(strings("user_not_found")) + bot = await app.get_chat_member(message.chat.id, client.me.id) + if user_id == client.me.id: + return await message.reply_text(strings("promote_self_err")) + if not bot.privileges.can_promote_members: + return await message.reply_text(strings("no_promote_perm")) + if message.command[0][0] == "f": + await message.chat.promote_member( + user_id=user_id, + privileges=ChatPrivileges( + can_change_info=bot.privileges.can_change_info, + can_invite_users=bot.privileges.can_invite_users, + can_delete_messages=bot.privileges.can_delete_messages, + can_restrict_members=bot.privileges.can_restrict_members, + can_pin_messages=bot.privileges.can_pin_messages, + can_promote_members=bot.privileges.can_promote_members, + can_manage_chat=bot.privileges.can_manage_chat, + can_manage_video_chats=bot.privileges.can_manage_video_chats, + ), + ) + return await message.reply_text( + strings("full_promote").format(umention=umention) + ) + + await message.chat.promote_member( + user_id=user_id, + privileges=ChatPrivileges( + can_change_info=False, + can_invite_users=bot.privileges.can_invite_users, + can_delete_messages=bot.privileges.can_delete_messages, + can_restrict_members=bot.privileges.can_restrict_members, + can_pin_messages=bot.privileges.can_pin_messages, + can_promote_members=False, + can_manage_chat=bot.privileges.can_manage_chat, + can_manage_video_chats=bot.privileges.can_manage_video_chats, + ), + ) + await message.reply_text(strings("normal_promote").format(umention=umention)) + + +# Demote Member +@app.on_cmd("demote", self_admin=True, group_only=True) +@app.adminsOnly("can_restrict_members") +@ratelimiter +@use_chat_lang() +async def demote(client, message, strings): + user_id = await extract_user(message) + if not user_id: + return await message.reply_text(strings("user_not_found")) + if user_id == client.me.id: + return await message.reply_text(strings("demote_self_err")) + if user_id in SUDO: + return await message.reply_text(strings("demote_sudo_err")) + await message.chat.promote_member( + user_id=user_id, + privileges=ChatPrivileges( + 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_video_chats=False, + ), + ) + umention = (await app.get_users(user_id)).mention + await message.reply_text(f"Demoted! {umention}") + + +# Pin Messages +@app.on_cmd(["pin", "unpin"]) +@app.adminsOnly("can_pin_messages") +@ratelimiter +@use_chat_lang() +async def pin(_, message, strings): + if not message.reply_to_message: + return await message.reply_text(strings("pin_no_reply")) + r = message.reply_to_message + try: + if message.command[0][0] == "u": + await r.unpin() + return await message.reply_text( + strings("unpin_success").format(link=r.link), + disable_web_page_preview=True, + ) + await r.pin(disable_notification=True) + await message.reply( + strings("pin_success").format(link=r.link), + disable_web_page_preview=True, + ) + except ChatAdminRequired: + await message.reply( + strings("pin_no_perm"), + disable_web_page_preview=True, + ) + + +# Mute members +@app.on_cmd(["mute", "tmute"], self_admin=True, group_only=True) +@app.adminsOnly("can_restrict_members") +@ratelimiter +@use_chat_lang() +async def mute(client, message, strings): + try: + user_id, reason = await extract_user_and_reason(message) + except Exception as err: + return await message.reply(f"ERROR: {err}") + if not user_id: + return await message.reply_text(strings("user_not_found")) + if user_id == client.me.id: + return await message.reply_text(strings("mute_self_err")) + if user_id in SUDO: + return await message.reply_text(strings("mute_sudo_err")) + if user_id in (await list_admins(message.chat.id)): + return await message.reply_text(strings("mute_admin_err")) + mention = (await app.get_users(user_id)).mention + keyboard = ikb({"🚨 Unmute 🚨": f"unmute_{user_id}"}) + msg = strings("mute_msg").format( + mention=mention, + muter=message.from_user.mention if message.from_user else "Anon", + ) + 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 += strings("muted_time").format(val=time_value) + if temp_reason: + msg += strings("banned_reason").format(reas=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(strings("no_more_99")) + except AttributeError: + pass + return + if reason: + msg += strings("banned_reason").format(reas=reason) + await message.chat.restrict_member(user_id, permissions=ChatPermissions()) + await message.reply_text(msg, reply_markup=keyboard) + + +# Unmute members +@app.on_cmd("unmute", self_admin=True, group_only=True) +@app.adminsOnly("can_restrict_members") +@ratelimiter +@use_chat_lang() +async def unmute(_, message, strings): + user_id = await extract_user(message) + if not user_id: + return await message.reply_text(strings("user_not_found")) + try: + await message.chat.unban_member(user_id) + umention = (await app.get_users(user_id)).mention + await message.reply_msg(strings("unmute_msg").format(umention=umention)) + except Exception as e: + await message.reply_msg(str(e)) + + +@app.on_cmd(["warn", "dwarn"], self_admin=True, group_only=True) +@app.adminsOnly("can_restrict_members") +@ratelimiter +@use_chat_lang() +async def warn_user(client, message, strings): + user_id, reason = await extract_user_and_reason(message) + chat_id = message.chat.id + if not user_id: + return await message.reply_text(strings("user_not_found")) + if user_id == client.me.id: + return await message.reply_text(strings("warn_self_err")) + if user_id in SUDO: + return await message.reply_text(strings("warn_sudo_err")) + if user_id in (await list_admins(chat_id)): + return await message.reply_text(strings("warn_admin_err")) + 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({strings("rm_warn_btn"): 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(strings("exceed_warn_msg").format(mention=mention)) + await remove_warns(chat_id, await int_to_alpha(user_id)) + else: + warn = {"warns": warns + 1} + msg = strings("warn_msg").format( + mention=mention, + warner=message.from_user.mention if message.from_user else "Anon", + reas=reason or "No Reason Provided.", + twarn=warns + 1, + ) + 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_")) +@ratelimiter +@use_chat_lang() +async def remove_warning(client, cq, strings): + from_user = cq.from_user + chat_id = cq.message.chat.id + permissions = await member_permissions(chat_id, from_user.id, client) + permission = "can_restrict_members" + if permission not in permissions: + return await cq.answer( + strings("no_permission_error").format(permissions=permission), + show_alert=True, + ) + user_id = int(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( + strings("user_no_warn").format( + mention=cq.message.reply_to_message.from_user.id + ) + ) + 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 += strings("unwarn_msg").format(mention=from_user.mention) + await cq.message.edit(text) + + +@app.on_callback_query(filters.regex("unmute_")) +@ratelimiter +@use_chat_lang() +async def unmute_user(client, cq, strings): + from_user = cq.from_user + chat_id = cq.message.chat.id + permissions = await member_permissions(chat_id, from_user.id, client) + permission = "can_restrict_members" + if permission not in permissions: + return await cq.answer( + strings("no_permission_error").format(permissions=permission), + show_alert=True, + ) + user_id = int(cq.data.split("_")[1]) + text = cq.message.text.markdown + text = f"~~{text}~~\n\n" + text += strings("rmmute_msg").format(mention=from_user.mention) + await cq.message.chat.unban_member(user_id) + await cq.message.edit(text) + + +@app.on_callback_query(filters.regex("unban_")) +@ratelimiter +@use_chat_lang() +async def unban_user(client, cq, strings): + from_user = cq.from_user + chat_id = cq.message.chat.id + permissions = await member_permissions(chat_id, from_user.id, client) + permission = "can_restrict_members" + if permission not in permissions: + return await cq.answer( + strings("no_permission_error").format(permissions=permission), + show_alert=True, + ) + user_id = int(cq.data.split("_")[1]) + text = cq.message.text.markdown + text = f"~~{text}~~\n\n" + text += strings("unban_msg").format(mention=from_user.mention) + try: + await cq.message.chat.unban_member(user_id) + await cq.message.edit(text) + except Exception as e: + await cq.answer(str(e)) + + +# Remove Warn +@app.on_cmd("rmwarn", self_admin=True, group_only=True) +@app.adminsOnly("can_restrict_members") +@ratelimiter +@use_chat_lang() +async def remove_warnings(_, message, strings): + if not message.reply_to_message: + return await message.reply_text(strings("reply_to_rm_warn")) + 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(strings("user_no_warn").format(mention=mention)) + else: + await remove_warns(chat_id, await int_to_alpha(user_id)) + await message.reply_text(strings("rmwarn_msg").format(mention=mention)) + + +# Warns +@app.on_cmd("warns", group_only=True) +@ratelimiter +@use_chat_lang() +async def check_warns(_, message, strings): + if not message.from_user: + return + user_id = await extract_user(message) + if not user_id: + return await message.reply_text(strings("user_not_found")) + 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(strings("user_no_warn").format(mention=mention)) + return await message.reply_text( + strings("ch_warn_msg").format(mention=mention, warns=warns) + ) + + +# Report User in Group +@app.on_message( + ( + filters.command("report", COMMAND_HANDLER) + | filters.command(["admins", "admin"], prefixes="@") + ) + & filters.group +) +@capture_err +@ratelimiter +@use_chat_lang() +async def report_user(_, ctx: Message, strings) -> "Message": + if not ctx.reply_to_message: + return await ctx.reply_msg(strings("report_no_reply")) + reply = ctx.reply_to_message + reply_id = reply.from_user.id if reply.from_user else reply.sender_chat.id + user_id = ctx.from_user.id if ctx.from_user else ctx.sender_chat.id + if reply_id == user_id: + return await ctx.reply_msg(strings("report_self_err")) + + list_of_admins = await list_admins(ctx.chat.id) + linked_chat = (await app.get_chat(ctx.chat.id)).linked_chat + if linked_chat is None: + if reply_id in list_of_admins or reply_id == ctx.chat.id: + return await ctx.reply_msg(strings("reported_is_admin")) + elif ( + reply_id in list_of_admins + or reply_id == ctx.chat.id + or reply_id == linked_chat.id + ): + return await ctx.reply_msg(strings("reported_is_admin")) + user_mention = ( + reply.from_user.mention if reply.from_user else reply.sender_chat.title + ) + text = strings("report_msg").format(user_mention=user_mention) + admin_data = [ + m + async for m in app.get_chat_members( + ctx.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" + await ctx.reply_msg(text, reply_to_message_id=ctx.reply_to_message.id) + + +@app.on_cmd("set_chat_title", self_admin=True, group_only=True) +@app.adminsOnly("can_change_info") +async def set_chat_title(_, ctx: Message): + if len(ctx.command) < 2: + return await ctx.reply_text(f"**Usage:**\n/{ctx.command[0]} NEW NAME") + old_title = ctx.chat.title + new_title = ctx.text.split(None, 1)[1] + await ctx.chat.set_title(new_title) + await ctx.reply_text( + f"Successfully Changed Group Title From {old_title} To {new_title}" + ) + + +@app.on_cmd("set_user_title", self_admin=True, group_only=True) +@app.adminsOnly("can_change_info") +async def set_user_title(_, ctx: Message): + if not ctx.reply_to_message: + return await ctx.reply_text("Reply to user's message to set his admin title") + if not ctx.reply_to_message.from_user: + return await ctx.reply_text("I can't change admin title of an unknown entity") + chat_id = ctx.chat.id + from_user = ctx.reply_to_message.from_user + if len(ctx.command) < 2: + return await ctx.reply_text( + "**Usage:**\n/set_user_title NEW ADMINISTRATOR TITLE" + ) + title = ctx.text.split(None, 1)[1] + await app.set_administrator_title(chat_id, from_user.id, title) + await ctx.reply_text( + f"Successfully Changed {from_user.mention}'s Admin Title To {title}" + ) + + +@app.on_cmd("set_chat_photo", self_admin=True, group_only=True) +@app.adminsOnly("can_change_info") +async def set_chat_photo(_, ctx: Message): + reply = ctx.reply_to_message + + if not reply: + return await ctx.reply_text("Reply to a photo to set it as chat_photo") + + file = reply.document or reply.photo + if not file: + return await ctx.reply_text( + "Reply to a photo or document to set it as chat_photo" + ) + + if file.file_size > 5000000: + return await ctx.reply("File size too large.") + + photo = await reply.download() + try: + await ctx.chat.set_photo(photo=photo) + await ctx.reply_text("Successfully Changed Group Photo") + except Exception as err: + await ctx.reply(f"Failed changed group photo. ERROR: {err}") + os.remove(photo) diff --git a/misskaty/plugins/afk.py b/misskaty/plugins/afk.py new file mode 100644 index 00000000..675dd1eb --- /dev/null +++ b/misskaty/plugins/afk.py @@ -0,0 +1,533 @@ +# +# Copyright (C) 2021-2022 by TeamYukki@Github, < https://github.com/TeamYukki >. +# +# This file is part of < https://github.com/TeamYukki/YukkiAFKBot > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/TeamYukki/YukkiAFKBot/blob/master/LICENSE > +# +# All rights reserved. +# + +import re + +# Modified plugin by me from https://github.com/TeamYukki/YukkiAFKBot to make compatible with pyrogram v2 +import time + +from pyrogram import Client, enums, filters +from pyrogram.types import Message + +from database.afk_db import add_afk, cleanmode_off, cleanmode_on, is_afk, remove_afk +from misskaty import app +from misskaty.core.decorator.permissions import adminsOnly +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper import get_readable_time2 +from misskaty.helper.localization import use_chat_lang +from utils import put_cleanmode + +__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 💔. +/afk [reply to media] - AFK with media. +/afkdel - Enable auto delete AFK message in group (Only for group admin). Default is **Enable**. +Just type something in group to remove AFK Status.""" + + +# Handle set AFK Command +@app.on_cmd("afk") +@ratelimiter +@use_chat_lang() +async def active_afk(_, ctx: Message, strings): + if ctx.sender_chat: + return await ctx.reply_msg(strings("no_channel"), del_in=6) + user_id = ctx.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 == "animation": + send = ( + await ctx.reply_animation( + data, + caption=strings("on_afk_msg_no_r").format( + usr=ctx.from_user.mention, id=ctx.from_user.id, tm=seenago + ), + ) + if str(reasonafk) == "None" + else await ctx.reply_animation( + data, + caption=strings("on_afk_msg_with_r").format( + usr=ctx.from_user.mention, + id=ctx.from_user.id, + tm=seenago, + reas=reasonafk, + ), + ) + ) + elif afktype == "photo": + send = ( + await ctx.reply_photo( + photo=f"downloads/{user_id}.jpg", + caption=strings("on_afk_msg_no_r").format( + usr=ctx.from_user.mention, id=ctx.from_user.id, tm=seenago + ), + ) + if str(reasonafk) == "None" + else await ctx.reply_photo( + photo=f"downloads/{user_id}.jpg", + caption=strings("on_afk_msg_with_r").format( + usr=ctx.from_user.first_name, tm=seenago, reas=reasonafk + ), + ) + ) + elif afktype == "text": + send = await ctx.reply_text( + strings("on_afk_msg_no_r").format( + usr=ctx.from_user.mention, id=ctx.from_user.id, tm=seenago + ), + disable_web_page_preview=True, + ) + elif afktype == "text_reason": + send = await ctx.reply_text( + strings("on_afk_msg_with_r").format( + usr=ctx.from_user.mention, + id=ctx.from_user.id, + tm=seenago, + reas=reasonafk, + ), + disable_web_page_preview=True, + ) + except Exception: + send = await ctx.reply_text( + strings("is_online").format( + usr=ctx.from_user.first_name, id=ctx.from_user.id + ), + disable_web_page_preview=True, + ) + await put_cleanmode(ctx.chat.id, send.id) + return + if len(ctx.command) == 1 and not ctx.reply_to_message: + details = { + "type": "text", + "time": time.time(), + "data": None, + "reason": None, + } + elif len(ctx.command) > 1 and not ctx.reply_to_message: + _reason = (ctx.text.split(None, 1)[1].strip())[:100] + details = { + "type": "text_reason", + "time": time.time(), + "data": None, + "reason": _reason, + } + elif len(ctx.command) == 1 and ctx.reply_to_message.animation: + _data = ctx.reply_to_message.animation.file_id + details = { + "type": "animation", + "time": time.time(), + "data": _data, + "reason": None, + } + elif len(ctx.command) > 1 and ctx.reply_to_message.animation: + _data = ctx.reply_to_message.animation.file_id + _reason = (ctx.text.split(None, 1)[1].strip())[:100] + details = { + "type": "animation", + "time": time.time(), + "data": _data, + "reason": _reason, + } + elif len(ctx.command) == 1 and ctx.reply_to_message.photo: + await app.download_media(ctx.reply_to_message, file_name=f"{user_id}.jpg") + details = { + "type": "photo", + "time": time.time(), + "data": None, + "reason": None, + } + elif len(ctx.command) > 1 and ctx.reply_to_message.photo: + await app.download_media(ctx.reply_to_message, file_name=f"{user_id}.jpg") + _reason = ctx.text.split(None, 1)[1].strip() + details = { + "type": "photo", + "time": time.time(), + "data": None, + "reason": _reason, + } + elif len(ctx.command) == 1 and ctx.reply_to_message.sticker: + if ctx.reply_to_message.sticker.is_animated: + details = { + "type": "text", + "time": time.time(), + "data": None, + "reason": None, + } + else: + await app.download_media(ctx.reply_to_message, file_name=f"{user_id}.jpg") + details = { + "type": "photo", + "time": time.time(), + "data": None, + "reason": None, + } + elif len(ctx.command) > 1 and ctx.reply_to_message.sticker: + _reason = (ctx.text.split(None, 1)[1].strip())[:100] + if ctx.reply_to_message.sticker.is_animated: + details = { + "type": "text_reason", + "time": time.time(), + "data": None, + "reason": _reason, + } + else: + await app.download_media(ctx.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) + send = await ctx.reply_msg( + strings("now_afk").format(usr=ctx.from_user.mention, id=ctx.from_user.id) + ) + await put_cleanmode(ctx.chat.id, send.id) + + +@app.on_cmd("afkdel", group_only=True) +@ratelimiter +@adminsOnly("can_change_info") +@use_chat_lang() +async def afk_state(_, ctx: Message, strings): + if not ctx.from_user: + return + if len(ctx.command) == 1: + return await ctx.reply_msg( + strings("afkdel_help").format(cmd=ctx.command[0]), del_in=6 + ) + chat_id = ctx.chat.id + state = ctx.text.split(None, 1)[1].strip() + state = state.lower() + if state == "enable": + await cleanmode_on(chat_id) + await ctx.reply_msg(strings("afkdel_enable")) + elif state == "disable": + await cleanmode_off(chat_id) + await ctx.reply_msg(strings("afkdel_disable")) + else: + await ctx.reply_msg(strings("afkdel_help").format(cmd=ctx.command[0]), del_in=6) + + +# Detect user that AFK based on Yukki Repo +@app.on_message( + filters.group & ~filters.bot & ~filters.via_bot, + group=1, +) +@use_chat_lang() +async def afk_watcher_func(self: Client, ctx: Message, strings): + if ctx.sender_chat: + return + userid = ctx.from_user.id + user_name = ctx.from_user.mention + if ctx.entities: + possible = ["/afk", f"/afk@{self.me.username}", "!afk"] + message_text = ctx.text or ctx.caption + for entity in ctx.entities: + try: + if ( + entity.type == enums.MessageEntityType.BOT_COMMAND + and (message_text[0 : 0 + entity.length]).lower() in possible + ): + return + except UnicodeDecodeError: # Some weird character make error + 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 += strings("on_afk_msg_no_r").format( + usr=user_name, id=userid, tm=seenago + ) + if afktype == "text_reason": + msg += strings("on_afk_msg_with_r").format( + usr=user_name, id=userid, tm=seenago, reas=reasonafk + ) + if afktype == "animation": + if str(reasonafk) == "None": + send = await ctx.reply_animation( + data, + caption=strings("on_afk_msg_no_r").format( + usr=user_name, id=userid, tm=seenago + ), + ) + else: + send = await ctx.reply_animation( + data, + caption=strings("on_afk_msg_with_r").format( + usr=user_name, id=userid, tm=seenago, reas=reasonafk + ), + ) + if afktype == "photo": + if str(reasonafk) == "None": + send = await ctx.reply_photo( + photo=f"downloads/{userid}.jpg", + caption=strings("on_afk_msg_no_r").format( + usr=user_name, id=userid, tm=seenago + ), + ) + else: + send = await ctx.reply_photo( + photo=f"downloads/{userid}.jpg", + caption=strings("on_afk_msg_with_r").format( + usr=user_name, id=userid, tm=seenago, reas=reasonafk + ), + ) + except: + msg += strings("is_online").format(usr=user_name, id=userid) + + # Replied to a User which is AFK + if ctx.reply_to_message: + try: + replied_first_name = ctx.reply_to_message.from_user.mention + replied_user_id = ctx.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 += strings("is_afk_msg_no_r").format( + usr=replied_first_name, id=replied_user_id, tm=seenago + ) + if afktype == "text_reason": + msg += strings("is_afk_msg_with_r").format( + usr=replied_first_name, + id=replied_user_id, + tm=seenago, + reas=reasonafk, + ) + if afktype == "animation": + if str(reasonafk) == "None": + send = await ctx.reply_animation( + data, + caption=strings("is_afk_msg_no_r").format( + usr=replied_first_name, + id=replied_user_id, + tm=seenago, + ), + ) + else: + send = await ctx.reply_animation( + data, + caption=strings("is_afk_msg_with_r").format( + usr=replied_first_name, + id=replied_user_id, + tm=seenago, + reas=reasonafk, + ), + ) + if afktype == "photo": + if str(reasonafk) == "None": + send = await ctx.reply_photo( + photo=f"downloads/{replied_user_id}.jpg", + caption=strings("is_afk_msg_no_r").format( + usr=replied_first_name, + id=replied_user_id, + tm=seenago, + ), + ) + else: + send = await ctx.reply_photo( + photo=f"downloads/{replied_user_id}.jpg", + caption=strings("is_afk_msg_with_r").format( + usr=replied_first_name, + id=replied_user_id, + tm=seenago, + reas=reasonafk, + ), + ) + except Exception: + msg += strings("is_afk").format( + usr=replied_first_name, id=replied_user_id + ) + except: + pass + + # If username or mentioned user is AFK + if ctx.entities: + entity = ctx.entities + j = 0 + for _ in range(len(entity)): + if (entity[j].type) == enums.MessageEntityType.MENTION: + found = re.findall("@([_0-9a-zA-Z]+)", ctx.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 += strings("is_afk_msg_no_r").format( + usr=user.first_name[:25], id=user.id, tm=seenago + ) + if afktype == "text_reason": + msg += strings("is_afk_msg_with_r").format( + usr=user.first_name[:25], + id=user.id, + tm=seenago, + reas=reasonafk, + ) + if afktype == "animation": + if str(reasonafk) == "None": + send = await ctx.reply_animation( + data, + caption=strings("is_afk_msg_no_r").format( + usr=user.first_name[:25], id=user.id, tm=seenago + ), + ) + else: + send = await ctx.reply_animation( + data, + caption=strings("is_afk_msg_with_r").format( + usr=user.first_name[:25], + id=user.id, + tm=seenago, + reas=reasonafk, + ), + ) + if afktype == "photo": + if str(reasonafk) == "None": + send = await ctx.reply_photo( + photo=f"downloads/{user.id}.jpg", + caption=strings("is_afk_msg_no_r").format( + usr=user.first_name[:25], id=user.id, tm=seenago + ), + ) + else: + send = await ctx.reply_photo( + photo=f"downloads/{user.id}.jpg", + caption=strings("is_afk_msg_with_r").format( + usr=user.first_name[:25], + id=user.id, + tm=seenago, + reas=reasonafk, + ), + ) + except: + msg += strings("is_afk").format( + usr=user.first_name[:25], id=user.id + ) + 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 += strings("is_afk_msg_no_r").format( + usr=first_name[:25], id=user_id, tm=seenago + ) + if afktype == "text_reason": + msg += strings("is_afk_msg_with_r").format( + usr=first_name[:25], + id=user_id, + tm=seenago, + reas=reasonafk, + ) + if afktype == "animation": + if str(reasonafk) == "None": + send = await ctx.reply_animation( + data, + caption=strings("is_afk_msg_no_r").format( + usr=first_name[:25], id=user_id, tm=seenago + ), + ) + else: + send = await ctx.reply_animation( + data, + caption=strings("is_afk_msg_with_r").format( + usr=first_name[:25], + id=user_id, + tm=seenago, + reas=reasonafk, + ), + ) + if afktype == "photo": + if str(reasonafk) == "None": + send = await ctx.reply_photo( + photo=f"downloads/{user_id}.jpg", + caption=strings("is_afk_msg_no_r").format( + usr=first_name[:25], id=user_id, tm=seenago + ), + ) + else: + send = await ctx.reply_photo( + photo=f"downloads/{user_id}.jpg", + caption=strings("is_afk_msg_with_r").format( + usr=first_name[:25], + id=user_id, + tm=seenago, + reas=reasonafk, + ), + ) + except: + msg += strings("is_afk").format(usr=first_name[:25], id=user_id) + j += 1 + if msg != "": + try: + send = await ctx.reply_text(msg, disable_web_page_preview=True) + except: + pass + try: + await put_cleanmode(ctx.chat.id, send.id) + except: + pass diff --git a/misskaty/plugins/anime.py b/misskaty/plugins/anime.py new file mode 100644 index 00000000..cad67015 --- /dev/null +++ b/misskaty/plugins/anime.py @@ -0,0 +1,242 @@ +import json +from calendar import month_name + +import aiohttp +from pyrogram import filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +from misskaty import app +from misskaty.helper.human_read import get_readable_time +from misskaty.vars import COMMAND_HANDLER + +anime_query = """ +query ($id: Int, $idMal: Int, $search: String) { + Media(id: $id, idMal: $idMal, type: ANIME, search: $search) { + id + idMal + title { + romaji + english + native + } + type + format + status(version: 2) + description(asHtml: true) + startDate { + year + month + day + } + endDate { + year + month + day + } + season + seasonYear + episodes + duration + chapters + volumes + countryOfOrigin + source + hashtag + trailer { + id + site + thumbnail + } + updatedAt + coverImage { + large + } + bannerImage + genres + synonyms + averageScore + meanScore + popularity + trending + favourites + tags { + name + description + rank + } + relations { + edges { + node { + id + title { + romaji + english + native + } + format + status + source + averageScore + siteUrl + } + relationType + } + } + characters { + edges { + role + node { + name { + full + native + } + siteUrl + } + } + } + studios { + nodes { + name + siteUrl + } + } + isAdult + nextAiringEpisode { + airingAt + timeUntilAiring + episode + } + airingSchedule { + edges { + node { + airingAt + timeUntilAiring + episode + } + } + } + externalLinks { + url + site + } + rankings { + rank + year + context + } + reviews { + nodes { + summary + rating + score + siteUrl + user { + name + } + } + } + siteUrl + } +} +""" + + +async def get_anime(title): + async with aiohttp.ClientSession() as sesi: + r = await sesi.post( + "https://graphql.anilist.co", + json={"query": anime_query, "variables": title}, + ) + return await r.read() + + +def shorten(description, info="anilist.co"): + ms_g = "" + if len(description) > 700: + description = f"{description[:500]}...." + ms_g += f'\nDescription: {description}More info' + else: + ms_g += f"\nDescription: {description}" + return ( + ms_g.replace("
", "") + .replace("
", "") + .replace("", "") + .replace("", "") + ) + + +@app.on_message(filters.command("anime", COMMAND_HANDLER)) +async def anime_search(_, mesg): + search = mesg.text.split(" ", 1) + reply = await mesg.reply("⏳ Please wait ...", quote=True) + if len(search) == 1: + return await reply.edit("⚠️ Give Anime name please.") + else: + search = search[1] + variables = {"search": search} + if not (res := json.loads(await get_anime(variables))["data"].get("Media", None)): + return await reply.edit("💢 No Resource Anime found! [404]") + durasi = ( + get_readable_time(int(res.get("duration") * 60)) + if res.get("duration") is not None + else "0" + ) + msg = f"{res['title']['romaji']} ({res['title']['native']})\nType: {res['format']}\nStatus: {res['status']}\nEpisodes: {res.get('episodes', 'N/A')}\nDuration : {durasi} Per Eps.\nScore: {res['averageScore']}%\nCategory: " + for x in res["genres"]: + msg += f"{x}, " + msg = msg[:-2] + "\n" + try: + sd = res["startDate"] + startdate = str(f"{month_name[sd['month']]} {sd['day']}, {sd['year']}") + except: + startdate = "-" + msg += f"Start date: {startdate}\n" + try: + ed = res["endDate"] + enddate = str(f"{month_name[ed['month']]} {ed['day']}, {ed['year']}") + except: + enddate = "-" + msg += f"End date: {enddate}\n" + msg += "Studios: " + for x in res["studios"]["nodes"]: + msg += f"{x['name']}, " + msg = msg[:-2] + "\n" + info = res.get("siteUrl") + trailer = res.get("trailer", None) + if trailer: + trailer_id = trailer.get("id", None) + site = trailer.get("site", None) + if site == "youtube": + trailer = f"https://youtu.be/{trailer_id}" + description = ( + res.get("description") + .replace("", "") + .replace("", "") + .replace("
", "") + if res.get("description") is not None + else "N/A" + ) + msg += shorten(description, info) + image = info.replace("anilist.co/anime/", "img.anili.st/media/") + btn = ( + [ + [ + InlineKeyboardButton("More info", url=info), + InlineKeyboardButton("Trailer 🎬", url=trailer), + ] + ] + if trailer + else [[InlineKeyboardButton("More info", url=info)]] + ) + + if image: + try: + await mesg.reply_photo( + image, caption=msg, reply_markup=InlineKeyboardMarkup(btn) + ) + except: + msg += f" [〽️]({image})" + await reply.edit(msg) + else: + await reply.edit(msg) + await reply.delete() diff --git a/misskaty/plugins/auto_approve.py b/misskaty/plugins/auto_approve.py new file mode 100644 index 00000000..82503ba9 --- /dev/null +++ b/misskaty/plugins/auto_approve.py @@ -0,0 +1,74 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +from pyrogram import filters +from pyrogram.errors import UserAlreadyParticipant, UserIsBlocked +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.ratelimiter import ratelimiter + + +# Filters Approve User by bot in channel @YMovieZNew +@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")) +@ratelimiter +async def approve_chat(c, q): + _, 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")) +@ratelimiter +async def decline_chat(c, q): + _, 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..28db9910 --- /dev/null +++ b/misskaty/plugins/auto_forwarder.py @@ -0,0 +1,97 @@ +# Code copy from https://github.com/AbirHasan2005/Forward-Client +from asyncio import sleep +from logging import getLogger + +from pyrogram import filters +from pyrogram.errors import FloodWait +from pyrogram.types import Message + +from misskaty import user +from misskaty.vars import ( + BLOCK_FILES_WITHOUT_EXTENSIONS, + BLOCKED_EXTENSIONS, + FORWARD_FILTERS, + FORWARD_FROM_CHAT_ID, + FORWARD_TO_CHAT_ID, + MINIMUM_FILE_SIZE, +) + +LOGGER = getLogger(__name__) + + +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 item in FORWARD_TO_CHAT_ID: + try: + await msg.copy(item) + except FloodWait as e: + await sleep(e.value) + LOGGER.warning(f"#FloodWait: Stopped Forwarder for {e.x}s!") + await ForwardMessage(client, msg) + except Exception as err: + LOGGER.warning( + f"#ERROR: {err}\n\nUnable to Forward Message to {str(item)}, reason: {err}" + ) + except: + pass + + +@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/ban_user_or_chat.py b/misskaty/plugins/ban_user_or_chat.py new file mode 100644 index 00000000..0c76b2ae --- /dev/null +++ b/misskaty/plugins/ban_user_or_chat.py @@ -0,0 +1,184 @@ +from pyrogram import Client, filters +from pyrogram.errors import ChannelPrivate, PeerIdInvalid +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message + +from database.users_chats_db import db +from misskaty import app +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER, LOG_CHANNEL, SUDO, SUPPORT_CHAT + + +@app.on_message(filters.incoming, group=-5) +async def ban_reply(_, ctx: Message): + if not ctx.from_user: + return + ban = await db.get_ban_status(ctx.from_user.id) + if (ban.get("is_banned") and ctx.chat.type.value == "private") or ( + ban.get("is_banned") + and ctx.chat.type.value == "supergroup" + and bool(ctx.command) + ): + await ctx.reply_msg( + f'I am sorry, You are banned to use Me. \nBan Reason: {ban["ban_reason"]}' + ) + await ctx.stop_propagation() + + +@app.on_message(filters.group & filters.incoming, group=-2) +@use_chat_lang() +async def grp_bd(self: Client, ctx: Message, strings): + if not ctx.from_user: + return + if not await db.is_chat_exist(ctx.chat.id): + try: + total = await self.get_chat_members_count(ctx.chat.id) + except ChannelPrivate: + await ctx.stop_propagation() + r_j = ctx.from_user.mention if ctx.from_user else "Anonymous" + await self.send_message( + LOG_CHANNEL, + strings("log_bot_added", context="grup_tools").format( + ttl=ctx.chat.title, cid=ctx.chat.id, tot=total, r_j=r_j + ), + ) + await db.add_chat(ctx.chat.id, ctx.chat.title) + chck = await db.get_chat(ctx.chat.id) + if chck["is_disabled"]: + buttons = [ + [InlineKeyboardButton("Support", url=f"https://t.me/{SUPPORT_CHAT}")] + ] + reply_markup = InlineKeyboardMarkup(buttons) + vazha = await db.get_chat(ctx.chat.id) + try: + k = await ctx.reply_msg( + f"CHAT NOT ALLOWED 🐞\n\nMy owner has restricted me from working here!\nReason : {vazha['reason']}.", + reply_markup=reply_markup, + ) + await k.pin() + except: + pass + try: + await self.leave_chat(ctx.chat.id) + except: + pass + await ctx.stop_propagation() + + +@app.on_message(filters.command("banuser", COMMAND_HANDLER) & filters.user(SUDO)) +async def ban_a_user(bot, message): + if len(message.command) == 1: + return await message.reply("Give me a user id / username") + 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: + pass + try: + k = await bot.get_users(chat) + except PeerIdInvalid: + return await message.reply( + "This is an invalid user, make sure i have met him before." + ) + except IndexError: + return await message.reply("This might be a channel, make sure its a user.") + except Exception as e: + return await message.reply(f"Error - {e}") + else: + jar = await db.get_ban_status(k.id) + if jar["is_banned"]: + return await message.reply( + f"{k.mention} is already banned\nReason: {jar['ban_reason']}" + ) + await db.ban_user(k.id, reason) + await message.reply(f"Successfully banned user {k.mention}!! Reason: {reason}") + + +@app.on_message(filters.command("unbanuser", COMMAND_HANDLER) & filters.user(SUDO)) +async def unban_a_user(bot, message): + if len(message.command) == 1: + return await message.reply("Give me a user id / username") + r = message.text.split(None) + chat = message.text.split(None, 2)[1] if len(r) > 2 else message.command[1] + try: + chat = int(chat) + except: + pass + try: + k = await bot.get_users(chat) + except PeerIdInvalid: + return await message.reply( + "This is an invalid user, make sure ia have met him before." + ) + except IndexError: + return await message.reply("This might be a channel, make sure its a user.") + except Exception as e: + return await message.reply(f"Error - {e}") + else: + jar = await db.get_ban_status(k.id) + if not jar["is_banned"]: + return await message.reply(f"{k.mention} is not yet banned.") + await db.remove_ban(k.id) + await message.reply(f"Successfully unbanned user {k.mention}!!!") + + +@app.on_message(filters.command("disablechat", COMMAND_HANDLER) & 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) + 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 owner has told me to leave from group so i go! If you wanna add me again contact my Owner. \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("enablechat", COMMAND_HANDLER) & filters.user(SUDO)) +async def re_enable_chat(_, ctx: Message): + if len(ctx.command) == 1: + return await ctx.reply("Give me a chat id") + chat = ctx.command[1] + try: + chat_ = int(chat) + except: + return await ctx.reply("Give Me A Valid Chat ID") + sts = await db.get_chat(int(chat)) + if not sts: + return await ctx.reply("Chat Not Found In DB !") + if not sts.get("is_disabled"): + return await ctx.reply("This chat is not yet disabled.") + await db.re_enable_chat(chat_) + await ctx.reply("Chat Succesfully re-enabled") diff --git a/misskaty/plugins/blacklist_chat.py b/misskaty/plugins/blacklist_chat.py new file mode 100644 index 00000000..3ccd63f6 --- /dev/null +++ b/misskaty/plugins/blacklist_chat.py @@ -0,0 +1,119 @@ +""" +MIT License + +Copyright (c) 2023 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 re +from time import time + +from pyrogram import filters +from pyrogram.types import ChatPermissions + +from database.blacklist_db import ( + delete_blacklist_filter, + get_blacklisted_words, + save_blacklist_filter, +) +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.permissions import adminsOnly, list_admins +from misskaty.vars import SUDO + +__MODULE__ = "Blacklist" +__HELP__ = """ +/blacklisted - Get All The Blacklisted Words In The Chat. +/blacklist [WORD|SENTENCE] - Blacklist A Word Or A Sentence. +/whitelist [WORD|SENTENCE] - Whitelist A Word Or A Sentence. +""" + + +@app.on_message(filters.command("blacklist") & ~filters.private) +@adminsOnly("can_restrict_members") +async def save_filters(_, message): + if len(message.command) < 2: + return await message.reply_text("Usage:\n/blacklist [WORD|SENTENCE]") + word = message.text.split(None, 1)[1].strip() + if not word: + return await message.reply_text("**Usage**\n__/blacklist [WORD|SENTENCE]__") + chat_id = message.chat.id + await save_blacklist_filter(chat_id, word) + await message.reply_text(f"__**Blacklisted {word}.**__") + + +@app.on_message(filters.command("blacklisted") & ~filters.private) +@capture_err +async def get_filterss(_, message): + data = await get_blacklisted_words(message.chat.id) + if not data: + await message.reply_text("**No blacklisted words in this chat.**") + else: + msg = f"List of blacklisted words in {message.chat.title} :\n" + for word in data: + msg += f"**-** `{word}`\n" + await message.reply_text(msg) + + +@app.on_message(filters.command("whitelist") & ~filters.private) +@adminsOnly("can_restrict_members") +async def del_filter(_, message): + if len(message.command) < 2: + return await message.reply_text("Usage:\n/whitelist [WORD|SENTENCE]") + word = message.text.split(None, 1)[1].strip() + if not word: + return await message.reply_text("Usage:\n/whitelist [WORD|SENTENCE]") + chat_id = message.chat.id + deleted = await delete_blacklist_filter(chat_id, word) + if deleted: + return await message.reply_text(f"**Whitelisted {word}.**") + await message.reply_text("**No such blacklist filter.**") + + +@app.on_message(filters.text & ~filters.private, group=8) +@capture_err +async def blacklist_filters_re(_, message): + text = message.text.lower().strip() + if not text: + return + chat_id = message.chat.id + user = message.from_user + if not user: + return + if user.id in SUDO: + return + list_of_filters = await get_blacklisted_words(chat_id) + for word in list_of_filters: + pattern = r"( |^|[^\w])" + re.escape(word) + r"( |$|[^\w])" + if re.search(pattern, text, flags=re.IGNORECASE): + if user.id in await list_admins(chat_id): + return + try: + await message.chat.restrict_member( + user.id, + ChatPermissions(), + until_date=int(time() + 3600), + ) + except Exception: + return + return await app.send_message( + chat_id, + f"Muted {user.mention} [`{user.id}`] for 1 hour " + + f"due to a blacklist match on {word}.", + ) diff --git a/misskaty/plugins/broadcast.py b/misskaty/plugins/broadcast.py new file mode 100644 index 00000000..cb4f16c3 --- /dev/null +++ b/misskaty/plugins/broadcast.py @@ -0,0 +1,47 @@ +import asyncio +import datetime +import time + +from pyrogram import filters +from pyrogram.types import Message + +from database.users_chats_db import db +from misskaty import app +from misskaty.vars import SUDO +from utils import broadcast_messages + + +@app.on_message(filters.command("broadcast") & filters.user(SUDO) & filters.reply) +async def broadcast(_, ctx: Message): + users = await db.get_all_users() + b_msg = ctx.reply_to_message + sts = await ctx.reply_msg("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 is False: + if sh == "Blocked": + 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_msg( + 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_msg( + 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..7365a82e --- /dev/null +++ b/misskaty/plugins/bypass.py @@ -0,0 +1,127 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @lastModified 2022-12-01 09:32:31 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +import re +import urllib.parse +from urllib.parse import unquote + +import requests +from pyrogram import filters +from pyrogram.errors import EntitiesTooLong, MessageTooLong +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message + +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper import get_readable_file_size, http, rentry +from misskaty.vars import COMMAND_HANDLER + +LIST_LINK = """ +- Pling and all aliases. +- Wetransfer +- Other link soon... +""" + +__MODULE__ = "Bypass" +__HELP__ = f""" +/directurl [Link] - Bypass URL. + +Supported Link: +{LIST_LINK} + +Credit: PyBypass +""" + +# Stopped development for this plugin since always changed time by time. + + +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 + + +def wetransfer_bypass(url: str) -> str: + if url.startswith("https://we.tl/"): + r = requests.head(url, allow_redirects=True) + url = r.url + recipient_id = None + params = urllib.parse.urlparse(url).path.split("/")[2:] + + if len(params) == 2: + transfer_id, security_hash = params + elif len(params) == 3: + transfer_id, recipient_id, security_hash = params + else: + return None + + j = { + "intent": "entire_transfer", + "security_hash": security_hash, + } + + if recipient_id: + j["recipient_id"] = recipient_id + try: + s = requests.Session() + r = s.get("https://wetransfer.com/") + m = re.search('name="csrf-token" content="([^"]+)"', r.text) + s.headers.update({"x-csrf-token": m[1], "x-requested-with": "XMLHttpRequest"}) + r = s.post( + f"https://wetransfer.com/api/v4/transfers/{transfer_id}/download", json=j + ) + j = r.json() + dl_url = j["direct_link"] + + return f"\n**Source Link** :\n`{url}`\n**Direct Link :**\n{dl_url}" + except Exception as er: + return er + + +@app.on_message(filters.command(["directurl"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +async def bypass(_, ctx: Message): + if len(ctx.command) == 1: + return await ctx.reply_msg( + f"Gunakan perintah /{ctx.command[0]} untuk bypass url", del_in=6 + ) + url = ctx.command[1] + msg = await ctx.reply_msg("Bypassing URL..", quote=True) + mention = f"**Bypasser:** {ctx.from_user.mention} ({ctx.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_msg(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_msg( + 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: + data = wetransfer_bypass(url) + await msg.edit_msg(f"{data}\n\n{mention}") diff --git a/misskaty/plugins/chatbot_ai.py b/misskaty/plugins/chatbot_ai.py new file mode 100644 index 00000000..7f6da31f --- /dev/null +++ b/misskaty/plugins/chatbot_ai.py @@ -0,0 +1,91 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import asyncio +import html +import random + +import openai +from aiohttp import ClientSession +from pyrogram import filters +from pyrogram.errors import MessageTooLong +from pyrogram.types import Message + +from misskaty import app +from misskaty.core import pyro_cooldown +from misskaty.helper import check_time_gap, post_to_telegraph +from misskaty.helper.http import http +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER, OPENAI_API, SUDO + +openai.api_key = OPENAI_API + + +# This only for testing things, since maybe in future it will got blocked +@app.on_message(filters.command("bard", COMMAND_HANDLER) & pyro_cooldown.wait(10)) +@use_chat_lang() +async def bard_chatbot(_, ctx: Message, strings): + if len(ctx.command) == 1: + return await ctx.reply_msg( + strings("no_question").format(cmd=ctx.command[0]), quote=True, del_in=5 + ) + msg = await ctx.reply_msg(strings("find_answers_str"), quote=True) + try: + req = await http.get( + f"https://yasirapi.eu.org/bard?input={ctx.text.split(' ', 1)[1]}" + ) + random_choice = random.choice(req.json().get("choices")) + await msg.edit_msg( + random_choice["content"][0] + if req.json().get("content") != "" + else "Failed getting data from Bard" + ) + except Exception as e: + await msg.edit_msg(str(e)) + + +@app.on_message(filters.command("ask", COMMAND_HANDLER) & pyro_cooldown.wait(10)) +@use_chat_lang() +async def openai_chatbot(_, ctx: Message, strings): + if len(ctx.command) == 1: + return await ctx.reply_msg( + strings("no_question").format(cmd=ctx.command[0]), quote=True, del_in=5 + ) + uid = ctx.from_user.id if ctx.from_user else ctx.sender_chat.id + is_in_gap, _ = await check_time_gap(uid) + if is_in_gap and (uid not in SUDO): + return await ctx.reply_msg(strings("dont_spam"), del_in=5) + openai.aiosession.set(ClientSession()) + pertanyaan = ctx.input + msg = await ctx.reply_msg(strings("find_answers_str"), quote=True) + num = 0 + answer = "" + try: + response = await openai.ChatCompletion.acreate( + model="gpt-3.5-turbo-0613", + messages=[{"role": "user", "content": pertanyaan}], + temperature=0.7, + stream=True, + ) + async for chunk in response: + if not chunk.choices[0].delta or chunk.choices[0].delta.get("role"): + continue + num += 1 + answer += chunk.choices[0].delta.content + if num == 30: + await msg.edit_msg(answer) + await asyncio.sleep(1.5) + num = 0 + await msg.edit_msg(answer) + except MessageTooLong: + answerlink = await post_to_telegraph( + False, "MissKaty ChatBot ", html.escape(f"{answer}") + ) + await msg.edit_msg( + strings("answers_too_long").format(answerlink=answerlink), + disable_web_page_preview=True, + ) + except Exception as err: + await msg.edit_msg(f"ERROR: {str(err)}") + await openai.aiosession.get().close() diff --git a/misskaty/plugins/code_tester.py b/misskaty/plugins/code_tester.py new file mode 100644 index 00000000..19d16ba0 --- /dev/null +++ b/misskaty/plugins/code_tester.py @@ -0,0 +1,747 @@ +import aiohttp +from pyrogram import enums, filters +from pyrogram.errors import MessageTooLong + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +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)) +@ratelimiter +async def list_lang(_, 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"], "!")) +@ratelimiter +async def assembly(_, 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"], "!")) +@ratelimiter +async def ats(_, 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"], "!")) +@ratelimiter +async def bash(_, 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"], "!")) +@ratelimiter +async def c(_, 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"], "!")) +@ratelimiter +async def clojure(_, 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"], "!")) +@ratelimiter +async def cobol(_, 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"], "!")) +@ratelimiter +async def coffeescript(_, 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"], "!")) +@ratelimiter +async def cpp(_, 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"], "!")) +@ratelimiter +async def crystal(_, 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"], "!")) +@ratelimiter +async def csharp(_, 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"], "!")) +@ratelimiter +async def d(_, 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"], "!")) +@ratelimiter +async def elixir(_, 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(_, 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"], "!")) +@ratelimiter +async def erlang(_, 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"], "!")) +@ratelimiter +async def fsharp(_, 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"], "!")) +@ratelimiter +async def go(_, 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"], "!")) +@ratelimiter +async def groovy(_, 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"], "!")) +@ratelimiter +async def haskell(_, 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"], "!")) +@ratelimiter +async def idris(_, 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"], "!")) +@ratelimiter +async def java(_, 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"], "!")) +@ratelimiter +async def javascript(_, 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"], "!")) +@ratelimiter +async def julia(_, 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"], "!")) +@ratelimiter +async def kotlin(_, 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"], "!")) +@ratelimiter +async def lua(_, 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"], "!")) +@ratelimiter +async def mercury(_, 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"], "!")) +@ratelimiter +async def nim(_, 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"], "!")) +@ratelimiter +async def nix(_, 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"], "!")) +@ratelimiter +async def ocaml(_, 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"], "!")) +@ratelimiter +async def perl(_, 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(_, 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"], "!")) +@ratelimiter +async def python(_, 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"], "!")) +@ratelimiter +async def raku(_, 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"], "!")) +@ratelimiter +async def ruby(_, 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"], "!")) +@ratelimiter +async def rust(_, 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"], "!")) +@ratelimiter +async def scala(_, 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(_, 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"], "!")) +@ratelimiter +async def typescript(_, 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..d80d23cc --- /dev/null +++ b/misskaty/plugins/copy_forward.py @@ -0,0 +1,112 @@ +from pyrogram import enums, filters +from pyrogram.errors import UserIsBlocked, UserNotParticipant +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +from misskaty import BOT_USERNAME, app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.vars import COMMAND_HANDLER + + +@app.on_message(filters.command(["copy"], COMMAND_HANDLER)) +@ratelimiter +async def copymsg(_, 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=f"https://t.me/{BOT_USERNAME}", + ) + ] + ] + ), + ) + 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 forwardmsg(_, 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=f"https://t.me/{BOT_USERNAME}", + ) + ] + ] + ), + ) + 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/currency.py b/misskaty/plugins/currency.py new file mode 100644 index 00000000..155589fd --- /dev/null +++ b/misskaty/plugins/currency.py @@ -0,0 +1,65 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import logging + +from pyrogram.types import Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.http import http +from misskaty.vars import CURRENCY_API + +__MODULE__ = "Currency" +__HELP__ = """ +/currency - Send structure message Telegram in JSON using Pyrogram Style. +""" + +LOGGER = logging.getLogger(__name__) + + +@app.on_cmd("currency") +@ratelimiter +async def currency(_, ctx: Message): + if CURRENCY_API is None: + return await ctx.reply_msg( + "Oops!!get the API from HERE & add it to config vars (CURRENCY_API)", + disable_web_page_preview=True, + ) + if len(ctx.text.split()) != 4: + return await ctx.reply_msg( + f"Use format /{ctx.command[0]} [amount] [currency_from] [currency_to] to convert currency.", + del_in=6, + ) + + _, amount, currency_from, currency_to = ctx.text.split() + if amount.isdigit() or ( + amount.replace(".", "", 1).isdigit() and amount.count(".") < 2 + ): + url = ( + f"https://v6.exchangerate-api.com/v6/{CURRENCY_API}/" + f"pair/{currency_from}/{currency_to}/{amount}" + ) + try: + res = await http.get(url) + data = res.json() + try: + conversion_rate = data["conversion_rate"] + conversion_result = data["conversion_result"] + target_code = data["target_code"] + base_code = data["base_code"] + last_update = data["time_last_update_utc"] + except KeyError: + return await ctx.reply_msg("Invalid response from api !") + await ctx.reply_msg( + f"**CURRENCY EXCHANGE RATE RESULT:**\n\n`{format(float(amount), ',')}` **{base_code}** = `{format(float(conversion_result), ',')}` **{target_code}**\nRate Today = `{format(float(conversion_rate), ',')}`\nLast Update: {last_update}" + ) + except Exception as err: + await ctx.reply_msg( + f"Failed convert currency, maybe you give wrong currency format or api down.\n\nERROR: {err}" + ) + else: + await ctx.reply_msg( + "This seems to be some alien currency, which I can't convert right now.. (⊙_⊙;)" + ) diff --git a/misskaty/plugins/dev.py b/misskaty/plugins/dev.py new file mode 100644 index 00000000..515d4374 --- /dev/null +++ b/misskaty/plugins/dev.py @@ -0,0 +1,543 @@ +import asyncio +import contextlib +import html +import io +import json +import os +import pickle +import re +import sys +import traceback +from datetime import datetime +from inspect import getfullargspec +from shutil import disk_usage +from time import time +from typing import Any, Optional, Tuple + +import aiohttp +import cloudscraper +from PIL import Image, ImageDraw, ImageFont +from psutil import Process, boot_time, cpu_count, cpu_percent +from psutil import disk_usage as disk_usage_percent +from psutil import net_io_counters, virtual_memory +from pyrogram import Client +from pyrogram import __version__ as pyrover +from pyrogram import enums, filters +from pyrogram.errors import FloodWait, PeerIdInvalid +from pyrogram.raw.types import UpdateBotStopped +from pyrogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup, + InputMediaPhoto, + Message, +) + +from database.gban_db import add_gban_user, is_gbanned_user, remove_gban_user +from database.users_chats_db import db +from misskaty import BOT_NAME, app, botStartTime, misskaty_version, user +from misskaty.core.decorator import new_task +from misskaty.helper.eval_helper import format_exception, meval +from misskaty.helper.functions import extract_user, extract_user_and_reason +from misskaty.helper.http import http +from misskaty.helper.human_read import get_readable_file_size, get_readable_time +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER, LOG_CHANNEL, SUDO + +__MODULE__ = "DevCommand" +__HELP__ = """ +**For Owner Bot Only.** +/run [args] - Run eval CMD +/logs [int] - Check logs bot +/shell [args] - Run Exec/Terminal CMD +/download [link/reply_to_telegram_file] - Download file from Telegram +/disablechat [chat id] - Remove blacklist group +/enablechat [chat id] - Add Blacklist group +/banuser [chat id] - Ban user and block user so cannot use bot +/unbanuser [chat id] - Unban user and make their can use bot again +/gban - To Ban A User Globally. +/ungban - To remove ban user globbaly. +/restart - update and restart bot. + +**For Public Use** +/stats - Check statistic bot +/json - Send structure message Telegram in JSON using Pyrogram Style. +""" + +var = {} +teskode = {} + + +async def edit_or_reply(msg, **kwargs): + func = msg.edit if msg.from_user.is_self else msg.reply + spec = getfullargspec(func.__wrapped__).args + await func(**{k: v for k, v in kwargs.items() if k in spec}) + + +@app.on_message(filters.command(["logs"], COMMAND_HANDLER) & filters.user(SUDO)) +@use_chat_lang() +async def log_file(_, ctx: Message, strings): + """Send log file""" + msg = await ctx.reply_msg("Reading bot logs ...") + if len(ctx.command) == 1: + await ctx.reply_document( + "MissKatyLogs.txt", + caption="Log Bot MissKatyPyro", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + strings("cl_btn"), + f"close#{ctx.from_user.id}", + ) + ] + ] + ), + ) + await msg.delete_msg() + elif len(ctx.command) == 2: + val = ctx.text.split() + tail = await shell_exec(f"tail -n {val[1]} -v MissKatyLogs.txt") + await msg.edit_msg(f"
{html.escape(tail[0])}
") + else: + await msg.edit_msg("Unsupported parameter") + + +@app.on_message(filters.command(["donate"], COMMAND_HANDLER)) +async def donate(_, ctx: Message): + await ctx.reply_photo( + "https://telegra.ph/file/9427d61d6968b8ee4fb2f.jpg", + caption=f"Hai {ctx.from_user.mention}, jika kamu merasa bot ini berguna kamu bisa melakukan donasi dengan scan QR menggunakan merchant yang support QRIS ya. Karena server bot ini menggunakan VPS dan tidaklah gratis. Terimakasih..\n\nHi {ctx.from_user.mention}, if you feel this bot is useful, you can make a donation via Paypal for international payment : https://paypal.me/yasirarism. Because this bot server is hosted in VPS and not free. Thank you..", + ) + + +@app.on_message( + filters.command(["balas"], COMMAND_HANDLER) & filters.user(SUDO) & filters.reply +) +async def balas(_, ctx: Message) -> "str": + pesan = ctx.input + await ctx.delete_msg() + await ctx.reply_msg(pesan, reply_to_message_id=ctx.reply_to_message.id) + + +@app.on_message(filters.command(["stats"], COMMAND_HANDLER)) +@new_task +async def server_stats(_, ctx: Message) -> "Message": + """ + Give system stats of the server. + """ + image = Image.open("assets/statsbg.jpg").convert("RGB") + IronFont = ImageFont.truetype("assets/IronFont.otf", 42) + draw = ImageDraw.Draw(image) + + def draw_progressbar(coordinate, progress): + progress = 110 + (progress * 10.8) + draw.ellipse((105, coordinate - 25, 127, coordinate), fill="#FFFFFF") + draw.rectangle((120, coordinate - 25, progress, coordinate), fill="#FFFFFF") + draw.ellipse( + (progress - 7, coordinate - 25, progress + 15, coordinate), fill="#FFFFFF" + ) + + total, used, free = disk_usage(".") + process = Process(os.getpid()) + + botuptime = get_readable_time(time() - botStartTime) + osuptime = get_readable_time(time() - boot_time()) + currentTime = get_readable_time(time() - botStartTime) + botusage = f"{round(process.memory_info()[0]/1024 ** 2)} MB" + + upload = get_readable_file_size(net_io_counters().bytes_sent) + download = get_readable_file_size(net_io_counters().bytes_recv) + + cpu_percentage = cpu_percent() + cpu_counts = cpu_count() + + ram_percentage = virtual_memory().percent + ram_total = get_readable_file_size(virtual_memory().total) + ram_used = get_readable_file_size(virtual_memory().used) + + disk_percenatge = disk_usage_percent("/").percent + disk_total = get_readable_file_size(total) + disk_used = get_readable_file_size(used) + disk_free = get_readable_file_size(free) + + neofetch = (await shell_exec("neofetch --stdout"))[0] + + caption = f"{BOT_NAME} {misskaty_version} is Up and Running successfully.\n\n{neofetch}\n\n**OS Uptime:** {osuptime}\nBot Uptime: {currentTime}\n**Bot Usage:** {botusage}\n\n**Total Space:** {disk_total}\n**Free Space:** {disk_free}\n\n**Download:** {download}\n**Upload:** {upload}\n\nPyrogram Version: {pyrover}\nPython Version: {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {sys.version_info[3].title()}" + + start = datetime.now() + msg = await ctx.reply_photo( + photo="https://te.legra.ph/file/30a82c22854971d0232c7.jpg", + caption=caption, + quote=True, + ) + end = datetime.now() + + draw_progressbar(243, int(cpu_percentage)) + draw.text( + (225, 153), + f"( {cpu_counts} core, {cpu_percentage}% )", + (255, 255, 255), + font=IronFont, + ) + + draw_progressbar(395, int(disk_percenatge)) + draw.text( + (335, 302), + f"( {disk_used} / {disk_total}, {disk_percenatge}% )", + (255, 255, 255), + font=IronFont, + ) + + draw_progressbar(533, int(ram_percentage)) + draw.text( + (225, 445), + f"( {ram_used} / {ram_total} , {ram_percentage}% )", + (255, 255, 255), + font=IronFont, + ) + + draw.text((335, 600), f"{botuptime}", (255, 255, 255), font=IronFont) + draw.text( + (857, 607), + f"{(end-start).microseconds/1000} ms", + (255, 255, 255), + font=IronFont, + ) + + image.save("stats.png") + await msg.edit_media(media=InputMediaPhoto("stats.png", caption=caption)) + os.remove("stats.png") + + +# Gban +@app.on_message(filters.command("gban", COMMAND_HANDLER) & filters.user(SUDO)) +async def ban_globally(self: Client, ctx: Message): + user_id, reason = await extract_user_and_reason(ctx) + if not user_id: + return await ctx.reply_text("I can't find that user.") + if not reason: + return await ctx.reply("No reason provided.") + + try: + getuser = await app.get_users(user_id) + user_mention = getuser.mention + user_id = user.id + except PeerIdInvalid: + user_mention = int(user_id) + user_id = int(user_id) + + from_user = ctx.from_user + + if user_id in [from_user.id, self.me.id] or user_id in SUDO: + return await ctx.reply_text("I can't ban that user.") + served_chats = await db.get_all_chats() + m = await ctx.reply_text( + f"**Banning {user_mention} Globally! This may take several times.**" + ) + await add_gban_user(user_id) + number_of_chats = 0 + async for served_chat in served_chats: + try: + await app.ban_chat_member(served_chat["id"], user_id) + number_of_chats += 1 + await asyncio.sleep(1) + except FloodWait as e: + await asyncio.sleep(int(e.value)) + except Exception: + pass + with contextlib.suppress(Exception): + await app.send_message( + user_id, + f"Hello, You have been globally banned by {from_user.mention}, You can appeal for this ban by talking to him.", + ) + await m.edit(f"Banned {user_mention} Globally!") + ban_text = f""" +__**New Global Ban**__ +**Origin:** {ctx.chat.title} [`{ctx.chat.id}`] +**Admin:** {from_user.mention} +**Banned User:** {user_mention} +**Banned User ID:** `{user_id}` +**Reason:** __{reason}__ +**Chats:** `{number_of_chats}`""" + try: + m2 = await app.send_message( + LOG_CHANNEL, + text=ban_text, + disable_web_page_preview=True, + ) + await m.edit( + f"Banned {user_mention} Globally!\nAction Log: {m2.link}", + disable_web_page_preview=True, + ) + except Exception: + await ctx.reply_text( + "User Gbanned, But This Gban Action Wasn't Logged, Add Me In LOG_CHANNEL" + ) + + +# Ungban +@app.on_message(filters.command("ungban", COMMAND_HANDLER) & filters.user(SUDO)) +async def unban_globally(_, ctx: Message): + user_id = await extract_user(ctx) + if not user_id: + return await ctx.reply_text("I can't find that user.") + try: + getuser = await app.get_users(user_id) + user_mention = getuser.mention + user_id = user.id + except PeerIdInvalid: + user_mention = int(user_id) + user_id = int(user_id) + + is_gbanned = await is_gbanned_user(user_id) + if not is_gbanned: + await ctx.reply_text("I don't remember Gbanning him.") + else: + await remove_gban_user(user_id) + await ctx.reply_text(f"Lifted {user_mention}'s Global Ban.'") + + +@app.on_message( + filters.command(["shell", "sh", "term"], COMMAND_HANDLER) & filters.user(SUDO) +) +@app.on_edited_message( + filters.command(["shell", "sh", "term"], COMMAND_HANDLER) & filters.user(SUDO) +) +@user.on_message(filters.command(["shell", "sh", "term"], ".") & filters.me) +@use_chat_lang() +async def shell_cmd(_, ctx: Message, strings): + if len(ctx.command) == 1: + return await edit_or_reply(ctx, text=strings("no_cmd")) + msg = ( + await ctx.edit_msg(strings("run_exec")) + if ctx.from_user.is_self + else await ctx.reply_msg(strings("run_exec")) + ) + shell = (await shell_exec(ctx.input))[0] + if len(shell) > 3000: + with io.BytesIO(str.encode(shell)) as doc: + doc.name = "shell_output.txt" + await ctx.reply_document( + document=doc, + caption=f"{ctx.input[: 4096 // 4 - 1]}", + file_name=doc.name, + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text=strings("cl_btn"), + callback_data=f"close#{ctx.from_user.id}", + ) + ] + ] + ), + ) + await msg.delete_msg() + elif len(shell) != 0: + await edit_or_reply( + ctx, + text=html.escape(shell), + parse_mode=enums.ParseMode.HTML, + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text=strings("cl_btn"), + callback_data=f"close#{ctx.from_user.id}", + ) + ] + ] + ), + ) + if not ctx.from_user.is_self: + await msg.delete_msg() + else: + await ctx.reply_msg(strings("no_reply"), del_in=5) + + +@app.on_message( + ( + filters.command(["ev", "run", "myeval"], COMMAND_HANDLER) + | filters.regex(r"app.run\(\)$") + ) + & filters.user(SUDO) +) +@app.on_edited_message( + (filters.command(["ev", "run", "myeval"]) | filters.regex(r"app.run\(\)$")) + & filters.user(SUDO) +) +@user.on_message(filters.command(["ev", "run", "myeval"], ".") & filters.me) +@use_chat_lang() +@new_task +async def cmd_eval(self: Client, ctx: Message, strings) -> Optional[str]: + if (ctx.command and len(ctx.command) == 1) or ctx.text == "app.run()": + return await edit_or_reply(ctx, text=strings("no_eval")) + status_message = ( + await ctx.edit_msg(strings("run_eval")) + if ctx.from_user.is_self + else await ctx.reply_msg(strings("run_eval"), quote=True) + ) + code = ( + ctx.text.split(" ", 1)[1] if ctx.command else ctx.text.split("\napp.run()")[0] + ) + out_buf = io.StringIO() + out = "" + humantime = get_readable_time + + async def _eval() -> Tuple[str, Optional[str]]: + # Message sending helper for convenience + async def send(*args: Any, **kwargs: Any) -> Message: + return await ctx.reply_msg(*args, **kwargs) + + # Print wrapper to capture output + # We don't override sys.stdout to avoid interfering with other output + def _print(*args: Any, **kwargs: Any) -> None: + if "file" not in kwargs: + kwargs["file"] = out_buf + return print(*args, **kwargs) + + def _help(*args: Any, **kwargs: Any) -> None: + with contextlib.redirect_stdout(out_buf): + help(*args, **kwargs) + + eval_vars = { + "self": self, + "humantime": humantime, + "ctx": ctx, + "var": var, + "teskode": teskode, + "re": re, + "os": os, + "asyncio": asyncio, + "cloudscraper": cloudscraper, + "json": json, + "aiohttp": aiohttp, + "print": _print, + "send": send, + "stdout": out_buf, + "traceback": traceback, + "http": http, + "replied": ctx.reply_to_message, + "help": _help, + } + eval_vars.update(var) + eval_vars.update(teskode) + try: + return "", await meval(code, globals(), **eval_vars) + except Exception as e: # skipcq: PYL-W0703 + # Find first traceback frame involving the snippet + first_snip_idx = -1 + tb = traceback.extract_tb(e.__traceback__) + for i, frame in enumerate(tb): + if frame.filename == "" or frame.filename.endswith("ast.py"): + first_snip_idx = i + break + # Re-raise exception if it wasn't caused by the snippet + # Return formatted stripped traceback + stripped_tb = tb[first_snip_idx:] + formatted_tb = format_exception(e, tb=stripped_tb) + return "⚠️ Error while executing snippet\n\n", formatted_tb + + before = time() + prefix, result = await _eval() + after = time() + # Always write result if no output has been collected thus far + if not out_buf.getvalue() or result is not None: + print(result, file=out_buf) + el_us = after - before + try: + el_str = get_readable_time(el_us) + except: + el_str = "1s" + if not el_str or el_str is None: + el_str = "0.1s" + + out = out_buf.getvalue() + # Strip only ONE final newline to compensate for our message formatting + if out.endswith("\n"): + out = out[:-1] + final_output = f"{prefix}INPUT:\n
{html.escape(code)}
\nOUTPUT:\n
{html.escape(out)}
\nExecuted Time: {el_str}" + if len(final_output) > 4096: + with io.BytesIO(str.encode(out)) as out_file: + out_file.name = "MissKatyEval.txt" + await ctx.reply_document( + document=out_file, + caption=f"{code[: 4096 // 4 - 1]}", + disable_notification=True, + thumb="assets/thumb.jpg", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text=strings("cl_btn"), + callback_data=f"close#{ctx.from_user.id}", + ) + ] + ] + ), + ) + await status_message.delete_msg() + else: + await edit_or_reply( + ctx, + text=final_output, + parse_mode=enums.ParseMode.HTML, + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text=strings("cl_btn"), + callback_data=f"close#{ctx.from_user.id}", + ) + ] + ] + ), + ) + if not ctx.from_user.is_self: + await status_message.delete_msg() + + +# Update and restart bot +@app.on_message(filters.command(["restart"], COMMAND_HANDLER) & filters.user(SUDO)) +@use_chat_lang() +async def update_restart(_, ctx: Message, strings): + msg = await ctx.reply_msg(strings("up_and_rest")) + await shell_exec("python3 update.py") + with open("restart.pickle", "wb") as status: + pickle.dump([ctx.chat.id, msg.id], status) + os.execvp(sys.executable, [sys.executable, "-m", "misskaty"]) + + +@app.on_raw_update(group=-99) +async def updtebot(client, update, users, _): + if isinstance(update, UpdateBotStopped): + niuser = users[update.user_id] + if update.stopped and await db.is_user_exist(niuser.id): + await db.delete_user(niuser.id) + await client.send_msg( + LOG_CHANNEL, + f"{niuser.first_name} ({niuser.id}) " + f"{'BLOCKED' if update.stopped else 'UNBLOCKED'} the bot at " + f"{datetime.fromtimestamp(update.date)}", + ) + + +async def aexec(code, c, m): + exec( + "async def __aexec(c, m): " + + "\n p = print" + + "\n replied = m.reply_to_message" + + "".join(f"\n {l_}" for l_ in code.split("\n")) + ) + return await locals()["__aexec"](c, m) + + +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..b2980288 --- /dev/null +++ b/misskaty/plugins/download_upload.py @@ -0,0 +1,227 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import asyncio +import math +import os +import time +from datetime import datetime +from logging import getLogger +from urllib.parse import unquote + +from pyrogram import filters +from pyrogram.file_id import FileId +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from pySmartDL import SmartDL + +from misskaty import app +from misskaty.core.decorator import capture_err, new_task +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.http import http +from misskaty.helper.pyro_progress import humanbytes, progress_for_pyrogram +from misskaty.vars import COMMAND_HANDLER, SUDO + +LOGGER = getLogger(__name__) + +__MODULE__ = "Download/Upload" +__HELP__ = """ +/download [url] - Download file from URL (Sudo Only) +/download [reply_to_TG_File] - Download TG File +/tgraph_up [reply_to_TG_File] - Download TG File +/tiktokdl [link] - Download TikTok Video, try use ytdown command if error. +/fbdl [link] - Download Facebook Video. +/anon [link] - Upload files to Anonfiles. +/ytdown [YT-DLP Supported URL] - Downloading YT-DLP Supported Video and Audio. +""" + + +@app.on_message(filters.command(["anon"], COMMAND_HANDLER)) +@ratelimiter +async def upload(bot, message): + if not message.reply_to_message: + return await message.reply("Please reply to media file.") + vid = [ + message.reply_to_message.video, + message.reply_to_message.document, + message.reply_to_message.audio, + message.reply_to_message.photo, + ] + media = next((v for v in vid if v is not None), None) + if not media: + return await message.reply("Unsupported media type..") + m = await message.reply("Download your file to my Server...") + now = time.time() + dc_id = FileId.decode(media.file_id).dc_id + fileku = await message.reply_to_message.download( + progress=progress_for_pyrogram, + progress_args=("Trying to download, please wait..", m, now, dc_id), + ) + 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(fileku) + + +@app.on_message(filters.command(["download"], COMMAND_HANDLER) & filters.user(SUDO)) +@capture_err +@new_task +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() + vid = [ + message.reply_to_message.video, + message.reply_to_message.document, + message.reply_to_message.audio, + message.reply_to_message.photo, + ] + media = next((v for v in vid if v is not None), None) + if not media: + return await pesan.edit_msg("Unsupported media type..") + dc_id = FileId.decode(media.file_id).dc_id + 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, dc_id), + ) + 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, timeout=10) + try: + downloader.start(blocking=False) + except Exception as err: + return await message.edit_msg(str(err)) + c_time = time.time() + while not downloader.isFinished(): + total_length = downloader.filesize or None + downloaded = downloader.get_dl_size(human=True) + display_message = "" + now = time.time() + diff = now - c_time + percentage = downloader.get_progress() * 100 + speed = downloader.get_speed(human=True) + 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: {unquote(custom_file_name)}\n" + ) + current_message += f"Speed: {speed}\n" + current_message += f"{progress_str}\n" + current_message += f"{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: + LOGGER.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 +@ratelimiter +async def tiktokdl(_, 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://apimu.my.id/downloader/tiktok3?link={link}") + ).json() + await message.reply_video( + r["hasil"]["download_mp4_hd"], + caption=f"Title: {r['hasil']['video_title']}\nUploader: {r['hasil']['name']}\n👍: {r['hasil']['like']} 🔁: {r['hasil']['share']} 💬: {r['hasil']['comment']}\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(_, 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, timeout=10) + 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}]", + thumb="assets/thumb.jpg", + ) + 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..03e92d0f --- /dev/null +++ b/misskaty/plugins/filter_request.py @@ -0,0 +1,444 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import random +import re +import shutil + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from pyrogram import enums, filters +from pyrogram.errors import PeerIdInvalid, UserNotParticipant +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.permissions import admins_in_chat +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.time_gap import check_time_gap +from utils import temp + +from .pypi_search import PYPI_DICT +from .web_scraper import SCRAP_DICT, data_kuso +from .ytdl_plugins import YT_DB + +chat = [-1001128045651, -1001255283935, -1001455886928] +REQUEST_DB = {} + + +@app.on_message(filters.regex(r"alamu'?ala[iy]ku+m", re.I) & filters.chat(chat)) +async def salamregex(_, 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( + f"Sabar dikit napa.. Tunggu {sleep_time} detik lagi 🙄" + ) + 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 + + +# To reduce cache and disk +async def clear_reqdict(): + SCRAP_DICT.clear() + data_kuso.clear() + REQUEST_DB.clear() + PYPI_DICT.clear() + YT_DB.clear() + admins_in_chat.clear() + temp.MELCOW.clear() + shutil.rmtree("downloads", ignore_errors=True) + shutil.rmtree("GensSS", ignore_errors=True) + + +@app.on_message( + filters.regex(r"makasi|thank|terimakasih|terima kasih|mksh", re.I) + & filters.chat(chat) +) +async def thankregex(_, 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")) +@ratelimiter +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, + ]: + _, 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")) +@ratelimiter +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, + ]: + _, 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")) +@ratelimiter +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, + ]: + _, 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")) +@ratelimiter +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, + ]: + _, 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$")) +@ratelimiter +async def callbackaft_done(_, 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$")) +@ratelimiter +async def callbackaft_rej(_, 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$")) +@ratelimiter +async def callbackaft_unav(_, 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$")) +@ratelimiter +async def callbackaft_dahada(_, 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=2, minute=0) +scheduler.start() diff --git a/misskaty/plugins/filters.py b/misskaty/plugins/filters.py new file mode 100644 index 00000000..f5965cb7 --- /dev/null +++ b/misskaty/plugins/filters.py @@ -0,0 +1,154 @@ +""" +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 re + +from pyrogram import filters + +from database.filters_db import ( + delete_filter, + get_filter, + get_filters_names, + save_filter, +) +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.permissions import adminsOnly +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.core.keyboard import ikb +from misskaty.helper.functions import extract_text_and_keyb + +__MODULE__ = "Filters" +__HELP__ = """/filters To Get All The Filters In The Chat. +/addfilter [FILTER_NAME] : To Save A Filter (Can be a sticker or text). +/stopfilter [FILTER_NAME] : To Stop A Filter. + +You can use markdown or html to save text too. +""" + + +@app.on_message(filters.command(["addfilter", "filter"]) & ~filters.private) +@adminsOnly("can_change_info") +@ratelimiter +async def save_filters(_, m): + if len(m.command) == 1 or not m.reply_to_message: + return await m.reply_msg( + "**Usage:**\nReply to a text or sticker with /addfilter [FILTER_NAME] to save it.", + del_in=6, + ) + if not m.reply_to_message.text and not m.reply_to_message.sticker: + return await m.reply_msg( + "__**You can only save text or stickers in filters for now.**__" + ) + name = m.text.split(None, 1)[1].strip() + if not name: + return await m.reply_msg("**Usage:**\n__/addfilter [FILTER_NAME]__", del_in=6) + chat_id = m.chat.id + _type = "text" if m.reply_to_message.text else "sticker" + _filter = { + "type": _type, + "data": m.reply_to_message.text.markdown + if _type == "text" + else m.reply_to_message.sticker.file_id, + } + await save_filter(chat_id, name, _filter) + await m.reply_msg(f"__**Saved filter {name}.**__") + + +@app.on_message(filters.command("filters") & ~filters.private) +@capture_err +@ratelimiter +async def get_filterss(_, m): + _filters = await get_filters_names(m.chat.id) + if not _filters: + return await m.reply_msg("**No filters in this chat.**") + _filters.sort() + msg = f"List of filters in {m.chat.title} - {m.chat.id}\n" + for _filter in _filters: + msg += f"**-** `{_filter}`\n" + await m.reply_msg(msg) + + +@app.on_message(filters.command(["stop", "stopfilter"]) & ~filters.private) +@adminsOnly("can_change_info") +@ratelimiter +async def del_filter(_, m): + if len(m.command) < 2: + return await m.reply_msg("**Usage:**\n__/stopfilter [FILTER_NAME]__", del_in=6) + name = m.text.split(None, 1)[1].strip() + if not name: + return await m.reply_msg("**Usage:**\n__/stopfilter [FILTER_NAME]__", del_in=6) + chat_id = m.chat.id + deleted = await delete_filter(chat_id, name) + if deleted: + await m.reply_msg(f"**Deleted filter {name}.**") + else: + await m.reply_msg("**No such filter.**") + + +@app.on_message( + filters.text & ~filters.private & ~filters.via_bot & ~filters.forwarded, + group=2, +) +async def filters_re(_, message): + text = message.text.lower().strip() + if not text: + return + chat_id = message.chat.id + list_of_filters = await get_filters_names(chat_id) + for word in list_of_filters: + pattern = r"( |^|[^\w])" + re.escape(word) + r"( |$|[^\w])" + if re.search(pattern, text, flags=re.IGNORECASE): + _filter = await get_filter(chat_id, word) + data_type = _filter["type"] + data = _filter["data"] + if data_type == "text": + keyb = None + if re.findall(r"\[.+\,.+\]", data): + if keyboard := extract_text_and_keyb(ikb, data): + data, keyb = keyboard + + if message.reply_to_message: + await message.reply_to_message.reply( + data, + reply_markup=keyb, + disable_web_page_preview=True, + ) + + if text.startswith("~"): + await message.delete() + return + + return await message.reply( + data, + reply_markup=keyb, + quote=True, + disable_web_page_preview=True, + ) + if message.reply_to_message: + await message.reply_to_message.reply_sticker(data) + + if text.startswith("~"): + await message.delete() + return + await message.reply_sticker(data, quote=True) diff --git a/misskaty/plugins/fun.py b/misskaty/plugins/fun.py new file mode 100644 index 00000000..c33dd249 --- /dev/null +++ b/misskaty/plugins/fun.py @@ -0,0 +1,186 @@ +import textwrap +from asyncio import gather +from os import remove as hapus + +from PIL import Image, ImageDraw, ImageFont +from pyrogram import filters + +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER + + +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( + "assets/MutantAcademyStyle.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): + text_bbox = m_font.getbbox(u_text) + (left, top, right, bottom) = text_bbox + u_width = abs(right - left) + u_height = abs(top - bottom) + + 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): + text_bbox = m_font.getbbox(l_text) + (left, top, right, bottom) = text_bbox + u_width = abs(right - left) + u_height = abs(top - bottom) + + draw.text( + xy=( + ((i_width - u_width) / 2) - 1, + i_height - u_height - int((20 / 500) * 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 / 500) * 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 / 500) * 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 / 500) * 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 / 500) * 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 = "misskatyfy.webp" + png_file = "misskatyfy.png" + new_size = (512, 512) + img.resize(new_size) + img.save(webp_file, "WebP") + img.save(png_file, "PNG") + img.close() + return webp_file, png_file + + +@app.on_message(filters.command(["mmf"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +async def memify(_, 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() + webp, png = await draw_meme_text( + file, message.text.split(None, 1)[1].strip() + ) + await gather(*[message.reply_document(png), message.reply_sticker(webp)]) + try: + hapus(webp) + hapus(png) + except: + pass + except Exception as err: + try: + hapus(webp) + hapus(png) + except: + pass + await message.reply_msg(f"ERROR: {err}") + else: + await message.reply_msg( + "Gunakan command /mmf dengan reply ke sticker, pisahkan dengan ; untuk membuat posisi text dibawah." + ) + + +@app.on_message(filters.command(["dice"], COMMAND_HANDLER)) +@use_chat_lang() +async def dice(c, m, strings): + dices = await c.send_dice(m.chat.id, reply_to_message_id=m.id) + await dices.reply_msg(strings("result").format(number=dices.dice.value), quote=True) diff --git a/misskaty/plugins/genss.py b/misskaty/plugins/genss.py new file mode 100644 index 00000000..3a153d44 --- /dev/null +++ b/misskaty/plugins/genss.py @@ -0,0 +1,206 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +import math +import os +import time +from asyncio import gather, sleep +from datetime import datetime +from logging import getLogger +from urllib.parse import unquote + +from pyrogram import Client, enums +from pyrogram.errors import FloodWait +from pyrogram.file_id import FileId +from pyrogram.types import Message +from pySmartDL import SmartDL + +from misskaty import app +from misskaty.core.decorator import new_task, ratelimiter +from misskaty.helper import is_url, progress_for_pyrogram, take_ss +from misskaty.helper.localization import use_chat_lang +from misskaty.helper.pyro_progress import humanbytes + +LOGGER = getLogger(__name__) + +__MODULE__ = "MediaTool" +__HELP__ = """" +/genss [reply to video] - Generate Screenshot From Video. (Support TG Media and Direct URL) +/mediainfo [link/reply to TG Video] - Get Mediainfo From File. +""" + + +@app.on_cmd("genss") +@ratelimiter +@new_task +@use_chat_lang() +async def genss(self: Client, ctx: Message, strings): + if not ctx.from_user: + return + replied = ctx.reply_to_message + if len(ctx.command) == 2 and is_url(ctx.command[1]): + pesan = await ctx.reply_msg(strings("wait_dl"), quote=True) + start_t = datetime.now() + the_url_parts = " ".join(ctx.command[1:]) + url = the_url_parts.strip() + file_name = os.path.basename(url) + download_file_path = os.path.join("downloads/", file_name) + downloader = SmartDL(url, download_file_path, progress_bar=False, timeout=10) + try: + downloader.start(blocking=False) + except Exception as err: + return await pesan.edit(str(err)) + c_time = time.time() + while not downloader.isFinished(): + total_length = downloader.filesize or None + downloaded = downloader.get_dl_size(human=True) + display_message = "" + now = time.time() + diff = now - c_time + percentage = downloader.get_progress() * 100 + speed = downloader.get_speed(human=True) + 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: {unquote(file_name)}\n" + current_message += f"Speed: {speed}\n" + current_message += f"{progress_str}\n" + current_message += f"{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 sleep(10) + except Exception as e: + LOGGER.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" + ) + try: + images = await take_ss(download_file_path) + await pesan.edit_msg(strings("up_progress")) + await self.send_chat_action( + chat_id=ctx.chat.id, action=enums.ChatAction.UPLOAD_PHOTO + ) + try: + await gather( + *[ + ctx.reply_document(images, reply_to_message_id=ctx.id), + ctx.reply_photo(images, reply_to_message_id=ctx.id), + ] + ) + except FloodWait as e: + await sleep(e.value) + await gather( + *[ + ctx.reply_document(images, reply_to_message_id=ctx.id), + ctx.reply_photo(images, reply_to_message_id=ctx.id), + ] + ) + await ctx.reply_msg( + strings("up_msg").format( + namma=ctx.from_user.mention, + id=ctx.from_user.id, + bot_uname=self.me.username, + ), + reply_to_message_id=ctx.id, + ) + await pesan.delete() + try: + os.remove(images) + os.remove(download_file_path) + except: + pass + except Exception as exc: + await ctx.reply_msg(strings("err_ssgen").format(exc=exc)) + try: + os.remove(images) + os.remove(download_file_path) + except: + pass + elif replied and replied.media: + vid = [replied.video, replied.document] + media = next((v for v in vid if v is not None), None) + if media is None: + return await ctx.reply_msg(strings("no_reply"), quote=True) + process = await ctx.reply_msg(strings("wait_dl"), quote=True) + if media.file_size > 2097152000: + return await process.edit_msg(strings("limit_dl")) + c_time = time.time() + dc_id = FileId.decode(media.file_id).dc_id + try: + dl = await replied.download( + file_name="/downloads/", + progress=progress_for_pyrogram, + progress_args=(strings("dl_progress"), process, c_time, dc_id), + ) + except FileNotFoundError: + return await process.edit_msg( + "ERROR: FileNotFound, maybe you're spam bot with same file." + ) + the_real_download_location = os.path.join("/downloads/", os.path.basename(dl)) + if the_real_download_location is not None: + try: + await process.edit_msg( + strings("success_dl_msg").format(path=the_real_download_location) + ) + await sleep(2) + images = await take_ss(the_real_download_location) + await process.edit_msg(strings("up_progress")) + await self.send_chat_action( + chat_id=ctx.chat.id, action=enums.ChatAction.UPLOAD_PHOTO + ) + + try: + await gather( + *[ + ctx.reply_document(images, reply_to_message_id=ctx.id), + ctx.reply_photo(images, reply_to_message_id=ctx.id), + ] + ) + except FloodWait as e: + await sleep(e.value) + await gather( + *[ + ctx.reply_document(images, reply_to_message_id=ctx.id), + ctx.reply_photo(images, reply_to_message_id=ctx.id), + ] + ) + await ctx.reply_msg( + strings("up_msg").format( + namma=ctx.from_user.mention, + id=ctx.from_user.id, + bot_uname=self.me.username, + ), + reply_to_message_id=ctx.id, + ) + await process.delete() + try: + os.remove(images) + os.remove(the_real_download_location) + except: + pass + except Exception as exc: + await ctx.reply_msg(strings("err_ssgen").format(exc=exc)) + try: + os.remove(images) + os.remove(the_real_download_location) + except: + pass + else: + await ctx.reply_msg(strings("no_reply"), del_in=6) diff --git a/misskaty/plugins/grup_tools.py b/misskaty/plugins/grup_tools.py new file mode 100644 index 00000000..993e7512 --- /dev/null +++ b/misskaty/plugins/grup_tools.py @@ -0,0 +1,340 @@ +import os +import textwrap +import time +from datetime import datetime, timedelta +from logging import getLogger + +from PIL import Image, ImageChops, ImageDraw, ImageFont +from pyrogram import enums, filters +from pyrogram.errors import ChatAdminRequired, MessageTooLong, RPCError +from pyrogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup + +from database.users_chats_db import db +from misskaty import BOT_USERNAME, app +from misskaty.core.decorator import asyncify, capture_err +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.http import http +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER, SUDO, SUPPORT_CHAT +from utils import temp + +LOGGER = getLogger(__name__) + + +def circle(pfp, size=(215, 215)): + pfp = pfp.resize(size, Image.LANCZOS).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.LANCZOS) + 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.size + y_text = text_start_height + lines = textwrap.wrap(text, width=50) + for line in lines: + text_bbox = font.getbbox(line) + (left, top, right, bottom) = text_bbox + line_width = abs(right - left) + line_height = abs(top - bottom) + draw.text( + ((image_width - line_width) / 2, y_text), line, font=font, fill="black" + ) + y_text += line_height + + +@asyncify +def welcomepic(pic, user, chat, id, strings): + background = Image.open("assets/bg.png") # <- Background Image (Should be PNG) + background = background.resize((1024, 500), Image.LANCZOS) + 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( + "assets/Calistoga-Regular.ttf", 37 + ) # <- Text Font of the Member Count. Change the text size for your preference + member_text = strings("welcpic_msg").format( + userr=user, id=id + ) # <- 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), + f"Generated by @{BOT_USERNAME}", + font=ImageFont.truetype("assets/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, -1001777794636]) +) +@use_chat_lang() +async def member_has_joined(c: app, member: ChatMemberUpdated, strings): + 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, + strings("sudo_join_msg"), + ) + 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" + try: + pic = await app.download_media( + user.photo.big_file_id, file_name=f"pp{user.id}.png" + ) + except AttributeError: + pic = "assets/profilepic.png" + try: + welcomeimg = await welcomepic( + pic, user.first_name, member.chat.title, user.id, strings + ) + 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}", + ) + except Exception as e: + LOGGER.info(e) + userspammer = "" + # 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 += strings("combot_msg").format( + umention=user.mention, uid=user.id + ) + 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, group=4) +@use_chat_lang() +async def greet_group(bot, message, strings): + for u in message.new_chat_members: + try: + pic = await app.download_media( + u.photo.big_file_id, file_name=f"pp{u.id}.png" + ) + except AttributeError: + pic = "assets/profilepic.png" + 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: + welcomeimg = await welcomepic( + pic, u.first_name, message.chat.title, u.id, strings + ) + temp.MELCOW[f"welcome-{message.chat.id}"] = await app.send_photo( + message.chat.id, + photo=welcomeimg, + caption=strings("capt_welc").format( + umention=u.mention, uid=u.id, ttl=message.chat.title + ), + ) + userspammer = "" + # Combot API Detection + try: + apicombot = ( + await http.get(f"https://api.cas.chat/check?user_id={u.id}") + ).json() + if apicombot.get("ok") == "true": + await app.ban_chat_member( + message.chat.id, u.id, datetime.now() + timedelta(seconds=30) + ) + userspammer += strings("combot_msg").format( + umention=u.mention, uid=u.id + ) + except Exception as err: + LOGGER.error(f"ERROR in Combot API Detection. {err}") + if userspammer != "": + await bot.send_message(message.chat.id, userspammer) + except Exception as e: + LOGGER.info(e) + 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: + pass + 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) + + +# Not to be used +# @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"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +async def adminlist(_, message): + if message.chat.type == enums.ChatType.PRIVATE: + return await message.reply("Perintah ini hanya untuk grup") + try: + msg = await message.reply_msg(f"Getting admin list in {message.chat.title}..") + administrators = [] + async for m in app.get_chat_members( + message.chat.id, filter=enums.ChatMembersFilter.ADMINISTRATORS + ): + uname = f"@{m.user.username}" if m.user.username else "" + administrators.append(f"{m.user.first_name} [{uname}]") + + res = "".join(f"💠 {i}\n" for i in administrators) + return await msg.edit_msg( + f"Admin in {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 +@ratelimiter +async def kickme(_, message): + reason = None + if len(message.text.split()) >= 2: + reason = message.text.split(None, 1)[1] + try: + await message.chat.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.chat.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)}" + ) + except Exception as err: + await message.reply(f"ERROR: {err}") + + +@app.on_message(filters.command("users") & filters.user(SUDO)) +async def list_users(_, message): + # https://t.me/GetTGLink/4184 + msg = 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 ID: {user.get('id')} -> {user.get('name')}" + if user["ban_status"]["is_banned"]: + out += "( Banned User )" + out += "\n" + try: + await msg.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") + await msg.delete_msg() + + +@app.on_message(filters.command("chats") & filters.user(SUDO)) +async def list_chats(_, message): + msg = 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.get('title')} ({chat.get('id')}) " + if chat["chat_status"]["is_disabled"]: + out += "( Disabled Chat )" + out += "\n" + try: + await msg.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") + await msg.delete_msg() diff --git a/misskaty/plugins/imdb_search.py b/misskaty/plugins/imdb_search.py new file mode 100644 index 00000000..fff50cd3 --- /dev/null +++ b/misskaty/plugins/imdb_search.py @@ -0,0 +1,723 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import json +import logging +import re +from urllib.parse import quote_plus + +from bs4 import BeautifulSoup +from deep_translator import GoogleTranslator +from pykeyboard import InlineButton, InlineKeyboard +from pyrogram import Client, enums +from pyrogram.errors import ( + MediaCaptionTooLong, + MediaEmpty, + MessageIdInvalid, + MessageNotModified, + PhotoInvalidDimensions, + WebpageCurlFailed, + WebpageMediaEmpty, +) +from pyrogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + InputMediaPhoto, + Message, +) + +from database.imdb_db import add_imdbset, is_imdbset, remove_imdbset +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.core.misskaty_patch.listen.listen import ListenerTimeout +from misskaty.helper import GENRES_EMOJI, Cache, get_random_string, http, search_jw +from utils import demoji + +LOGGER = logging.getLogger(__name__) +LIST_CARI = Cache(filename="imdb_cache.db", path="cache", in_memory=False) +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" +} + + +# IMDB Choose Language +@app.on_cmd("imdb") +@ratelimiter +async def imdb_choose(_, ctx: Message): + if len(ctx.command) == 1: + return await ctx.reply_msg( + f"ℹ️ Please add query after CMD!\nEx: /{ctx.command[0]} Jurassic World", + del_in=7, + ) + if ctx.sender_chat: + return await ctx.reply_msg( + "Cannot identify user, please use in private chat.", del_in=7 + ) + kuery = ctx.text.split(" ", 1)[1] + is_imdb, lang = await is_imdbset(ctx.from_user.id) + if is_imdb: + if lang == "eng": + return await imdb_search_en(kuery, ctx) + else: + return await imdb_search_id(kuery, ctx) + buttons = InlineKeyboard() + ranval = get_random_string(4) + LIST_CARI.add(ranval, kuery, timeout=180) + buttons.row( + InlineButton("🇺🇸 English", f"imdbcari#eng#{ranval}#{ctx.from_user.id}"), + InlineButton("🇮🇩 Indonesia", f"imdbcari#ind#{ranval}#{ctx.from_user.id}"), + ) + buttons.row(InlineButton("🚩 Set Default Language", f"imdbset#{ctx.from_user.id}")) + buttons.row(InlineButton("❌ Close", f"close#{ctx.from_user.id}")) + msg = await ctx.reply_photo( + "https://telegra.ph/file/270955ef0d1a8a16831a9.jpg", + caption=f"Hi {ctx.from_user.mention}, Please select the language you want to use on IMDB Search. If you want use default lang for every user, click third button. So no need click select lang if use CMD.", + reply_markup=buttons, + quote=True, + ) + try: + await msg.wait_for_click(from_user_id=ctx.from_user.id, timeout=30) + except ListenerTimeout: + del LIST_CARI[ranval] + try: + await msg.edit_caption( + "😶‍🌫️ Callback Query Timeout. Task Has Been Canceled!" + ) + except MessageIdInvalid: + pass + + +@app.on_cb("imdbset") +@ratelimiter +async def imdblangset(_, query: CallbackQuery): + _, uid = query.data.split("#") + if query.from_user.id != int(uid): + return await query.answer("⚠️ Access Denied!", True) + buttons = InlineKeyboard() + buttons.row( + InlineButton("🇺🇸 English", f"setimdb#eng#{query.from_user.id}"), + InlineButton("🇮🇩 Indonesia", f"setimdb#ind#{query.from_user.id}"), + ) + is_imdb, _ = await is_imdbset(query.from_user.id) + if is_imdb: + buttons.row( + InlineButton("🗑 Remove UserSetting", f"setimdb#rm#{query.from_user.id}") + ) + buttons.row(InlineButton("❌ Close", f"close#{query.from_user.id}")) + msg = await query.message.edit_caption( + "Please select available language below..", reply_markup=buttons + ) + try: + await msg.wait_for_click(from_user_id=int(uid), timeout=30) + except ListenerTimeout: + try: + await msg.edit_caption( + "😶‍🌫️ Callback Query Timeout. Task Has Been Canceled!" + ) + except MessageIdInvalid: + pass + + +@app.on_cb("setimdb") +@ratelimiter +async def imdbsetlang(_, query: CallbackQuery): + _, lang, uid = query.data.split("#") + if query.from_user.id != int(uid): + return await query.answer("⚠️ Access Denied!", True) + _, langset = await is_imdbset(query.from_user.id) + if langset == lang: + return await query.answer(f"⚠️ Your Setting Already in ({langset})!", True) + if lang == "eng": + await add_imdbset(query.from_user.id, lang) + await query.message.edit_caption( + "Language interface for IMDB has been changed to English." + ) + elif lang == "ind": + await add_imdbset(query.from_user.id, lang) + await query.message.edit_caption( + "Bahasa tampilan IMDB sudah diubah ke Indonesia." + ) + else: + await remove_imdbset(query.from_user.id) + await query.message.edit_caption( + "UserSetting for IMDB has been deleted from database." + ) + + +async def imdb_search_id(kueri, message): + BTN = [] + k = await message.reply_photo( + "https://telegra.ph/file/270955ef0d1a8a16831a9.jpg", + caption=f"🔎 Menelusuri {kueri} di database IMDb ...", + quote=True, + ) + msg = "" + buttons = InlineKeyboard(row_width=4) + try: + r = await http.get( + f"https://v3.sg.media-imdb.com/suggestion/titles/x/{quote_plus(kueri)}.json", + headers=headers, + ) + res = r.json().get("d") + if not res: + return await k.edit_caption( + f"⛔️ Tidak ditemukan hasil untuk kueri: {kueri}" + ) + msg += f"🎬 Ditemukan ({len(res)}) hasil untuk kueri: {kueri}\n\n" + for num, movie in enumerate(res, start=1): + title = movie.get("l") + if year := movie.get("yr"): + year = f"({year})" + elif year := movie.get("y"): + year = f"({year})" + else: + year = "(N/A)" + typee = movie.get("q", "N/A").replace("feature", "movie").title() + movieID = re.findall(r"tt(\d+)", movie.get("id"))[0] + msg += f"{num}. {title} {year} - {typee}\n" + BTN.append( + InlineKeyboardButton( + text=num, + callback_data=f"imdbres_id#{message.from_user.id}#{movieID}", + ) + ) + BTN.extend( + ( + InlineKeyboardButton( + text="🚩 Language", + callback_data=f"imdbset#{message.from_user.id}", + ), + InlineKeyboardButton( + text="❌ Close", + callback_data=f"close#{message.from_user.id}", + ), + ) + ) + buttons.add(*BTN) + msg = await k.edit_caption(msg, reply_markup=buttons) + try: + await msg.wait_for_click(from_user_id=message.from_user.id, timeout=30) + except ListenerTimeout: + try: + await msg.edit_caption("😶‍🌫️ Waktu Habis. Task Telah Dibatalkan!") + except MessageIdInvalid: + pass + except Exception as err: + await k.edit_caption( + f"Ooppss, gagal mendapatkan daftar judul di IMDb. Mungkin terkena rate limit atau down.\n\nERROR: {err}" + ) + + +async def imdb_search_en(kueri, message): + BTN = [] + k = await message.reply_photo( + "https://telegra.ph/file/270955ef0d1a8a16831a9.jpg", + caption=f"🔎 Searching {kueri} in IMDb Database...", + quote=True, + ) + msg = "" + buttons = InlineKeyboard(row_width=4) + try: + r = await http.get( + f"https://v3.sg.media-imdb.com/suggestion/titles/x/{quote_plus(kueri)}.json", + headers=headers, + ) + res = r.json().get("d") + if not res: + return await k.edit_caption( + f"⛔️ Result not found for keywords: {kueri}" + ) + msg += f"🎬 Found ({len(res)}) result for keywords: {kueri}\n\n" + for num, movie in enumerate(res, start=1): + title = movie.get("l") + if year := movie.get("yr"): + year = f"({year})" + elif year := movie.get("y"): + year = f"({year})" + else: + year = "(N/A)" + typee = movie.get("q", "N/A").replace("feature", "movie").title() + movieID = re.findall(r"tt(\d+)", movie.get("id"))[0] + msg += f"{num}. {title} {year} - {typee}\n" + BTN.append( + InlineKeyboardButton( + text=num, + callback_data=f"imdbres_en#{message.from_user.id}#{movieID}", + ) + ) + BTN.extend( + ( + InlineKeyboardButton( + text="🚩 Language", + callback_data=f"imdbset#{message.from_user.id}", + ), + InlineKeyboardButton( + text="❌ Close", + callback_data=f"close#{message.from_user.id}", + ), + ) + ) + buttons.add(*BTN) + msg = await k.edit_caption(msg, reply_markup=buttons) + try: + await msg.wait_for_click(from_user_id=message.from_user.id, timeout=30) + except ListenerTimeout: + try: + await msg.edit_caption("😶‍🌫️ Timeout. Task Has Been Cancelled!") + except MessageIdInvalid: + pass + except Exception as err: + await k.edit_caption( + f"Failed when requesting movies title. Maybe got rate limit or down.\n\nERROR: {err}" + ) + + +@app.on_cb("imdbcari") +@ratelimiter +async def imdbcari(_, query: CallbackQuery): + BTN = [] + _, lang, msg, uid = query.data.split("#") + if lang == "ind": + if query.from_user.id != int(uid): + return await query.answer("⚠️ Akses Ditolak!", True) + try: + kueri = LIST_CARI.get(msg) + del LIST_CARI[msg] + except KeyError: + return await query.message.edit_caption("⚠️ Callback Query Sudah Expired!") + await query.message.edit_caption("🔎 Sedang mencari di Database IMDB..") + msg = "" + buttons = InlineKeyboard(row_width=4) + try: + r = await http.get( + f"https://v3.sg.media-imdb.com/suggestion/titles/x/{quote_plus(kueri)}.json", + headers=headers, + ) + res = r.json().get("d") + if not res: + return await query.message.edit_caption( + f"⛔️ Tidak ditemukan hasil untuk kueri: {kueri}" + ) + msg += f"🎬 Ditemukan ({len(res)}) hasil dari: {kueri} ~ {query.from_user.mention}\n\n" + for num, movie in enumerate(res, start=1): + title = movie.get("l") + if year := movie.get("yr"): + year = f"({year})" + elif year := movie.get("y"): + year = f"({year})" + else: + year = "(N/A)" + typee = movie.get("q", "N/A").replace("feature", "movie").title() + movieID = re.findall(r"tt(\d+)", movie.get("id"))[0] + msg += f"{num}. {title} {year} - {typee}\n" + BTN.append( + InlineKeyboardButton( + text=num, callback_data=f"imdbres_id#{uid}#{movieID}" + ) + ) + BTN.extend( + ( + InlineKeyboardButton( + text="🚩 Language", callback_data=f"imdbset#{uid}" + ), + InlineKeyboardButton(text="❌ Close", callback_data=f"close#{uid}"), + ) + ) + buttons.add(*BTN) + try: + msg = await query.message.edit_caption(msg, reply_markup=buttons) + await msg.wait_for_click(from_user_id=int(uid), timeout=30) + except ListenerTimeout: + try: + await msg.edit_caption("😶‍🌫️ Waktu Habis. Task Telah Dibatalkan!") + except MessageIdInvalid: + await msg.reply("😶‍🌫️ Waktu Habis. Task Telah Dibatalkan!") + except MessageIdInvalid: + pass + except Exception as err: + await query.message.edit_caption( + f"Ooppss, gagal mendapatkan daftar judul di IMDb. Mungkin terkena rate limit atau down.\n\nERROR: {err}" + ) + else: + if query.from_user.id != int(uid): + return await query.answer("⚠️ Access Denied!", True) + try: + kueri = LIST_CARI.get(msg) + del LIST_CARI[msg] + except KeyError: + return await query.message.edit_caption("⚠️ Callback Query Expired!") + await query.message.edit_caption("🔎 Looking in the IMDB Database..") + msg = "" + buttons = InlineKeyboard(row_width=4) + try: + r = await http.get( + f"https://v3.sg.media-imdb.com/suggestion/titles/x/{quote_plus(kueri)}.json", + headers=headers, + ) + res = r.json().get("d") + if not res: + return await query.message.edit_caption( + f"⛔️ Result not found for keywords: {kueri}" + ) + msg += f"🎬 Found ({len(res)}) result for keywords: {kueri} ~ {query.from_user.mention}\n\n" + for num, movie in enumerate(res, start=1): + title = movie.get("l") + if year := movie.get("yr"): + year = f"({year})" + elif year := movie.get("y"): + year = f"({year})" + else: + year = "(N/A)" + typee = movie.get("q", "N/A").replace("feature", "movie").title() + movieID = re.findall(r"tt(\d+)", movie.get("id"))[0] + msg += f"{num}. {title} {year} - {typee}\n" + BTN.append( + InlineKeyboardButton( + text=num, callback_data=f"imdbres_en#{uid}#{movieID}" + ) + ) + BTN.extend( + ( + InlineKeyboardButton( + text="🚩 Language", callback_data=f"imdbset#{uid}" + ), + InlineKeyboardButton(text="❌ Close", callback_data=f"close#{uid}"), + ) + ) + buttons.add(*BTN) + try: + msg = await query.message.edit_caption(msg, reply_markup=buttons) + await msg.wait_for_click(from_user_id=int(uid), timeout=30) + except ListenerTimeout: + try: + await msg.edit_caption("😶‍🌫️ Timeout. Task Has Been Cancelled!") + except MessageIdInvalid: + await msg.reply("😶‍🌫️ Timeout. Task Has Been Cancelled!") + except MessageIdInvalid: + pass + except Exception as err: + await query.message.edit_caption( + f"Failed when requesting movies title. Maybe got rate limit or down.\n\nERROR: {err}" + ) + + +@app.on_cb("imdbres_id") +@ratelimiter +async def imdb_id_callback(self: Client, query: CallbackQuery): + 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_caption("⏳ Permintaan kamu sedang diproses.. ") + imdb_url = f"https://www.imdb.com/title/tt{movie}/" + resp = await http.get(imdb_url, headers=headers) + sop = BeautifulSoup(resp, "lxml") + r_json = json.loads( + sop.find("script", attrs={"type": "application/ld+json"}).contents[0] + ) + ott = await search_jw(r_json.get("name"), "ID") + typee = r_json.get("@type", "") + res_str = "" + tahun = ( + re.findall(r"\d{4}\W\d{4}|\d{4}-?", sop.title.text)[0] + if re.findall(r"\d{4}\W\d{4}|\d{4}-?", sop.title.text) + else "N/A" + ) + res_str += f"📹 Judul: {r_json.get('name')} [{tahun}] ({typee})\n" + if aka := r_json.get("alternateName"): + res_str += f"📢 AKA: {aka}\n\n" + else: + res_str += "\n" + if durasi := sop.select('li[data-testid="title-techspec_runtime"]'): + durasi = ( + durasi[0].find(class_="ipc-metadata-list-item__content-container").text + ) + res_str += f"Durasi: {GoogleTranslator('auto', 'id').translate(durasi)}\n" + if kategori := r_json.get("contentRating"): + res_str += f"Kategori: {kategori} \n" + if rating := r_json.get("aggregateRating"): + res_str += f"Peringkat: {rating['ratingValue']}⭐️ dari {rating['ratingCount']} pengguna\n" + if release := sop.select('li[data-testid="title-details-releasedate"]'): + rilis = ( + release[0] + .find( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + .text + ) + rilis_url = release[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 genre := 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"] + ) + res_str += f"Genre: {genre[:-2]}\n" + if negara := sop.select('li[data-testid="title-details-origin"]'): + country = "".join( + f"{demoji(country.text)} #{country.text.replace(' ', '_').replace('-', '_')}, " + for country in negara[0].findAll( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + ) + res_str += f"Negara: {country[:-2]}\n" + if bahasa := sop.select('li[data-testid="title-details-languages"]'): + language = "".join( + f"#{lang.text.replace(' ', '_').replace('-', '_')}, " + for lang in bahasa[0].findAll( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + ) + res_str += f"Bahasa: {language[:-2]}\n" + res_str += "\n🙎 Info Cast:\n" + if directors := r_json.get("director"): + director = "".join( + f"{i['name']}, " for i in directors + ) + res_str += f"Sutradara: {director[:-2]}\n" + if creators := r_json.get("creator"): + creator = "".join( + f"{i['name']}, " + for i in creators + if i["@type"] == "Person" + ) + res_str += f"Penulis: {creator[:-2]}\n" + if actors := r_json.get("actor"): + actor = "".join(f"{i['name']}, " for i in actors) + res_str += f"Pemeran: {actor[:-2]}\n\n" + if deskripsi := r_json.get("description"): + summary = GoogleTranslator("auto", "id").translate(deskripsi) + res_str += f"📜 Plot: {summary}\n\n" + if keywd := r_json.get("keywords"): + key_ = "".join( + f"#{i.replace(' ', '_').replace('-', '_')}, " for i in keywd.split(",") + ) + res_str += f"🔥 Kata Kunci: {key_[:-2]} \n" + if award := sop.select('li[data-testid="award_information"]'): + awards = ( + award[0].find(class_="ipc-metadata-list-item__list-content-item").text + ) + res_str += f"🏆 Penghargaan: {GoogleTranslator('auto', 'id').translate(awards)}\n" + else: + res_str += "\n" + if ott != "": + res_str += f"Tersedia di:\n{ott}\n" + res_str += f"©️ IMDb by @{self.me.username}" + if trailer := r_json.get("trailer"): + trailer_url = trailer["url"] + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("🎬 Open IMDB", url=imdb_url), + InlineKeyboardButton("▶️ Trailer", url=trailer_url), + ] + ] + ) + else: + markup = InlineKeyboardMarkup( + [[InlineKeyboardButton("🎬 Open IMDB", url=imdb_url)]] + ) + if thumb := r_json.get("image"): + try: + await query.message.edit_media( + InputMediaPhoto( + thumb, caption=res_str, parse_mode=enums.ParseMode.HTML + ), + reply_markup=markup, + ) + except (PhotoInvalidDimensions, WebpageMediaEmpty): + poster = thumb.replace(".jpg", "._V1_UX360.jpg") + await query.message.edit_media( + InputMediaPhoto( + poster, caption=res_str, parse_mode=enums.ParseMode.HTML + ), + reply_markup=markup, + ) + except ( + MediaEmpty, + MediaCaptionTooLong, + WebpageCurlFailed, + MessageNotModified, + ): + await query.message.reply( + res_str, parse_mode=enums.ParseMode.HTML, reply_markup=markup + ) + except Exception as err: + LOGGER.error(f"Terjadi error saat menampilkan data IMDB. ERROR: {err}") + else: + await query.message.edit_caption( + res_str, parse_mode=enums.ParseMode.HTML, reply_markup=markup + ) + except AttributeError: + await query.message.edit_caption("Maaf, gagal mendapatkan info data dari IMDB.") + except (MessageNotModified, MessageIdInvalid): + pass + + +@app.on_cb("imdbres_en") +@ratelimiter +async def imdb_en_callback(self: Client, query: CallbackQuery): + i, userid, movie = query.data.split("#") + if query.from_user.id != int(userid): + return await query.answer("⚠️ Access Denied!", True) + try: + await query.message.edit_caption("⏳ Getting IMDb source..") + imdb_url = f"https://www.imdb.com/title/tt{movie}/" + resp = await http.get(imdb_url, headers=headers) + sop = BeautifulSoup(resp, "lxml") + r_json = json.loads( + sop.find("script", attrs={"type": "application/ld+json"}).contents[0] + ) + ott = await search_jw(r_json.get("name"), "US") + typee = r_json.get("@type", "") + res_str = "" + tahun = ( + re.findall(r"\d{4}\W\d{4}|\d{4}-?", sop.title.text)[0] + if re.findall(r"\d{4}\W\d{4}|\d{4}-?", sop.title.text) + else "N/A" + ) + res_str += f"📹 Judul: {r_json.get('name')} [{tahun}] ({typee})\n" + if aka := r_json.get("alternateName"): + res_str += f"📢 AKA: {aka}\n\n" + else: + res_str += "\n" + if durasi := sop.select('li[data-testid="title-techspec_runtime"]'): + durasi = ( + durasi[0].find(class_="ipc-metadata-list-item__content-container").text + ) + res_str += f"Duration: {durasi}\n" + if kategori := r_json.get("contentRating"): + res_str += f"Category: {kategori} \n" + if rating := r_json.get("aggregateRating"): + res_str += f"Rating: {rating['ratingValue']}⭐️ from {rating['ratingCount']} users\n" + if release := sop.select('li[data-testid="title-details-releasedate"]'): + rilis = ( + release[0] + .find( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + .text + ) + rilis_url = release[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 genre := 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"] + ) + res_str += f"Genre: {genre[:-2]}\n" + if negara := sop.select('li[data-testid="title-details-origin"]'): + country = "".join( + f"{demoji(country.text)} #{country.text.replace(' ', '_').replace('-', '_')}, " + for country in negara[0].findAll( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + ) + res_str += f"Country: {country[:-2]}\n" + if bahasa := sop.select('li[data-testid="title-details-languages"]'): + language = "".join( + f"#{lang.text.replace(' ', '_').replace('-', '_')}, " + for lang in bahasa[0].findAll( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + ) + res_str += f"Language: {language[:-2]}\n" + res_str += "\n🙎 Cast Info:\n" + if r_json.get("director"): + director = "".join( + f"{i['name']}, " for i in r_json["director"] + ) + res_str += f"Director: {director[:-2]}\n" + if r_json.get("creator"): + creator = "".join( + f"{i['name']}, " + for i in r_json["creator"] + if i["@type"] == "Person" + ) + res_str += f"Writer: {creator[:-2]}\n" + if r_json.get("actor"): + actors = actors = "".join( + f"{i['name']}, " for i in r_json["actor"] + ) + res_str += f"Stars: {actors[:-2]}\n\n" + if description := r_json.get("description"): + res_str += f"📜 Summary: {description}\n\n" + if r_json.get("keywords"): + key_ = "".join( + f"#{i.replace(' ', '_').replace('-', '_')}, " + for i in r_json["keywords"].split(",") + ) + res_str += f"🔥 Keywords: {key_[:-2]} \n" + if award := sop.select('li[data-testid="award_information"]'): + awards = ( + award[0].find(class_="ipc-metadata-list-item__list-content-item").text + ) + res_str += f"🏆 Awards: {awards}\n" + else: + res_str += "\n" + if ott != "": + res_str += f"Available On:\n{ott}\n" + res_str += f"©️ IMDb by @{self.me.username}" + if trailer := r_json.get("trailer"): + trailer_url = trailer["url"] + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton("🎬 Open IMDB", url=imdb_url), + InlineKeyboardButton("▶️ Trailer", url=trailer_url), + ] + ] + ) + else: + markup = InlineKeyboardMarkup( + [[InlineKeyboardButton("🎬 Open IMDB", url=imdb_url)]] + ) + if thumb := r_json.get("image"): + try: + await query.message.edit_media( + InputMediaPhoto( + thumb, caption=res_str, parse_mode=enums.ParseMode.HTML + ), + reply_markup=markup, + ) + except (PhotoInvalidDimensions, WebpageMediaEmpty): + poster = thumb.replace(".jpg", "._V1_UX360.jpg") + await query.message.edit_media( + InputMediaPhoto( + poster, caption=res_str, parse_mode=enums.ParseMode.HTML + ), + reply_markup=markup, + ) + except ( + MediaCaptionTooLong, + WebpageCurlFailed, + MediaEmpty, + MessageNotModified, + ): + await query.message.reply( + res_str, parse_mode=enums.ParseMode.HTML, reply_markup=markup + ) + except Exception as err: + LOGGER.error(f"Error while displaying IMDB Data. ERROR: {err}") + else: + await query.message.edit_caption( + res_str, parse_mode=enums.ParseMode.HTML, reply_markup=markup + ) + except AttributeError: + await query.message.edit_caption("Sorry, failed getting data from IMDB.") + except (MessageNotModified, MessageIdInvalid): + pass diff --git a/misskaty/plugins/inkick_user.py b/misskaty/plugins/inkick_user.py new file mode 100644 index 00000000..8666621e --- /dev/null +++ b/misskaty/plugins/inkick_user.py @@ -0,0 +1,265 @@ +import time +from asyncio import sleep + +from pyrogram import enums, filters +from pyrogram.errors import FloodWait +from pyrogram.errors.exceptions.bad_request_400 import ( + ChatAdminRequired, + UserAdminInvalid, +) +from pyrogram.errors.exceptions.forbidden_403 import ChatWriteForbidden + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.vars import COMMAND_HANDLER + +__MODULE__ = "Inkick" +__HELP__ = """" +/instatus - View member status in group. +/ban_ghosts - Remove deleted account from group. +""" + + +@app.on_message( + filters.incoming & ~filters.private & filters.command(["inkick"], COMMAND_HANDLER) +) +@ratelimiter +@app.adminsOnly("can_restrict_members") +async def inkick(_, message): + if message.sender_chat: + return await message.reply_msg( + "This feature not available for channel.", del_in=4 + ) + 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) +) +@ratelimiter +@app.adminsOnly("can_restrict_members") +async def uname(_, message): + if message.sender_chat: + return await message.reply_msg( + "This feature not available for channel.", del_in=4 + ) + 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(["ban_ghosts"], COMMAND_HANDLER) +) +@ratelimiter +@app.adminsOnly("can_restrict_members") +async def rm_delacc(_, message): + if message.sender_chat: + return await message.reply_msg( + "This feature not available for channel.", del_in=4 + ) + 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 Nooo, i'm doesn't have admin permission in this group. Make sure i'm have admin permission to ban users." + ) + break + except FloodWait as e: + await sleep(e.value) + if count == 0: + return await sent_message.edit_msg( + "There are no deleted accounts in this chat." + ) + await sent_message.edit_msg(f"✔️ **Berhasil menendang {count} akun terhapus.**") + 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) +) +@ratelimiter +@app.adminsOnly("can_restrict_members") +async def instatus(client, message): + if message.sender_chat: + return await message.reply_msg("Not supported channel.", del_in=4) + bstat = await app.get_chat_member(message.chat.id, client.me.id) + if bstat.status.value != "administrator": + return await message.reply_msg( + "Please give me all basic admin permission, to run this command." + ) + 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 _ in app.get_chat_members( + message.chat.id, filter=enums.ChatMembersFilter.BANNED + ): + banned += 1 + async for _ 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_msg( + "💠 {}\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 (/ban_ghosts): {}\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..0ff28458 --- /dev/null +++ b/misskaty/plugins/inline_search.py @@ -0,0 +1,760 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import json +import re +import traceback +from logging import getLogger +from sys import platform +from sys import version as pyver + +from bs4 import BeautifulSoup +from deep_translator import GoogleTranslator +from pykeyboard import InlineButton, InlineKeyboard +from pyrogram import __version__ as pyrover +from pyrogram import enums, filters +from pyrogram.errors import MessageIdInvalid, MessageNotModified +from pyrogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup, + InlineQuery, + InlineQueryResultArticle, + InlineQueryResultPhoto, + InputTextMessageContent, +) + +from misskaty import BOT_USERNAME, app, user +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper import GENRES_EMOJI, http, post_to_telegraph, search_jw +from misskaty.plugins.dev import shell_exec +from misskaty.plugins.misc_tools import get_content +from misskaty.vars import USER_SESSION +from utils import demoji + +__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. +~ info [user id/username] - Check info about a user. +""" + +keywords_list = ["imdb", "pypi", "git", "google", "secretmsg", "info", "botapi"] + +PRVT_MSGS = {} +LOGGER = getLogger() + + +@app.on_inline_query() +async def inline_menu(_, inline_query: InlineQuery): + if inline_query.query.strip().lower().strip() == "": + aspymon_ver = (await shell_exec("pip freeze | grep async-pymongo"))[0] + 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 USER_SESSION and await app.get_me() else "Dead" + ubot_state = "Alive" if USER_SESSION and 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 Stats:** `{bot_state}` +**UserBot Stats:** `{ubot_state}` +**Python:** `{pyver.split()[0]}` +**Pyrogram:** `{pyrover}` +**MongoDB:** `{aspymon_ver}` +**Platform:** `{platform}` +**Bot:** {(await app.get_me()).first_name} +""" + if USER_SESSION: + msg += f"**UserBot:** {(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( + f"Github Repo @{BOT_USERNAME}\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] == "botapi": + if len(inline_query.query.strip().lower().split()) < 2: + return await inline_query.answer( + results=[], + switch_pm_text="Bot Api Docs | botapi [QUERY]", + switch_pm_parameter="inline", + ) + kueri = 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" + } + jsonapi = await http.get( + "https://github.com/PaulSonOfLars/telegram-bot-api-spec/raw/main/api.json", + headers=headers, + follow_redirects=True, + ) + parsemethod = jsonapi.json().get("methods") + parsetypes = jsonapi.json().get("types") + datajson = [] + method = None + is_img = False + for method in parsemethod: + if kueri.lower() in method.lower(): + link = parsemethod[method]["href"] + description = parsemethod[method]["description"][0] + buttons = InlineKeyboard() + buttons.row( + InlineButton("Open Docs", url=link), + InlineButton( + "Search Again", + switch_inline_query_current_chat=inline_query.query, + ), + ) + buttons.row( + InlineButton("Give Coffee", url="https://yasirpedia.eu.org"), + ) + returns = "".join(f"{i}, " for i in parsemethod[method]["returns"]) + msg = f"{method} ({returns[:-2]})\n" + msg += f"{description}\n\n" + msg += "Variables:\n" + if parsemethod[method].get("fields"): + for i in parsemethod[method]["fields"]: + msg += f"{i['name']} ({i['types'][0]})\nRequired: {i['required']}\n{i['description']}\n\n" + if len(msg.encode("utf-8")) > 4096: + body_text = f""" +
{msg.replace("", "(user_id)")}
+ """ + msg = await post_to_telegraph(is_img, method, body_text) + datajson.append( + InlineQueryResultArticle( + title=method, + input_message_content=InputTextMessageContent( + message_text=msg, + parse_mode=enums.ParseMode.HTML, + disable_web_page_preview=True, + ), + url=link, + description=description, + thumb_url="https://img.freepik.com/premium-vector/open-folder-folder-with-documents-document-protection-concept_183665-104.jpg", + reply_markup=buttons, + ) + ) + for types in parsetypes: + if kueri.lower() in types.lower(): + link = parsetypes[types]["href"] + description = parsetypes[types]["description"][0] + buttons = InlineKeyboard() + buttons.row( + InlineButton("Open Docs", url=link), + InlineButton( + "Search Again", + switch_inline_query_current_chat=inline_query.query, + ), + ) + buttons.row( + InlineButton("Give Coffee", url="https://yasirpedia.eu.org"), + ) + msg = f"{types}\n" + msg += f"{description}\n\n" + msg += "Variables:\n" + if parsetypes[types].get("fields"): + for i in parsetypes[types]["fields"]: + msg += f"{i['name']} ({i['types'][0]})\nRequired: {i['required']}\n{i['description']}\n\n" + if len(msg.encode("utf-8")) > 4096: + body_text = f""" +
{msg}
+ """ + msg = await post_to_telegraph(is_img, method, body_text) + datajson.append( + InlineQueryResultArticle( + title=types, + input_message_content=InputTextMessageContent( + message_text=msg, + parse_mode=enums.ParseMode.HTML, + disable_web_page_preview=True, + ), + url=link, + description=description, + thumb_url="https://img.freepik.com/premium-vector/open-folder-folder-with-documents-document-protection-concept_183665-104.jpg", + reply_markup=buttons, + ) + ) + await inline_query.answer( + results=datajson[:50], + is_gallery=False, + is_personal=False, + next_offset="", + cache_time=5, + switch_pm_text=f"Found {len(datajson)} results", + switch_pm_parameter="help", + ) + 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/107.0.0.0 Safari/537.36 Edge/107.0.1418.42" + } + 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.find_all("div", class_="kvH3mc BToiNc UK95Uc"): + link = result.find("div", class_="yuRUbf").find("a").get("href") + title = result.find("div", class_="yuRUbf").find("h3").get_text() + try: + snippet = result.find( + "div", class_="VwiC3b yXK7lf MUxGbd yDYNvb lyLwlc lEBKkf" + ).get_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] == "info": + if len(inline_query.query.strip().lower().split()) < 2: + return await inline_query.answer( + results=[], + switch_pm_text="User Info | info [id/username]", + switch_pm_parameter="inline", + ) + userr = inline_query.query.split(None, 1)[1].strip() + if "t.me" in userr: + r = re.search(r"t.me/(\w+)", userr) + userr = r[1] + try: + diaa = await app.get_users(userr) + except Exception: # pylint: disable=broad-except + inline_query.stop_propagation() + return + namanya = ( + f"{diaa.first_name} {diaa.last_name}" if diaa.last_name else diaa.first_name + ) + msg = f"🏷 Name: {namanya}\n🆔 ID: {diaa.id}\n" + if diaa.username: + msg += f"🌐 Username: @{diaa.username}\n" + if diaa.status: + msg += f"🕰 User Status: {diaa.status}\n" + if diaa.dc_id: + msg += f"🌏 DC: {diaa.dc_id}\n" + msg += f"✨ Premium: {diaa.is_premium}\n" + msg += f"⭐️ Verified: {diaa.is_verified}\n" + msg += f"🤖 Bot: {diaa.is_bot}\n" + if diaa.language_code: + msg += f"🇮🇩 Language: {diaa.language_code}" + results = [ + InlineQueryResultArticle( + title=f"Get information off {diaa.id}", + input_message_content=InputTextMessageContent(msg), + description=f"Get information off {diaa.id}", + ) + ] + await inline_query.answer(results=results, cache_time=3) + 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://yasirapi.eu.org/pypi?q={query}") + srch_results = search_results.json() + data = [] + for sraeo in srch_results["result"]: + title = sraeo.get("name") + link = sraeo.get("url") + deskripsi = sraeo.get("description") + 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', '')})" + 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\((.+)\)")) +@ratelimiter +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\((.+)\)")) +@ratelimiter +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, _ = 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#")) +@ratelimiter +async def imdb_inl(_, query): + i, cbuser, movie = query.data.split("#") + if cbuser == f"{query.from_user.id}": + try: + await query.edit_message_caption( + "⏳ Permintaan kamu sedang diproses.. " + ) + 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] + ) + ott = await search_jw(r_json["name"], "en_ID") + res_str = "" + typee = r_json.get("@type", "") + tahun = ( + re.findall(r"\d{4}\W\d{4}|\d{4}-?", sop.title.text)[0] + if re.findall(r"\d{4}\W\d{4}|\d{4}-?", sop.title.text) + else "N/A" + ) + res_str += f"📹 Judul: {r_json['name']} [{tahun}] ({typee})\n" + if r_json.get("alternateName"): + res_str += ( + f"📢 AKA: {r_json.get('alternateName')}\n\n" + ) + else: + res_str += "\n" + if durasi := sop.select('li[data-testid="title-techspec_runtime"]'): + durasi = ( + durasi[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 release := sop.select('li[data-testid="title-details-releasedate"]'): + rilis = ( + release[0] + .find( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + .text + ) + rilis_url = release[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"] + ) + res_str += f"Genre: {genre[:-2]}\n" + if negara := sop.select('li[data-testid="title-details-origin"]'): + country = "".join( + f"{demoji(country.text)} #{country.text.replace(' ', '_').replace('-', '_')}, " + for country in negara[0].findAll( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + ) + res_str += f"Negara: {country[:-2]}\n" + if bahasa := sop.select('li[data-testid="title-details-languages"]'): + language = "".join( + f"#{lang.text.replace(' ', '_').replace('-', '_')}, " + for lang in bahasa[0].findAll( + class_="ipc-metadata-list-item__list-content-item ipc-metadata-list-item__list-content-item--link" + ) + ) + res_str += f"Bahasa: {language[:-2]}\n" + res_str += "\n🙎 Info Cast:\n" + if r_json.get("director"): + director = "".join( + f"{i['name']}, " + for i in r_json["director"] + ) + res_str += f"Sutradara: {director[:-2]}\n" + if r_json.get("creator"): + creator = "".join( + f"{i['name']}, " + for i in r_json["creator"] + if i["@type"] == "Person" + ) + res_str += f"Penulis: {creator[:-2]}\n" + if r_json.get("actor"): + actors = "".join( + f"{i['name']}, " for i in r_json["actor"] + ) + res_str += f"Pemeran: {actors[:-2]}\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"): + key_ = "".join( + f"#{i.replace(' ', '_').replace('-', '_')}, " + for i in r_json["keywords"].split(",") + ) + res_str += f"🔥 Kata Kunci: {key_[:-2]} \n" + if award := sop.select('li[data-testid="award_information"]'): + awards = ( + award[0] + .find(class_="ipc-metadata-list-item__list-content-item") + .text + ) + res_str += f"🏆 Penghargaan: {GoogleTranslator('auto', 'id').translate(awards)}\n" + else: + res_str += "\n" + if ott != "": + res_str += f"Available On:\n{ott}\n" + res_str += "©️ IMDb by @MissKatyRoBot" + if r_json.get("trailer"): + trailer_url = r_json["trailer"]["url"] + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "🎬 Buka IMDB", + url=url, + ), + InlineKeyboardButton("▶️ Trailer", url=trailer_url), + ] + ] + ) + else: + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "🎬 Open IMDB", + url=url, + ) + ] + ] + ) + await query.edit_message_caption( + res_str, parse_mode=enums.ParseMode.HTML, reply_markup=markup + ) + except (MessageNotModified, MessageIdInvalid): + pass + 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..aa47eaec --- /dev/null +++ b/misskaty/plugins/json.py @@ -0,0 +1,48 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" + +import os + +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter + + +# View Structure Telegram Message As JSON +@app.on_cmd("json") +@ratelimiter +async def jsonify(_, message: 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}", + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text="❌ Close", + callback_data=f"close#{message.from_user.id if message.from_user else 2024984460}", + ) + ] + ] + ), + ) + except Exception as e: + with open("json.txt", "w+", encoding="utf8") as out_file: + out_file.write(str(the_real_message)) + await message.reply_document( + document="json.txt", + caption=f"{str(e)}", + disable_notification=True, + reply_to_message_id=reply_to_id, + thumb="assets/thumb.jpg", + ) + os.remove("json.txt") diff --git a/misskaty/plugins/karma.py b/misskaty/plugins/karma.py new file mode 100644 index 00000000..17913d9b --- /dev/null +++ b/misskaty/plugins/karma.py @@ -0,0 +1,216 @@ +import re + +from pyrogram import filters + +from database.karma_db import ( + get_karma, + get_karmas, + is_karma_on, + karma_off, + karma_on, + update_karma, +) +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 + +__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|tq|thank you|thanx|thanks|pro|cool|good|agree|makasih|👍|\+\+ .+)$" +regex_downvote = r"^(-|--|-1|not cool|disagree|worst|bad|👎|-- .+)$" + +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.is_bot: + 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_msg( + 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.is_bot: + return + if message.reply_to_message.from_user.id == message.from_user.id: + return + + chat_id = message.chat.id + user_id = message.from_user.id + 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) + 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_msg( + 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_msg("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.") + try: + userdb = await get_user_id_and_usernames(app) + except (AttributeError, TypeError): + return + 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 has 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("can_change_info") +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 for this chat.") + elif state == "disable": + await karma_off(chat_id) + await message.reply_text("Disabled Karma System for this chat.") + else: + await message.reply_text(usage) diff --git a/misskaty/plugins/lang_setting.py b/misskaty/plugins/lang_setting.py new file mode 100644 index 00000000..f251b5c8 --- /dev/null +++ b/misskaty/plugins/lang_setting.py @@ -0,0 +1,120 @@ +from functools import partial +from typing import Union + +from pyrogram import filters +from pyrogram.enums import ChatType +from pyrogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, +) + +from database.locale_db import set_db_lang +from misskaty import app +from misskaty.core.misskaty_patch.listen.listen import ListenerTimeout +from misskaty.vars import COMMAND_HANDLER + +from ..core.decorator.permissions import require_admin +from ..helper.localization import ( + default_language, + get_locale_string, + langdict, + use_chat_lang, +) + + +def gen_langs_kb(): + langs = list(langdict) + kb = [] + while langs: + lang = langdict[langs[0]]["main"] + a = [ + InlineKeyboardButton( + f"{lang['language_flag']} {lang['language_name']}", + callback_data=f"set_lang {langs[0]}", + ) + ] + + langs.pop(0) + if langs: + lang = langdict[langs[0]]["main"] + a.append( + InlineKeyboardButton( + f"{lang['language_flag']} {lang['language_name']}", + callback_data=f"set_lang {langs[0]}", + ) + ) + + langs.pop(0) + kb.append(a) + return kb + + +@app.on_callback_query(filters.regex("^chlang$")) +@app.on_message(filters.command(["setchatlang", "setlang"], COMMAND_HANDLER)) +@require_admin(allow_in_private=True) +@use_chat_lang() +async def chlang(_, m: Union[CallbackQuery, Message], strings): + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + *gen_langs_kb(), + [ + InlineKeyboardButton( + strings("back_btn", context="general"), callback_data="start_back" + ) + ], + ] + ) + + if isinstance(m, CallbackQuery): + msg = m.message + sender = msg.edit_text + else: + msg = m + sender = msg.reply_text + if not msg.from_user: + return + + res = ( + strings("language_changer_private") + if msg.chat.type == ChatType.PRIVATE + else strings("language_changer_chat") + ) + msg = await sender(res, reply_markup=keyboard) + try: + await msg.wait_for_click(from_user_id=m.from_user.id, timeout=30) + except ListenerTimeout: + await msg.edit_msg(strings("exp_task", context="general")) + + +@app.on_callback_query(filters.regex("^set_lang ")) +@require_admin(allow_in_private=True) +@use_chat_lang() +async def set_chat_lang(_, m: CallbackQuery, strings): + lang = m.data.split()[1] + await set_db_lang(m.message.chat.id, m.message.chat.type, lang) + + strings = partial( + get_locale_string, + langdict[lang].get("lang_setting", langdict[default_language]["lang_setting"]), + lang, + "lang_setting", + ) + + if m.message.chat.type == ChatType.PRIVATE: + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + strings("back_btn", context="general"), + callback_data="start_back", + ) + ] + ] + ) + else: + keyboard = None + await m.message.edit_msg( + strings("language_changed_successfully"), reply_markup=keyboard + ) diff --git a/misskaty/plugins/locks.py b/misskaty/plugins/locks.py new file mode 100644 index 00000000..746a8b05 --- /dev/null +++ b/misskaty/plugins/locks.py @@ -0,0 +1,190 @@ +""" +MIT License + +Copyright (c) 2023 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 asyncio + +from pyrogram import filters +from pyrogram.errors import ChatNotModified, FloodWait +from pyrogram.types import ChatPermissions + +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.permissions import adminsOnly, list_admins +from misskaty.helper.functions import get_urls_from_text +from misskaty.vars import COMMAND_HANDLER, SUDO + +__MODULE__ = "Locks" +__HELP__ = """ +Commands: /lock | /unlock | /locks [No Parameters Required] + +Parameters: + messages | stickers | gifs | media | games | polls + + inline | url | group_info | user_add | pin + +You can only pass the "all" parameter with /lock, not with /unlock + +Example: + /lock all +""" + +incorrect_parameters = "Incorrect Parameters, Check Locks Section In Help." +# Using disable_preview as a switch for url checker +# That way we won't need an additional db to check +# If url lock is enabled/disabled for a chat +data = { + "messages": "can_send_messages", + "stickers": "can_send_other_messages", + "gifs": "can_send_other_messages", + "media": "can_send_media_messages", + "games": "can_send_other_messages", + "inline": "can_send_other_messages", + "url": "can_add_web_page_previews", + "polls": "can_send_polls", + "group_info": "can_change_info", + "useradd": "can_invite_users", + "pin": "can_pin_messages", +} + + +async def current_chat_permissions(chat_id): + perms = [] + try: + perm = (await app.get_chat(chat_id)).permissions + except FloodWait as e: + await asyncio.sleep(e.value) + perm = (await app.get_chat(chat_id)).permissions + if perm.can_send_messages: + perms.append("can_send_messages") + if perm.can_send_media_messages: + perms.append("can_send_media_messages") + if perm.can_send_other_messages: + perms.append("can_send_other_messages") + if perm.can_add_web_page_previews: + perms.append("can_add_web_page_previews") + if perm.can_send_polls: + perms.append("can_send_polls") + if perm.can_change_info: + perms.append("can_change_info") + if perm.can_invite_users: + perms.append("can_invite_users") + if perm.can_pin_messages: + perms.append("can_pin_messages") + + return perms + + +async def tg_lock(message, permissions: list, perm: str, lock: bool): + if lock: + if perm not in permissions: + return await message.reply_text("Already locked.") + permissions.remove(perm) + elif perm in permissions: + return await message.reply_text("Already Unlocked.") + else: + permissions.append(perm) + + permissions = {perm: True for perm in list(set(permissions))} + + try: + await app.set_chat_permissions(message.chat.id, ChatPermissions(**permissions)) + except ChatNotModified: + return await message.reply_text( + "To unlock this, you have to unlock 'messages' first." + ) + + await message.reply_text(("Locked." if lock else "Unlocked.")) + + +@app.on_message(filters.command(["lock", "unlock"], COMMAND_HANDLER) & ~filters.private) +@adminsOnly("can_restrict_members") +async def locks_func(_, message): + if len(message.command) != 2: + return await message.reply_text(incorrect_parameters) + + chat_id = message.chat.id + parameter = message.text.strip().split(None, 1)[1].lower() + state = message.command[0].lower() + + if parameter not in data and parameter != "all": + return await message.reply_text(incorrect_parameters) + + permissions = await current_chat_permissions(chat_id) + + if parameter in data: + await tg_lock(message, permissions, data[parameter], state == "lock") + elif parameter == "all" and state == "lock": + await app.set_chat_permissions(chat_id, ChatPermissions()) + await message.reply_text(f"Locked Everything in {message.chat.title}") + + elif parameter == "all" and state == "unlock": + await app.set_chat_permissions( + chat_id, + ChatPermissions( + can_send_messages=True, + can_send_media_messages=True, + can_send_other_messages=True, + can_add_web_page_previews=True, + can_send_polls=True, + can_change_info=False, + can_invite_users=True, + can_pin_messages=False, + ), + ) + await message.reply(f"Unlocked Everything in {message.chat.title}") + + +@app.on_message(filters.command("locks", COMMAND_HANDLER) & ~filters.private) +@capture_err +async def locktypes(_, message): + permissions = await current_chat_permissions(message.chat.id) + + if not permissions: + return await message.reply_text("No Permissions.") + + perms = "".join(f"__**{i}**__\n" for i in permissions) + await message.reply_text(perms) + + +@app.on_message(filters.text & ~filters.private, group=69) +async def url_detector(_, message): + user = message.from_user + chat_id = message.chat.id + text = message.text.lower().strip() + + if not text or not user: + return + mods = await list_admins(chat_id) + if user.id in mods or user.id in SUDO: + return + + if get_urls_from_text(text): + permissions = await current_chat_permissions(chat_id) + if "can_add_web_page_previews" not in permissions: + try: + await message.delete_msg() + except Exception: + await message.reply_msg( + "This message contains a URL, " + + "but i don't have enough permissions to delete it" + ) diff --git a/misskaty/plugins/media_extractor.py b/misskaty/plugins/media_extractor.py new file mode 100644 index 00000000..806663ff --- /dev/null +++ b/misskaty/plugins/media_extractor.py @@ -0,0 +1,221 @@ +""" + * @author yasir + * @created 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +import json +import os +from logging import getLogger +from re import I +from re import split as ngesplit +from time import time +from urllib.parse import unquote + +from pyrogram import Client, filters +from pyrogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, +) + +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.core.misskaty_patch.listen.listen import ListenerTimeout +from misskaty.helper.human_read import get_readable_time +from misskaty.helper.localization import use_chat_lang +from misskaty.helper.pyro_progress import progress_for_pyrogram +from misskaty.helper.tools import get_random_string +from misskaty.plugins.dev import shell_exec +from misskaty.vars import COMMAND_HANDLER + +LOGGER = getLogger(__name__) + +ARCH_EXT = ( + "mkv", + "mp4", + "mov", + "wmv", + "3gp", + "mpg", + "webm", + "avi", + "flv", + "m4v", +) + +__MODULE__ = "MediaExtract" +__HELP__ = """ +/extractmedia [URL] - Extract subtitle or audio from video using link. (Not support TG File to reduce bandwith usage.) +/converttosrt [Reply to .ass or .vtt TG File] - Convert from .ass or .vtt to srt +/converttoass [Reply to .srt or .vtt TG File] - Convert from .srt or .vtt to srt +""" + + +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(lang, url, ext): + 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 or not get_base_name( + os.path.basename(unquote(scheme_removed)) + ): + return f"[{lang.upper()}] MissKatySub{get_random_string(4)}.{ext}" + return f"[{lang.upper()}] {get_base_name(os.path.basename(unquote(scheme_removed)))}{get_random_string(3)}.{ext}" + + +@app.on_message(filters.command(["ceksub", "extractmedia"], COMMAND_HANDLER)) +@ratelimiter +@use_chat_lang() +async def ceksub(_, ctx: Message, strings): + if len(ctx.command) == 1: + return await ctx.reply_msg( + strings("sub_extr_help").format(cmd=ctx.command[0]), quote=True, del_in=5 + ) + link = ctx.command[1] + start_time = time() + pesan = await ctx.reply_msg(strings("progress_str"), 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#{lang}#0:{mapping}#{stream_name}", + ) + ] + ) + timelog = time() - start_time + buttons.append( + [InlineKeyboardButton(strings("cancel_btn"), f"close#{ctx.from_user.id}")] + ) + msg = await pesan.edit_msg( + strings("press_btn_msg").format(timelog=get_readable_time(timelog)), + reply_markup=InlineKeyboardMarkup(buttons), + ) + await msg.wait_for_click(from_user_id=ctx.from_user.id, timeout=30) + except ListenerTimeout: + await msg.edit_msg(strings("exp_task", context="general")) + except Exception: + await pesan.edit_msg(strings("fail_extr_media")) + + +@app.on_message(filters.command(["converttosrt", "converttoass"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +@use_chat_lang() +async def convertsrt(self: Client, ctx: Message, strings): + reply = ctx.reply_to_message + if ( + not reply + or not reply.document + or not reply.document.file_name + or not reply.document.file_name.endswith((".vtt", ".ass", ".srt")) + ): + return await ctx.reply_msg( + strings("conv_sub_help").format(cmd=ctx.command[0]), del_in=6 + ) + msg = await ctx.reply_msg(strings("convert_str"), quote=True) + if not os.path.exists("downloads"): + os.makedirs("downloads") + dl = await reply.download(file_name="downloads/") + filename = dl.split("/", 3)[3] + LOGGER.info( + f"ConvertSub: {filename} by {ctx.from_user.first_name if ctx.from_user else ctx.sender_chat.title} [{ctx.from_user.id if ctx.from_user else ctx.sender_chat.id}]" + ) + suffix = "srt" if ctx.command[0] == "converttosrt" else "ass" + await shell_exec(f"ffmpeg -i '{dl}' 'downloads/{filename}.{suffix}'") + c_time = time() + await ctx.reply_document( + f"downloads/{filename}.{suffix}", + caption=strings("capt_conv_sub").format(nf=filename, bot=self.me.username), + thumb="assets/thumb.jpg", + progress=progress_for_pyrogram, + progress_args=(strings("up_str"), msg, c_time, self.me.dc_id), + ) + await msg.delete_msg() + try: + os.remove(dl) + os.remove(f"downloads/{filename}.{suffix}") + except: + pass + + +@app.on_callback_query(filters.regex(r"^streamextract#")) +@ratelimiter +@use_chat_lang() +async def stream_extract(self: Client, update: CallbackQuery, strings): + cb_data = update.data + usr = update.message.reply_to_message + if update.from_user.id != usr.from_user.id: + return await update.answer(strings("unauth_cb"), True) + _, lang, map_code, codec = cb_data.split("#") + try: + link = update.message.reply_to_message.command[1] + except: + return await update.answer(strings("invalid_cb"), True) + await update.message.edit_msg(strings("progress_str")) + if codec == "aac": + ext = "aac" + elif codec == "mp3": + ext = "mp3" + elif codec == "eac3": + ext = "eac3" + else: + ext = "srt" + start_time = time() + namafile = get_subname(lang, link, ext) + try: + LOGGER.info( + f"ExtractSub: {namafile} by {update.from_user.first_name} [{update.from_user.id}]" + ) + (await shell_exec(f"ffmpeg -i {link} -map {map_code} '{namafile}'"))[0] + timelog = time() - start_time + c_time = time() + await update.message.reply_document( + namafile, + caption=strings("capt_extr_sub").format( + nf=namafile, bot=self.me.username, timelog=get_readable_time(timelog) + ), + reply_to_message_id=usr.id, + thumb="assets/thumb.jpg", + progress=progress_for_pyrogram, + progress_args=(strings("up_str"), update.message, c_time, self.me.dc_id), + ) + await update.message.delete_msg() + try: + os.remove(namafile) + except: + pass + except Exception as e: + try: + os.remove(namafile) + except: + pass + await update.message.edit_msg(strings("fail_extr_sub").format(link=link, e=e)) diff --git a/misskaty/plugins/mediainfo.py b/misskaty/plugins/mediainfo.py new file mode 100644 index 00000000..3bd6f929 --- /dev/null +++ b/misskaty/plugins/mediainfo.py @@ -0,0 +1,136 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +import io +import subprocess +import time +from os import path +from os import remove as osremove + +from pyrogram import filters +from pyrogram.file_id import FileId +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper import post_to_telegraph, progress_for_pyrogram, runcmd +from misskaty.helper.localization import use_chat_lang +from misskaty.helper.mediainfo_paste import mediainfo_paste +from misskaty.vars import COMMAND_HANDLER +from utils import get_file_id + + +@app.on_message(filters.command(["mediainfo"], COMMAND_HANDLER)) +@ratelimiter +@use_chat_lang() +async def mediainfo(_, ctx: Message, strings): + if not ctx.from_user: + return + if ctx.reply_to_message and ctx.reply_to_message.media: + process = await ctx.reply_msg(strings("processing_text"), quote=True) + file_info = get_file_id(ctx.reply_to_message) + if file_info is None: + return await process.edit_msg(strings("media_invalid")) + if ( + ctx.reply_to_message.video + and ctx.reply_to_message.video.file_size > 2097152000 + ) or ( + ctx.reply_to_message.document + and ctx.reply_to_message.document.file_size > 2097152000 + ): + return await process.edit_msg(strings("dl_limit_exceeded"), del_in=6) + c_time = time.time() + dc_id = FileId.decode(file_info.file_id).dc_id + try: + dl = await ctx.reply_to_message.download( + file_name="/downloads/", + progress=progress_for_pyrogram, + progress_args=(strings("dl_args_text"), process, c_time, dc_id), + ) + except FileNotFoundError: + return await process.edit_msg( + "ERROR: FileNotFound, maybe you're spam bot with same file." + ) + file_path = path.join("/downloads/", path.basename(dl)) + output_ = await runcmd(f'mediainfo "{file_path}"') + out = output_[0] if len(output_) != 0 else None + body_text = f""" +MissKatyBot MediaInfo +JSON +{file_info}.type + +DETAILS +{out or 'Not Supported'} + """ + try: + link = await mediainfo_paste(out, "MissKaty Mediainfo") + markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text=strings("viweb"), url=link)]] + ) + except: + try: + link = await post_to_telegraph(False, "MissKaty MediaInfo", body_text) + markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text=strings("viweb"), url=link)]] + ) + except: + markup = None + with io.BytesIO(str.encode(body_text)) as out_file: + out_file.name = "MissKaty_Mediainfo.txt" + await ctx.reply_document( + out_file, + caption=strings("capt_media").format(ment=ctx.from_user.mention), + thumb="assets/thumb.jpg", + reply_markup=markup, + ) + await process.delete() + try: + osremove(file_path) + except Exception: + pass + else: + try: + link = ctx.input + process = await ctx.reply_msg(strings("wait_msg")) + try: + output = subprocess.check_output(["mediainfo", f"{link}"]).decode( + "utf-8" + ) + except Exception: + return await process.edit_msg(strings("err_link")) + body_text = f""" + MissKatyBot MediaInfo + {output} + """ + # link = await post_to_telegraph(False, title, body_text) + try: + link = await mediainfo_paste(out, "MissKaty Mediainfo") + markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text=strings("viweb"), url=link)]] + ) + except: + try: + link = await post_to_telegraph( + False, "MissKaty MediaInfo", body_text + ) + markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text=strings("viweb"), url=link)]] + ) + except: + markup = None + with io.BytesIO(str.encode(output)) as out_file: + out_file.name = "MissKaty_Mediainfo.txt" + await ctx.reply_document( + out_file, + caption=strings("capt_media").format(ment=ctx.from_user.mention), + thumb="assets/thumb.jpg", + reply_markup=markup, + ) + await process.delete() + except IndexError: + return await ctx.reply_msg( + strings("mediainfo_help").format(cmd=ctx.command[0]), del_in=6 + ) diff --git a/misskaty/plugins/misc_tools.py b/misskaty/plugins/misc_tools.py new file mode 100644 index 00000000..d18069dd --- /dev/null +++ b/misskaty/plugins/misc_tools.py @@ -0,0 +1,574 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" + +import asyncio +import json +import os +import traceback +from logging import getLogger +from urllib.parse import quote + +import aiohttp +from bs4 import BeautifulSoup +from deep_translator import GoogleTranslator +from gtts import gTTS +from PIL import Image +from pyrogram import Client, filters +from pyrogram.errors import ( + ChatAdminRequired, + MessageTooLong, + QueryIdInvalid, + UserNotParticipant, +) +from pyrogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, +) + +from misskaty import BOT_USERNAME, app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.http import http +from misskaty.helper.tools import rentry +from misskaty.vars import COMMAND_HANDLER +from utils import extract_user, get_file_id + +LOGGER = getLogger(__name__) + +__MODULE__ = "Misc" +__HELP__ = """ +/carbon [text or reply to text or caption] - Make beautiful snippet code on carbon from text. +/kbbi [keyword] - Search definition on KBBI (For Indonesian People) +/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 (Available in English and Indonesia version). +/readqr [reply to photo] - Read QR Code From Photo. +/createqr [text] - Convert Text to QR Code. +/anime [query] - Search title in myanimelist. +/info - Get info user with Pic and full description if user set profile picture. +/id - Get simple user ID. +""" + + +def remove_html_tags(text): + """Remove html tags from a string""" + import re + + clean = re.compile("<.*?>") + return re.sub(clean, "", text) + + +headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edge/107.0.1418.42" +} + + +async def get_content(url): + async with aiohttp.ClientSession() as session: + r = await session.get(url, headers=headers) + return await r.read() + + +@app.on_cmd("kbbi") +async def kbbi_search(_, ctx: Client): + if len(ctx.command) == 1: + return await ctx.reply_msg("Please add keyword to search definition in kbbi") + r = (await http.get(f"https://yasirapi.eu.org/kbbi?kata={ctx.input}")).json() + if nomsg := r.get("detail"): + return await ctx.reply_msg(nomsg) + kbbi_btn = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="Open in Web", url=r.get("link"))]] + ) + res = "Definisi:\n" + for _, a in enumerate(r.get("result"), start=1): + submakna = "".join(f"{a}, " for a in a["makna"][0]["submakna"])[:-2] + contoh = "".join(f"{a}, " for a in a["makna"][0]["contoh"])[:-2] + kt_dasar = "".join(f"{a}, " for a in a["kata_dasar"])[:-2] + bt_takbaku = "".join(f"{a}, " for a in a["bentuk_tidak_baku"])[:-2] + res += f"{a['nama']} ({a['makna'][0]['kelas'][0]['nama']}: {a['makna'][0]['kelas'][0]['deskripsi']})\nKata Dasar: {kt_dasar if kt_dasar else '-'}\nBentuk Tidak Baku: {bt_takbaku if bt_takbaku else '-'}\nSubmakna: {submakna}\nContoh: {contoh if contoh else '-'}\n\n" + await ctx.reply(f"{res}By YasirPedia API", reply_markup=kbbi_btn) + + +@app.on_cmd("carbon") +async def carbon_make(self: Client, ctx: Message): + if ctx.reply_to_message and ctx.reply_to_message.text: + text = ctx.reply_to_message.text + elif ctx.reply_to_message and ctx.reply_to_message.caption: + text = ctx.reply_to_message.caption + elif len(ctx.command) > 1: + text = ctx.input + else: + return await ctx.reply( + "Please reply text to make carbon or add text after command." + ) + json_data = { + "code": text, + "backgroundColor": "#1F816D", + } + + response = await http.post( + "https://carbon.yasirapi.eu.org/api/cook", json=json_data + ) + if response.status_code != 200: + return await ctx.reply_photo(f"https://http.cat/{response.status_code}", caption="🤧 Carbon API ERROR") + fname = ( + f"carbonBY_{ctx.from_user.id if ctx.from_user else ctx.sender_chat.title}.png" + ) + with open(fname, "wb") as e: + e.write(response.content) + await ctx.reply_photo(fname, caption=f"Generated by @{self.me.username}") + os.remove(fname) + + +@app.on_message(filters.command("readqr", COMMAND_HANDLER)) +@ratelimiter +async def readqr(c, m): + if not m.reply_to_message: + return await m.reply("Please reply photo that contain valid QR Code.") + if not m.reply_to_message.photo: + return await m.reply("Please reply photo that contain valid QR Code.") + foto = await m.reply_to_message.download() + myfile = {"file": (foto, open(foto, "rb"), "application/octet-stream")} + url = "http://api.qrserver.com/v1/read-qr-code/" + r = await http.post(url, files=myfile) + os.remove(foto) + if res := r.json()[0]["symbol"][0]["data"] is None: + return await m.reply_msg(res) + await m.reply_msg( + f"QR Code Reader by @{c.me.username}: {r.json()[0]['symbol'][0]['data']}", + quote=True, + ) + + +@app.on_message(filters.command("createqr", COMMAND_HANDLER)) +@ratelimiter +async def makeqr(c, m): + if m.reply_to_message and m.reply_to_message.text: + teks = m.reply_to_message.text + elif len(m.command) > 1: + teks = m.text.split(None, 1)[1] + else: + return await m.reply( + "Please add text after command to convert text -> QR Code." + ) + url = f"https://api.qrserver.com/v1/create-qr-code/?data={quote(teks)}&size=300x300" + await m.reply_photo( + url, caption=f"QR Code Maker by @{c.me.username}", quote=True + ) + + +@app.on_message(filters.command(["sof"], COMMAND_HANDLER)) +@capture_err +async def stackoverflow(_, 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() + msg = await message.reply("Getting data..") + 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 msg.edit(hasil) + except MessageTooLong: + url = await rentry(hasil) + await msg.edit(f"Your text pasted to rentry because has long text:\n{url}") + except Exception as e: + await msg.edit(e) + + +@app.on_message(filters.command(["google"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +async def gsearch(_, 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: + 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.find_all("div", class_="kvH3mc BToiNc UK95Uc"): + link = result.find("div", class_="yuRUbf").find("a").get("href") + title = result.find("div", class_="yuRUbf").find("h3").get_text() + try: + snippet = result.find( + "div", class_="VwiC3b yXK7lf MUxGbd yDYNvb lyLwlc lEBKkf" + ).get_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 @{BOT_USERNAME}", + disable_web_page_preview=True, + ) + + +@app.on_message(filters.command(["tr", "trans", "translate"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +async def translate(_, 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) < 3: + return await message.reply_msg( + "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_msg("Menerjemahkan...") + try: + my_translator = GoogleTranslator(source="auto", target=target_lang) + result = my_translator.translate(text=text) + await msg.edit_msg( + f"Translation using source = {my_translator.source} and target = {my_translator.target}\n\n-> {result}" + ) + except MessageTooLong: + url = await rentry(result) + await msg.edit_msg( + f"Your translated text pasted to rentry because has long text:\n{url}" + ) + except Exception as err: + await msg.edit_msg(f"Oppss, Error: {str(err)}") + + +@app.on_message(filters.command(["tts"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +async def tts_convert(_, 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).\n*Usage:* /tts en [text]", + ) + return + target_lang = message.text.split(None, 2)[1] + text = message.text.split(None, 2)[2] + msg = await message.reply("Converting to voice...") + fname = f"tts_BY_{message.from_user.id if message.from_user else message.sender_chat.title}.mp3" + try: + tts = gTTS(text, lang=target_lang) + tts.save(fname) + except ValueError as err: + await msg.edit(f"Error: {str(err)}") + return + await msg.delete() + await msg.reply_audio(fname) + if os.path.exists(fname): + os.remove(fname) + + +@app.on_message(filters.command(["tosticker"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +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"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +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 message.reply_to_message.download() + im = Image.open(photo).convert("RGB") + filename = f"toimg_{message.from_user.id}.png" + im.save(filename, "png") + await asyncio.gather( + *[ + message.reply_document(filename), + message.reply_photo( + filename, caption=f"Sticker -> Image\n@{client.me.username}" + ), + ] + ) + os.remove(photo) + os.remove(filename) + except Exception as e: + await message.reply_text(str(e)) + + +@app.on_message(filters.command(["id"], COMMAND_HANDLER)) +@ratelimiter +async def showid(_, message): + chat_type = message.chat.type.value + 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)) +@ratelimiter +async def who_is(client, message): + # https://github.com/SpEcHiDe/PyroGramBot/blob/master/pyrobot/plugins/admemes/whois.py#L19 + if message.sender_chat: + return await message.reply_msg("Not supported channel..") + 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 Doesn't Have Profile Pic]" + message_out_str += f"➲Data Centre: {dc_id}\n" + message_out_str += f"➲User Name: @{username}\n" + message_out_str += f"➲User Link: Click Here\n" + if message.chat.type.value in (("supergroup", "channel")): + try: + chat_member_p = await message.chat.get_member(from_user.id) + joined_date = chat_member_p.joined_date + message_out_str += ( + "➲Joined this Chat on: " f"{joined_date}" "\n" + ) + except (UserNotParticipant, ChatAdminRequired): + 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=f"close#{message.from_user.id}" + ) + ] + ] + 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=f"close#{message.from_user.id}" + ) + ] + ] + 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() + + +@app.on_callback_query(filters.regex("^close")) +@ratelimiter +async def close_callback(_, query: CallbackQuery): + _, userid = query.data.split("#") + if query.from_user.id != int(userid): + try: + return await query.answer("⚠️ Access Denied!", True) + except QueryIdInvalid: + return + try: + await query.answer("Deleting this message in 5 seconds.") + await asyncio.sleep(5) + await query.message.delete() + await query.message.reply_to_message.delete() + except: + pass + + +async def mdlapi(title): + link = f"https://kuryana.vercel.app/search/q/{title}" + async with aiohttp.ClientSession() as ses, ses.get(link) as result: + return await result.json() + + +@app.on_message(filters.command(["mdl"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +async def mdlsearch(_, message): + if " " in message.text: + _, 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")) +@ratelimiter +async def mdl_callback(_, query: CallbackQuery): + _, user, _, 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) diff --git a/misskaty/plugins/nightmodev2.py b/misskaty/plugins/nightmodev2.py new file mode 100644 index 00000000..d115b1e8 --- /dev/null +++ b/misskaty/plugins/nightmodev2.py @@ -0,0 +1,261 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import platform +import re +from datetime import datetime, timedelta + +import pytz +from apscheduler.jobstores.base import ConflictingIdError +from pyrogram import __version__, filters +from pyrogram.errors import ( + ChannelInvalid, + ChannelPrivate, + ChatAdminRequired, + ChatNotModified, +) +from pyrogram.types import ChatPermissions, InlineKeyboardButton, InlineKeyboardMarkup + +from database.locale_db import get_db_lang +from misskaty import BOT_NAME, app, scheduler +from misskaty.core.decorator.permissions import require_admin +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.localization import langdict, use_chat_lang +from misskaty.vars import COMMAND_HANDLER, LOG_CHANNEL, TZ + +__MODULE__ = "NightMode" +__HELP__ = """Enable or disable nightmode (locks the chat at specified intervals everyday) +Flags: +'-s': "Specify starting time in 24hr format." +'-e': "Specify duration in hours / minute" +'-d': "Disable nightmode for chat." + +Examples: +/nightmode -s=23:53 -e=6h +/nightmode -s=23:50 -e=120m +/nightmode -d +""" + +TIME_ZONE = pytz.timezone(TZ) +reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="❤️", callback_data="nightmd")]] +) + + +# 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 + + +def tglsekarang(): + 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") + return f"{days[now.weekday()]}, {tgl} {month[now.month]} {tahun} {jam}" + + +def extract_time(time_val: str): + if any(time_val.endswith(unit) for unit in ("m", "h")): + unit = time_val[-1] + time_num = time_val[:-1] + if not time_num.isdigit(): + return "" + if unit == "m": + time = int(time_num) * 60 + elif unit == "h": + time = int(time_num) * 60 * 60 + else: + return "" + return time + return "" + + +async def un_mute_chat(chat_id: int, perm: ChatPermissions): + getlang = await get_db_lang(chat_id) + getlang = getlang or "en-US" + try: + await app.set_chat_permissions(chat_id, perm) + except ChatAdminRequired: + await app.send_message( + LOG_CHANNEL, + langdict[getlang]["nightmodev2"]["nmd_off_not_admin"].format( + chat_id=chat_id, bname=BOT_NAME + ), + ) + except (ChannelInvalid, ChannelPrivate): + scheduler.remove_job(f"enable_nightmode_{chat_id}") + scheduler.remove_job(f"disable_nightmode_{chat_id}") + await app.send_message( + LOG_CHANNEL, + langdict[getlang]["nightmodev2"]["nmd_off_not_present"].format( + chat_id=chat_id, bname=BOT_NAME + ), + ) + except ChatNotModified: + pass + except Exception as e: + await app.send_message( + LOG_CHANNEL, + langdict[getlang]["nightmodev2"]["nmd_off_err"].format( + chat_id=chat_id, e=e + ), + ) + else: + job = scheduler.get_job(f"enable_nightmode_{chat_id}") + close_at = job.next_run_time + await app.send_message( + chat_id, + langdict[getlang]["nightmodev2"]["nmd_off_success"].format( + dt=tglsekarang(), close_at=close_at + ), + reply_markup=reply_markup, + ) + + +async def mute_chat(chat_id: int): + getlang = await get_db_lang(chat_id) + getlang = getlang or "en-US" + try: + await app.set_chat_permissions(chat_id, ChatPermissions()) + except ChatAdminRequired: + await app.send_message( + LOG_CHANNEL, + langdict[getlang]["nightmodev2"]["nmd_on_not_admin"].format( + chat_id=chat_id, bname=BOT_NAME + ), + ) + except (ChannelInvalid, ChannelPrivate): + scheduler.remove_job(f"enable_nightmode_{chat_id}") + scheduler.remove_job(f"disable_nightmode_{chat_id}") + await app.send_message( + LOG_CHANNEL, + langdict[getlang]["nightmodev2"]["nmd_on_not_present"].format( + chat_id=chat_id, bname=BOT_NAME + ), + ) + except ChatNotModified: + pass + except Exception as e: + await app.send_message( + LOG_CHANNEL, + langdict[getlang]["nightmodev2"]["nmd_on_err"].format(chat_id=chat_id, e=e), + ) + else: + job = scheduler.get_job(f"disable_nightmode_{chat_id}") + open_at = job.next_run_time + await app.send_message( + chat_id, + langdict[getlang]["nightmodev2"]["nmd_on_success"].format( + dt=tglsekarang(), open_at=open_at + ), + reply_markup=reply_markup, + ) + + +@app.on_message(filters.command("nightmode", COMMAND_HANDLER) & filters.group) +@require_admin(permissions=["can_change_info"]) +@use_chat_lang() +async def nightmode_handler(_, msg, strings): + chat_id = msg.chat.id + + if "-d" in msg.text: + if scheduler.get_job(job_id=f"enable_nightmode_{chat_id}"): + scheduler.remove_job(job_id=f"enable_nightmode_{chat_id}") + scheduler.remove_job(job_id=f"disable_nightmode_{chat_id}") + if not bool(scheduler.get_jobs()) and bool(scheduler.state): + scheduler.shutdown() + return await msg.reply_msg(strings("nmd_disabled")) + return await msg.reply_msg(strings("nmd_not_enabled")) + + starttime = re.findall(r"-s=(\d+:\d+)", msg.text) + start = starttime[0] if starttime else "00:00" + now = datetime.now(TIME_ZONE) + + try: + start_timestamp = TIME_ZONE.localize( + datetime.strptime((now.strftime("%m:%d:%Y - ") + start), "%m:%d:%Y - %H:%M") + ) + except ValueError: + return await msg.reply_msg(strings("invalid_time_format"), del_in=6) + lockdur = re.findall(r"-e=(\w+)", msg.text) + lockdur = lockdur[0] if lockdur else "6h" + lock_dur = extract_time(lockdur.lower()) + + if not lock_dur: + return await msg.reply_msg(strings("invalid_lockdur"), del_in=6) + + if start_timestamp < now: + start_timestamp = start_timestamp + timedelta(days=1) + end_time_stamp = start_timestamp + timedelta(seconds=int(lock_dur)) + try: + # schedule to enable nightmode + scheduler.add_job( + mute_chat, + "interval", + [chat_id], + id=f"enable_nightmode_{chat_id}", + days=1, + next_run_time=start_timestamp, + max_instances=50, + misfire_grace_time=None, + ) + + # schedule to disable nightmode + scheduler.add_job( + un_mute_chat, + "interval", + [chat_id, msg.chat.permissions], + id=f"disable_nightmode_{chat_id}", + days=1, + next_run_time=end_time_stamp, + max_instances=50, + misfire_grace_time=None, + ) + except ConflictingIdError: + return await msg.reply_msg(strings("schedule_already_on")) + await msg.reply_msg( + strings("nmd_enable_success").format( + st=start_timestamp.strftime("%H:%M:%S"), lockdur=lockdur + ) + ) + if not bool(scheduler.state): + scheduler.start() + + +@app.on_callback_query(filters.regex(r"^nightmd$")) +@ratelimiter +@use_chat_lang() +async def callbackanightmd(c, q, strings): + await q.answer( + strings("nmd_cb").format( + bname=c.me.first_name, ver=__version__, pyver=platform.python_version() + ), + show_alert=True, + ) diff --git a/misskaty/plugins/notes.py b/misskaty/plugins/notes.py new file mode 100644 index 00000000..ca6d119e --- /dev/null +++ b/misskaty/plugins/notes.py @@ -0,0 +1,132 @@ +""" +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 re import findall + +from pyrogram import filters + +from database.notes_db import delete_note, get_note, get_note_names, save_note +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.permissions import adminsOnly +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.core.keyboard import ikb +from misskaty.helper.functions import extract_text_and_keyb + +__MODULE__ = "Notes" +__HELP__ = """/notes To Get All The Notes In The Chat. + +(/save, /addnote) [NOTE_NAME] To Save A Note (Can be a sticker or text). + +#NOTE_NAME To Get A Note. + +(/clear, /delnote) [NOTE_NAME] To Delete A Note. +""" + + +@app.on_message(filters.command(["addnote", "save"]) & ~filters.private) +@adminsOnly("can_change_info") +@ratelimiter +async def save_notee(_, message): + if len(message.command) < 2 or not message.reply_to_message: + await message.reply( + text="**Usage:**\nReply to a text or sticker with /addnote [NOTE_NAME] to save it.", + ) + + elif not message.reply_to_message.text and not message.reply_to_message.sticker: + await message.reply("__**You can only save text or stickers in notes.**__") + else: + name = message.text.split(None, 1)[1].strip() + if not name: + return await message.reply("**Usage**\n__/save [NOTE_NAME]__") + _type = "text" if message.reply_to_message.text else "sticker" + note = { + "type": _type, + "data": message.reply_to_message.text.markdown + if _type == "text" + else message.reply_to_message.sticker.file_id, + } + chat_id = message.chat.id + await save_note(chat_id, name, note) + await message.reply(f"__**Saved note {name}.**__") + + +@app.on_message(filters.command("notes") & ~filters.private) +@capture_err +@ratelimiter +async def get_notes(_, message): + chat_id = message.chat.id + + _notes = await get_note_names(chat_id) + + if not _notes: + return await message.reply("**No notes in this chat.**") + _notes.sort() + msg = f"List of notes in {message.chat.title} - {message.chat.id}\n" + for note in _notes: + msg += f"**-** `{note}`\n" + await message.reply(msg) + + +@app.on_message(filters.regex(r"^#.+") & filters.text & ~filters.private) +@capture_err +@ratelimiter +async def get_one_note(_, message): + name = message.text.replace("#", "", 1) + if not name: + return + _note = await get_note(message.chat.id, name) + if not _note: + return + if _note["type"] == "text": + data = _note["data"] + keyb = None + if findall(r"\[.+\,.+\]", data): + if keyboard := extract_text_and_keyb(ikb, data): + data, keyb = keyboard + await message.reply_text( + data, + reply_markup=keyb, + disable_web_page_preview=True, + ) + else: + await message.reply_sticker(_note["data"]) + + +@app.on_message(filters.command(["delnote", "clear"]) & ~filters.private) +@adminsOnly("can_change_info") +@ratelimiter +async def del_note(_, message): + if len(message.command) == 1: + return await message.reply("**Usage**\n__/delnote [NOTE_NAME]__") + name = message.text.split(None, 1)[1].strip() + if not name: + return await message.reply("**Usage**\n__/delnote [NOTE_NAME]__") + + chat_id = message.chat.id + + deleted = await delete_note(chat_id, name) + if deleted: + await message.reply(f"**Deleted note {name} successfully.**") + else: + await message.reply("**No such note.**") diff --git a/misskaty/plugins/nulis.py b/misskaty/plugins/nulis.py new file mode 100644 index 00000000..2bfb8b20 --- /dev/null +++ b/misskaty/plugins/nulis.py @@ -0,0 +1,65 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import os + +from PIL import Image, ImageDraw, ImageFont +from pyrogram import filters + +from misskaty import app +from misskaty.vars import COMMAND_HANDLER + +__MODULE__ = "nulis" +__HELP__ = """ +Command: /nulis [reply to msg or after cmd] +Desc: For those of you who are lazy to write. +""" + + +def text_set(text): + lines = [] + if len(text) <= 55: + lines.append(text) + else: + all_lines = text.split("\n") + for line in all_lines: + if len(line) <= 55: + lines.append(line) + else: + k = len(line) // 55 + lines.extend(line[((z - 1) * 55) : (z * 55)] for z in range(1, k + 2)) + return lines[:25] + + +@app.on_message(filters.command(["nulis"], COMMAND_HANDLER)) +async def handwrite(client, message): + if message.reply_to_message and message.reply_to_message.text: + txt = message.reply_to_message.text + elif len(message.command) > 1: + txt = message.text.split(None, 1)[1] + else: + return await message.reply( + "Please reply to message or write after command to use Nulis CMD." + ) + nan = await message.reply_msg("Processing...") + try: + img = Image.open("assets/kertas.jpg") + draw = ImageDraw.Draw(img) + font = ImageFont.truetype("assets/assfont.ttf", 30) + x, y = 150, 140 + lines = text_set(txt) + line_height = font.getbbox("hg")[3] + for line in lines: + draw.text((x, y), line, fill=(1, 22, 55), font=font) + y = y + line_height - 5 + file = f"nulis_{message.from_user.id}.jpg" + img.save(file) + if os.path.exists(file): + await message.reply_photo( + photo=file, caption=f"Written By : {client.me.mention}" + ) + os.remove(file) + await nan.delete() + except Exception as e: + return await message.reply(e) diff --git a/misskaty/plugins/ocr.py b/misskaty/plugins/ocr.py new file mode 100644 index 00000000..a274c564 --- /dev/null +++ b/misskaty/plugins/ocr.py @@ -0,0 +1,62 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" + + +import os + +from pyrogram import filters +from pyrogram.types import Message +from telegraph.aio import Telegraph + +from misskaty import app +from misskaty.core.decorator.errors import capture_err +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.http import http +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER + +__MODULE__ = "OCR" +__HELP__ = "/ocr [reply to photo] - Read Text From Image" + + +@app.on_message(filters.command(["ocr"], COMMAND_HANDLER)) +@capture_err +@ratelimiter +@use_chat_lang() +async def ocr(_, ctx: Message, strings): + reply = ctx.reply_to_message + if ( + not reply + or not reply.sticker + and not reply.photo + and (not reply.document or not reply.document.mime_type.startswith("image")) + ): + return await ctx.reply_msg( + strings("no_photo").format(cmd=ctx.command[0]), quote=True + ) + msg = await ctx.reply_msg(strings("read_ocr"), quote=True) + try: + file_path = await reply.download() + if reply.sticker: + file_path = await reply.download( + f"ocr_{ctx.from_user.id if ctx.from_user else ctx.sender_chat.id}.jpg" + ) + response = await Telegraph().upload_file(file_path) + url = f"https://telegra.ph{response[0]['src']}" + req = ( + await http.get( + f"https://script.google.com/macros/s/AKfycbwURISN0wjazeJTMHTPAtxkrZTWTpsWIef5kxqVGoXqnrzdLdIQIfLO7jsR5OQ5GO16/exec?url={url}", + follow_redirects=True, + ) + ).json() + await msg.edit_msg(strings("result_ocr").format(result=req["text"])) + if os.path.exists(file_path): + os.remove(file_path) + except Exception as e: + await msg.edit_msg(str(e)) + if os.path.exists(file_path): + os.remove(file_path) diff --git a/misskaty/plugins/paste.py b/misskaty/plugins/paste.py new file mode 100644 index 00000000..3158995a --- /dev/null +++ b/misskaty/plugins/paste.py @@ -0,0 +1,522 @@ +""" + * @author Yasir Aris M + * @created 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +from json import loads as json_loads +from os import remove +from re import compile as compiles + +from pyrogram import filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper import http, post_to_telegraph, rentry +from misskaty.vars import COMMAND_HANDLER + +__MODULE__ = "Paste" +__HELP__ = """ +/paste [Text/Reply To Message] - Post text to My Pastebin. +/sbin [Text/Reply To Message] - Post text to Spacebin. +/neko [Text/Reply To Message] - Post text to Nekobin. +/tgraph [Text/Reply To Message] - Post text/media to Telegra.ph. +/rentry [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(size, int): + try: + pass + 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(["tgraph"], COMMAND_HANDLER)) +@ratelimiter +async def telegraph_paste(_, message): + reply = message.reply_to_message + if not reply and len(message.command) < 2: + return await message.reply_msg( + f"**Reply To A Message With /{message.command[0]} or with command**", + del_in=6, + ) + + if message.from_user: + if message.from_user.username: + uname = f"@{message.from_user.username} [{message.from_user.id}]" + else: + uname = f"[{message.from_user.first_name}](tg://user?id={message.from_user.id}) [{message.from_user.id}]" + else: + uname = message.sender_chat.title + msg = await message.reply_msg("`Pasting to Telegraph...`") + if reply and (reply.photo or reply.animation): + file = await reply.download() + try: + url = await post_to_telegraph(True, media=file) + except Exception as err: + remove(file) + return msg.edit_msg(f"Failed to upload. ERR: {err}") + button = [ + [InlineKeyboardButton("Open Link", url=url)], + [ + InlineKeyboardButton( + "Share Link", url=f"https://telegram.me/share/url?url={url}" + ) + ], + ] + + pasted = f"**Successfully upload your media to Telegraph.\n\nUpload by {uname}**" + remove(file) + return await msg.edit_msg( + pasted, + disable_web_page_preview=True, + reply_markup=InlineKeyboardMarkup(button), + ) + data = "" + limit = 1024 * 1024 + if reply and reply.document: + if reply.document.file_size > limit: + return await msg.edit_msg( + f"**You can only paste files smaller than {humanbytes(limit)}.**" + ) + if not pattern.search(reply.document.mime_type): + return await msg.edit_msg("**Only text files can be pasted.**") + file = await reply.download() + title = ( + message.text.split(None, 1)[1] + if len(message.command) > 1 + else "MissKaty Paste" + ) + try: + with open(file, "r") as text: + data = text.read() + remove(file) + except UnicodeDecodeError: + try: + remove(file) + except: + pass + return await msg.edit_msg("`File Not Supported !`") + elif reply and (reply.text or reply.caption): + title = ( + message.text.split(None, 1)[1] + if len(message.command) > 1 + else "MissKaty Paste" + ) + data = reply.text.html.replace("\n", "
") or reply.caption.html.replace( + "\n", "
" + ) + elif not reply and len(message.command) >= 2: + title = "MissKaty Paste" + data = message.text.split(None, 1)[1] + + try: + url = await post_to_telegraph(False, title, data) + except Exception as e: + return await msg.edit_msg(f"ERROR: {e}") + + if not url: + return await msg.edit_msg("Text Too Short Or File Problems") + button = [ + [InlineKeyboardButton("Open Link", url=url)], + [ + InlineKeyboardButton( + "Share Link", url=f"https://telegram.me/share/url?url={url}" + ) + ], + ] + + pasted = f"**Successfully pasted your data to Telegraph.\n\nPaste by {uname}**" + await msg.edit_msg(pasted, reply_markup=InlineKeyboardMarkup(button)) + + +# Default Paste to Wastebin using Deta +@app.on_message(filters.command(["paste"], COMMAND_HANDLER)) +@ratelimiter +async def wastepaste(_, 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_msg( + f"**Reply To A Message With /{target} or with command**", del_in=6 + ) + + msg = await message.reply_msg("`Pasting to YasirBin...`") + data = "" + limit = 1024 * 1024 + if reply and reply.document: + if reply.document.file_size > limit: + return await msg.edit_msg( + f"**You can only paste files smaller than {humanbytes(limit)}.**" + ) + if not pattern.search(reply.document.mime_type): + return await msg.edit_msg("**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_msg("`File Not Supported !`") + elif reply and (reply.text or reply.caption): + data = reply.text or reply.caption + 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} [{message.from_user.id}]" + else: + uname = f"[{message.from_user.first_name}](tg://user?id={message.from_user.id}) [{message.from_user.id}]" + else: + uname = message.sender_chat.title + + try: + json_data = { + "content": data, + "highlighting_language": "auto", + "ephemeral": False, + "expire_at": 0, + "expire_in": 0, + } + response = await http.post("https://paste.yasir.eu.org/api/new", json=json_data) + url = f"https://paste.yasir.eu.org/{response.json()['id']}" + except Exception as e: + return await msg.edit_msg(f"ERROR: {e}") + + if not url: + return await msg.edit_msg("Text Too Short Or File Problems") + button = [ + [InlineKeyboardButton("Open Link", url=url)], + [ + InlineKeyboardButton( + "Share Link", url=f"https://telegram.me/share/url?url={url}" + ) + ], + ] + + pasted = f"**Successfully pasted your data to YasirBin.\n\nPaste by {uname}**" + await msg.edit_msg(pasted, reply_markup=InlineKeyboardMarkup(button)) + + +# Nekobin Paste +@app.on_message(filters.command(["neko"], COMMAND_HANDLER)) +@ratelimiter +async def nekopaste(_, 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_msg( + f"**Reply To A Message With /{target} or with command**", del_in=6 + ) + + msg = await message.reply_msg("`Pasting to Nekobin...`") + data = "" + limit = 1024 * 1024 + if reply and reply.document: + if reply.document.file_size > limit: + return await message.edit_msg( + f"**You can only paste files smaller than {humanbytes(limit)}.**" + ) + if not pattern.search(reply.document.mime_type): + return await message.edit_msg("**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 message.edit_msg("`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: + x = ( + await http.post("https://nekobin.com/api/documents", json={"content": data}) + ).json() + url = f"https://nekobin.com/{x['result']['key']}" + except Exception as e: + return await msg.edit_msg(f"ERROR: {e}") + + if not url: + return await msg.edit_msg("Text Too Short Or File Problems") + button = [ + [InlineKeyboardButton("Open Link", url=url)], + [ + InlineKeyboardButton( + "Share Link", url=f"https://telegram.me/share/url?url={url}" + ) + ], + ] + + pasted = f"**Successfully pasted your data to Nekobin.\n\nPaste by {uname}**" + await msg.edit_msg(pasted, reply_markup=InlineKeyboardMarkup(button)) + + +# Paste as spacebin +@app.on_message(filters.command(["sbin"], COMMAND_HANDLER)) +@ratelimiter +async def spacebinn(_, 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_msg( + f"**Reply To A Message With /{target} or with command**", del_in=6 + ) + + msg = await message.reply_msg("`Pasting to Spacebin...`") + data = "" + limit = 1024 * 1024 + if reply and reply.document: + if reply.document.file_size > limit: + return await msg.edit_msg( + f"**You can only paste files smaller than {humanbytes(limit)}.**" + ) + if not pattern.search(reply.document.mime_type): + return await msg.edit_msg("**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_msg("`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: + siteurl = "https://spaceb.in/api/v1/documents/" + response = await http.post(siteurl, data={"content": data, "extension": "txt"}) + response = response.json() + url = "https://spaceb.in/" + response["payload"]["id"] + except Exception as e: + return await msg.edit_msg(f"ERROR: {e}") + + if not url: + return await msg.edit_msg("Text Too Short Or File Problems") + button = [ + [InlineKeyboardButton("Open Link", url=url)], + [ + InlineKeyboardButton( + "Share Link", url=f"https://telegram.me/share/url?url={url}" + ) + ], + ] + + pasted = f"**Successfully pasted your data to Spacebin.\n\nPaste by {uname}**" + await msg.edit_msg(pasted, reply_markup=InlineKeyboardMarkup(button)) + + +# Rentry paste +@app.on_message(filters.command(["rentry"], COMMAND_HANDLER)) +@ratelimiter +async def rentrypaste(_, 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_msg( + f"**Reply To A Message With /{target} or with command**", del_in=6 + ) + + msg = await message.reply_msg("`Pasting to Rentry...`") + data = "" + limit = 1024 * 1024 + if reply and reply.document: + if reply.document.file_size > limit: + return await msg.edit_msg( + f"**You can only paste files smaller than {humanbytes(limit)}.**" + ) + if not pattern.search(reply.document.mime_type): + return await msg.edit_msg("**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_msg("`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: + return await msg.edit(f"`{e}`") + + if not url: + return await msg.edit_msg("Text Too Short Or File Problems") + button = [ + [InlineKeyboardButton("Open Link", url=url)], + [ + 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_msg(pasted, reply_markup=InlineKeyboardMarkup(button)) + + +# Tempaste pastebin +@app.on_message(filters.command(["temp_paste"], COMMAND_HANDLER)) +@ratelimiter +async def tempaste(_, 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.edit_msg( + f"**Reply To A Message With /{target} or with command**", del_in=6 + ) + + msg = await message.reply_msg("`Pasting to TempPaste...`") + data = "" + limit = 1024 * 1024 + if reply and reply.document: + if reply.document.file_size > limit: + return await msg.edit_msg( + f"**You can only paste files smaller than {humanbytes(limit)}.**" + ) + if not pattern.search(reply.document.mime_type): + return await msg.edit_msg("**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_msg("`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": 1, + }, + ) + url = f"https://tempaste.com/{json_loads(req.text)['url']}" + except Exception as e: + return await msg.edit_msg(f"`{e}`") + + if not url: + return await msg.edit_msg("Text Too Short Or File Problems") + button = [ + [InlineKeyboardButton("Open Link", url=url)], + [ + 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_msg(pasted, reply_markup=InlineKeyboardMarkup(button)) diff --git a/misskaty/plugins/ping.py b/misskaty/plugins/ping.py new file mode 100644 index 00000000..0282c30c --- /dev/null +++ b/misskaty/plugins/ping.py @@ -0,0 +1,66 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +import platform +import time +from asyncio import Lock +from re import MULTILINE, findall +from subprocess import run as srun + +from pyrogram import __version__ as pyrover +from pyrogram import filters +from pyrogram.types import Message + +from misskaty import app, botStartTime, misskaty_version +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.human_read import get_readable_time +from misskaty.vars import COMMAND_HANDLER + + +@app.on_message(filters.command(["ping"], COMMAND_HANDLER)) +@ratelimiter +async def ping(_, ctx: Message): + currentTime = get_readable_time(time.time() - botStartTime) + start_t = time.time() + rm = await ctx.reply_msg("🐱 Pong!!...") + end_t = time.time() + time_taken_s = round(end_t - start_t, 3) + await rm.edit_msg( + f"🐈 MissKatyBot {misskaty_version} based Pyrogram {pyrover} Online.\n\nPing: {time_taken_s} detik\nUptime: {currentTime}\nPython Version: {platform.python_version()}" + ) + + +@app.on_message(filters.command(["ping_dc"], COMMAND_HANDLER)) +@ratelimiter +async def ping_handler(_, ctx: Message): + m = await ctx.reply_msg("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_msg(text) diff --git a/misskaty/plugins/pypi_search.py b/misskaty/plugins/pypi_search.py new file mode 100644 index 00000000..5f731439 --- /dev/null +++ b/misskaty/plugins/pypi_search.py @@ -0,0 +1,157 @@ +""" + * @author yasir + * @date 2023-01-23 19:41:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +from pykeyboard import InlineButton, InlineKeyboard +from pyrogram import filters +from pyrogram.errors import QueryIdInvalid +from pyrogram.types import CallbackQuery, Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper import http, Cache +from misskaty.plugins.web_scraper import headers, split_arr +from misskaty.vars import COMMAND_HANDLER + +PYPI_DICT = Cache(filename="pypi_cache.db", path="cache", in_memory=False) + + +async def getDataPypi(msg, kueri, CurrentPage, user): + if not PYPI_DICT.get(msg.id): + pypijson = (await http.get(f"https://yasirapi.eu.org/pypi?q={kueri}")).json() + if not pypijson.get("result"): + await msg.edit_msg("Sorry could not find any matching results!", del_in=6) + return None, 0, None + PYPI_DICT.add(msg.id, [split_arr(pypijson["result"], 6), kueri], timeout=1600) + try: + index = int(CurrentPage - 1) + PageLen = len(PYPI_DICT[msg.id][0]) + extractbtn = [] + pypiResult = f"#Pypi Results For: {kueri}\n\n" + for c, i in enumerate(PYPI_DICT[msg.id][0][index], start=1): + pypiResult += f"{c}. {i['name']} {i['version']}\nCreated: {i['created']}\nDesc: {i['description']}\n\n" + extractbtn.append( + InlineButton(c, f"pypidata#{CurrentPage}#{c}#{user}#{msg.id}") + ) + pypiResult = "".join(i for i in pypiResult if i not in "[]") + return pypiResult, PageLen, extractbtn + except (IndexError, KeyError): + await msg.edit_msg("Sorry could not find any matching results!", del_in=6) + return None, 0, None + + +@app.on_message(filters.command(["pypi"], COMMAND_HANDLER)) +@ratelimiter +async def pypi_s(_, ctx: Message): + kueri = " ".join(ctx.command[1:]) + if not kueri: + return await ctx.reply_msg( + "Please add query after command. Ex: /pypi pyrogram", del_in=6 + ) + pesan = await ctx.reply_msg("⏳ Please wait, getting data from pypi..", quote=True) + CurrentPage = 1 + pypires, PageLen, btn = await getDataPypi( + pesan, kueri, CurrentPage, ctx.from_user.id + ) + if not pypires: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, CurrentPage, "page_pypi#{number}" + f"#{pesan.id}#{ctx.from_user.id}" + ) + keyboard.row(InlineButton("👇 Get Info ", "Hmmm")) + keyboard.row(*btn) + keyboard.row(InlineButton("❌ Close", f"close#{ctx.from_user.id}")) + await pesan.edit_msg(pypires, reply_markup=keyboard) + + +@app.on_callback_query(filters.create(lambda _, __, query: "page_pypi#" in query.data)) +@ratelimiter +async def pypipage_callback(_, callback_query: CallbackQuery): + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer("Not yours..", True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + try: + kueri = PYPI_DICT[message_id][1] + except KeyError: + return await callback_query.answer( + "Invalid callback data, please send CMD again.." + ) + except QueryIdInvalid: + return + + try: + pypires, PageLen, btn = await getDataPypi( + callback_query.message, kueri, CurrentPage, callback_query.from_user.id + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_pypi#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row(InlineButton("👇 Extract Data ", "Hmmm")) + keyboard.row(*btn) + keyboard.row(InlineButton("❌ Close", f"close#{callback_query.from_user.id}")) + await callback_query.message.edit_msg(pypires, reply_markup=keyboard) + + +@app.on_callback_query(filters.create(lambda _, __, query: "pypidata#" in query.data)) +@ratelimiter +async def pypi_getdata(_, callback_query: CallbackQuery): + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer("Not yours..", True) + idlink = int(callback_query.data.split("#")[2]) + message_id = int(callback_query.data.split("#")[4]) + CurrentPage = int(callback_query.data.split("#")[1]) + try: + pkgname = PYPI_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("name") + except KeyError: + return await callback_query.answer( + "Invalid callback data, please send CMD again.." + ) + + keyboard = InlineKeyboard() + keyboard.row( + InlineButton( + "↩️ Back", + f"page_pypi#{CurrentPage}#{message_id}#{callback_query.from_user.id}", + ), + InlineButton("❌ Close", f"close#{callback_query.from_user.id}"), + ) + try: + html = await http.get(f"https://pypi.org/pypi/{pkgname}/json", headers=headers) + res = html.json() + requirement = ( + "".join(f"{i}, " for i in res["info"].get("requires_dist")) + if res["info"].get("requires_dist") + else "Unknown" + ) + msg = "" + msg += f"Package Name: {res['info'].get('name', 'Unknown')}\n" + msg += f"Version: {res['info'].get('version', 'Unknown')}\n" + msg += f"License: {res['info'].get('license', 'Unknown')}\n" + msg += f"Author: {res['info'].get('author', 'Unknown')}\n" + msg += f"Author Email: {res['info'].get('author_email', 'Unknown')}\n" + msg += f"Requirements: {requirement}\n" + msg += ( + f"Requires Python: {res['info'].get('requires_python', 'Unknown')}\n" + ) + msg += f"HomePage: {res['info'].get('home_page', 'Unknown')}\n" + msg += f"Bug Track: {res['info'].get('vulnerabilities', 'Unknown')}\n" + if res["info"].get("project_urls"): + msg += f"Docs Url: {res['info']['project_urls'].get('Documentation', 'Unknown')}\n" + msg += f"Description: {res['info'].get('summary', 'Unknown')}\n" + msg += ( + f"Pip Command: pip3 install {res['info'].get('name', 'Unknown')}\n" + ) + msg += f"Keywords: {res['info'].get('keywords', 'Unknown')}\n" + await callback_query.message.edit_msg(msg, reply_markup=keyboard) + except Exception as err: + await callback_query.message.edit_msg(f"ERROR: {err}", reply_markup=keyboard) diff --git a/misskaty/plugins/quotly.py b/misskaty/plugins/quotly.py new file mode 100644 index 00000000..ffd2c0ce --- /dev/null +++ b/misskaty/plugins/quotly.py @@ -0,0 +1,299 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +from io import BytesIO + +from pyrogram import Client, filters +from pyrogram.types import Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.http import http + +__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(ctx: Message): + if ctx.forward_date: + if ctx.forward_sender_name: + return 1 + elif ctx.forward_from: + return ctx.forward_from.id + elif ctx.forward_from_chat: + return ctx.forward_from_chat.id + else: + return 1 + elif ctx.from_user: + return ctx.from_user.id + elif ctx.sender_chat: + return ctx.sender_chat.id + else: + return 1 + + +async def get_message_sender_name(ctx: Message): + if ctx.forward_date: + if ctx.forward_sender_name: + return ctx.forward_sender_name + elif ctx.forward_from: + return ( + f"{ctx.forward_from.first_name} {ctx.forward_from.last_name}" + if ctx.forward_from.last_name + else ctx.forward_from.first_name + ) + + elif ctx.forward_from_chat: + return ctx.forward_from_chat.title + else: + return "" + elif ctx.from_user: + if ctx.from_user.last_name: + return f"{ctx.from_user.first_name} {ctx.from_user.last_name}" + else: + return ctx.from_user.first_name + elif ctx.sender_chat: + return ctx.sender_chat.title + else: + return "" + + +async def get_custom_emoji(ctx: Message): + if ctx.forward_date: + return ( + "" + if ctx.forward_sender_name + or not ctx.forward_from + and ctx.forward_from_chat + or not ctx.forward_from + else ctx.forward_from.emoji_status.custom_emoji_id + ) + + return ctx.from_user.emoji_status.custom_emoji_id if ctx.from_user else "" + + +async def get_message_sender_username(ctx: Message): + if ctx.forward_date: + if ( + not ctx.forward_sender_name + and not ctx.forward_from + and ctx.forward_from_chat + and ctx.forward_from_chat.username + ): + return ctx.forward_from_chat.username + elif ( + not ctx.forward_sender_name + and not ctx.forward_from + and ctx.forward_from_chat + or ctx.forward_sender_name + or not ctx.forward_from + ): + return "" + else: + return ctx.forward_from.username or "" + elif ctx.from_user and ctx.from_user.username: + return ctx.from_user.username + elif ( + ctx.from_user + or ctx.sender_chat + and not ctx.sender_chat.username + or not ctx.sender_chat + ): + return "" + else: + return ctx.sender_chat.username + + +async def get_message_sender_photo(ctx: Message): + if ctx.forward_date: + if ( + not ctx.forward_sender_name + and not ctx.forward_from + and ctx.forward_from_chat + and ctx.forward_from_chat.photo + ): + return { + "small_file_id": ctx.forward_from_chat.photo.small_file_id, + "small_photo_unique_id": ctx.forward_from_chat.photo.small_photo_unique_id, + "big_file_id": ctx.forward_from_chat.photo.big_file_id, + "big_photo_unique_id": ctx.forward_from_chat.photo.big_photo_unique_id, + } + elif ( + not ctx.forward_sender_name + and not ctx.forward_from + and ctx.forward_from_chat + or ctx.forward_sender_name + or not ctx.forward_from + ): + return "" + else: + return ( + { + "small_file_id": ctx.forward_from.photo.small_file_id, + "small_photo_unique_id": ctx.forward_from.photo.small_photo_unique_id, + "big_file_id": ctx.forward_from.photo.big_file_id, + "big_photo_unique_id": ctx.forward_from.photo.big_photo_unique_id, + } + if ctx.forward_from.photo + else "" + ) + + elif ctx.from_user and ctx.from_user.photo: + return { + "small_file_id": ctx.from_user.photo.small_file_id, + "small_photo_unique_id": ctx.from_user.photo.small_photo_unique_id, + "big_file_id": ctx.from_user.photo.big_file_id, + "big_photo_unique_id": ctx.from_user.photo.big_photo_unique_id, + } + elif ( + ctx.from_user + or ctx.sender_chat + and not ctx.sender_chat.photo + or not ctx.sender_chat + ): + return "" + else: + return { + "small_file_id": ctx.sender_chat.photo.small_file_id, + "small_photo_unique_id": ctx.sender_chat.photo.small_photo_unique_id, + "big_file_id": ctx.sender_chat.photo.big_file_id, + "big_photo_unique_id": ctx.sender_chat.photo.big_photo_unique_id, + } + + +async def get_text_or_caption(ctx: Message): + if ctx.text: + return ctx.text + elif ctx.caption: + return ctx.caption + else: + return "" + + +async def pyrogram_to_quotly(messages, is_reply): + 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 and is_reply: + 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", "qr"]) & filters.reply) +@ratelimiter +async def msg_quotly_cmd(self: Client, ctx: Message): + is_reply = False + if ctx.command[0][1] == "r": + is_reply = True + if len(ctx.text.split()) > 1: + check_arg = isArgInt(ctx.command[1]) + if check_arg[0]: + if check_arg[1] < 2 or check_arg[1] > 10: + return await ctx.reply_msg("Invalid range", del_in=6) + try: + messages = [ + i + for i in await self.get_messages( + chat_id=ctx.chat.id, + message_ids=range( + ctx.reply_to_message.id, + ctx.reply_to_message.id + (check_arg[1] + 5), + ), + replies=-1, + ) + if not i.empty and not i.media + ] + except Exception: + return await ctx.reply_text("🤷🏻‍♂️") + try: + make_quotly = await pyrogram_to_quotly(messages, is_reply=is_reply) + bio_sticker = BytesIO(make_quotly) + bio_sticker.name = "biosticker.webp" + return await ctx.reply_sticker(bio_sticker) + except Exception: + return await ctx.reply_msg("🤷🏻‍♂️") + try: + messages_one = await self.get_messages( + chat_id=ctx.chat.id, message_ids=ctx.reply_to_message.id, replies=-1 + ) + messages = [messages_one] + except Exception: + return await ctx.reply_msg("🤷🏻‍♂️") + try: + make_quotly = await pyrogram_to_quotly(messages, is_reply=is_reply) + bio_sticker = BytesIO(make_quotly) + bio_sticker.name = "misskatyquote_sticker.webp" + return await ctx.reply_sticker(bio_sticker) + except Exception as e: + return await ctx.reply_msg(f"ERROR: {e}") diff --git a/misskaty/plugins/sangmata.py b/misskaty/plugins/sangmata.py new file mode 100644 index 00000000..461ef5cb --- /dev/null +++ b/misskaty/plugins/sangmata.py @@ -0,0 +1,122 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +from pyrogram import filters +from pyrogram.types import Message + +from database.sangmata_db import ( + add_userdata, + cek_userdata, + get_userdata, + is_sangmata_on, + sangmata_off, + sangmata_on, +) +from misskaty import app +from misskaty.core.decorator.permissions import adminsOnly +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER + +__MODULE__ = "SangMata" +__HELP__ = """" +This feature inspired from SangMata Bot. I'm created simple detection to check user data include username, first_name, and last_name. +/sangmata_set [on/off] - Enable/disable sangmata in groups. +""" + + +# Check user that change first_name, last_name and usernaname +@app.on_message( + filters.group & ~filters.bot & ~filters.via_bot, + group=5, +) +@use_chat_lang() +async def cek_mataa(_, ctx: Message, strings): + if ctx.sender_chat or not await is_sangmata_on(ctx.chat.id): + return + if not await cek_userdata(ctx.from_user.id): + return await add_userdata( + ctx.from_user.id, + ctx.from_user.username, + ctx.from_user.first_name, + ctx.from_user.last_name, + ) + usernamebefore, first_name, lastname_before = await get_userdata(ctx.from_user.id) + msg = "" + if ( + usernamebefore != ctx.from_user.username + or first_name != ctx.from_user.first_name + or lastname_before != ctx.from_user.last_name + ): + msg += f"👀 Mata MissKaty\n\n🌞 User: {ctx.from_user.mention} [{ctx.from_user.id}]\n" + if usernamebefore != ctx.from_user.username: + usernamebefore = f"@{usernamebefore}" if usernamebefore else strings("no_uname") + usernameafter = ( + f"@{ctx.from_user.username}" + if ctx.from_user.username + else strings("no_uname") + ) + msg += strings("uname_change_msg").format(bef=usernamebefore, aft=usernameafter) + await add_userdata( + ctx.from_user.id, + ctx.from_user.username, + ctx.from_user.first_name, + ctx.from_user.last_name, + ) + if first_name != ctx.from_user.first_name: + msg += strings("firstname_change_msg").format( + bef=first_name, aft=ctx.from_user.first_name + ) + await add_userdata( + ctx.from_user.id, + ctx.from_user.username, + ctx.from_user.first_name, + ctx.from_user.last_name, + ) + if lastname_before != ctx.from_user.last_name: + lastname_before = lastname_before or strings("no_last_name") + lastname_after = ctx.from_user.last_name or strings("no_last_name") + msg += strings("lastname_change_msg").format( + bef=lastname_before, aft=lastname_after + ) + await add_userdata( + ctx.from_user.id, + ctx.from_user.username, + ctx.from_user.first_name, + ctx.from_user.last_name, + ) + if msg != "": + await ctx.reply_msg(msg, quote=True) + + +@app.on_message( + filters.group + & filters.command("sangmata_set", COMMAND_HANDLER) + & ~filters.bot + & ~filters.via_bot +) +@adminsOnly("can_change_info") +@ratelimiter +@use_chat_lang() +async def set_mataa(_, ctx: Message, strings): + if len(ctx.command) == 1: + return await ctx.reply_msg( + strings("set_sangmata_help").format(cmd=ctx.command[0]), del_in=6 + ) + if ctx.command[1] == "on": + cekset = await is_sangmata_on(ctx.chat.id) + if cekset: + await ctx.reply_msg(strings("sangmata_already_on")) + else: + await sangmata_on(ctx.chat.id) + await ctx.reply_msg(strings("sangmata_enabled")) + elif ctx.command[1] == "off": + cekset = await is_sangmata_on(ctx.chat.id) + if not cekset: + await ctx.reply_msg(strings("sangmata_already_off")) + else: + await sangmata_off(ctx.chat.id) + await ctx.reply_msg(strings("sangmata_disabled")) + else: + await ctx.reply_msg(strings("wrong_param"), del_in=6) diff --git a/misskaty/plugins/sed.py b/misskaty/plugins/sed.py new file mode 100644 index 00000000..e4d86ca9 --- /dev/null +++ b/misskaty/plugins/sed.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2018-2022 Amano Team + +import html + +import regex +from pyrogram import Client, filters +from pyrogram.errors import MessageEmpty +from pyrogram.types import Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter + + +@app.on_message(filters.regex(r"^s/(.+)?/(.+)?(/.+)?") & filters.reply) +@ratelimiter +async def sed(self: Client, ctx: 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 = ctx.reply_to_message.text or ctx.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 ctx.reply_msg("Oops, your regex pattern has run for too long.") + except regex.error as e: + return await ctx.reply_msg(str(e)) + else: + try: + await self.send_msg( + ctx.chat.id, + f"
{html.escape(res)}
", + reply_to_message_id=ctx.reply_to_message.id, + ) + except MessageEmpty: + return await ctx.reply_msg( + "Please reply message to use this feature.", del_in=5 + ) + except Exception as e: + return await ctx.reply_msg(f"ERROR: {str(e)}") diff --git a/misskaty/plugins/session_generator.py b/misskaty/plugins/session_generator.py new file mode 100644 index 00000000..aa71de97 --- /dev/null +++ b/misskaty/plugins/session_generator.py @@ -0,0 +1,278 @@ +import traceback +from logging import getLogger + +from pyrogram import Client, filters +from pyrogram.errors import ( + ApiIdInvalid, + PasswordHashInvalid, + PhoneCodeExpired, + PhoneCodeInvalid, + PhoneNumberInvalid, + SessionPasswordNeeded, +) +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from telethon import TelegramClient +from telethon.errors import ( + ApiIdInvalidError, + PasswordHashInvalidError, + PhoneCodeExpiredError, + PhoneCodeInvalidError, + PhoneNumberInvalidError, + SessionPasswordNeededError, +) +from telethon.sessions import StringSession + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.core.misskaty_patch.listen.listen import ListenerTimeout +from misskaty.vars import API_HASH, API_ID, COMMAND_HANDLER + +LOGGER = getLogger(__name__) + +__MODULE__ = "SessionGen" +__HELP__ = """ +/genstring - Generate string session using this bot. Only support Pyrogram v2 and Telethon. +""" + + +ask_ques = "**» Please choose the library for which you want generate string :**\n\nNote: I'm not collecting any personal info from this feature, you can deploy own bot if you want." +buttons_ques = [ + [ + InlineKeyboardButton("Pyrogram", callback_data="pyrogram"), + InlineKeyboardButton("Telethon", callback_data="telethon"), + ], + [ + InlineKeyboardButton("Pyrogram Bot", callback_data="pyrogram_bot"), + InlineKeyboardButton("Telethon Bot", callback_data="telethon_bot"), + ], +] + +gen_button = [ + [InlineKeyboardButton(text="🙄 Generate Session 🙄", callback_data="genstring")] +] + + +async def is_batal(msg): + if msg.text == "/cancel": + await msg.reply( + "**» Cancelled the ongoing string session generation process !**", + quote=True, + reply_markup=InlineKeyboardMarkup(gen_button), + ) + return True + elif msg.text == "/skip": + return False + elif msg.text.startswith("/"): # Bot Commands + await msg.reply( + "**» Cancelled the ongoing string session generation process !**", + quote=True, + ) + return True + else: + return False + + +@app.on_callback_query( + filters.regex(pattern=r"^(genstring|pyrogram|pyrogram_bot|telethon_bot|telethon)$") +) +@ratelimiter +async def callbackgenstring(bot, callback_query): + query = callback_query.matches[0].group(1) + if query == "genstring": + await callback_query.answer() + await callback_query.message.reply( + ask_ques, reply_markup=InlineKeyboardMarkup(buttons_ques) + ) + elif query.startswith("pyrogram") or query.startswith("telethon"): + try: + if query == "pyrogram": + await callback_query.answer() + await generate_session(bot, callback_query.message) + elif query == "pyrogram_bot": + await callback_query.answer( + "» The session generator will be of Pyrogram v2.", show_alert=True + ) + await generate_session(bot, callback_query.message, is_bot=True) + elif query == "telethon_bot": + await callback_query.answer() + await generate_session( + bot, callback_query.message, telethon=True, is_bot=True + ) + elif query == "telethon": + await callback_query.answer() + await generate_session(bot, callback_query.message, telethon=True) + except Exception as e: + LOGGER.error(traceback.format_exc()) + ERROR_MESSAGE = ( + "Something went wrong. \n\n**ERROR** : {} " + "\n\n**Please forward this message to my Owner**, if this message " + "doesn't contain any sensitive data " + "because this error is **not logged by bot.** !" + ) + await callback_query.message.reply(ERROR_MESSAGE.format(str(e))) + + +@app.on_message( + filters.private & ~filters.forwarded & filters.command("genstring", COMMAND_HANDLER) +) +@ratelimiter +async def genstringg(_, msg): + await msg.reply(ask_ques, reply_markup=InlineKeyboardMarkup(buttons_ques)) + + +async def generate_session(bot, msg, telethon=False, is_bot: bool = False): + ty = "Telethon" if telethon else "Pyrogram" + if is_bot: + ty += " Bot" + await msg.reply(f"» Trying to start **{ty}** session generator...") + api_id_msg = await msg.chat.ask( + "Please send your **API_ID** to proceed.\n\nClick on /skip for using bot's api.", + filters=filters.text, + ) + if await is_batal(api_id_msg): + return + if api_id_msg.text == "/skip": + api_id = API_ID + api_hash = API_HASH + else: + try: + api_id = int(api_id_msg.text) + await api_id_msg.delete() + except ValueError: + return await api_id_msg.reply( + "**API_ID** must be integer, start generating your session again.", + quote=True, + reply_markup=InlineKeyboardMarkup(gen_button), + ) + api_hash_msg = await msg.chat.ask( + "» Now please send your **API_HASH** to continue.", filters=filters.text + ) + if await is_batal(api_hash_msg): + return + api_hash = api_hash_msg.text + await api_hash_msg.delete() + t = ( + "Please send your **BOT_TOKEN** to continue.\nExample : `5432198765:abcdanonymousterabaaplol`'" + if is_bot + else "» Please send your **PHONE_NUMBER** with country code for which you want generate session. \nᴇxᴀᴍᴩʟᴇ : `+6286356837789`'" + ) + phone_number_msg = await msg.chat.ask(t, filters=filters.text) + if await is_batal(phone_number_msg): + return + phone_number = phone_number_msg.text + await phone_number_msg.delete() + if not is_bot: + await msg.reply("» Trying to send OTP at the given number...") + else: + await msg.reply("» Trying to login using Bot Token...") + if telethon and is_bot or telethon: + client = TelegramClient(StringSession(), api_id, api_hash) + elif is_bot: + client = Client( + name="bot", + api_id=api_id, + api_hash=api_hash, + bot_token=phone_number, + in_memory=True, + ) + else: + client = Client(name="user", api_id=api_id, api_hash=api_hash, in_memory=True) + await client.connect() + try: + code = None + if not is_bot: + if telethon: + code = await client.send_code_request(phone_number) + else: + code = await client.send_code(phone_number) + except (ApiIdInvalid, ApiIdInvalidError): + return await msg.reply( + "» Your **API_ID** and **API_HASH** combination doesn't match. \n\nPlease start generating your session again.", + reply_markup=InlineKeyboardMarkup(gen_button), + ) + except (PhoneNumberInvalid, PhoneNumberInvalidError): + return await msg.reply( + "» The **PHONE_NUMBER** you've doesn't belong to any account in Telegram.\n\nPlease start generating your session again.", + reply_markup=InlineKeyboardMarkup(gen_button), + ) + try: + phone_code_msg = None + if not is_bot: + phone_code_msg = await msg.chat.ask( + "» Please send the **OTP** That you've received from Telegram on your account.\nIf OTP is `12345`, **please send it as** `1 2 3 4 5`.", + filters=filters.text, + timeout=600, + ) + if await is_batal(phone_code_msg): + return + except ListenerTimeout: + return await msg.reply( + "» Time limit reached of 10 minutes.\n\nPlease start generating your session again.", + reply_markup=InlineKeyboardMarkup(gen_button), + ) + if not is_bot: + phone_code = phone_code_msg.text.replace(" ", "") + await phone_code_msg.delete() + try: + if telethon: + await client.sign_in(phone_number, phone_code, password=None) + else: + await client.sign_in(phone_number, code.phone_code_hash, phone_code) + except (PhoneCodeInvalid, PhoneCodeInvalidError): + return await msg.reply( + "» The OTP you've sent is **wrong.**\n\nPlease start generating your session again.", + reply_markup=InlineKeyboardMarkup(gen_button), + ) + except (PhoneCodeExpired, PhoneCodeExpiredError): + return await msg.reply( + "» The OTP you've sent is **expired.**\n\nPlease start generating your session again.", + reply_markup=InlineKeyboardMarkup(gen_button), + ) + except (SessionPasswordNeeded, SessionPasswordNeededError): + try: + two_step_msg = await msg.chat.ask( + "» Please enter your **Two Step Verification** password to continue.", + filters=filters.text, + timeout=300, + ) + except ListenerTimeout: + return await msg.reply( + "» Time limit reached of 5 minutes.\n\nPlease start generating your session again.", + reply_markup=InlineKeyboardMarkup(gen_button), + ) + try: + password = two_step_msg.text + await two_step_msg.delete() + if telethon: + await client.sign_in(password=password) + else: + await client.check_password(password=password) + if await is_batal(api_id_msg): + return + except (PasswordHashInvalid, PasswordHashInvalidError): + return await two_step_msg.reply( + "» The password you've sent is wrong.\n\nPlease start generating session again.", + quote=True, + reply_markup=InlineKeyboardMarkup(gen_button), + ) + elif telethon: + await client.start(bot_token=phone_number) + else: + await client.sign_in_bot(phone_number) + if telethon: + string_session = client.session.save() + else: + string_session = await client.export_session_string() + text = f"**This is your {ty} String Session** \n\n`{string_session}` \n\n**Generated By :** @{bot.me.username}\n🍒 **Note :** Don't share it to anyone And don't forget to support this owner bot if you like🥺" + try: + if not is_bot: + await client.send_message("me", text) + else: + await bot.send_message(msg.chat.id, text) + except KeyError: + pass + await client.disconnect() + await bot.send_message( + msg.chat.id, + f'» Successfully generated your {"Telethon" if telethon else "Pyrogram"} String Session.\n\nPlease check saved messages to get it ! \n\n**A String Generator bot by ** @IAmCuteCodes', + ) diff --git a/misskaty/plugins/start_help.py b/misskaty/plugins/start_help.py new file mode 100644 index 00000000..9842d966 --- /dev/null +++ b/misskaty/plugins/start_help.py @@ -0,0 +1,259 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved + """ +import re + +from pyrogram import Client, filters +from pyrogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, +) + +from misskaty import BOT_NAME, BOT_USERNAME, HELPABLE, app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper import bot_sys_stats, paginate_modules +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER + +home_keyboard_pm = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton(text="Commands ❓", callback_data="bot_commands"), + InlineKeyboardButton( + text="Source Code 🛠", + 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=f"http://t.me/{BOT_USERNAME}?startgroup=new", + ) + ], + ] +) + +home_text_pm = f"Hey there! My name is {BOT_NAME}. 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=f"t.me/{BOT_USERNAME}?start=help"), + InlineKeyboardButton( + text="Source Code �", + 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", COMMAND_HANDLER)) +@use_chat_lang() +async def start(_, ctx: Message, strings): + if ctx.chat.type.value != "private": + nama = ctx.from_user.mention if ctx.from_user else ctx.sender_chat.title + return await ctx.reply_photo( + photo="https://telegra.ph/file/90e9a448bc2f8b055b762.jpg", + caption=strings("start_msg").format(kamuh=nama), + reply_markup=keyboard, + ) + if len(ctx.text.split()) > 1: + name = (ctx.text.split(None, 1)[1]).lower() + if "_" in name: + module = name.split("_", 1)[1] + text = ( + strings("help_name").format(mod=HELPABLE[module].__MODULE__) + + HELPABLE[module].__HELP__ + ) + await ctx.reply_msg(text, disable_web_page_preview=True) + elif name == "help": + text, keyb = await help_parser(ctx.from_user.first_name) + await ctx.reply_msg( + text, + reply_markup=keyb, + ) + else: + await ctx.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")) +@ratelimiter +async def commands_callbacc(_, cb: CallbackQuery): + text, keyb = await help_parser(cb.from_user.mention) + await app.send_message( + cb.message.chat.id, + text=text, + reply_markup=keyb, + ) + await cb.message.delete_msg() + + +@app.on_callback_query(filters.regex("stats_callback")) +@ratelimiter +async def stats_callbacc(_, cb: CallbackQuery): + text = await bot_sys_stats() + await app.answer_callback_query(cb.id, text, show_alert=True) + + +@app.on_message(filters.command("help", COMMAND_HANDLER)) +@ratelimiter +@use_chat_lang() +async def help_command(_, ctx: Message, strings): + if ctx.chat.type.value != "private": + if len(ctx.command) >= 2: + name = (ctx.text.split(None, 1)[1]).replace(" ", "_").lower() + if str(name) in HELPABLE: + key = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text=strings("click_me"), + url=f"t.me/{BOT_USERNAME}?start=help_{name}", + ) + ], + ] + ) + await ctx.reply_msg( + strings("click_btn").format(nm=name), + reply_markup=key, + ) + else: + await ctx.reply_msg(strings("pm_detail"), reply_markup=keyboard) + else: + await ctx.reply_msg(strings("pm_detail"), reply_markup=keyboard) + elif len(ctx.command) >= 2: + name = (ctx.text.split(None, 1)[1]).replace(" ", "_").lower() + if str(name) in HELPABLE: + text = ( + strings("help_name").format(mod=HELPABLE[name].__MODULE__) + + HELPABLE[name].__HELP__ + ) + await ctx.reply_msg(text, disable_web_page_preview=True) + else: + text, help_keyboard = await help_parser(ctx.from_user.first_name) + await ctx.reply_msg( + text, + reply_markup=help_keyboard, + disable_web_page_preview=True, + ) + else: + text, help_keyboard = await help_parser(ctx.from_user.first_name) + await ctx.reply_msg( + text, reply_markup=help_keyboard, disable_web_page_preview=True + ) + + +async def help_parser(name, keyb=None): + if not keyb: + keyb = 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 change language bot using /setlang command, but it's still in beta stage. +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", + ), + keyb, + ) + + +@app.on_callback_query(filters.regex(r"help_(.*?)")) +@ratelimiter +@use_chat_lang() +async def help_button(self: Client, query: CallbackQuery, strings): + 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 = strings("help_txt").format( + kamuh=query.from_user.first_name, bot=self.me.first_name + ) + if mod_match: + module = mod_match[1].replace(" ", "_") + text = ( + strings("help_name").format(mod=HELPABLE[module].__MODULE__) + + HELPABLE[module].__HELP__ + ) + + await query.message.edit_msg( + text=text, + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton(strings("back_btn"), callback_data="help_back")]] + ), + disable_web_page_preview=True, + ) + elif home_match: + await app.send_msg( + query.from_user.id, + text=home_text_pm, + reply_markup=home_keyboard_pm, + ) + await query.message.delete_msg() + elif prev_match: + curr_page = int(prev_match[1]) + await query.message.edit_msg( + 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_msg( + 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_msg( + text=top_text, + reply_markup=InlineKeyboardMarkup(paginate_modules(0, HELPABLE, "help")), + disable_web_page_preview=True, + ) + + elif create_match: + text, keyb = await help_parser(query) + await query.message.edit_msg( + text=text, + reply_markup=keyb, + disable_web_page_preview=True, + ) + + try: + await self.answer_callback_query(query.id) + except: + pass diff --git a/misskaty/plugins/stickers.py b/misskaty/plugins/stickers.py new file mode 100644 index 00000000..b8a49ee0 --- /dev/null +++ b/misskaty/plugins/stickers.py @@ -0,0 +1,412 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import asyncio +import os +import re +import shutil +import tempfile + +from PIL import Image +from pyrogram import Client, emoji, enums, filters +from pyrogram.errors import BadRequest, PeerIdInvalid, StickersetInvalid +from pyrogram.file_id import FileId +from pyrogram.raw.functions.messages import GetStickerSet, SendMedia +from pyrogram.raw.functions.stickers import ( + AddStickerToSet, + CreateStickerSet, + RemoveStickerFromSet, +) +from pyrogram.raw.types import ( + DocumentAttributeFilename, + InputDocument, + InputMediaUploadedDocument, + InputStickerSetItem, + InputStickerSetShortName, +) +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.http import http +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER, LOG_CHANNEL + +__MODULE__ = "Stickers" +__HELP__ = """ +/kang [Reply to sticker] - Add sticker to your pack. +/unkang [Reply to sticker] - Remove sticker from your pack (Only can remove sticker that added by this bot.). +/getsticker - Convert sticker to png. +/stickerid - View sticker ID +""" + + +def get_emoji_regex(): + e_list = [ + getattr(emoji, e).encode("unicode-escape").decode("ASCII") + for e in dir(emoji) + if not e.startswith("_") + ] + # to avoid re.error excluding char that start with '*' + e_sort = sorted([x for x in e_list if not x.startswith("*")], reverse=True) + # Sort emojis by length to make sure multi-character emojis are + # matched first + pattern_ = f"({'|'.join(e_sort)})" + return re.compile(pattern_) + + +EMOJI_PATTERN = get_emoji_regex() +SUPPORTED_TYPES = ["jpeg", "png", "webp"] + + +@app.on_cmd("getsticker") +@ratelimiter +@use_chat_lang() +async def getsticker_(self: Client, ctx: Message, strings): + if not ctx.reply_to_message or ctx.reply_to_message.sticker: + await ctx.reply_msg(strings("not_sticker")) + else: + sticker = ctx.reply_to_message.sticker + if sticker.is_animated: + await ctx.reply_msg(strings("no_anim_stick")) + else: + with tempfile.TemporaryDirectory() as tempdir: + path = os.path.join(tempdir, "getsticker") + sticker_file = await self.download_media( + message=ctx.reply_to_message, + file_name=f"{path}/{sticker.set_name}.png", + ) + await ctx.reply_to_message.reply_document( + document=sticker_file, + caption=f"Emoji: {sticker.emoji}\n" + f"Sticker ID: {sticker.file_id}\n\n" + f"Send by: @{self.me.username}", + ) + shutil.rmtree(tempdir, ignore_errors=True) + + +@app.on_message(filters.command("stickerid", COMMAND_HANDLER) & filters.reply) +@ratelimiter +async def getstickerid(_, ctx: Message): + if ctx.reply_to_message.sticker: + await ctx.reply_msg( + "The ID of this sticker is: {stickerid}".format( + stickerid=ctx.reply_to_message.sticker.file_id + ) + ) + + +@app.on_message(filters.command("unkang", COMMAND_HANDLER) & filters.reply) +@ratelimiter +@use_chat_lang() +async def unkangs(self: Client, ctx: Message, strings): + if not ctx.from_user: + return await ctx.reply("You're anon, unkang in my PM") + if sticker := ctx.reply_to_message.sticker: + if str(ctx.from_user.id) not in sticker.set_name: + return await ctx.reply_msg("This sticker is not your pack, don't do it..") + pp = await ctx.reply_msg(strings("unkang_msg")) + try: + decoded = FileId.decode(sticker.file_id) + sticker = InputDocument( + id=decoded.media_id, + access_hash=decoded.access_hash, + file_reference=decoded.file_reference, + ) + await app.invoke(RemoveStickerFromSet(sticker=sticker)) + await pp.edit_msg(strings("unkang_success")) + except Exception as e: + await pp.edit_msg(strings("unkang_error").format(e=e)) + else: + await ctx.reply_msg(strings("unkang_help").format(c=self.me.username), del_in=6) + + +@app.on_cmd(["curi", "kang"]) +@ratelimiter +@use_chat_lang() +async def kang_sticker(self: Client, ctx: Message, strings): + if not ctx.from_user: + return await ctx.reply_msg(strings("anon_warn"), del_in=6) + prog_msg = await ctx.reply_msg(strings("kang_msg")) + sticker_emoji = "🤔" + packnum = 0 + packname_found = False + resize = False + animated = False + videos = False + convert = False + reply = ctx.reply_to_message + user = await self.resolve_peer(ctx.from_user.username or ctx.from_user.id) + + if reply and reply.media: + if reply.photo: + resize = True + elif reply.animation: + videos = True + convert = True + elif reply.video: + convert = True + videos = True + elif reply.document: + if "image" in reply.document.mime_type: + # mime_type: image/webp + resize = True + elif reply.document.mime_type in ( + enums.MessageMediaType.VIDEO, + enums.MessageMediaType.ANIMATION, + ): + # mime_type: application/video + videos = True + convert = True + elif "tgsticker" in reply.document.mime_type: + # mime_type: application/x-tgsticker + animated = True + elif reply.sticker: + if not reply.sticker.file_name: + return await prog_msg.edit_msg(strings("stick_no_name")) + if reply.sticker.emoji: + sticker_emoji = reply.sticker.emoji + animated = reply.sticker.is_animated + videos = reply.sticker.is_video + if videos: + convert = False + elif not reply.sticker.file_name.endswith(".tgs"): + resize = True + else: + return await prog_msg.edit_msg() + + pack_prefix = "anim" if animated else "vid" if videos else "a" + packname = f"{pack_prefix}_{ctx.from_user.id}_by_{self.me.username}" + + if ( + len(ctx.command) > 1 + and ctx.command[1].isdigit() + and int(ctx.command[1]) > 0 + ): + # provide pack number to kang in desired pack + packnum = ctx.command.pop(1) + packname = ( + f"{pack_prefix}{packnum}_{ctx.from_user.id}_by_{self.me.username}" + ) + if len(ctx.command) > 1: + # matches all valid emojis in input + sticker_emoji = ( + "".join(set(EMOJI_PATTERN.findall("".join(ctx.command[1:])))) + or sticker_emoji + ) + filename = await self.download_media(ctx.reply_to_message) + if not filename: + # Failed to download + await prog_msg.delete() + return + elif ctx.entities and len(ctx.entities) > 1: + pack_prefix = "a" + filename = "sticker.png" + packname = f"c{ctx.from_user.id}_by_{self.me.username}" + img_url = next( + ( + ctx.text[y.offset : (y.offset + y.length)] + for y in ctx.entities + if y.type == "url" + ), + None, + ) + + if not img_url: + await prog_msg.delete() + return + try: + r = await http.get(img_url) + if r.status_code == 200: + with open(filename, mode="wb") as f: + f.write(r.read()) + except Exception as r_e: + return await prog_msg.edit_msg(f"{r_e.__class__.__name__} : {r_e}") + if len(ctx.command) > 2: + # m.command[1] is image_url + if ctx.command[2].isdigit() and int(ctx.command[2]) > 0: + packnum = ctx.command.pop(2) + packname = f"a{packnum}_{ctx.from_user.id}_by_{self.me.username}" + if len(ctx.command) > 2: + sticker_emoji = ( + "".join(set(EMOJI_PATTERN.findall("".join(ctx.command[2:])))) + or sticker_emoji + ) + resize = True + else: + return await prog_msg.edit_msg(strings("kang_help")) + try: + if resize: + filename = resize_image(filename) + elif convert: + filename = await convert_video(filename) + if filename is False: + return await prog_msg.edit_msg("Error", del_in=6) + max_stickers = 50 if animated else 120 + while not packname_found: + try: + stickerset = await self.invoke( + GetStickerSet( + stickerset=InputStickerSetShortName(short_name=packname), + hash=0, + ) + ) + if stickerset.set.count >= max_stickers: + packnum += 1 + packname = f"{pack_prefix}_{packnum}_{ctx.from_user.id}_by_{self.me.username}" + else: + packname_found = True + except StickersetInvalid: + break + file = await self.save_file(filename) + media = await self.invoke( + SendMedia( + peer=(await self.resolve_peer(LOG_CHANNEL)), + media=InputMediaUploadedDocument( + file=file, + mime_type=self.guess_mime_type(filename), + attributes=[DocumentAttributeFilename(file_name=filename)], + ), + message=f"#Sticker kang by UserID -> {ctx.from_user.id}", + random_id=self.rnd_id(), + ), + ) + msg_ = media.updates[-1].message + stkr_file = msg_.media.document + if packname_found: + await prog_msg.edit_msg(strings("exist_pack")) + await self.invoke( + AddStickerToSet( + stickerset=InputStickerSetShortName(short_name=packname), + sticker=InputStickerSetItem( + document=InputDocument( + id=stkr_file.id, + access_hash=stkr_file.access_hash, + file_reference=stkr_file.file_reference, + ), + emoji=sticker_emoji, + ), + ) + ) + else: + await prog_msg.edit_msg(strings("new_packs")) + stkr_title = f"{ctx.from_user.first_name}'s" + if animated: + stkr_title += "AnimPack" + elif videos: + stkr_title += "VidPack" + if packnum != 0: + stkr_title += f" v{packnum}" + try: + await self.invoke( + CreateStickerSet( + user_id=user, + title=stkr_title, + short_name=packname, + stickers=[ + InputStickerSetItem( + document=InputDocument( + id=stkr_file.id, + access_hash=stkr_file.access_hash, + file_reference=stkr_file.file_reference, + ), + emoji=sticker_emoji, + ) + ], + animated=animated, + videos=videos, + ) + ) + except PeerIdInvalid: + return await prog_msg.edit_msg( + strings("please_start_msg"), + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + strings("click_me"), + url=f"https://t.me/{self.me.username}?start", + ) + ] + ] + ), + ) + + except BadRequest: + return await prog_msg.edit_msg(strings("pack_full")) + except Exception as all_e: + await prog_msg.edit_msg(f"{all_e.__class__.__name__} : {all_e}") + else: + markup = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + text=strings("viewpack"), + url=f"https://t.me/addstickers/{packname}", + ) + ] + ] + ) + await prog_msg.edit_msg( + strings("kang_success").format(emot=sticker_emoji), + reply_markup=markup, + ) + # Cleanup + await self.delete_messages( + chat_id=LOG_CHANNEL, message_ids=msg_.id, revoke=True + ) + try: + os.remove(filename) + except OSError: + pass + + +def resize_image(filename: str) -> str: + im = Image.open(filename) + maxsize = 512 + scale = maxsize / max(im.width, im.height) + sizenew = (int(im.width * scale), int(im.height * scale)) + im = im.resize(sizenew, Image.NEAREST) + downpath, f_name = os.path.split(filename) + # not hardcoding png_image as "sticker.png" + png_image = os.path.join(downpath, f"{f_name.split('.', 1)[0]}.png") + im.save(png_image, "PNG") + if png_image != filename: + os.remove(filename) + return png_image + + +async def convert_video(filename: str) -> str: + downpath, f_name = os.path.split(filename) + webm_video = os.path.join(downpath, f"{f_name.split('.', 1)[0]}.webm") + cmd = [ + "ffmpeg", + "-loglevel", + "quiet", + "-i", + filename, + "-t", + "00:00:03", + "-vf", + "fps=30", + "-c:v", + "vp9", + "-b:v:", + "500k", + "-preset", + "ultrafast", + "-s", + "512x512", + "-y", + "-an", + webm_video, + ] + + proc = await asyncio.create_subprocess_exec(*cmd) + # Wait for the subprocess to finish + await proc.communicate() + + if webm_video != filename: + os.remove(filename) + return webm_video diff --git a/misskaty/plugins/subscene_dl.py b/misskaty/plugins/subscene_dl.py new file mode 100644 index 00000000..bce9602e --- /dev/null +++ b/misskaty/plugins/subscene_dl.py @@ -0,0 +1,252 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import asyncio +import logging +import os + +import cloudscraper +from bs4 import BeautifulSoup +from pykeyboard import InlineButton, InlineKeyboard +from pyrogram import filters +from pyrogram.types import CallbackQuery, Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.subscene_helper import down_page +from misskaty.vars import COMMAND_HANDLER + +from .web_scraper import split_arr + +LOGGER = logging.getLogger(__name__) +SUB_TITLE_DICT = {} +SUB_DL_DICT = {} + + +# Get list title based on query +async def getTitleSub(msg, kueri, CurrentPage, user): + if not SUB_TITLE_DICT.get(msg.id): + sdata = [] + scraper = cloudscraper.create_scraper() + param = {"query": kueri} + r = scraper.post( + "https://subscene.com/subtitles/searchbytitle", data=param + ).text + soup = BeautifulSoup(r, "lxml") + lists = soup.find("div", {"class": "search-result"}) + entry = lists.find_all("div", {"class": "title"}) + # if "Tidak Ditemukan" in entry[0].text: + # await msg.edit_msg(f"Sorry, could not find any result for: {kueri}") + # return None, 0, None + for sub in entry: + title = sub.find("a").text + link = f"https://subscene.com{sub.find('a').get('href')}" + sdata.append({"title": title, "link": link}) + SUB_TITLE_DICT[msg.id] = [split_arr(sdata, 10), kueri] + try: + index = int(CurrentPage - 1) + PageLen = len(SUB_TITLE_DICT[msg.id][0]) + extractbtn1 = [] + extractbtn2 = [] + subResult = f"#Subscene Results For: {kueri}\n\n" + for c, i in enumerate(SUB_TITLE_DICT[msg.id][0][index], start=1): + subResult += f"{c}. {i['title']}\n" + if c < 6: + extractbtn1.append( + InlineButton(c, f"sublist#{CurrentPage}#{c}#{msg.id}#{user}") + ) + else: + extractbtn2.append( + InlineButton(c, f"sublist#{CurrentPage}#{c}#{msg.id}#{user}") + ) + subResult = "".join(i for i in subResult if i not in "[]") + return subResult, PageLen, extractbtn1, extractbtn2 + except (IndexError, KeyError): + await msg.edit_msg("Sorry could not find any matching results!", del_in=5) + return None, 0, None + + +# Get list all subtitles from title +async def getListSub(msg, link, CurrentPage, user): + if not SUB_DL_DICT.get(msg.id): + sdata = [] + scraper = cloudscraper.create_scraper() + kuki = { + "LanguageFilter": "13,44,50" + } # Only filter language English, Malay, Indonesian + r = scraper.get(link, cookies=kuki).text + soup = BeautifulSoup(r, "lxml") + for i in soup.findAll(class_="a1"): + lang = i.find("a").findAll("span")[0].text.strip() + title = i.find("a").findAll("span")[1].text.strip() + if i.find(class_="l r neutral-icon"): + rate = "😐" + elif i.find(class_="l r positive-icon"): + rate = "🥰" + else: + rate = "☹️" + dllink = f"https://subscene.com{i.find('a').get('href')}" + sdata.append({"title": title, "lang": lang, "rate": rate, "link": dllink}) + SUB_DL_DICT[msg.id] = [split_arr(sdata, 10), link] + try: + index = int(CurrentPage - 1) + PageLen = len(SUB_DL_DICT[msg.id][0]) + extractbtn1 = [] + extractbtn2 = [] + subResult = f"#Subscene Results For: {link}\n\n" + for c, i in enumerate(SUB_DL_DICT[msg.id][0][index], start=1): + subResult += f"{c}. {i['title']} [{i['rate']}]\n{i['lang']}\n" + if c < 6: + extractbtn1.append( + InlineButton(c, f"extractsubs#{CurrentPage}#{c}#{msg.id}#{user}") + ) + else: + extractbtn2.append( + InlineButton(c, f"extractsubs#{CurrentPage}#{c}#{msg.id}#{user}") + ) + subResult = "".join(i for i in subResult if i not in "[]") + return subResult, PageLen, extractbtn1, extractbtn2 + except (IndexError, KeyError): + await msg.edit_msg("Sorry could not find any matching results!") + return None, 0, None + + +# Subscene CMD +@app.on_message(filters.command(["subscene"], COMMAND_HANDLER)) +@ratelimiter +async def subscene_cmd(_, ctx: Message): + if not ctx.input: + return await ctx.reply_msg( + f"ℹ️ Please add query after CMD!\nEx: /{ctx.command[0]} Jurassic World" + ) + pesan = await ctx.reply_msg( + "⏳ Please wait, getting data from subscene..", quote=True + ) + CurrentPage = 1 + subres, PageLen, btn1, btn2 = await getTitleSub( + pesan, ctx.input, CurrentPage, ctx.from_user.id + ) + if not subres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "subscenepage#{number}" + f"#{pesan.id}#{ctx.from_user.id}", + ) + keyboard.row(InlineButton("👇 Extract Data ", "Hmmm")) + keyboard.row(*btn1) + if btn2: + keyboard.row(*btn2) + keyboard.row(InlineButton("❌ Close", f"close#{ctx.from_user.id}")) + await pesan.edit_msg(subres, disable_web_page_preview=True, reply_markup=keyboard) + + +# Callback list title +@app.on_callback_query( + filters.create(lambda _, __, query: "subscenepage#" in query.data) +) +@ratelimiter +async def subpage_callback(_, callback_query: CallbackQuery): + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer("Not yours..", True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + try: + kueri = SUB_TITLE_DICT[message_id][1] + except KeyError: + await callback_query.answer("Invalid callback data, please send CMD again..") + await asyncio.sleep(3) + return await callback_query.message.delete_msg() + + try: + subres, PageLen, btn1, btn2 = await getTitleSub( + callback_query.message, kueri, CurrentPage, callback_query.from_user.id + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "subscenepage#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row(InlineButton("👇 Get Subtitle List", "Hmmm")) + keyboard.row(*btn1) + if btn2: + keyboard.row(*btn2) + keyboard.row(InlineButton("❌ Close", f"close#{callback_query.from_user.id}")) + await callback_query.message.edit_msg( + subres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Callback list title +@app.on_callback_query(filters.create(lambda _, __, query: "sublist#" in query.data)) +@ratelimiter +async def subdlpage_callback(_, callback_query: CallbackQuery): + if callback_query.from_user.id != int(callback_query.data.split("#")[4]): + return await callback_query.answer("Not yours..", True) + idlink = int(callback_query.data.split("#")[2]) + message_id = int(callback_query.data.split("#")[3]) + CurrentPage = int(callback_query.data.split("#")[1]) + try: + link = SUB_TITLE_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("link") + except KeyError: + await callback_query.answer("Invalid callback data, please send CMD again..") + await asyncio.sleep(3) + return await callback_query.message.delete_msg() + + try: + subres, PageLen, btn1, btn2 = await getListSub( + callback_query.message, link, CurrentPage, callback_query.from_user.id + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "sublist#{number}" + f"#{idlink}#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row(InlineButton("👇 Download Subtitle", "Hmmm")) + keyboard.row(*btn1) + if btn2: + keyboard.row(*btn2) + keyboard.row(InlineButton("❌ Close", f"close#{callback_query.from_user.id}")) + await callback_query.message.edit_msg( + subres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Callback dl subtitle +@app.on_callback_query( + filters.create(lambda _, __, query: "extractsubs#" in query.data) +) +@ratelimiter +async def dlsub_callback(_, callback_query: CallbackQuery): + if callback_query.from_user.id != int(callback_query.data.split("#")[4]): + return await callback_query.answer("Not yours..", True) + idlink = int(callback_query.data.split("#")[2]) + message_id = int(callback_query.data.split("#")[3]) + CurrentPage = int(callback_query.data.split("#")[1]) + try: + link = SUB_DL_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("link") + title = SUB_DL_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("title") + except KeyError: + await callback_query.answer("Invalid callback data, please send CMD again..") + await asyncio.sleep(3) + return await callback_query.message.delete_msg() + scraper = cloudscraper.create_scraper() + res = await down_page(link) + dl = scraper.get(res.get("download_url")) + with open(f"{title}.zip", mode="wb") as f: + f.write(dl.content) + await callback_query.message.reply_document( + f"{title}.zip", + caption=f"Title: {res.get('title')}\nIMDb: {res['imdb']}\nAuthor: {res['author_name']}\nRelease Info: ", + ) + os.remove(f"{title}.zip") diff --git a/misskaty/plugins/tes_session.py b/misskaty/plugins/tes_session.py new file mode 100644 index 00000000..de1b9760 --- /dev/null +++ b/misskaty/plugins/tes_session.py @@ -0,0 +1,15 @@ +# This plugin to learn session using pyrogram +from pyrogram.types import Message + +from misskaty import app + + +@app.on_cmd("session") +async def session(_, ctx: Message): + nama = await ctx.chat.ask("Ketik nama kamu:") + umur = await ctx.chat.ask("Ketik umur kamu") + alamat = await ctx.chat.ask("Ketik alamat kamu:") + await app.send_msg( + ctx.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..6c169d86 --- /dev/null +++ b/misskaty/plugins/ubot_plugin.py @@ -0,0 +1,189 @@ +""" + * @author yasir + * @date 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +# Code in this plugin to learn basic userbot in pyrogram +import os +from datetime import datetime + +from pyrogram import enums, filters +from pyrogram.raw import functions +from pyrogram.types import ( + ChatEventFilter, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, +) + +from misskaty import app, user + +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(_, 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(_, 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(_, 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(_, 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(self, message: Message): + members = [] + async for m in self.iter_chat_members(message.chat.id): + members.append( + ( + m.user.first_name, + m.joined_date or (await self.get_messages(message.chat.id, 1)).date, + ) + ) + members.sort(key=lambda member: member[1]) + + with open("joined_date.txt", "w", encoding="utf8") as fj: + fj.write("Join Date First Name\n") + for member in members: + fj.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(_, 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(_, 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(_, 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/urban_dict.py b/misskaty/plugins/urban_dict.py new file mode 100644 index 00000000..5442871e --- /dev/null +++ b/misskaty/plugins/urban_dict.py @@ -0,0 +1,78 @@ +from pykeyboard import InlineKeyboard +from pyrogram.types import CallbackQuery, Message + +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.http import http + + +async def getData(chat_id, message_id, GetWord, CurrentPage): + UDJson = ( + await http.get(f"https://api.urbandictionary.com/v0/define?term={GetWord}") + ).json() + + if "list" not in UDJson: + return await app.send_msg( + chat_id=chat_id, + reply_to_message_id=message_id, + text=f"Word: {GetWord}\nResults: Sorry could not find any matching results!", + del_in=5, + ) + try: + index = int(CurrentPage - 1) + PageLen = len(UDJson["list"]) + UDReasult = ( + f"**Definition of {GetWord}**\n" + f"{UDJson['list'][index]['definition']}\n\n" + "**📌 Examples**\n" + f"__{UDJson['list'][index]['example']}__" + ) + UDFReasult = "".join(i for i in UDReasult if i not in "[]") + return (UDFReasult, PageLen) + + except (IndexError, KeyError): + await app.send_msg( + chat_id=chat_id, + reply_to_message_id=message_id, + text=f"Word: {GetWord}\nResults: Sorry could not find any matching results!", + del_in=5, + ) + + +@app.on_cmd("ud", no_channel=True) +@ratelimiter +async def urbanDictionary(_, ctx: Message): + message_id = ctx.id + chat_id = ctx.chat.id + GetWord = " ".join(ctx.command[1:]) + if not GetWord: + message = await ctx.chat.ask("Now give any word for query!") + GetWord = message.text + + CurrentPage = 1 + try: + UDReasult, PageLen = await getData(chat_id, message_id, GetWord, CurrentPage) + except: + return await ctx.reply_msg("😭 Failed getting info from urban dictionary.") + + keyboard = InlineKeyboard() + keyboard.paginate(PageLen, CurrentPage, "pagination_urban#{number}" + f"#{GetWord}") + await ctx.reply_msg(text=f"{UDReasult}", reply_markup=keyboard) + + +@app.on_cb("pagination_urban#") +@ratelimiter +async def ud_callback(_, callback_query: CallbackQuery): + message_id = callback_query.message.id + chat_id = callback_query.message.chat.id + CurrentPage = int(callback_query.data.split("#")[1]) + GetWord = callback_query.data.split("#")[2] + + try: + UDReasult, PageLen = await getData(chat_id, message_id, GetWord, CurrentPage) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate(PageLen, CurrentPage, "pagination_urban#{number}" + f"#{GetWord}") + await callback_query.message.edit_msg(text=UDReasult, reply_markup=keyboard) diff --git a/misskaty/plugins/web_scraper.py b/misskaty/plugins/web_scraper.py new file mode 100644 index 00000000..8684a285 --- /dev/null +++ b/misskaty/plugins/web_scraper.py @@ -0,0 +1,1429 @@ +""" + * @author yasir + * @created 2022-12-01 09:12:27 + * @projectName MissKatyPyro + * Copyright @YasirPedia All rights reserved +""" +import logging +import re +import traceback + +import cloudscraper +from bs4 import BeautifulSoup +from pykeyboard import InlineButton, InlineKeyboard +from pyrogram.errors import QueryIdInvalid +from pyrogram.types import Message + +from database import dbname +from misskaty import app +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper import Cache, http +from misskaty.helper.kuso_utils import Kusonime +from misskaty.helper.localization import use_chat_lang + +__MODULE__ = "WebScraper" +__HELP__ = """ +/melongmovie [query ] - Scrape website data from MelongMovie Web. +/lk21 [query ] - Scrape website data from LayarKaca21. +/pahe [query ] - Scrape website data from Pahe.li. +/terbit21 [query ] - Scrape website data from Terbit21. +/savefilm21 [query ] - Scrape website data from Savefilm21. +/movieku [query ] - Scrape website data from Movieku.cc +/kusonime [query ] - Scrape website data from Kusonime +/lendrive [query ] - Scrape website data from Lendrive +/gomov [query ] - Scrape website data from GoMov. +/samehadaku [query ] - Scrape website data from Samehadaku. +""" + +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" +} + +LOGGER = logging.getLogger(__name__) +SCRAP_DICT = Cache(filename="scraper_cache.db", path="cache", in_memory=False) +data_kuso = Cache(filename="kuso_cache.db", path="cache", in_memory=False) +webdb = dbname["web"] + +web = { + "yasirapi": "https://yasirapi.eu.org", + "pahe": "https://pahe.li", + "savefilm21": "https://savefilm21.skin", + "melongmovie": "https://melongmovie.site", + "terbit21": "https://terbit21.gold", + "lk21": "https://tv1.lk21official.pro", + "gomov": "https://gomov.bio", + "movieku": "https://107.152.37.223", + "kusonime": "https://kusonime.com", + "lendrive": "https://lendrive.web.id", + "samehadaku": "https://samehadaku.bio", + "oplovers": "https://oploverz.top", +} + + +def split_arr(arr, size: 5): + arrs = [] + while len(arr) > size: + pice = arr[:size] + arrs.append(pice) + arr = arr[size:] + arrs.append(arr) + return arrs + + +# Terbit21 GetData +async def getDataTerbit21(msg, kueri, CurrentPage, strings): + if not SCRAP_DICT.get(msg.id): + try: + terbitjson = ( + (await http.get(f"{web['yasirapi']}/terbit21?q={kueri}")).json() + if kueri + else (await http.get("https://yasirapi.eu.org/terbit21")).json() + ) + except: + await msg.edit_msg(strings("err_getapi")) + return None, None + if not terbitjson.get("result"): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + SCRAP_DICT.add( + msg.id, [split_arr(terbitjson["result"], 6), kueri], timeout=1800 + ) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + + if kueri: + TerbitRes = strings("header_with_query").format(web="Terbit21", kueri=kueri) + else: + TerbitRes = strings("header_no_query").format( + web="Terbit21", cmd="terbit21" + ) + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + TerbitRes += f"{index*6+c}. {i['judul']}\n{strings('cat_text')}: {i['kategori']}\n" + TerbitRes += ( + "\n" + if re.search(r"Complete|Ongoing", i["kategori"]) + else f"{strings('dl_text')}\n\n" + ) + TerbitRes = "".join(i for i in TerbitRes if i not in "[]") + return TerbitRes, PageLen + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + + +# LK21 GetData +async def getDatalk21(msg, kueri, CurrentPage, strings): + if not SCRAP_DICT.get(msg.id): + try: + lk21json = ( + (await http.get(f"{web['yasirapi']}/lk21?q={kueri}")).json() + if kueri + else (await http.get("https://yasirapi.eu.org/lk21")).json() + ) + except: + await msg.edit_msg(strings("err_getapi")) + return None, None + if not lk21json.get("result"): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + SCRAP_DICT.add(msg.id, [split_arr(lk21json["result"], 6), kueri], timeout=1800) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + + if kueri: + lkResult = strings("header_with_query").format( + web="Layarkaca21", kueri=kueri + ) + else: + lkResult = strings("header_no_query").format(web="Layarkaca21", cmd="lk21") + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + lkResult += f"{index*6+c}. {i['judul']}\n{strings('cat_text')}: {i['kategori']}\n" + lkResult += ( + "\n" + if re.search(r"Complete|Ongoing", i["kategori"]) + else f"{strings('dl_text')}\n\n" + ) + lkResult = "".join(i for i in lkResult if i not in "[]") + return lkResult, PageLen + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + + +# Pahe GetData +async def getDataPahe(msg, kueri, CurrentPage, strings): + if not SCRAP_DICT.get(msg.id): + try: + pahejson = (await http.get(f"{web['yasirapi']}/pahe?q={kueri}")).json() + except: + await msg.edit_msg(strings("err_getapi")) + return None, None + if not pahejson.get("result"): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + SCRAP_DICT.add(msg.id, [split_arr(pahejson["result"], 6), kueri], timeout=1800) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + + paheResult = ( + strings("header_with_query").format(web="Pahe", kueri=kueri) + if kueri + else strings("header_no_query").format(web="Pahe", cmd="pahe") + ) + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + paheResult += ( + f"{index*6+c}. {i['judul']}\n\n" + ) + paheResult = "".join(i for i in paheResult if i not in "[]") + return paheResult, PageLen + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + + +# Kusonime GetData +async def getDataKuso(msg, kueri, CurrentPage, user, strings): + if not SCRAP_DICT.get(msg.id): + kusodata = [] + try: + data = await http.get( + f"{web['kusonime']}/?s={kueri}", headers=headers, follow_redirects=True + ) + except Exception as err: + await msg.edit_msg(strings("err_getweb").format(err=err)) + return None, None + res = BeautifulSoup(data, "lxml").find_all("h2", {"class": "episodeye"}) + for i in res: + ress = i.find_all("a")[0] + title = ress.text + link = ress["href"] + kusodata.append({"title": title, "link": link}) + if not kusodata: + await msg.edit_msg(strings("no_result"), del_in=5) + return None, 0, None, None + SCRAP_DICT.add(msg.id, [split_arr(kusodata, 10), kueri], timeout=1800) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + extractbtn1 = [] + extractbtn2 = [] + + kusoResult = ( + strings("header_no_query").format(web="Kusonime", cmd="kusonime") + if kueri == "" + else strings("header_with_query").format(web="Kusonime", kueri=kueri) + ) + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + kusoResult += f"{index*6+c}. {i['title']}\n{i['link']}\n\n" + if c < 6: + extractbtn1.append( + InlineButton( + index * 6 + c, f"kusoextract#{CurrentPage}#{c}#{user}#{msg.id}" + ) + ) + else: + extractbtn2.append( + InlineButton( + index * 6 + c, f"kusoextract#{CurrentPage}#{c}#{user}#{msg.id}" + ) + ) + kusoResult = "".join(i for i in kusoResult if i not in "[]") + return kusoResult, PageLen, extractbtn1, extractbtn2 + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, 0, None, None + + +# Movieku GetData +async def getDataMovieku(msg, kueri, CurrentPage, strings): + if not SCRAP_DICT.get(msg.id): + moviekudata = [] + try: + data = await http.get( + f"{web['movieku']}/?s={kueri}", headers=headers, follow_redirects=True + ) + except Exception as err: + await msg.edit_msg(strings("err_getweb").format(err=err)) + return None, None + r = BeautifulSoup(data, "lxml") + res = r.find_all(class_="bx") + for i in res: + judul = i.find_all("a")[0]["title"] + link = i.find_all("a")[0]["href"] + typ = i.find(class_="overlay").text + typee = typ.strip() if typ.strip() != "" else "~" + moviekudata.append({"judul": judul, "link": link, "type": typee}) + if not moviekudata: + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + SCRAP_DICT.add(msg.id, [split_arr(moviekudata, 6), kueri], timeout=1800) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + + moviekuResult = ( + strings("header_no_query").format(web="Movieku", cmd="movieku") + if kueri == "" + else strings("header_with_query").format(web="Movieku", kueri=kueri) + ) + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + moviekuResult += f"{index*6+c}. {i['judul']}\n{strings('quality')}/Status: {i['type']}\nExtract: /movieku_scrap {i['link']}\n\n" + moviekuResult = "".join(i for i in moviekuResult if i not in "[]") + return moviekuResult, PageLen + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + + +# Savefilm21 GetData +async def getDataSavefilm21(msg, kueri, CurrentPage, user, strings): + if not SCRAP_DICT.get(msg.id): + sfdata = [] + try: + data = await http.get( + f"{web['savefilm21']}/?s={kueri}", + headers=headers, + follow_redirects=True, + ) + except Exception as err: + await msg.edit_msg(strings("err_getweb").format(err=err)) + return None, 0, None + text = BeautifulSoup(data, "lxml") + entry = text.find_all(class_="entry-header") + if "Tidak Ditemukan" in entry[0].text: + if not kueri: + await msg.edit_msg(strings("no_result"), del_in=5) + else: + await msg.edit_msg( + strings("no_result_w_query").format(kueri=kueri), del_in=5 + ) + return None, 0, None + for i in entry: + genre = i.find(class_="gmr-movie-on").text + genre = f"{genre}" if genre != "" else "N/A" + judul = i.find(class_="entry-title").find("a").text + link = i.find(class_="entry-title").find("a").get("href") + sfdata.append({"judul": judul, "link": link, "genre": genre}) + SCRAP_DICT.add(msg.id, [split_arr(sfdata, 6), kueri], timeout=1800) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + extractbtn = [] + sfResult = ( + strings("header_no_query").format(web="Savefilm21", cmd="savefilm21") + if kueri == "" + else strings("header_with_query").format(web="Savefilm21", kueri=kueri) + ) + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + sfResult += f"{index*6+c}. {i['judul']}\nGenre: {i['genre']}\n\n" + extractbtn.append( + InlineButton( + index * 6 + c, f"sf21extract#{CurrentPage}#{c}#{user}#{msg.id}" + ) + ) + sfResult = "".join(i for i in sfResult if i not in "[]") + return sfResult, PageLen, extractbtn + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, 0, None + + +# Lendrive GetData +async def getDataLendrive(msg, kueri, CurrentPage, user, strings): + if not SCRAP_DICT.get(msg.id): + try: + data = await http.get( + f"{web['lendrive']}/?s={kueri}", headers=headers, follow_redirects=True + ) + except Exception as err: + await msg.edit_msg(strings("err_getweb").format(err=err)) + return None, None + soup = BeautifulSoup(data, "lxml") + lenddata = [] + for o in soup.find_all(class_="bsx"): + title = o.find("a")["title"] + link = o.find("a")["href"] + status = o.find(class_="epx").text + kualitas = ( + o.find(class_="typez TV").text + if o.find(class_="typez TV") + else o.find(class_="typez BD") + ) + lenddata.append( + {"judul": title, "link": link, "quality": kualitas, "status": status} + ) + if not lenddata: + await msg.edit_msg(strings("no_result"), del_in=5) + return None, 0, None + SCRAP_DICT.add(msg.id, [split_arr(lenddata, 6), kueri], timeout=1800) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + extractbtn = [] + + lenddataResult = ( + strings("header_no_query").format(web="Lendrive", cmd="lendrive") + if kueri == "" + else strings("header_with_query").format(web="Lendrive", kueri=kueri) + ) + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + lenddataResult += f"{index*6+c}. {i['judul']}\n{strings('quality')}: {i['quality']}\nStatus: {i['status']}\n\n" + extractbtn.append( + InlineButton( + index * 6 + c, f"lendriveextract#{CurrentPage}#{c}#{user}#{msg.id}" + ) + ) + lenddataResult = "".join(i for i in lenddataResult if i not in "[]") + return lenddataResult, PageLen, extractbtn + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, 0, None + + +# MelongMovie GetData +async def getDataMelong(msg, kueri, CurrentPage, user, strings): + if not SCRAP_DICT.get(msg.id): + try: + data = await http.get( + f"{web['melongmovie']}/?s={kueri}", + headers=headers, + follow_redirects=True, + ) + except Exception as err: + await msg.edit_msg(strings("err_getweb").format(err=err)) + return None, 0, None + bs4 = BeautifulSoup(data, "lxml") + melongdata = [] + for res in bs4.select(".box"): + dd = res.select("a") + url = dd[0]["href"] + title = dd[0]["title"] + try: + quality = dd[0].find(class_="quality").text + except: + quality = "N/A" + melongdata.append({"judul": title, "link": url, "quality": quality}) + if not melongdata: + await msg.edit_msg(strings("no_result"), del_in=5) + return None, 0, None + SCRAP_DICT.add(msg.id, [split_arr(melongdata, 6), kueri], timeout=1800) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + extractbtn = [] + + melongResult = ( + strings("header_no_query").format(web="Melongmovie", cmd="melongmovie") + if kueri == "" + else strings("header_with_query").format(web="Melongmovie", kueri=kueri) + ) + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + melongResult += f"{index*6+c}. {i['judul']}\n{strings('quality')}: {i['quality']}\n\n" + extractbtn.append( + InlineButton( + index * 6 + c, f"melongextract#{CurrentPage}#{c}#{user}#{msg.id}" + ) + ) + melongResult = "".join(i for i in melongResult if i not in "[]") + return melongResult, PageLen, extractbtn + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, 0, None + + +# GoMov GetData +async def getDataGomov(msg, kueri, CurrentPage, user, strings): + if not SCRAP_DICT.get(msg.id): + try: + gomovv = await http.get( + f"{web['gomov']}/?s={kueri}", headers=headers, follow_redirects=True + ) + except Exception as err: + await msg.edit_msg(strings("err_getweb").format(err=err)) + return None, None + text = BeautifulSoup(gomovv, "lxml") + entry = text.find_all(class_="entry-header") + if entry[0].text.strip() == "Nothing Found": + if not kueri: + await msg.edit_msg(strings("no_result"), del_in=5) + else: + await msg.edit_msg( + strings("no_result_w_query").format(kueri=kueri), del_in=5 + ) + return None, 0, None + data = [] + for i in entry: + genre = i.find(class_="gmr-movie-on").text + genre = f"{genre}" if genre != "" else "N/A" + judul = i.find(class_="entry-title").find("a").text + link = i.find(class_="entry-title").find("a").get("href") + data.append({"judul": judul, "link": link, "genre": genre}) + SCRAP_DICT.add(msg.id, [split_arr(data, 6), kueri], timeout=1800) + try: + index = int(CurrentPage - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + extractbtn = [] + + gomovResult = ( + strings("header_with_query").format(web="GoMov", kueri=kueri) + if kueri + else strings("header_no_query").format(web="GoMov", cmd="gomov") + ) + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1): + gomovResult += f"{index*6+c}. {i['judul']}\nGenre: {i['genre']}\n\n" + if not re.search(r"Series", i["genre"]): + extractbtn.append( + InlineButton( + index * 6 + c, f"gomovextract#{CurrentPage}#{c}#{user}#{msg.id}" + ) + ) + gomovResult += strings("unsupport_dl_btn") + gomovResult = "".join(i for i in gomovResult if i not in "[]") + return gomovResult, PageLen, extractbtn + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, 0, None + + +# getData samehada +async def getSame(msg, query, current_page, strings): + if not SCRAP_DICT.get(msg.id): + cfse = cloudscraper.create_scraper() + try: + if query: + data = cfse.get(f"{web['samehadaku']}/?s={query}", headers=headers) + else: + data = cfse.get(web["samehadaku"], headers=headers) + except Exception as err: + await msg.edit_msg(strings("err_getweb").format(err=err)) + return None, None + res = BeautifulSoup(data.text, "lxml").find_all(class_="animposx") + sdata = [] + for i in res: + url = i.find("a")["href"] + title = i.find("a")["title"] + sta = ( + i.find(class_="type TV").text if i.find(class_="type TV") else "Ongoing" + ) + rate = i.find(class_="score") + sdata.append({"url": url, "title": title, "sta": sta, "rate": rate}) + if not sdata: + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + SCRAP_DICT.add(msg.id, [split_arr(sdata, 10), query], timeout=1800) + try: + index = int(current_page - 1) + PageLen = len(SCRAP_DICT[msg.id][0]) + sameresult = "".join( + f"{index * 6 + c}. {i['title']}\nStatus: {i['sta']}\nRating:
{i['rate']}\n\n" + for c, i in enumerate(SCRAP_DICT[msg.id][0][index], start=1) + ) + sameresult = "".join(i for i in sameresult if i not in "[]") + return sameresult, PageLen + except (IndexError, KeyError): + await msg.edit_msg(strings("no_result"), del_in=5) + return None, None + + +# SameHada CMD +@app.on_cmd("samehadaku", no_channel=True) +@ratelimiter +@use_chat_lang() +async def same_search(_, msg, strings): + query = msg.text.split(" ", 1)[1] if len(msg.command) > 1 else None + bmsg = await msg.reply_msg(strings("get_data"), quote=True) + sameres, PageLen = await getSame(bmsg, query, 1, strings) + if not sameres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, 1, "page_same#{number}" + f"#{bmsg.id}#{msg.from_user.id}" + ) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{msg.from_user.id}")) + await bmsg.edit_msg(sameres, disable_web_page_preview=True, reply_markup=keyboard) + + +# Terbit21 CMD +@app.on_cmd("terbit21", no_channel=True) +@ratelimiter +@use_chat_lang() +async def terbit21_s(_, message, strings): + kueri = " ".join(message.command[1:]) + if not kueri: + kueri = None + pesan = await message.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + terbitres, PageLen = await getDataTerbit21(pesan, kueri, CurrentPage, strings) + if not terbitres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_terbit21#{number}" + f"#{pesan.id}#{message.from_user.id}", + ) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{message.from_user.id}")) + await pesan.edit_msg( + terbitres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# LK21 CMD +@app.on_cmd("lk21", no_channel=True) +@ratelimiter +@use_chat_lang() +async def lk21_s(_, message, strings): + kueri = " ".join(message.command[1:]) + if not kueri: + kueri = None + pesan = await message.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + lkres, PageLen = await getDatalk21(pesan, kueri, CurrentPage, strings) + if not lkres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_lk21#{number}" + f"#{pesan.id}#{message.from_user.id}", + ) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{message.from_user.id}")) + await pesan.edit_msg(lkres, disable_web_page_preview=True, reply_markup=keyboard) + + +# Pahe CMD +@app.on_cmd("pahe", no_channel=True) +@ratelimiter +@use_chat_lang() +async def pahe_s(_, message, strings): + kueri = " ".join(message.command[1:]) + if not kueri: + kueri = "" + pesan = await message.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + paheres, PageLen = await getDataPahe(pesan, kueri, CurrentPage, strings) + if not paheres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_pahe#{number}" + f"#{pesan.id}#{message.from_user.id}", + ) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{message.from_user.id}")) + await pesan.edit_msg(paheres, disable_web_page_preview=True, reply_markup=keyboard) + + +# Gomov CMD +@app.on_cmd("gomov", no_channel=True) +@ratelimiter +@use_chat_lang() +async def gomov_s(_, message, strings): + kueri = " ".join(message.command[1:]) + if not kueri: + kueri = "" + pesan = await message.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + gomovres, PageLen, btn = await getDataGomov( + pesan, kueri, CurrentPage, message.from_user.id, strings + ) + if not gomovres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_gomov#{number}" + f"#{pesan.id}#{message.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=message.from_user.id)) + keyboard.row(*btn) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{message.from_user.id}")) + await pesan.edit_msg(gomovres, disable_web_page_preview=True, reply_markup=keyboard) + + +# MelongMovie CMD +@app.on_cmd("melongmovie", no_channel=True) +@ratelimiter +@use_chat_lang() +async def melong_s(_, message, strings): + kueri = " ".join(message.command[1:]) + if not kueri: + kueri = "" + pesan = await message.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + melongres, PageLen, btn = await getDataMelong( + pesan, kueri, CurrentPage, message.from_user.id, strings + ) + if not melongres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_melong#{number}" + f"#{pesan.id}#{message.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=message.from_user.id)) + keyboard.row(*btn) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{message.from_user.id}")) + try: + await pesan.edit_msg( + melongres, disable_web_page_preview=True, reply_markup=keyboard + ) + except Exception as err: + await pesan.edit_msg( + f"ERROR: {err}", disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Savefilm21 CMD +@app.on_cmd("savefilm21", no_channel=True) +@ratelimiter +@use_chat_lang() +async def savefilm_s(_, message, strings): + kueri = " ".join(message.command[1:]) + if not kueri: + kueri = "" + pesan = await message.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + savefilmres, PageLen, btn = await getDataSavefilm21( + pesan, kueri, CurrentPage, message.from_user.id, strings + ) + if not savefilmres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_sf21#{number}" + f"#{pesan.id}#{message.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=message.from_user.id)) + keyboard.row(*btn) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{message.from_user.id}")) + await pesan.edit_msg( + savefilmres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Kusonime CMD +@app.on_cmd("kusonime", no_channel=True) +@ratelimiter +@use_chat_lang() +async def kusonime_s(_, message, strings): + kueri = " ".join(message.command[1:]) + if not kueri: + kueri = "" + pesan = await message.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + kusores, PageLen, btn1, btn2 = await getDataKuso( + pesan, kueri, CurrentPage, message.from_user.id, strings + ) + if not kusores: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_kuso#{number}" + f"#{pesan.id}#{message.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=message.from_user.id)) + keyboard.row(*btn1) + if btn2: + keyboard.row(*btn2) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{message.from_user.id}")) + await pesan.edit_msg(kusores, disable_web_page_preview=True, reply_markup=keyboard) + + +# Lendrive CMD +@app.on_cmd("lendrive", no_channel=True) +@ratelimiter +@use_chat_lang() +async def lendrive_s(_, ctx: Message, strings): + kueri = ctx.input + if not kueri: + kueri = "" + pesan = await ctx.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + lendres, PageLen, btn = await getDataLendrive( + pesan, kueri, CurrentPage, ctx.from_user.id, strings + ) + if not lendres: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_lendrive#{number}" + f"#{pesan.id}#{ctx.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=ctx.from_user.id)) + keyboard.row(*btn) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{ctx.from_user.id}")) + await pesan.edit_msg(lendres, disable_web_page_preview=True, reply_markup=keyboard) + + +# Movieku CMD +@app.on_cmd("movieku", no_channel=True) +@ratelimiter +@use_chat_lang() +async def movieku_s(_, ctx: Message, strings): + kueri = ctx.input + if not kueri: + kueri = "" + pesan = await ctx.reply_msg(strings("get_data"), quote=True) + CurrentPage = 1 + moviekures, PageLen = await getDataMovieku(pesan, kueri, CurrentPage, strings) + if not moviekures: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_movieku#{number}" + f"#{pesan.id}#{ctx.from_user.id}", + ) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{ctx.from_user.id}")) + await pesan.edit_msg( + moviekures, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Savefillm21 Page Callback +@app.on_cb("page_sf21#") +@ratelimiter +@use_chat_lang() +async def sf21page_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except (IndexError, ValueError): # Gatau napa err ini + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + except QueryIdInvalid: + return + + try: + savefilmres, PageLen, btn = await getDataSavefilm21( + callback_query.message, + kueri, + CurrentPage, + callback_query.from_user.id, + strings, + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_sf21#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=callback_query.from_user.id)) + keyboard.row(*btn) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + savefilmres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Kuso Page Callback +@app.on_cb("page_kuso#") +@ratelimiter +@use_chat_lang() +async def kusopage_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + try: + kusores, PageLen, btn1, btn2 = await getDataKuso( + callback_query.message, + kueri, + CurrentPage, + callback_query.from_user.id, + strings, + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_kuso#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=callback_query.from_user.id)) + keyboard.row(*btn1) + if btn2: + keyboard.row(*btn2) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + kusores, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Lendrive Page Callback +@app.on_cb("page_lendrive#") +@ratelimiter +@use_chat_lang() +async def lendrivepage_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + try: + lendres, PageLen, btn = await getDataLendrive( + callback_query.message, + kueri, + CurrentPage, + callback_query.from_user.id, + strings, + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_lendrive#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=callback_query.from_user.id)) + keyboard.row(*btn) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + lendres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Movieku Page Callback +@app.on_cb("page_movieku#") +@ratelimiter +@use_chat_lang() +async def moviekupage_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb"), True) + + try: + moviekures, PageLen = await getDataMovieku( + callback_query.message, kueri, CurrentPage, strings + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_movieku#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + moviekures, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Samehada Page Callback +@app.on_cb("page_same#") +@ratelimiter +@use_chat_lang() +async def samepg(_, query, strings): + try: + _, current_page, _id, user_id = query.data.split("#") + if int(user_id) != query.from_user.id: + return await query.answer(strings("unauth"), True) + lquery = SCRAP_DICT[int(_id)][1] + except QueryIdInvalid: + return + except KeyError: + return await query.message.edit_msg(strings("invalid_cb")) + try: + sameres, PageLen = await getSame( + query.message, lquery, int(current_page), strings + ) + except TypeError: + return + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + int(current_page), + "page_same#{number}" + f"#{_id}#{query.from_user.id}", + ) + keyboard.row(InlineButton(strings("cl_btn"), f"close#{query.from_user.id}")) + await query.message.edit_msg( + sameres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Terbit21 Page Callback +@app.on_cb("page_terbit21#") +@ratelimiter +@use_chat_lang() +async def terbit21page_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + try: + terbitres, PageLen = await getDataTerbit21( + callback_query.message, kueri, CurrentPage, strings + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_terbit21#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + terbitres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Page Callback Melong +@app.on_cb("page_melong#") +@ratelimiter +@use_chat_lang() +async def melongpage_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + try: + terbitres, PageLen, btn = await getDataMelong( + callback_query.message, + kueri, + CurrentPage, + callback_query.from_user.id, + strings, + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_melong#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=callback_query.from_user.id)) + keyboard.row(*btn) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + terbitres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Lk21 Page Callback +@app.on_cb("page_lk21#") +@ratelimiter +@use_chat_lang() +async def lk21page_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + try: + lkres, PageLen = await getDatalk21( + callback_query.message, kueri, CurrentPage, strings + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_lk21#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + lkres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Pahe Page Callback +@app.on_cb("page_pahe#") +@ratelimiter +@use_chat_lang() +async def pahepage_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + try: + lkres, PageLen = await getDataPahe( + callback_query.message, kueri, CurrentPage, strings + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_pahe#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + lkres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +# Gomov Page Callback +@app.on_cb("page_gomov#") +@ratelimiter +@use_chat_lang() +async def gomovpage_callback(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + message_id = int(callback_query.data.split("#")[2]) + CurrentPage = int(callback_query.data.split("#")[1]) + kueri = SCRAP_DICT[message_id][1] + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + try: + gomovres, PageLen, btn = await getDataGomov( + callback_query.message, + kueri, + CurrentPage, + callback_query.from_user.id, + strings, + ) + except TypeError: + return + + keyboard = InlineKeyboard() + keyboard.paginate( + PageLen, + CurrentPage, + "page_gomov#{number}" + f"#{message_id}#{callback_query.from_user.id}", + ) + keyboard.row(InlineButton(strings("ex_data"), user_id=callback_query.from_user.id)) + keyboard.row(*btn) + keyboard.row( + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}") + ) + await callback_query.message.edit_msg( + gomovres, disable_web_page_preview=True, reply_markup=keyboard + ) + + +### Scrape DDL Link From Web ### +# Kusonime DDL +@app.on_cb("kusoextract#") +@ratelimiter +@use_chat_lang() +async def kusonime_scrap(client, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + idlink = int(callback_query.data.split("#")[2]) + message_id = int(callback_query.data.split("#")[4]) + CurrentPage = int(callback_query.data.split("#")[1]) + link = SCRAP_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("link") + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + kuso = Kusonime() + keyboard = InlineKeyboard() + keyboard.row( + InlineButton( + strings("back_btn"), + f"page_kuso#{CurrentPage}#{message_id}#{callback_query.from_user.id}", + ), + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}"), + ) + try: + init_url = data_kuso.get(link, None) + if init_url is not None: + ph = init_url.get("ph_url") + await callback_query.message.edit_msg( + strings("res_scrape").format(link=link, kl=ph), + reply_markup=keyboard, + disable_web_page_preview=False, + ) + tgh = await kuso.telegraph(link, client.me.username) + if tgh["error"]: + return await callback_query.message.edit_msg( + f"ERROR: {tgh['error_message']}", reply_markup=keyboard + ) + except Exception: + err = traceback.format_exc() + return await callback_query.message.edit_msg( + f"ERROR: {err}", reply_markup=keyboard + ) + data_kuso[link] = {"ph_url": tgh["url"]} + await callback_query.message.edit_msg( + strings("res_scrape").format(link=link, kl=tgh["url"]), + reply_markup=keyboard, + disable_web_page_preview=False, + ) + + +# Savefilm21 DDL +@app.on_cb("sf21extract#") +@ratelimiter +@use_chat_lang() +async def savefilm21_scrap(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + idlink = int(callback_query.data.split("#")[2]) + message_id = int(callback_query.data.split("#")[4]) + CurrentPage = int(callback_query.data.split("#")[1]) + link = SCRAP_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("link") + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + keyboard = InlineKeyboard() + keyboard.row( + InlineButton( + strings("back_btn"), + f"page_sf21#{CurrentPage}#{message_id}#{callback_query.from_user.id}", + ), + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}"), + ) + try: + html = await http.get(link, headers=headers) + 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 callback_query.message.edit_msg( + strings("res_scrape").format(link=link, kl=res), reply_markup=keyboard + ) + except Exception as err: + await callback_query.message.edit_msg(f"ERROR: {err}", reply_markup=keyboard) + + +# Scrape Link Download Movieku.CC +@app.on_cmd("movieku_scrap#") +@ratelimiter +@use_chat_lang() +async def muviku_scrap(_, message, strings): + try: + link = message.text.split(" ", maxsplit=1)[1] + 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(strings("no_result")) + 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( + strings("invalid_cmd_scrape").format(cmd=message.command[0]) + ) + except Exception as e: + await message.reply(f"ERROR: {str(e)}") + + +# Scrape DDL Link Melongmovie +@app.on_cb("melongextract#") +@ratelimiter +@use_chat_lang() +async def melong_scrap(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + idlink = int(callback_query.data.split("#")[2]) + message_id = int(callback_query.data.split("#")[4]) + CurrentPage = int(callback_query.data.split("#")[1]) + link = SCRAP_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("link") + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + keyboard = InlineKeyboard() + keyboard.row( + InlineButton( + strings("back_btn"), + f"page_melong#{CurrentPage}#{message_id}#{callback_query.from_user.id}", + ), + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}"), + ) + try: + html = await http.get(link, headers=headers) + soup = BeautifulSoup(html.text, "lxml") + rep = "" + 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 callback_query.message.edit_msg( + strings("res_scrape").format(link=link, kl=rep), reply_markup=keyboard + ) + except Exception as err: + await callback_query.message.edit_msg(f"ERROR: {err}", reply_markup=keyboard) + + +# Scrape DDL Link Gomov +@app.on_cb("gomovextract#") +@ratelimiter +@use_chat_lang() +async def gomov_dl(_, callback_query, strings): + try: + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + idlink = int(callback_query.data.split("#")[2]) + message_id = int(callback_query.data.split("#")[4]) + CurrentPage = int(callback_query.data.split("#")[1]) + link = SCRAP_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("link") + except QueryIdInvalid: + return + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + keyboard = InlineKeyboard() + keyboard.row( + InlineButton( + strings("back_btn"), + f"page_gomov#{CurrentPage}#{message_id}#{callback_query.from_user.id}", + ), + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}"), + ) + try: + html = await http.get(link, headers=headers) + soup = BeautifulSoup(html.text, "lxml") + entry = soup.find(class_="gmr-download-wrap clearfix") + hasil = soup.find(class_="title-download").text + for i in entry.find(class_="list-inline gmr-download-list clearfix"): + title = i.find("a").text + ddl = i.find("a")["href"] + hasil += f"\n{title}\n{ddl}\n" + await callback_query.message.edit_msg( + strings("res_scrape").format(link=link, kl=hasil), reply_markup=keyboard + ) + except Exception as err: + await callback_query.message.edit_msg(f"ERROR: {err}", reply_markup=keyboard) + + +@app.on_cb("lendriveextract#") +@ratelimiter +@use_chat_lang() +async def lendrive_dl(_, callback_query, strings): + if callback_query.from_user.id != int(callback_query.data.split("#")[3]): + return await callback_query.answer(strings("unauth"), True) + idlink = int(callback_query.data.split("#")[2]) + message_id = int(callback_query.data.split("#")[4]) + CurrentPage = int(callback_query.data.split("#")[1]) + try: + link = SCRAP_DICT[message_id][0][CurrentPage - 1][idlink - 1].get("link") + except KeyError: + return await callback_query.message.edit_msg(strings("invalid_cb")) + + keyboard = InlineKeyboard() + keyboard.row( + InlineButton( + strings("back_btn"), + f"page_lendrive#{CurrentPage}#{message_id}#{callback_query.from_user.id}", + ), + InlineButton(strings("cl_btn"), f"close#{callback_query.from_user.id}"), + ) + try: + hmm = await http.get(link, headers=headers) + q = BeautifulSoup(hmm.text, "lxml") + j = q.findAll("div", class_="soraurlx") + kl = "" + for i in j: + if not i.find("a"): + continue + kl += f"{i.find('strong')}:\n" + kl += "".join( + f"[ {a.text} ]\n" for a in i.findAll("a") + ) + await callback_query.message.edit_msg( + strings("res_scrape").format(link=link, kl=kl), reply_markup=keyboard + ) + except Exception as err: + await callback_query.message.edit_msg(f"ERROR: {err}", reply_markup=keyboard) diff --git a/misskaty/plugins/webss.py b/misskaty/plugins/webss.py new file mode 100644 index 00000000..7c05eb9a --- /dev/null +++ b/misskaty/plugins/webss.py @@ -0,0 +1,50 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import os +from asyncio import gather + +from pyrogram.types import Message +from pySmartDL import SmartDL + +from misskaty import app +from misskaty.core.decorator import new_task +from misskaty.core.decorator.ratelimiter import ratelimiter +from misskaty.helper.localization import use_chat_lang + +__MODULE__ = "WebSS" +__HELP__ = """ +/webss [URL] - Take A Screenshot Of A Webpage. +""" + + +@app.on_cmd("webss") +@ratelimiter +@new_task +@use_chat_lang() +async def take_ss(_, ctx: Message, strings): + if len(ctx.command) == 1: + return await ctx.reply_msg(strings("no_url"), del_in=6) + url = ( + ctx.command[1] + if ctx.command[1].startswith("http") + else f"https://{ctx.command[1]}" + ) + download_file_path = os.path.join("downloads/", f"webSS_{ctx.from_user.id}.png") + msg = await ctx.reply_msg(strings("wait_str")) + try: + url = f"https://webss.yasirapi.eu.org/api?url={url}&width=1280&height=720" + downloader = SmartDL(url, download_file_path, progress_bar=False, timeout=10) + downloader.start(blocking=True) + await gather( + *[ + ctx.reply_document(download_file_path), + ctx.reply_photo(download_file_path, caption=strings("str_credit")), + ] + ) + await msg.delete_msg() + if os.path.exists(download_file_path): + os.remove(download_file_path) + except Exception as e: + await msg.edit_msg(strings("ss_failed_str").format(err=str(e))) diff --git a/misskaty/plugins/ytdl_plugins.py b/misskaty/plugins/ytdl_plugins.py new file mode 100644 index 00000000..d2970f55 --- /dev/null +++ b/misskaty/plugins/ytdl_plugins.py @@ -0,0 +1,323 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +from logging import getLogger +from uuid import uuid4 + +from iytdl import Process, iYTDL, main +from iytdl.constants import YT_VID_URL +from iytdl.exceptions import DownloadFailedError +from pyrogram import Client, filters +from pyrogram.enums import ParseMode +from pyrogram.errors import ( + MessageEmpty, + MessageIdInvalid, + QueryIdInvalid, + WebpageMediaEmpty, +) +from pyrogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + InputMediaPhoto, + Message, +) + +from misskaty import app +from misskaty.core import pyro_cooldown +from misskaty.core.decorator import capture_err, ratelimiter +from misskaty.helper.http import http +from misskaty.helper.localization import use_chat_lang +from misskaty.vars import COMMAND_HANDLER, LOG_CHANNEL, SUDO + +LOGGER = getLogger(__name__) +YT_REGEX = r"^(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/(watch\?v=|embed/|v/|.+\?v=)?(?P[A-Za-z0-9\-=_]{11})" +YT_DB = {} + + +def rand_key(): + return str(uuid4())[:8] + + +@app.on_cmd("ytsearch", no_channel=True) +@ratelimiter +@use_chat_lang() +async def ytsearch(_, ctx: Message, strings): + if len(ctx.command) == 1: + return await ctx.reply_msg(strings("no_query")) + query = ctx.text.split(" ", maxsplit=1)[1] + search_key = rand_key() + YT_DB[search_key] = query + search = await main.VideosSearch(query).next() + if search["result"] == []: + return await ctx.reply_msg(strings("no_res").format(kweri=query)) + i = search["result"][0] + out = f"{i['title']}\n" + out = strings("yts_msg").format( + pub=i["publishedTime"], + dur=i["duration"], + vi=i["viewCount"]["short"], + clink=i["channel"]["link"], + cname=i["channel"]["name"], + ) + btn = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + f"1/{len(search['result'])}", + callback_data=f"ytdl_scroll|{search_key}|1", + ) + ], + [ + InlineKeyboardButton( + strings("dl_btn"), callback_data=f"yt_gen|{i['id']}" + ) + ], + ] + ) + img = await get_ytthumb(i["id"]) + caption = out + markup = btn + await ctx.reply_photo( + img, caption=caption, reply_markup=markup, parse_mode=ParseMode.HTML, quote=True + ) + + +@app.on_message( + filters.command(["ytdown"], COMMAND_HANDLER) + | filters.regex(YT_REGEX) + & ~filters.channel + & ~filters.via_bot + & pyro_cooldown.wait(60) +) +@capture_err +@ratelimiter +@use_chat_lang() +async def ytdownv2(_, ctx: Message, strings): + if not ctx.from_user: + return await ctx.reply_msg(strings("no_channel")) + if ctx.command and len(ctx.command) == 1: + return await ctx.reply_msg(strings("invalid_link")) + url = ctx.input if ctx.command and len(ctx.command) > 1 else ctx.text + async with iYTDL(log_group_id=0, cache_path="cache", silent=True) as ytdl: + try: + x = await ytdl.parse(url, extract=True) + if x is None: + return await ctx.reply_msg( + strings("err_parse"), parse_mode=ParseMode.HTML + ) + caption = x.caption + markup = x.buttons + photo = x.image_url + try: + await ctx.reply_photo( + photo, + caption=caption, + reply_markup=markup, + parse_mode=ParseMode.HTML, + quote=True, + ) + except WebpageMediaEmpty: + await ctx.reply_photo( + "assets/thumb.jpg", + caption=caption, + reply_markup=markup, + parse_mode=ParseMode.HTML, + quote=True, + ) + except Exception as err: + try: + await ctx.reply_msg(str(err), parse_mode=ParseMode.HTML) + except MessageEmpty: + await ctx.reply("Invalid link.") + + +@app.on_cb(filters.regex(r"^yt_listall")) +@ratelimiter +@use_chat_lang() +async def ytdl_listall_callback(_, cq: CallbackQuery, strings): + if cq.from_user.id != cq.message.reply_to_message.from_user.id: + return await cq.answer(strings("unauth"), True) + callback = cq.data.split("|") + async with iYTDL( + log_group_id=0, cache_path="cache", ffmpeg_location="/usr/bin/ffmpeg" + ) as ytdl: + media, buttons = await ytdl.listview(callback[1]) + await cq.edit_message_media( + media=media, reply_markup=buttons.add(cq.from_user.id) + ) + + +@app.on_callback_query(filters.regex(r"^yt_extract_info")) +@ratelimiter +@use_chat_lang() +async def ytdl_extractinfo_callback(_, cq: CallbackQuery, strings): + if cq.from_user.id != cq.message.reply_to_message.from_user.id: + try: + return await cq.answer(strings("unauth"), True) + except QueryIdInvalid: + return + try: + await cq.answer(strings("wait")) + except QueryIdInvalid: + pass + callback = cq.data.split("|") + async with iYTDL( + log_group_id=0, cache_path="cache", ffmpeg_location="/usr/bin/ffmpeg" + ) as ytdl: + try: + if data := await ytdl.extract_info_from_key(callback[1]): + if len(callback[1]) == 11: + await cq.edit_message_text( + text=data.caption, + reply_markup=data.buttons.add(cq.from_user.id), + ) + else: + await cq.edit_message_media( + media=( + InputMediaPhoto( + media=data.image_url, + caption=data.caption, + ) + ), + reply_markup=data.buttons.add(cq.from_user.id), + ) + except Exception as e: + await cq.edit_message_text(f"Extract Info Failed -> {e}") + + +@app.on_callback_query(filters.regex(r"^yt_(gen|dl)")) +@ratelimiter +@use_chat_lang() +async def ytdl_gendl_callback(self: Client, cq: CallbackQuery, strings): + if not (cq.message.reply_to_message and cq.message.reply_to_message.from_user): + return + match = cq.data.split("|") + if cq.from_user.id != cq.message.reply_to_message.from_user.id: + try: + return await cq.answer(strings("unauth"), True) + except QueryIdInvalid: + return + if match[2] in ["mkv", "mp4"] and cq.from_user.id not in SUDO: + try: + return await cq.answer(strings("vip-btn"), True) + except QueryIdInvalid: + return + async with iYTDL( + log_group_id=LOG_CHANNEL, + cache_path="cache", + ffmpeg_location="/usr/bin/ffmpeg", + delete_media=True, + ) as ytdl: + try: + if match[0] == "yt_gen": + yt_url = False + video_link = await ytdl.cache.get_url(match[1]) + else: + yt_url = True + video_link = f"{YT_VID_URL}{match[1]}" + + media_type = "video" if match[3] == "v" else "audio" + uid, _ = ytdl.get_choice_by_id(match[2], media_type, yt_url=yt_url) + key = await ytdl.download( + url=video_link, + uid=uid, + downtype=media_type, + update=cq, + ) + await ytdl.upload( + client=self, + key=key[0], + downtype=media_type, + update=cq, + ) + except DownloadFailedError as e: + await cq.edit_message_caption(f"Download Failed - {e}") + except Exception as err: + try: + await cq.edit_message_caption( + f"Download Failed for url -> {video_link}\n\nERROR: {err}" + ) + except MessageIdInvalid: + pass + + +@app.on_callback_query(filters.regex(r"^yt_cancel")) +@ratelimiter +@use_chat_lang() +async def ytdl_cancel_callback(_, cq: CallbackQuery, strings): + if cq.from_user.id != cq.message.reply_to_message.from_user.id: + return await cq.answer(strings("unauth"), True) + callback = cq.data.split("|") + try: + await cq.answer("Trying to Cancel Process..") + except QueryIdInvalid: + pass + process_id = callback[1] + try: + Process.cancel_id(process_id) + await cq.edit_message_caption("✔️ `Stopped Successfully`") + except: + return + + +@app.on_callback_query(filters.regex(r"^ytdl_scroll")) +@ratelimiter +@use_chat_lang() +async def ytdl_scroll_callback(_, cq: CallbackQuery, strings): + if cq.from_user.id != cq.message.reply_to_message.from_user.id: + return await cq.answer(strings("unauth"), True) + callback = cq.data.split("|") + search_key = callback[1] + page = int(callback[2]) + query = YT_DB[search_key] + search = await main.VideosSearch(query).next() + i = search["result"][page] + out = f"{i['title']}" + out = strings("yts_msg").format( + pub=i["publishedTime"], + dur=i["duration"], + vi=i["viewCount"]["short"], + clink=i["channel"]["link"], + cname=i["channel"]["name"], + ) + scroll_btn = [ + [ + InlineKeyboardButton( + strings("back"), callback_data=f"ytdl_scroll|{search_key}|{page-1}" + ), + InlineKeyboardButton( + f"{page+1}/{len(search['result'])}", + callback_data=f"ytdl_scroll|{search_key}|{page+1}", + ), + ] + ] + if page == 0: + if len(search["result"]) == 1: + return await cq.answer(strings("endlist"), show_alert=True) + scroll_btn = [[scroll_btn.pop().pop()]] + elif page == (len(search["result"]) - 1): + scroll_btn = [[scroll_btn.pop().pop(0)]] + btn = [[InlineKeyboardButton(strings("dl_btn"), callback_data=f"yt_gen|{i['id']}")]] + btn = InlineKeyboardMarkup(scroll_btn + btn) + await cq.edit_message_media( + InputMediaPhoto(await get_ytthumb(i["id"]), caption=out), reply_markup=btn + ) + + +async def get_ytthumb(videoid: str): + thumb_quality = [ + "maxresdefault.jpg", # Best quality + "hqdefault.jpg", + "sddefault.jpg", + "mqdefault.jpg", + "default.jpg", # Worst quality + ] + thumb_link = "https://i.imgur.com/4LwPLai.png" + for qualiy in thumb_quality: + link = f"https://i.ytimg.com/vi/{videoid}/{qualiy}" + if (await http.get(link)).status_code == 200: + thumb_link = link + break + return thumb_link diff --git a/misskaty/vars.py b/misskaty/vars.py new file mode 100644 index 00000000..24d8825b --- /dev/null +++ b/misskaty/vars.py @@ -0,0 +1,75 @@ +# * @author Yasir Aris M +# * @date 2023-06-21 22:12:27 +# * @projectName MissKatyPyro +# * Copyright ©YasirPedia All rights reserved +import sys +from logging import getLogger +from os import environ + +LOGGER = 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") + LOG_CHANNEL = int(environ.get("LOG_CHANNEL")) +except Exception as e: + LOGGER.error(f"One or more env variables missing! Exiting now.\n{e}") + sys.exit(1) + +USER_SESSION = environ.get("USER_SESSION") +DATABASE_NAME = environ.get("DATABASE_NAME", "MissKatyDB") +TZ = environ.get("TZ", "Asia/Jakarta") +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") +OPENAI_API = getConfig("OPENAI_API") + +## 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") +CURRENCY_API = environ.get("CURRENCY_API") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..db79a155 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,31 @@ +emoji +git+https://github.com/yasirarism/pyrofork +tgcrypto +async_pymongo +pymongo +python-dotenv +requests +beautifulsoup4 +aiohttp +chevron +gTTS +regex +apscheduler +pytz +pykeyboard +pySmartDL +psutil +python-dateutil +telegraph +hachoir +Pillow==10.0.0 +httpx[http2] +git+https://github.com/yasirarism/vcsi +git+https://github.com/yasirarism/iytdl +deep-translator +telethon +pyrate_limiter +cachetools +cloudscraper +openai +GitPython \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100644 index 00000000..1d9128b9 --- /dev/null +++ b/start.sh @@ -0,0 +1 @@ +python3 update.py && python3 -m misskaty diff --git a/update.py b/update.py new file mode 100644 index 00000000..8b87ac74 --- /dev/null +++ b/update.py @@ -0,0 +1,68 @@ +import os +import subprocess +from logging import INFO, StreamHandler, basicConfig, getLogger, handlers + +import dotenv +import requests +from git import Repo + +if os.path.exists("MissKatyLogs.txt"): + with open("MissKatyLogs.txt", "r+") as f: + f.truncate(0) + +basicConfig( + level=INFO, + format="[%(asctime)s - %(levelname)s] - %(name)s.%(funcName)s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", + handlers=[ + handlers.RotatingFileHandler("MissKatyLogs.txt", mode="w+", maxBytes=1000000), + StreamHandler(), + ], +) + +LOGGER = getLogger(__name__) + +ENV_URL = os.environ.get("ENV_URL") +try: + if len(ENV_URL) == 0: + raise TypeError + try: + res = requests.get(ENV_URL) + if res.status_code == 200: + with open("config.env", "wb+") as f: + f.write(res.content) + else: + LOGGER.error(f"config.env err: {res.status_code}") + except Exception as e: + LOGGER.error(f"ENV_URL: {e}") +except: + pass + +dotenv.load_dotenv("config.env", override=True) + +UPSTREAM_REPO_URL = os.environ.get("UPSTREAM_REPO_URL") +UPSTREAM_REPO_BRANCH = os.environ.get("UPSTREAM_REPO_BRANCH") + +if all([UPSTREAM_REPO_URL, UPSTREAM_REPO_BRANCH]): + if os.path.exists(".git"): + subprocess.run(["rm", "-rf", ".git"], check=True) + + try: + repo = Repo.init() + origin = repo.create_remote("upstream", UPSTREAM_REPO_URL) + origin.fetch() + repo.create_head(UPSTREAM_REPO_BRANCH, origin.refs[UPSTREAM_REPO_BRANCH]) + repo.heads[UPSTREAM_REPO_BRANCH].set_tracking_branch( + origin.refs[UPSTREAM_REPO_BRANCH] + ) + repo.heads[UPSTREAM_REPO_BRANCH].checkout(True) + ups_rem = repo.remote("upstream") + ups_rem.fetch(UPSTREAM_REPO_BRANCH) + LOGGER.info(f"Successfully update with latest branch > {UPSTREAM_REPO_BRANCH}") + except Exception as e: + LOGGER.error(e) + pass +else: + LOGGER.warning( + "UPSTREAM_REPO_URL or UPSTREAM_REPO_BRANCH is not defined, Skipping auto update" + ) diff --git a/utils.py b/utils.py new file mode 100644 index 00000000..a2d6690c --- /dev/null +++ b/utils.py @@ -0,0 +1,146 @@ +import asyncio +import os +from datetime import datetime, timedelta +from logging import getLogger +from typing import Union + +import emoji +from pyrogram.errors import ( + FloodWait, + InputUserDeactivated, + PeerIdInvalid, + UserIsBlocked, +) +from pyrogram.types import Message + +from database.afk_db import is_cleanmode_on +from database.users_chats_db import db +from misskaty import app, cleanmode + +LOGGER = getLogger(__name__) +BANNED = {} + +loop = asyncio.get_event_loop() + + +async def put_cleanmode(chat_id, message_id): + if chat_id not in cleanmode: + cleanmode[chat_id] = [] + time_now = datetime.now() + put = { + "msg_id": message_id, + "timer_after": time_now + timedelta(minutes=1), + } + cleanmode[chat_id].append(put) + + +async def auto_clean(): + while not await asyncio.sleep(30): + try: + for chat_id in cleanmode: + if not await is_cleanmode_on(chat_id): + continue + for x in cleanmode[chat_id]: + if datetime.now() <= x["timer_after"]: + continue + try: + await app.delete_messages(chat_id, x["msg_id"]) + except FloodWait as e: + await asyncio.sleep(e.value) + except: + continue + except: + continue + + +# temp db for banned +class temp: + 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)) + LOGGER.info(f"{user_id}-Removed from Database, since deleted account.") + return False, "Deleted" + except UserIsBlocked: + LOGGER.info(f"{user_id} -Blocked the bot.") + return False, "Blocked" + except PeerIdInvalid: + await db.delete_user(int(user_id)) + LOGGER.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)