diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..636e2e0 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,78 @@ +# Plugin Development +PagerMaid has a powerful plugin system, which enables convenient debugging of code sharing. This document explains + how to use the PagerMaid framework to develop your own PagerMaid plugin, and share code you have written for the + platform. + +## Plugin Manager +The plugin manager is a utility to help you manage and share plugins installed to your PagerMaid instance. + +### Install +To install a plugin, issue the plugin command replying to a message containing the plugin in it's attachments, with the + install argument. PagerMaid will download and validate the plugin, and restart after it is installed. + +### Remove +To remove a plugin, issue the plugin command with the remove argument, and append the plugin name after the argument, + PagerMaid will look for the plugin and if present, remove it and restart. + +### Enable +To enable a disabled plugin, issue the plugin command with the enable argument, and append the plugin name after the + argument, PagerMaid will look for the plugin and if present and disabled, enable it and restart. + +### Disable +To disable an enabled plugin, issue the plugin command with the disable argument, and append the plugin name after the + argument, PagerMaid will look for the plugin and if present and enabled, disable it and restart. + +### Upload +To upload a plugin, issue the plugin command with the upload argument, and append the plugin name after the argument, + PagerMaid will look for the plugin and if present, upload it to the current chat. + +### Status +To view the status of plugins, issue the plugin command with the status argument, active plugins refer to plugins that + are enabled and loaded, failed plugins refer to plugins that are enabled but failed to load, disabled plugins refer to + plugins that are disabled. + +## Event Listener +The most important part of a plugin is the event listener, which is how commands are implemented. To register an event + listener, import listener from pagermaid.listener and use the listener annotation on an async class. + +```python +""" Plugin description. """ +from pagermaid.listener import listener + + +@listener(outgoing=True, command="command", diagnostics=False, ignore_edited=False, + description="My PagerMaid plugin command.", + parameters="") +async def command(context): + await context.edit(f"Hello, World! Welcome, {context.parameter[0]}.") +``` + +The outgoing variable specifies if the message has to be sent by the user, if it is set to False the event listener + will be invoked on every single message event. + +The command variable specifies the command used to trigger the event listener, which will insert the arguments of the + command to context.pattern_match.group(1). + +The pattern variable specifies a regex pattern that overrides the command to be used to trigger the event listener. Case + insensitivity is implemented within the listener. + +The diagnostics variable specifies if exceptions of the event listener is handled by the diagnostics module which sends + the error report to Kat, defaults to True, you should disable it if you are using exceptions to trigger events. + +The ignore_edited variable specifies if the event listener will be triggered by an edit of a message, defaults to False. + +The description variable specifies the description that will appear in the help message. + +the parameters variable specifies the parameters indicator that will appear in the help message. + +## Logging +The PagerMaid framework includes two sets of logging: console and in-chat logging. In-Chat logging respects the + configuration file. To use logging, import log from the pagermaid class. + +```python +from pagermaid import log +await log("This is a log message!") +``` + +The logging handler will output an entry to the console, and, if configured, will send a message into the logging + channel the user specified. Beware that log() is a coroutine object, so please do not forget to await it. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5b6a1ec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM archlinux/base:latest +RUN pacman -Syu --needed --noconfirm \ + git \ + libffi \ + tesseract \ + openssl \ + bzip2 \ + zlib \ + readline \ + sqlite \ + fortune-mod \ + figlet \ + python-virtualenv \ + redis \ + libxslt \ + libxml2 \ + libpqxx \ + linux-api-headers \ + freetype2 \ + jpeg-archive \ + curl \ + wget \ + neofetch \ + sudo \ + gcc \ + gcc8 \ + imagemagick \ + libwebp \ + zbar \ + ffmpeg \ + file \ + procps-ng +RUN sed -e 's;^# \(%wheel.*NOPASSWD.*\);\1;g' -i /etc/sudoers +RUN useradd pagermaid -u 3333 -r -m -d /pagermaid +RUN usermod -aG wheel,users pagermaid +USER pagermaid +RUN mkdir /pagermaid/workdir +RUN git clone -b master https://git.stykers.moe/scm/~stykers/pagermaid.git /pagermaid/workdir +WORKDIR /pagermaid/workdir +RUN python3 -m virtualenv /pagermaid/venv +RUN source /pagermaid/venv/bin/activate; pip3 install -r requirements.txt +CMD ["sh","utils/entrypoint.sh"] diff --git a/Dockerfile.persistant b/Dockerfile.persistant new file mode 100644 index 0000000..0fbb61c --- /dev/null +++ b/Dockerfile.persistant @@ -0,0 +1,44 @@ +FROM archlinux/base:latest +RUN pacman -Syu --needed --noconfirm \ + git \ + libffi \ + tesseract \ + openssl \ + bzip2 \ + zlib \ + readline \ + sqlite \ + fortune-mod \ + figlet \ + python-virtualenv \ + redis \ + libxslt \ + libxml2 \ + libpqxx \ + linux-api-headers \ + freetype2 \ + jpeg-archive \ + curl \ + wget \ + neofetch \ + sudo \ + gcc \ + gcc8 \ + imagemagick \ + libwebp \ + zbar \ + ffmpeg \ + procps-ng +RUN sed -e 's;^# \(%wheel.*NOPASSWD.*\);\1;g' -i /etc/sudoers +RUN useradd pagermaid -r -m -d /pagermaid +RUN usermod -aG wheel,users pagermaid +USER pagermaid +RUN mkdir /pagermaid/workdir +RUN git clone -b master https://git.stykers.moe/scm/~stykers/pagermaid.git /pagermaid/workdir +WORKDIR /pagermaid/workdir +COPY ./pagermaid.session ./config.yml /pagermaid/workdir/ +RUN sudo chown pagermaid:pagermaid /pagermaid/workdir/config.yml +RUN sudo chown -f pagermaid:pagermaid /pagermaid/workdir/pagermaid.session; exit 0 +RUN python3 -m virtualenv venv +RUN source venv/bin/activate; pip3 install -r requirements.txt +CMD ["sh","utils/entrypoint.sh"] diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..b80f730 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,76 @@ +# 安装 + +这是开始使用 `PagerMaid` 所需的说明,支持各种初始化系统。 + +## 要求 + +您需要 `Linux` 或 `*BSD` 系统,并且您的系统应该至少运行 `python 3.6` ,推荐虚拟环境运行。 + +## 快速开始 + +如果您的系统与docker兼容,并且您想要快速且受支持的安装,Docker 将帮助您快速入门。尽管很方便,但这种安装方法将系统范围的包限制在容器内。 + +在 https://my.telegram.org/ 上创建您的应用程序,然后运行以下命令: + + +``` +curl -fsSL https://git.stykers.moe/users/stykers/repos/pagermaid/raw/utils/docker.sh | sh +``` + +如果您想在运行之前检查脚本内容: + +``` +curl https://git.stykers.moe/users/stykers/repos/pagermaid/raw/utils/docker.sh -o docker.sh +vim docker.sh +chmod 0755 docker.sh +./docker.sh +``` + +## 配置 + +将文件 `config.gen.yml` 复制一份到 `config.yml` ,并使用您最喜欢的文本编辑器,编辑配置文件,直到您满意为止。 + +## 从源代码安装 + +将 `PagerMaid-Modify` 工作目录复制到 `/var/lib` ,然后输入 `/var/lib/pagermaid` ,激活虚拟环境(如果需要),并从 `requirements.txt` 安装所有依赖项 + +``` +python3 -m pagermaid +``` + +现在确保 `zbar` , `neofetch` , `tesseract` 和 `ImageMagick` 软件包是通过软件包管理器安装的,并且您已经准备好启动 `PagerMaid` 。 + +``` +python3 -m pagermaid +``` + +## 从PyPi安装 + +为 `PagerMaid` 创建一个工作目录,通常为 `/var/lib/pagermaid` ,建议设置虚拟环境。 + +安装PagerMaid模块: + +``` +pip3 install pagermaid +``` + +现在确保 `zbar` , `neofetch` , `tesseract` 和 `ImageMagick` 软件包是通过软件包管理器安装的,并且您已经准备好启动 `PagerMaid` 。 + +``` +pagermaid +``` + +## 进程守护 + +确保您至少手动运行过 `PagerMaid` 一次,或者已经存在 session 文件。 +- Runit:在 `/etc/sv/pagermaid` 中创建一个目录,然后将 `utils/run` 复制到其中 +- SystemD:将 `utils/pagermaid.service` 复制到 `/ var/lib/systemd/system` 中 +- 直接:运行 `utils/start.sh` + +## 身份验证 + +有时(或大部分时间),当您在服务器部署 `PagerMaid` 时,登录会有问题,当出现了问题,请在应用程序的配置步骤配置唯一的 `application key` 和 `hash` ,然后在您的PC上运行 `utils/mksession.py` ,将 `pagermaid.session` 复制到服务器。 + +## 插件 + +`some-plugins` 已经内置了部分插件,请根据需要复制到 plugins 启用。 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..473785a --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + pagermaid + Copyright (C) 2019 Katherine ❄ + + 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + pagermaid Copyright (C) 2019 Katherine ❄ + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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. But first, please read +. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ce97cf6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include pagermaid/assets/* diff --git a/README.md b/README.md index ab94894..cd1cc88 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ -# PagerMaid-Modify -PagerMaid Telegram utility daemon. +# PagerMaid + +Pagermaid 是一个用在 Telegram 的实用工具。 + +它通过响应账号通过其他客户端发出的命令来自动执行一系列任务。 + +原英文更新频道: https://t.me/PagerMaid + +## 安装 + +请阅读 [INSTALL.md](https://github.com/xtaodada/PagerMaid-Modify/blob/master/INSTALL.md) + +## 与原作者联络 + +您可以在 Telegram 上使用 -contact 并点击链接进入通过 Pagermaid 自动打开的 PM ,如果您安装上出现了问题,请通过 [stykers@stykers.moe](mailto:stykers@stykers.moe) 给我发电子邮件,或在 Telegram [@KatOnKeyboard](https://t.me/KatOnKeyboard)上给我发消息。 + +## 特别感谢 +[Amine Oversoul](https://bitbucket.org/oversoul/pagermaid-ui) + +## 修改内容 + +- `PagerMaid-Modify/README.md` + +- `PagerMaid-Modify/INSTALL.md` + +- `PagerMaid-Modify/some-plugins/autorespond.py` + +- `PagerMaid-Modify/some-plugins/yt-dl.py` + +- `PagerMaid-Modify/pagermaid/listener.py` + +- `PagerMaid-Modify/pagermaid/utils.py` + +- `PagerMaid-Modify/pagermaid/__main__.py` + +- `PagerMaid-Modify/pagermaid/__init__.py` + +- `pagermaid-Modify/interface/__init__.py` + +- `PagerMaid-Modify/pagermaid/interface/__main__.py` + +- `PagerMaid-Modify/pagermaid/interface/views.py` + +- `PagerMaid-Modify/pagermaid/modules/account.py` + +- `PagerMaid-Modify/pagermaid/modules/avoid.py` + +- `PagerMaid-Modify/pagermaid/modules/captions.py` + +- `PagerMaid-Modify/pagermaid/modules/clock.py` + +- `PagerMaid-Modify/pagermaid/modules/external.py` + +- `PagerMaid-Modify/pagermaid/modules/fun.py` + +- `PagerMaid-Modify/pagermaid/modules/help.py` + +- `PagerMaid-Modify/pagermaid/modules/message.py` + +- `PagerMaid-Modify/pagermaid/modules/plugin.py` + +- `PagerMaid-Modify/pagermaid/modules/prune.py` + +- `PagerMaid-Modify/pagermaid/modules/qr.py` + +- `PagerMaid-Modify/pagermaid/modules/status.py` + +- `PagerMaid-Modify/pagermaid/modules/sticker.py` + +- `PagerMaid-Modify/pagermaid/modules/system.py` + +- `PagerMaid-Modify/pagermaid/modules/update.py` + diff --git a/config.gen.yml b/config.gen.yml new file mode 100644 index 0000000..70592e7 --- /dev/null +++ b/config.gen.yml @@ -0,0 +1,46 @@ +# =================================================================== +# __________ _____ .__ .___ +# \______ \_____ ____ ___________ / \ _____ |__| __| _/ +# | ___/\__ \ / ___\_/ __ \_ __ \/ \ / \\__ \ | |/ __ | +# | | / __ \_/ /_/ > ___/| | \/ Y \/ __ \| / /_/ | +# |____| (____ /\___ / \___ >__| \____|__ (____ /__\____ | +# \//_____/ \/ \/ \/ \/ +# =================================================================== + +# API Credentials of your telegram application created at https://my.telegram.org/apps +api_key: "****" +api_hash: "**********" + +# Either debug logging is enabled or not +debug: "False" +error_report: "True" + +# Admin interface related +web_interface: + enable: "False" + secret_key: "RANDOM_STRING_HERE" + host: "127.0.0.1" + port: "3333" + +# Redis connection information +redis: + host: "localhost" + port: "6379" + db: "14" + +# Locale settings +application_language: "en" +application_region: "United States" + +# In-Chat logging settings, default settings logs directly into Kat, strongly advised to change +log: "False" +log_chatid: "503691334" + +# Google search preferences +result_length: "5" + +# TopCloud image output preferences +width: "1920" +height: "1080" +background: "#101010" +margin: "20" \ No newline at end of file diff --git a/docker-compose.gen.yml b/docker-compose.gen.yml new file mode 100644 index 0000000..6f3dc5f --- /dev/null +++ b/docker-compose.gen.yml @@ -0,0 +1,13 @@ +version: "3.7" +services: + pagermaid: + build: + context: . + image: pagermaid:custom + restart: always + container_name: "pagermaid" + hostname: "pagermaid" + ports: + - "3333:3333" + volumes: + - ./:/pagermaid/workdir diff --git a/pagermaid/__init__.py b/pagermaid/__init__.py new file mode 100644 index 0000000..a10a619 --- /dev/null +++ b/pagermaid/__init__.py @@ -0,0 +1,99 @@ +""" PagerMaid initialization. """ + +from os import getcwd, makedirs +from os.path import exists +from sys import version_info, platform +from yaml import load, FullLoader +from shutil import copyfile +from redis import StrictRedis +from logging import getLogger, INFO, DEBUG, StreamHandler +from distutils2.util import strtobool +from coloredlogs import ColoredFormatter +from telethon import TelegramClient + +persistent_vars = {} +module_dir = __path__[0] +working_dir = getcwd() +logging_format = "%(levelname)s [%(asctime)s] [%(name)s] %(message)s" +config = None +help_messages = {} +logs = getLogger(__name__) +logging_handler = StreamHandler() +logging_handler.setFormatter(ColoredFormatter(logging_format)) +logs.addHandler(logging_handler) +logs.setLevel(INFO) + +try: + config = load(open(r"config.yml"), Loader=FullLoader) +except FileNotFoundError: + logs.fatal("出错了呜呜呜 ~ 配置文件不存在,正在生成新的配置文件。") + copyfile(f"{module_dir}/assets/config.gen.yml", "config.yml") + exit(1) + +if strtobool(config['debug']): + logs.setLevel(DEBUG) +else: + logs.setLevel(INFO) + +if platform == "linux" or platform == "linux2" or platform == "darwin" or platform == "freebsd7" \ + or platform == "freebsd8" or platform == "freebsdN" or platform == "openbsd6": + logs.info( + "将平台检测为“ " + platform + ",进入PagerMaid的早期加载过程。" + ) +else: + logs.error( + "出错了呜呜呜 ~ 你的平台 " + platform + " 不支持运行 PagerMaid,请在Linux或 *BSD 上启动 PagerMaid。" + ) + exit(1) + +if version_info[0] < 3 or version_info[1] < 6: + logs.error( + "出错了呜呜呜 ~ 请将您的 python 升级到至少3.6版。" + ) + exit(1) + +if not exists(f"{getcwd()}/data"): + makedirs(f"{getcwd()}/data") + +api_key = config['api_key'] +api_hash = config['api_hash'] +try: + redis_host = config['redis']['host'] +except KeyError: + redis_host = 'localhost' +try: + redis_port = config['redis']['port'] +except KeyError: + redis_port = 6379 +try: + redis_db = config['redis']['db'] +except KeyError: + redis_db = 14 +if api_key is None or api_hash is None: + logs.info( + "出错了呜呜呜 ~ 请在工作目录中放置一个有效的配置文件。" + ) + exit(1) + +bot = TelegramClient("pagermaid", api_key, api_hash, auto_reconnect=True) +redis = StrictRedis(host=redis_host, port=redis_port, db=redis_db) + + +def redis_status(): + try: + redis.ping() + return True + except BaseException: + return False + + +async def log(message): + logs.info( + message.replace('`', '\"') + ) + if not strtobool(config['log']): + return + await bot.send_message( + int(config['log_chatid']), + message + ) diff --git a/pagermaid/__main__.py b/pagermaid/__main__.py new file mode 100644 index 0000000..109f000 --- /dev/null +++ b/pagermaid/__main__.py @@ -0,0 +1,41 @@ +""" PagerMaid launch sequence. """ + +from sys import path +from importlib import import_module +from telethon.errors.rpcerrorlist import PhoneNumberInvalidError +from pagermaid import bot, logs, working_dir +from pagermaid.modules import module_list, plugin_list +try: + from pagermaid.interface import server +except TypeError: + logs.error("出错了呜呜呜 ~ Web 界面配置绑定到了一个无效地址。") + server = None +except KeyError: + logs.error("出错了呜呜呜 ~ 配置文件中缺少 Web 界面配置。") + server = None + + +path.insert(1, f"{working_dir}/plugins") + +try: + bot.start() +except PhoneNumberInvalidError: + print('出错了呜呜呜 ~ 输入的电话号码无效。 请确保附加国家代码。') + exit(1) +for module_name in module_list: + try: + import_module("pagermaid.modules." + module_name) + except BaseException: + logs.info(f"模块 {module_name} 加载出错。") +for plugin_name in plugin_list: + try: + import_module("plugins." + plugin_name) + except BaseException as exception: + logs.info(f"模块 {plugin_name} 加载出错: {exception}") + plugin_list.remove(plugin_name) +if server is not None: + import_module("pagermaid.interface") +logs.info("PagerMaid-Modify 已启动,在任何聊天中输入 -help 以获得帮助消息。") +bot.run_until_disconnected() +if server is not None: + server.stop() diff --git a/pagermaid/assets/Impact-Regular.ttf b/pagermaid/assets/Impact-Regular.ttf new file mode 100644 index 0000000..114e6c1 Binary files /dev/null and b/pagermaid/assets/Impact-Regular.ttf differ diff --git a/pagermaid/assets/caption-gif.sh b/pagermaid/assets/caption-gif.sh new file mode 100644 index 0000000..ce8231b --- /dev/null +++ b/pagermaid/assets/caption-gif.sh @@ -0,0 +1,26 @@ +#!/bin/sh -ex + +src=$1 +dest="result.gif" +font=$2 +header=$3 +footer=$4 + +width=$(identify -format %w "${src}") +caption_height=$((width/8)) +strokewidth=$((width/500)) + +ffmpeg -i "${src}" \ + -vf "fps=10,scale=320:-1:flags=lanczos" \ + -c:v pam \ + -f image2pipe - | \ + convert -delay 10 \ + - -loop 0 \ + -layers optimize \ + output.gif + +convert "output.gif" \ +\( -clone 0 -coalesce -gravity South -background none -size 435x65.5 caption:"${header}" \) -swap -1,0 \ +"${dest}" + +rm output.gif \ No newline at end of file diff --git a/pagermaid/assets/caption.sh b/pagermaid/assets/caption.sh new file mode 100644 index 0000000..4f19a38 --- /dev/null +++ b/pagermaid/assets/caption.sh @@ -0,0 +1,22 @@ +#!/bin/sh -ex + +src=$1 +dest="result.png" +font=$2 +header=$3 +footer=$4 + +width=$(identify -format %w "${src}") +caption_height=$((width/8)) +strokewidth=$((width/500)) + +convert "${src}" \ + -background none \ + -font "${font}" \ + -fill white \ + -stroke black \ + -strokewidth ${strokewidth} \ + -size "${width}"x${caption_height} \ + -gravity north caption:"${header}" -composite \ + -gravity south caption:"${footer}" -composite \ + "${dest}" diff --git a/pagermaid/assets/config.gen.yml b/pagermaid/assets/config.gen.yml new file mode 100644 index 0000000..d4ce6fa --- /dev/null +++ b/pagermaid/assets/config.gen.yml @@ -0,0 +1,46 @@ +# =================================================================== +# __________ _____ .__ .___ +# \______ \_____ ____ ___________ / \ _____ |__| __| _/ +# | ___/\__ \ / ___\_/ __ \_ __ \/ \ / \\__ \ | |/ __ | +# | | / __ \_/ /_/ > ___/| | \/ Y \/ __ \| / /_/ | +# |____| (____ /\___ / \___ >__| \____|__ (____ /__\____ | +# \//_____/ \/ \/ \/ \/ +# =================================================================== + +# API Credentials of your telegram application created at https://my.telegram.org/apps +api_key: "143461" +api_hash: "7b8a66cb31224f4241102d7fc57b5bcd" + +# Either debug logging is enabled or not +debug: "False" +error_report: "True" + +# Admin interface related +web_interface: + enable: "False" + secret_key: "RANDOM_STRING_HERE" + host: "127.0.0.1" + port: "3333" + +# Redis connection information +redis: + host: "localhost" + port: "6379" + db: "14" + +# Locale settings +application_language: "en" +application_region: "United States" + +# In-Chat logging settings, default settings logs directly into Kat, strongly advised to change +log: "False" +log_chatid: "503691334" + +# Google search preferences +result_length: "5" + +# TopCloud image output preferences +width: "1920" +height: "1080" +background: "#101010" +margin: "20" \ No newline at end of file diff --git a/pagermaid/assets/graffiti.flf b/pagermaid/assets/graffiti.flf new file mode 100644 index 0000000..fea27a8 --- /dev/null +++ b/pagermaid/assets/graffiti.flf @@ -0,0 +1,617 @@ +flf2a$ 6 5 32 15 4 +Font name is graffiti.flf +This figlet font designed by Leigh Purdie (purdie@zeus.usq.edu.au) +'fig-fonted' by Leigh Purdie and Tim Maggio (tim@claremont.com) +Date: 5 Mar 1994 +$@ +$@ +$@ +$@ +$@ +$@@ +._.@ +| |@ +| |@ + \|@ + __@ + \/@@ +/\/\@ +)/)/@ + @ + @ + @ + @@ + _ _ @ +__| || |__@ +\ __ /@ + | || | @ +/_ ~~ _\@ + |_||_| @@ + ____/\__@ + / / /_/@ + \__/ / \ @ + / / / \@ +/_/ /__ /@ + \/ \/ @@ + _ /\ @ +/ \ / / @ +\_// /_ @ + / // \@ + / / \_/@ + \/ @@ + ____ @ + / _ \ @ + > _ < @ +/ -- \@ +\______ /@ + \/ @@ + ________ @ +/ __ \@ +\____ /@ + / / @ + /____/ @ + @@ +$ $@ +$/\$@ +$\/$@ +$/\$@ +$\/$@ +$ $@@ +$ $@ +$/\$@ +$\/$@ +$/\$@ +$)/$@ +$ $@@ +$ __$@ +$ / /$@ +$/ / $@ +$\ \ $@ +$ \_\$@ +$ $@@ +$ $@ +$ ______$@ +$/_____/$@ +$/_____/$@ +$ $@ +$ $@@ +$__ $@ +$\ \ $@ +$ \ \$@ +$ / /$@ +$/_/ $@ +$ $@@ +_________ @ +\_____ \@ + / __/@ + | | @ + |___| @ + <___> @@ + _____ @ + / ___ \ @ + / / ._\ \@ +< \_____/@ + \_____\ @ + @@ + _____ @ + / _ \ @ + / /_\ \ @ +/ | \@ +\____|__ /@ + \/ @@ +__________ @ +\______ \@ + | | _/@ + | | \@ + |______ /@ + \/ @@ +_________ @ +\_ ___ \ @ +/ \ \/ @ +\ \____@ + \______ /@ + \/ @@ +________ @ +\______ \ @ + | | \ @ + | ` \@ +/_______ /@ + \/ @@ +___________@ +\_ _____/@ + | __)_ @ + | \@ +/_______ /@ + \/ @@ +___________@ +\_ _____/@ + | __) @ + | \ @ + \___ / @ + \/ @@ + ________ @ + / _____/ @ +/ \ ___ @ +\ \_\ \@ + \______ /@ + \/ @@ + ___ ___ @ + / | \ @ +/ ~ \@ +\ Y /@ + \___|_ / @ + \/ @@ +.___ @ +| |@ +| |@ +| |@ +|___|@ + @@ + ____.@ + | |@ + | |@ +/\__| |@ +\________|@ + @@ + ____ __.@ +| |/ _|@ +| < @ +| | \ @ +|____|__ \@ + \/@@ +.____ @ +| | @ +| | @ +| |___ @ +|_______ \@ + \/@@ + _____ @ + / \ @ + / \ / \ @ +/ Y \@ +\____|__ /@ + \/ @@ + _______ @ + \ \ @ + / | \ @ +/ | \@ +\____|__ /@ + \/ @@ +________ @ +\_____ \ @ + / | \ @ +/ | \@ +\_______ /@ + \/ @@ +__________ @ +\______ \@ + | ___/@ + | | @ + |____| @ + @@ +________ @ +\_____ \ @ + / / \ \ @ +/ \_/. \@ +\_____\ \_/@ + \__>@@ +__________ @ +\______ \@ + | _/@ + | | \@ + |____|_ /@ + \/ @@ + _________@ + / _____/@ + \_____ \ @ + / \@ +/_______ /@ + \/ @@ +___________@ +\__ ___/@ + | | @ + | | @ + |____| @ + @@ + ____ ___ @ +| | \@ +| | /@ +| | / @ +|______/ @ + @@ +____ ____@ +\ \ / /@ + \ Y / @ + \ / @ + \___/ @ + @@ + __ __ @ +/ \ / \@ +\ \/\/ /@ + \ / @ + \__/\ / @ + \/ @@ +____ ___@ +\ \/ /@ + \ / @ + / \ @ +/___/\ \@ + \_/@@ +_____.___.@ +\__ | |@ + / | |@ + \____ |@ + / ______|@ + \/ @@ +__________@ +\____ /@ + / / @ + / /_ @ +/_______ \@ + \/@@ +$.____ $@ +$| _|$@ +$| | $@ +$| | $@ +$| |_ $@ +$|____|$@@ +/\ @ +\ \ @ + \ \ @ + \ \ @ + \ \@ + \/@@ +$ ____.$@ +$|_ |$@ +$ | |$@ +$ | |$@ +$ _| |$@ +$|____|$@@ +$ /\ $@ +$/ \$@ +$\/\/$@ +$ $@ +$ $@ +$ $@@ + @ + @ + @ + @ + ______@ +/_____/@@ +/\@ +\(@ + @ + @ + @ + @@ + @ +_____ @ +\__ \ @ + / __ \_@ +(____ /@ + \/ @@ +___. @ +\_ |__ @ + | __ \ @ + | \_\ \@ + |___ /@ + \/ @@ + @ + ____ @ +_/ ___\ @ +\ \___ @ + \___ >@ + \/ @@ + .___@ + __| _/@ + / __ | @ +/ /_/ | @ +\____ | @ + \/ @@ + @ + ____ @ +_/ __ \ @ +\ ___/ @ + \___ >@ + \/ @@ + _____ @ +_/ ____\@ +\ __\ @ + | | @ + |__| @ + @@ + @ + ____ @ + / ___\ @ + / /_/ >@ + \___ / @ +/_____/ @@ +.__ @ +| |__ @ +| | \ @ +| Y \@ +|___| /@ + \/ @@ +.__ @ +|__|@ +| |@ +| |@ +|__|@ + @@ + __ @ + |__|@ + | |@ + | |@ +/\__| |@ +\______|@@ + __ @ +| | __@ +| |/ /@ +| < @ +|__|_ \@ + \/@@ +.__ @ +| | @ +| | @ +| |__@ +|____/@ + @@ + @ + _____ @ + / \ @ +| Y Y \@ +|__|_| /@ + \/ @@ + @ + ____ @ + / \ @ +| | \@ +|___| /@ + \/ @@ + @ + ____ @ + / _ \ @ +( <_> )@ + \____/ @ + @@ + @ +______ @ +\____ \ @ +| |_> >@ +| __/ @ +|__| @@ + @ + ______@ + / ____/@ +< <_| |@ + \__ |@ + |__|@@ + @ +_______ @ +\_ __ \@ + | | \/@ + |__| @ + @@ + @ + ______@ + / ___/@ + \___ \ @ +/____ >@ + \/ @@ + __ @ +_/ |_ @ +\ __\@ + | | @ + |__| @ + @@ + @ + __ __ @ +| | \@ +| | /@ +|____/ @ + @@ + @ +___ __@ +\ \/ /@ + \ / @ + \_/ @ + @@ + @ +__ _ __@ +\ \/ \/ /@ + \ / @ + \/\_/ @ + @@ + @ +___ ___@ +\ \/ /@ + > < @ +/__/\_ \@ + \/@@ + @ + ___.__.@ +< | |@ + \___ |@ + / ____|@ + \/ @@ + @ +________@ +\___ /@ + / / @ +/_____ \@ + \/@@ +$ ___$@ +$/ / $@ +$\ \ $@ +$< < $@ +$/ / $@ +$\_\_$@@ +$._.$@ +$| |$@ +$|_|$@ +$|-|$@ +$| |$@ +$|_|$@@ +$___ $@ +$ \ \$@ +$ / /$@ +$ > >$@ +$ \ \$@ +$_/_/$@@ +$ ___ $@ +$/ _ \_/\$@ +$\/ \___/$@ +$ $@ +$ $@ +$ $@@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@@ diff --git a/pagermaid/assets/pagermaid b/pagermaid/assets/pagermaid new file mode 100644 index 0000000..b05e278 --- /dev/null +++ b/pagermaid/assets/pagermaid @@ -0,0 +1,4 @@ +#!/bin/bash +source venv/bin/activate; +/usr/bin/env python3 -m pagermaid + diff --git a/pagermaid/assets/replacements.json b/pagermaid/assets/replacements.json new file mode 100644 index 0000000..de81715 --- /dev/null +++ b/pagermaid/assets/replacements.json @@ -0,0 +1,20 @@ +{"cute": "kawaii", +"idiot": "baka", +"cat": "neko", +"fox": "kitsune", +"dog": "inu", +"right": "ne", +"god": "kami-sama", +"what": "nani", +"it is": "desu", +"meow": "nya", +"comic": "manga", +"girl": "shoujo", +"cartoon|cartoons|show": "anime", +"porn": "hentai", +"child": "loli", +"wife": "waifu", +"thank you": "arigatou", +"sorry": "gomenasai", +"i like you": "suki", +"i am": "watashi wa"} \ No newline at end of file diff --git a/pagermaid/interface/__init__.py b/pagermaid/interface/__init__.py new file mode 100644 index 0000000..38b4abc --- /dev/null +++ b/pagermaid/interface/__init__.py @@ -0,0 +1,56 @@ +""" PagerMaid web interface utility. """ + +from threading import Thread +from distutils2.util import strtobool +from importlib import import_module +from cheroot.wsgi import Server as WSGIServer, PathInfoDispatcher +from flask import Flask +from flask.logging import default_handler +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_bcrypt import Bcrypt + +try: + from pagermaid import config, working_dir, logs, logging_handler +except ModuleNotFoundError: + print("出错了呜呜呜 ~ 此模块不应直接运行。") + exit(1) + +app = Flask("pagermaid") +app.config['CSRF_ENABLED'] = True +app.config['SECRET_KEY'] = config['web_interface']['secret_key'] +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{working_dir}/data/web_interface.db" +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) +login = LoginManager() +login.init_app(app) + + +@app.before_first_request +def init_db(): + db.create_all() + + +import_module('pagermaid.interface.views') +import_module('pagermaid.interface.modals') + +dispatcher = PathInfoDispatcher({'/': app}) +server = WSGIServer((config['web_interface']['host'], int(config['web_interface']['port'])), dispatcher) + + +def start(): + if strtobool(config['web_interface']['enable']): + logs.info(f"已经启动Web界面 {config['web_interface']['host']}:{config['web_interface']['port']}") + app.logger.removeHandler(default_handler) + app.logger.addHandler(logging_handler) + try: + server.start() + except OSError: + logs.fatal("出错了呜呜呜 ~ 另一个进程绑定到了 PagerMaid 需要的端口!") + return + else: + logs.info("Web 界面已禁用。") + + +Thread(target=start).start() diff --git a/pagermaid/interface/__main__.py b/pagermaid/interface/__main__.py new file mode 100644 index 0000000..05b607c --- /dev/null +++ b/pagermaid/interface/__main__.py @@ -0,0 +1,5 @@ +""" PagerMaid web interface startup. """ + +from pagermaid import logs + +logs.info("出错了呜呜呜 ~ 此模块不应直接运行。") diff --git a/pagermaid/interface/forms.py b/pagermaid/interface/forms.py new file mode 100644 index 0000000..c7d0966 --- /dev/null +++ b/pagermaid/interface/forms.py @@ -0,0 +1,24 @@ +""" PagerMaid interface forms. """ + +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField +from wtforms.validators import Email, DataRequired + + +class LoginForm(FlaskForm): + username = StringField(u'Username', validators=[DataRequired()]) + password = PasswordField(u'Password', validators=[DataRequired()]) + + +class SetupForm(FlaskForm): + full_name = StringField(u'Full Name', validators=[DataRequired()]) + username = StringField(u'Full Name', validators=[DataRequired()]) + password = PasswordField(u'Full Name', validators=[DataRequired()]) + email = StringField(u'Full Name', validators=[DataRequired(), Email()]) + + +class ModifyForm(FlaskForm): + full_name = StringField(u'Full Name') + username = StringField(u'Username') + password = PasswordField(u'Password') + email = StringField(u'Email Address', validators=[Email()]) diff --git a/pagermaid/interface/modals.py b/pagermaid/interface/modals.py new file mode 100644 index 0000000..e25130a --- /dev/null +++ b/pagermaid/interface/modals.py @@ -0,0 +1,24 @@ +""" Account related modals. """ + +from pagermaid.interface import db +from flask_login import UserMixin + + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + user = db.Column(db.String(64), unique=True) + email = db.Column(db.String(120), unique=True) + password = db.Column(db.String(500)) + + def __init__(self, user, email, password): + self.user = user + self.password = password + self.email = email + + def __repr__(self): + return str(self.id) + ' - ' + str(self.user) + + def save(self): + db.session.add(self) + db.session.commit() + return self diff --git a/pagermaid/interface/views.py b/pagermaid/interface/views.py new file mode 100644 index 0000000..2596534 --- /dev/null +++ b/pagermaid/interface/views.py @@ -0,0 +1,143 @@ +""" Static views generated for PagerMaid. """ + +from pathlib import Path +from psutil import virtual_memory +from os.path import exists +from sys import platform +from platform import uname, python_version +from telethon import version as telethon_version +from flask import render_template, request, url_for, redirect, send_from_directory +from flask_login import login_user, logout_user, current_user +from pagermaid import logs, redis_status +from pagermaid.interface import app, login +from pagermaid.interface.modals import User +from pagermaid.interface.forms import LoginForm, SetupForm, ModifyForm + + +@login.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + + +@app.route("/logout") +def logout(): + logout_user() + return redirect(url_for('index')) + + +@app.route("/setup", methods=['GET', 'POST']) +def setup(): + form = SetupForm(request.form) + msg = None + if request.method == 'GET': + if exists('data/.user_configured'): + return redirect(url_for('login'), code=302) + return render_template('pages/setup.html', form=form, msg=msg) + if form.validate_on_submit(): + username = request.form.get('username', '', type=str) + password = request.form.get('password', '', type=str) + email = request.form.get('email', '', type=str) + user = User.query.filter_by(user=username).first() + user_by_email = User.query.filter_by(email=email).first() + if user or user_by_email: + msg = 'This email already exist on this system, sign in if it is yours.' + else: + pw_hash = password + user = User(username, email, pw_hash) + user.save() + Path('data/.user_configured').touch() + return redirect(url_for('login'), code=302) + else: + msg = 'Invalid input.' + return render_template('pages/setup.html', form=form, msg=msg) + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if not exists('data/.user_configured'): + return redirect(url_for('setup'), code=302) + form = LoginForm(request.form) + msg = None + if form.validate_on_submit(): + username = request.form.get('username', '', type=str) + password = request.form.get('password', '', type=str) + user = User.query.filter_by(user=username).first() + if user: + if user.password == password: + login_user(user) + return redirect(url_for('index')) + else: + msg = "用户名或密码错误。" + else: + msg = "此用户不存在" + return render_template('pages/login.html', form=form, msg=msg) + + +@app.route('/style.css') +def style(): + return send_from_directory('static', 'style.css') + + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory('static', 'favicon.ico') + + +@app.route('/settings') +def settings(): + if not current_user.is_authenticated: + return redirect(url_for('login')) + return render_template('pages/settings.html') + + +@app.route('/profile') +def profile(): + if not current_user.is_authenticated: + return redirect(url_for('profile')) + form = ModifyForm(request.form) + msg = None + return render_template('pages/profile.html', form=form, msg=msg) + + +@app.route('/') +def index(): + if not current_user.is_authenticated: + return redirect(url_for('login')) + memory = virtual_memory() + memory_total = memory.total + memory_available = memory.available + memory_available_percentage = round(100 * float(memory_available)/float(memory_total), 2) + memory_free = memory.free + memory_free_percentage = round(100 * float(memory_free) / float(memory_total), 2) + memory_buffered = memory.buffers + memory_buffered_percentage = round(100 * float(memory_buffered) / float(memory_total), 2) + memory_cached = memory.cached + memory_cached_percentage = round(100 * float(memory_cached) / float(memory_total), 2) + return render_template('pages/index.html', + hostname=uname().node, + platform=platform, + kernel=uname().release, + python=python_version(), + telethon=telethon_version.__version__, + redis="Connected" if redis_status() else "Disconnected", + memory_total=round(memory_total/1048576, 2), + memory_available=round(memory_available/1048576, 2), + memory_available_percentage=memory_available_percentage, + memory_free=round(memory_free/1048576, 2), + memory_free_percentage=memory_free_percentage, + memory_buffered=round(memory_buffered/1048576, 2), + memory_buffered_percentage=memory_buffered_percentage, + memory_cached=round(memory_cached/1048576, 2), + memory_cached_percentage=memory_cached_percentage) + + +@app.errorhandler(404) +def no_such_file_or_directory(exception): + logs.debug(exception) + return render_template('pages/404.html') + + +@app.errorhandler(500) +def internal_server_error(exception): + logs.error(exception) + return render_template('pages/500.html') diff --git a/pagermaid/listener.py b/pagermaid/listener.py new file mode 100644 index 0000000..15f8411 --- /dev/null +++ b/pagermaid/listener.py @@ -0,0 +1,92 @@ +""" PagerMaid event listener. """ + +from telethon import events +from telethon.errors import MessageTooLongError +from distutils2.util import strtobool +from traceback import format_exc +from time import gmtime, strftime, time +from sys import exc_info +from telethon.events import StopPropagation +from pagermaid import bot, config, help_messages +from pagermaid.utils import attach_log + + +def listener(**args): + """ Register an event listener. """ + command = args.get('command', None) + description = args.get('description', None) + parameters = args.get('parameters', None) + pattern = args.get('pattern', None) + diagnostics = args.get('diagnostics', True) + ignore_edited = args.get('ignore_edited', False) + if command is not None: + if command in help_messages: + raise ValueError(f"出错了呜呜呜 ~ 命令 \"{command}\" 已经被注册。") + pattern = fr"^-{command}(?: |$)([\s\S]*)" + if pattern is not None and not pattern.startswith('(?i)'): + args['pattern'] = f"(?i){pattern}" + else: + args['pattern'] = pattern + if 'ignore_edited' in args: + del args['ignore_edited'] + if 'command' in args: + del args['command'] + if 'diagnostics' in args: + del args['diagnostics'] + if 'description' in args: + del args['description'] + if 'parameters' in args: + del args['parameters'] + + def decorator(function): + + async def handler(context): + try: + try: + parameter = context.pattern_match.group(1).split(' ') + if parameter == ['']: + parameter = [] + context.parameter = parameter + context.arguments = context.pattern_match.group(1) + except BaseException: + context.parameter = None + context.arguments = None + await function(context) + except StopPropagation: + raise StopPropagation + except MessageTooLongError: + await context.edit("出错了呜呜呜 ~ 生成的输出太长,无法显示。") + except BaseException: + try: + await context.edit("出错了呜呜呜 ~ 执行此命令时发生错误。") + except BaseException: + pass + if not diagnostics: + return + if not strtobool(config['error_report']): + pass + report = f"# Generated: {strftime('%H:%M %d/%m/%Y', gmtime())}. \n" \ + f"# ChatID: {str(context.chat_id)}. \n" \ + f"# UserID: {str(context.sender_id)}. \n" \ + f"# Message: \n-----BEGIN TARGET MESSAGE-----\n" \ + f"{context.text}\n-----END TARGET MESSAGE-----\n" \ + f"# Traceback: \n-----BEGIN TRACEBACK-----\n" \ + f"{str(format_exc())}\n-----END TRACEBACK-----\n" \ + f"# Error: \"{str(exc_info()[1])}\". \n" + await attach_log(report, 503691334, f"exception.{time()}.pagermaid", None, "Error report generated.") + + if not ignore_edited: + bot.add_event_handler(handler, events.MessageEdited(**args)) + bot.add_event_handler(handler, events.NewMessage(**args)) + + return handler + + if description is not None and command is not None: + if parameters is None: + parameters = "" + help_messages.update({ + f"{command}": f"**使用:** `-{command} {parameters}`\ + \n{description}" + }) + + return decorator diff --git a/pagermaid/modules/__init__.py b/pagermaid/modules/__init__.py new file mode 100644 index 0000000..c6754ba --- /dev/null +++ b/pagermaid/modules/__init__.py @@ -0,0 +1,49 @@ +""" PagerMaid module and plugin init. """ + +from os.path import dirname, basename, isfile, exists +from os import getcwd, makedirs +from glob import glob +from pagermaid import logs + + +def __list_modules(): + module_paths = glob(dirname(__file__) + "/*.py") + result = [ + basename(file)[:-3] + for file in module_paths + if isfile(file) and file.endswith(".py") and not file.endswith("__init__.py") + ] + return result + + +def __list_plugins(): + plugin_paths = glob(f"{getcwd()}/plugins" + "/*.py") + if not exists(f"{getcwd()}/plugins"): + makedirs(f"{getcwd()}/plugins") + result = [ + basename(file)[:-3] + for file in plugin_paths + if isfile(file) and file.endswith(".py") and not file.endswith("__init__.py") + ] + return result + + +module_list_string = "" +plugin_list_string = "" + +for module in sorted(__list_modules()): + module_list_string += f"{module}, " + +module_list_string = module_list_string[:-2] + +for plugin in sorted(__list_plugins()): + plugin_list_string += f"{plugin}, " + +plugin_list_string = plugin_list_string[:-2] + +module_list = sorted(__list_modules()) +plugin_list = sorted(__list_plugins()) +logs.info("Loading modules: %s", module_list_string) +if len(plugin_list) > 0: + logs.info("Loading plugins: %s", plugin_list_string) +__all__ = __list_modules() + ["module_list"] + __list_plugins() + ["plugin_list"] diff --git a/pagermaid/modules/account.py b/pagermaid/modules/account.py new file mode 100644 index 0000000..8e41c04 --- /dev/null +++ b/pagermaid/modules/account.py @@ -0,0 +1,232 @@ +""" This module contains utils to configure your account. """ + +from os import remove +from telethon.errors import ImageProcessFailedError, PhotoCropSizeSmallError +from telethon.errors.rpcerrorlist import PhotoExtInvalidError, UsernameOccupiedError, AboutTooLongError, \ + FirstNameInvalidError, UsernameInvalidError +from telethon.tl.functions.account import UpdateProfileRequest, UpdateUsernameRequest +from telethon.tl.functions.photos import DeletePhotosRequest, GetUserPhotosRequest, UploadProfilePhotoRequest +from telethon.tl.functions.users import GetFullUserRequest +from telethon.tl.types import InputPhoto, MessageMediaPhoto, MessageEntityMentionName +from struct import error as StructError +from pagermaid import bot, log +from pagermaid.listener import listener + + +@listener(outgoing=True, command="username", + description="通过命令快捷设置 username", + parameters="") +async def username(context): + """ 重新配置您的用户名。 """ + if len(context.parameter) > 1: + await context.edit("无效的参数。") + if len(context.parameter) == 1: + result = context.parameter[0] + else: + result = "" + try: + await bot(UpdateUsernameRequest(result)) + except UsernameOccupiedError: + await context.edit("用户名已存在。") + return + except UsernameInvalidError: + await context.edit("出错了呜呜呜 ~ 您好像输入了一个无效的用户名。") + return + await context.edit("用户名设置完毕。") + if result == "": + await log("用户名已被取消。") + return + await log(f"用户名已被设置为 `{result}`.") + + +@listener(outgoing=True, command="name", + description="换个显示名称。", + parameters=" ") +async def name(context): + """ Updates your display name. """ + if len(context.parameter) == 2: + first_name = context.parameter[0] + last_name = context.parameter[1] + elif len(context.parameter) == 1: + first_name = context.parameter[0] + last_name = " " + else: + await context.edit("无效的参数。") + return + try: + await bot(UpdateProfileRequest( + first_name=first_name, + last_name=last_name)) + except FirstNameInvalidError: + await context.edit("出错了呜呜呜 ~ 您好像输入了一个无效的 first name.") + return + await context.edit("显示名称已成功更改。") + if last_name != " ": + await log(f"显示名称已被更改为 `{first_name} {last_name}`.") + else: + await log(f"显示名称已被更改为 `{first_name}`.") + + +@listener(outgoing=True, command="pfp", + description="回复某条带附件的消息然后把它变成咱的头像") +async def pfp(context): + """ Sets your profile picture. """ + reply = await context.get_reply_message() + photo = None + await context.edit("设置头像中 . . .") + if reply.media: + if isinstance(reply.media, MessageMediaPhoto): + photo = await bot.download_media(message=reply.photo) + elif "image" in reply.media.document.mime_type.split('/'): + photo = await bot.download_file(reply.media.document) + else: + await context.edit("出错了呜呜呜 ~ 无法将附件解析为图片。") + + if photo: + try: + await bot(UploadProfilePhotoRequest( + await bot.upload_file(photo) + )) + remove(photo) + await context.edit("头像修改成功啦 ~") + except PhotoCropSizeSmallError: + await context.edit("出错了呜呜呜 ~ 图像尺寸小于最小要求。") + except ImageProcessFailedError: + await context.edit("出错了呜呜呜 ~ 服务器解释命令时发生错误。") + except PhotoExtInvalidError: + await context.edit("出错了呜呜呜 ~ 无法将附件解析为图片。") + + +@listener(outgoing=True, command="bio", + description="设置咱的公开情报", + parameters="") +async def bio(context): + """ Sets your bio. """ + try: + await bot(UpdateProfileRequest(about=context.arguments)) + except AboutTooLongError: + await context.edit("出错了呜呜呜 ~ 情报太长啦") + return + await context.edit("此情报公成功啦") + if context.arguments == "": + await log("公开的情报成功关闭啦") + return + await log(f"公开的情报已被设置为 `{context.arguments}`.") + + +@listener(outgoing=True, command="rmpfp", + description="删除指定数量的头像", + parameters="<整数>") +async def rmpfp(context): + """ Removes your profile picture. """ + group = context.text[8:] + if group == 'all': + limit = 0 + elif group.isdigit(): + limit = int(group) + else: + limit = 1 + + pfp_list = await bot(GetUserPhotosRequest( + user_id=context.from_id, + offset=0, + max_id=0, + limit=limit)) + input_photos = [] + for sep in pfp_list.photos: + input_photos.append( + InputPhoto( + id=sep.id, + access_hash=sep.access_hash, + file_reference=sep.file_reference + ) + ) + await bot(DeletePhotosRequest(id=input_photos)) + await context.edit(f"`删除了 {len(input_photos)} 张头像。`") + + +@listener(outgoing=True, command="profile", + description="生成一个用户简介 ~ 消息有点长", + parameters="") +async def profile(context): + """ Queries profile of a user. """ + if len(context.parameter) > 1: + await context.edit("无效的参数。") + return + + await context.edit("正在生成用户简介摘要 . . .") + if context.reply_to_msg_id: + reply_message = await context.get_reply_message() + user_id = reply_message.from_id + target_user = await context.client(GetFullUserRequest(user_id)) + else: + if len(context.parameter) == 1: + user = context.parameter[0] + if user.isnumeric(): + user = int(user) + else: + user_object = await context.client.get_me() + user = user_object.id + if context.message.entities is not None: + if isinstance(context.message.entities[0], MessageEntityMentionName): + return await context.client(GetFullUserRequest(context.message.entities[0].user_id)) + try: + user_object = await context.client.get_entity(user) + target_user = await context.client(GetFullUserRequest(user_object.id)) + except (TypeError, ValueError, OverflowError, StructError) as exception: + if str(exception).startswith("出错了呜呜呜 ~ 找不到与之对应的任何内容"): + await context.edit("出错了呜呜呜 ~ 指定的用户不存在。") + return + if str(exception).startswith("出错了呜呜呜 ~ 没有用户"): + await context.edit("出错了呜呜呜 ~ 指定的用户名不存在。") + return + if str(exception).startswith("出错了呜呜呜 ~ 您确定输入了东西?") or isinstance(exception, StructError): + await context.edit("出错了呜呜呜 ~ 指定的UserID与用户不对应。") + return + if isinstance(exception, OverflowError): + await context.edit("出错了呜呜呜 ~ 指定的 UserID 已超出整数限制,您确定输对了?") + return + raise exception + user_type = "Bot" if target_user.user.bot else "User" + username_system = f"@{target_user.user.username}" if target_user.user.username is not None else ( + "This user have not yet defined their username.") + first_name = target_user.user.first_name.replace("\u2060", "") + last_name = target_user.user.last_name.replace("\u2060", "") if target_user.user.last_name is not None else ( + "This user did not define a last name." + ) + biography = target_user.about if target_user.about is not None else "This user did not define a biography string." + caption = f"**Profile:** \n" \ + f"Username: {username_system} \n" \ + f"UserID: {target_user.user.id} \n" \ + f"First Name: {first_name} \n" \ + f"Last Name: {last_name} \n" \ + f"Biography: {biography} \n" \ + f"Common Groups: {target_user.common_chats_count} \n" \ + f"Verified: {target_user.user.verified} \n" \ + f"Restricted: {target_user.user.restricted} \n" \ + f"Type: {user_type} \n" \ + f"Permanent Link: [{first_name}](tg://user?id={target_user.user.id})" + reply_to = context.message.reply_to_msg_id + photo = await context.client.download_profile_photo( + target_user.user.id, + "./" + str(target_user.user.id) + ".jpg", + download_big=True + ) + if not reply_to: + reply_to = None + try: + await context.client.send_file( + context.chat_id, + photo, + caption=caption, + link_preview=False, + force_document=False, + reply_to=reply_to + ) + if not photo.startswith("http"): + remove(photo) + await context.delete() + return + except TypeError: + await context.edit(caption) + remove(photo) diff --git a/pagermaid/modules/avoid.py b/pagermaid/modules/avoid.py new file mode 100644 index 0000000..f05f6d7 --- /dev/null +++ b/pagermaid/modules/avoid.py @@ -0,0 +1,94 @@ +""" PagerMaid module for different ways to avoid users. """ + +from pagermaid import redis, log, redis_status +from pagermaid.listener import listener + + +@listener(outgoing=True, command="ghost", + description="开启对话的自动已读,需要 Redis", + parameters="") +async def ghost(context): + """ Toggles ghosting of a user. """ + if not redis_status(): + await context.edit("出错了呜呜呜 ~ Redis 离线,无法运行。") + return + if len(context.parameter) != 1: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + myself = await context.client.get_me() + self_user_id = myself.id + if context.parameter[0] == "true": + if context.chat_id == self_user_id: + await context.edit("在?为什么要在收藏夹里面用?") + return + redis.set("ghosted.chat_id." + str(context.chat_id), "true") + await context.delete() + await log(f"ChatID {str(context.chat_id)} 已被添加到自动已读对话列表中。") + elif context.parameter[0] == "false": + if context.chat_id == self_user_id: + await context.edit("在?为什么要在收藏夹里面用?") + return + redis.delete("ghosted.chat_id." + str(context.chat_id)) + await context.delete() + await log(f"ChatID {str(context.chat_id)} 已从自动已读对话列表中移除。") + elif context.parameter[0] == "status": + if redis.get("ghosted.chat_id." + str(context.chat_id)): + await context.edit("emm...当前对话已被加入自动已读对话列表中。") + else: + await context.edit("emm...当前对话已从自动已读对话列表中移除。") + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + + +@listener(outgoing=True, command="deny", + description="切换拒绝聊天功能,需要 redis", + parameters="") +async def deny(context): + """ Toggles denying of a user. """ + if not redis_status(): + await context.edit("出错了呜呜呜 ~ Redis 离线,无法运行。") + return + if len(context.parameter) != 1: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + myself = await context.client.get_me() + self_user_id = myself.id + if context.parameter[0] == "true": + if context.chat_id == self_user_id: + await context.edit("在?为什么要在收藏夹里面用?") + return + redis.set("denied.chat_id." + str(context.chat_id), "true") + await context.delete() + await log(f"ChatID {str(context.chat_id)} 已被添加到自动拒绝对话列表中。") + elif context.parameter[0] == "false": + if context.chat_id == self_user_id: + await context.edit("在?为什么要在收藏夹里面用?") + return + redis.delete("denied.chat_id." + str(context.chat_id)) + await context.delete() + await log(f"ChatID {str(context.chat_id)} 已从自动拒绝对话列表中移除。") + elif context.parameter[0] == "status": + if redis.get("denied.chat_id." + str(context.chat_id)): + await context.edit("emm...当前对话已被加入自动拒绝对话列表中。") + else: + await context.edit("emm...当前对话已从自动拒绝对话列表移除。") + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + + +@listener(incoming=True, ignore_edited=True) +async def set_read_acknowledgement(context): + """ Event handler to infinitely read ghosted messages. """ + if not redis_status(): + return + if redis.get("ghosted.chat_id." + str(context.chat_id)): + await context.client.send_read_acknowledge(context.chat_id) + + +@listener(incoming=True, ignore_edited=True) +async def message_removal(context): + """ Event handler to infinitely delete denied messages. """ + if not redis_status(): + return + if redis.get("denied.chat_id." + str(context.chat_id)): + await context.delete() diff --git a/pagermaid/modules/captions.py b/pagermaid/modules/captions.py new file mode 100644 index 0000000..8582073 --- /dev/null +++ b/pagermaid/modules/captions.py @@ -0,0 +1,176 @@ +""" PagerMaid module for adding captions to image. """ + +from os import remove +from magic import Magic +from pygments import highlight as syntax_highlight +from pygments.formatters import img +from pygments.lexers import guess_lexer +from pagermaid import log, module_dir +from pagermaid.listener import listener +from pagermaid.utils import execute, upload_attachment + + +@listener(outgoing=True, command="convert", + description="回复某附件消息然后转换为图片输出") +async def convert(context): + """ Converts image to png. """ + reply = await context.get_reply_message() + await context.edit("正在转换中 . . .") + target_file_path = await context.download_media() + reply_id = context.reply_to_msg_id + if reply: + target_file_path = await context.client.download_media( + await context.get_reply_message() + ) + if target_file_path is None: + await context.edit("出错了呜呜呜 ~ 回复的消息中好像没有附件。") + result = await execute(f"{module_dir}/assets/caption.sh \"" + target_file_path + + "\" result.png" + " \"" + str("") + + "\" " + "\"" + str("") + "\"") + if not result: + await handle_failure(context, target_file_path) + return + if not await upload_attachment("result.png", context.chat_id, reply_id): + await context.edit("出错了呜呜呜 ~ 转换期间发生了错误。") + remove(target_file_path) + return + await context.delete() + remove(target_file_path) + remove("result.png") + + +@listener(outgoing=True, command="caption", + description="将两行字幕添加到回复的图片中,字幕将分别添加到顶部和底部,字幕需要以逗号分隔。", + parameters=", ") +async def caption(context): + """ Generates images with captions. """ + await context.edit("正在渲染图像 . . .") + if context.arguments: + if ',' in context.arguments: + string_1, string_2 = context.arguments.split(',', 1) + else: + string_1 = context.arguments + string_2 = " " + else: + await context.edit("出错了呜呜呜 ~ 错误的语法。") + return + reply = await context.get_reply_message() + target_file_path = await context.download_media() + reply_id = context.reply_to_msg_id + if reply: + target_file_path = await context.client.download_media( + await context.get_reply_message() + ) + if target_file_path is None: + await context.edit("出错了呜呜呜 ~ 目标消息中没有附件") + if not target_file_path.endswith(".mp4"): + result = await execute(f"{module_dir}/assets/caption.sh \"{target_file_path}\" " + f"{module_dir}/assets/Impact-Regular.ttf " + f"\"{str(string_1)}\" \"{str(string_2)}\"") + result_file = "result.png" + else: + result = await execute(f"{module_dir}/assets/caption-gif.sh \"{target_file_path}\" " + f"{module_dir}/assets/Impact-Regular.ttf " + f"\"{str(string_1)}\" \"{str(string_2)}\"") + result_file = "result.gif" + if not result: + await handle_failure(context, target_file_path) + return + if not await upload_attachment(result_file, context.chat_id, reply_id): + await context.edit("出错了呜呜呜 ~ 转换期间发生了错误。") + remove(target_file_path) + return + await context.delete() + if string_2 != " ": + message = string_1 + "` 和 `" + string_2 + else: + message = string_1 + remove(target_file_path) + remove(result_file) + await log(f"字幕 `{message}` 添加到了一张图片.") + + +@listener(outgoing=True, command="ocr", + description="从回复的图片中提取文本") +async def ocr(context): + """ Extracts texts from images. """ + reply = await context.get_reply_message() + await context.edit("`正在处理图片,请稍候 . . .`") + if reply: + target_file_path = await context.client.download_media( + await context.get_reply_message() + ) + else: + target_file_path = await context.download_media() + if target_file_path is None: + await context.edit("`出错了呜呜呜 ~ 回复的消息中没有附件。`") + return + result = await execute(f"tesseract {target_file_path} stdout") + if not result: + await context.edit("`出错了呜呜呜 ~ 请向原作者报告此问题。`") + try: + remove(target_file_path) + except FileNotFoundError: + pass + return + success = False + if result == "/bin/sh: fbdump: command not found": + await context.edit("出错了呜呜呜 ~ 您好像少安装了个包?") + else: + result = await execute(f"tesseract {target_file_path} stdout", False) + await context.edit(f"**以下是提取到的文字: **\n{result}") + success = True + remove(target_file_path) + if not success: + return + + +@listener(outgoing=True, command="highlight", + description="生成有语法高亮显示的图片。", + parameters="") +async def highlight(context): + """ Generates syntax highlighted images. """ + if context.fwd_from: + return + reply = await context.get_reply_message() + reply_id = None + await context.edit("正在渲染图片,请稍候 . . .") + if reply: + reply_id = reply.id + target_file_path = await context.client.download_media( + await context.get_reply_message() + ) + if target_file_path is None: + message = reply.text + else: + if Magic(mime=True).from_file(target_file_path) != 'text/plain': + message = reply.text + else: + with open(target_file_path, 'r') as file: + message = file.read() + remove(target_file_path) + else: + if context.arguments: + message = context.arguments + else: + await context.edit("`出错了呜呜呜 ~ 无法检索目标消息。`") + return + lexer = guess_lexer(message) + formatter = img.JpgImageFormatter(style="colorful") + result = syntax_highlight(message, lexer, formatter, outfile=None) + await context.edit("正在上传图片 . . .") + await context.client.send_file( + context.chat_id, + result, + reply_to=reply_id + ) + await context.delete() + + +async def handle_failure(context, target_file_path): + await context.edit("出错了呜呜呜 ~ 请向原作者报告此问题。") + try: + remove("result.png") + remove(target_file_path) + except FileNotFoundError: + pass diff --git a/pagermaid/modules/clock.py b/pagermaid/modules/clock.py new file mode 100644 index 0000000..2643a81 --- /dev/null +++ b/pagermaid/modules/clock.py @@ -0,0 +1,68 @@ +""" This module handles world clock related utility. """ + +from datetime import datetime +from pytz import country_names, country_timezones, timezone +from pagermaid import config +from pagermaid.listener import listener + + +@listener(outgoing=True, command="time", + description="显示特定区域的时间,如果参数为空,则默认显示配置值。", + parameters="<地区>") +async def time(context): + """ For querying time. """ + if len(context.parameter) > 1: + context.edit("出错了呜呜呜 ~ 无效的参数。") + if len(context.parameter) == 1: + country = context.parameter[0].title() + else: + country = config['application_region'] + time_form = "%I:%M %p" + date_form = "%A %d/%m/%y" + if not country: + time_zone = await get_timezone(config['application_region']) + await context.edit( + f"**Time in {config['application_region']}**\n" + f"`{datetime.now(time_zone).strftime(date_form)} " + f"{datetime.now(time_zone).strftime(time_form)}`" + ) + return + + time_zone = await get_timezone(country) + if not time_zone: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + + try: + country_name = country_names[country] + except KeyError: + country_name = country + + await context.edit(f"**{country_name} 时间:**\n" + f"`{datetime.now(time_zone).strftime(date_form)} " + f"{datetime.now(time_zone).strftime(time_form)}`") + + +async def get_timezone(target): + """ Returns timezone of the parameter in command. """ + if "(Uk)" in target: + target = target.replace("Uk", "UK") + if "(Us)" in target: + target = target.replace("Us", "US") + if " Of " in target: + target = target.replace(" Of ", " of ") + if "(Western)" in target: + target = target.replace("(Western)", "(western)") + if "Minor Outlying Islands" in target: + target = target.replace("Minor Outlying Islands", "minor outlying islands") + if "Nl" in target: + target = target.replace("Nl", "NL") + + for country_code in country_names: + if target == country_names[country_code]: + return timezone(country_timezones[country_code][0]) + try: + if country_names[target]: + return timezone(country_timezones[target][0]) + except KeyError: + return diff --git a/pagermaid/modules/external.py b/pagermaid/modules/external.py new file mode 100644 index 0000000..b4da259 --- /dev/null +++ b/pagermaid/modules/external.py @@ -0,0 +1,230 @@ +""" PagerMaid features that uses external HTTP APIs other than Telegram. """ + +from googletrans import Translator, LANGUAGES +from os import remove +from urllib import request, parse +from math import ceil +from time import sleep +from threading import Thread +from bs4 import BeautifulSoup +from gtts import gTTS +from re import compile as regex_compile +from re import search, sub +from collections import deque +from pagermaid import log +from pagermaid.listener import listener, config +from pagermaid.utils import clear_emojis, attach_log, fetch_youtube_audio + + +@listener(outgoing=True, command="translate", + description="通过Google翻译将目标消息翻译成指定的语言。", + parameters="") +async def translate(context): + """ PagerMaid universal translator. """ + translator = Translator() + reply = await context.get_reply_message() + message = context.arguments + lang = config['application_language'] + if message: + pass + elif reply: + message = reply.text + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + + try: + await context.edit("生成翻译中 . . .") + result = translator.translate(clear_emojis(message), dest=lang) + except ValueError: + await context.edit("出错了呜呜呜 ~ 找不到目标语言,请更正配置文件中的错误。") + return + + source_lang = LANGUAGES[f'{result.src.lower()}'] + trans_lang = LANGUAGES[f'{result.dest.lower()}'] + result = f"**Translated** from {source_lang.title()}:\n{result.text}" + + if len(result) > 4096: + await context.edit("出错了呜呜呜 ~ 输出超出 TG 限制,正在附加文件。") + await attach_log(result, context.chat_id, "translation.txt", context.id) + return + await context.edit(result) + if len(result) <= 4096: + await log(f"Translated `{message}` from {source_lang} to {trans_lang}.") + else: + await log(f"Translated message from {source_lang} to {trans_lang}.") + + +@listener(outgoing=True, command="tts", + description="通过 Google文本到语音 基于字符串生成语音消息。", + parameters="") +async def tts(context): + """ Send TTS stuff as voice message. """ + reply = await context.get_reply_message() + message = context.arguments + lang = config['application_language'] + if message: + pass + elif reply: + message = reply.text + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + + try: + await context.edit("生成语音中 . . .") + gTTS(message, lang) + except AssertionError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + except ValueError: + await context.edit('出错了呜呜呜 ~ 找不到目标语言,请更正配置文件中的错误。') + return + except RuntimeError: + await context.edit('出错了呜呜呜 ~ 加载语言数组时出错。') + return + google_tts = gTTS(message, lang) + google_tts.save("vocals.mp3") + with open("vocals.mp3", "rb") as audio: + line_list = list(audio) + line_count = len(line_list) + if line_count == 1: + google_tts = gTTS(message, lang) + google_tts.save("vocals.mp3") + with open("vocals.mp3", "r"): + await context.client.send_file(context.chat_id, "vocals.mp3", voice_note=True) + remove("vocals.mp3") + if len(message) <= 4096: + await log(f"生成了一条文本到语音的音频消息 : `{message}`.") + else: + await log("生成了一条文本到语音的音频消息。") + await context.delete() + + +@listener(outgoing=True, command="google", + description="使用 Google 查询", + parameters="") +async def google(context): + """ Searches Google for a string. """ + if context.arguments == "": + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + query = context.arguments + await context.edit("正在拉取结果 . . .") + search_results = GoogleSearch().search(query=query) + results = "" + count = 0 + for result in search_results.results: + if count == int(config['result_length']): + break + count += 1 + title = result.title + link = result.url + desc = result.text + results += f"\n[{title}]({link}) \n`{desc}`\n" + await context.edit(f"**Google** |`{query}`| 🎙 🔍 \n" + f"{results}", + link_preview=False) + await log(f"在Google搜索引擎上查询了 `{query}`") + + +@listener(outgoing=True, command="fetchaudio", + description="从多个平台获取音频文件。", + parameters="") +async def fetchaudio(context): + """ Fetches audio from provided URL. """ + url = context.arguments + reply = await context.get_reply_message() + reply_id = None + await context.edit("拉取音频中 . . .") + if reply: + reply_id = reply.id + if url is None: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + youtube_pattern = regex_compile(r"^(http(s)?://)?((w){3}.)?youtu(be|.be)?(\.com)?/.+") + if youtube_pattern.match(url): + if not await fetch_youtube_audio(url, context.chat_id, reply_id): + await context.edit("出错了呜呜呜 ~ 原声带下载失败。") + await log(f"从链接中获取了一条音频,链接: {url}.") + + +class GoogleSearch: + USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0" + SEARCH_URL = "https://google.com/search" + RESULT_SELECTOR = "div.r > a" + TOTAL_SELECTOR = "#resultStats" + RESULTS_PER_PAGE = 10 + DEFAULT_HEADERS = [ + ('User-Agent', USER_AGENT), + ("Accept-Language", "en-US,en;q=0.5"), + ] + + def search(self, query, num_results=10, prefetch_pages=True, prefetch_threads=10): + search_results = [] + pages = int(ceil(num_results / float(GoogleSearch.RESULTS_PER_PAGE))) + fetcher_threads = deque([]) + total = None + for i in range(pages): + start = i * GoogleSearch.RESULTS_PER_PAGE + opener = request.build_opener() + opener.addheaders = GoogleSearch.DEFAULT_HEADERS + response = opener.open(GoogleSearch.SEARCH_URL + "?q=" + parse.quote(query) + ("" if start == 0 else ( + "&start=" + str(start)))) + soup = BeautifulSoup(response.read(), "lxml") + response.close() + if total is None: + total_text = soup.select(GoogleSearch.TOTAL_SELECTOR)[0].children.__next__() + total = int(sub("[', ]", "", search("(([0-9]+[', ])*[0-9]+)", total_text).group(1))) + results = self.parse_results(soup.select(GoogleSearch.RESULT_SELECTOR)) + if len(search_results) + len(results) > num_results: + del results[num_results - len(search_results):] + search_results += results + if prefetch_pages: + for result in results: + while True: + running = 0 + for thread in fetcher_threads: + if thread.is_alive(): + running += 1 + if running < prefetch_threads: + break + sleep(1) + fetcher_thread = Thread(target=result.get_text) + fetcher_thread.start() + fetcher_threads.append(fetcher_thread) + for thread in fetcher_threads: + thread.join() + return SearchResponse(search_results, total) + + @staticmethod + def parse_results(results): + search_results = [] + for result in results: + url = result["href"] + title = result.find_all('h3')[0].text + text = result.parent.parent.find_all('div', {'class': 's'})[0].text + search_results.append(SearchResult(title, url, text)) + return search_results + + +class SearchResponse: + def __init__(self, results, total): + self.results = results + self.total = total + + +class SearchResult: + def __init__(self, title, url, text): + self.title = title + self.url = url + self.text = text + + def get_text(self): + return self.text + + def __str__(self): + return str(self.__dict__) + + def __repr__(self): + return self.__str__() diff --git a/pagermaid/modules/fun.py b/pagermaid/modules/fun.py new file mode 100644 index 0000000..b7f865b --- /dev/null +++ b/pagermaid/modules/fun.py @@ -0,0 +1,354 @@ +""" Fun related chat utilities. """ + +from asyncio import sleep +from random import choice, random, randint, randrange, seed +from telethon.errors.rpcerrorlist import MessageNotModifiedError +from cowpy import cow +from pagermaid import module_dir +from pagermaid.listener import listener +from pagermaid.utils import owoify, execute, random_gen, obtain_message + + +@listener(outgoing=True, command="animate", + description="使用消息制作文本动画。", + parameters="") +async def animate(context): + """ Make a text animation using a message. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + interval = 0.3 + words = message.split(" ") + count = 0 + buffer = "" + while count != len(words): + await sleep(interval) + buffer = f"{buffer} {words[count]}" + await context.edit(buffer) + count += 1 + + +@listener(outgoing=True, command="teletype", + description="通过编辑消息来制作打字动画。", + parameters="") +async def teletype(context): + """ Makes a typing animation via edits to the message. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + interval = 0.03 + cursor = "█" + buffer = '' + await context.edit(cursor) + await sleep(interval) + for character in message: + buffer = f"{buffer}{character}" + buffer_commit = f"{buffer}{cursor}" + await context.edit(buffer_commit) + await sleep(interval) + await context.edit(buffer) + await sleep(interval) + + +@listener(outgoing=True, command="mock", + description="通过怪异的大写字母来嘲笑人们。", + parameters="") +async def mock(context): + """ Mock people with weird capitalization. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + result = mocker(message) + reply = await context.get_reply_message() + await context.edit(result) + if reply: + if reply.sender.is_self: + try: + await reply.edit(result) + except MessageNotModifiedError: + await context.edit("A rare event of two mocking messages being the same just occurred.") + return + await context.delete() + + +@listener(outgoing=True, command="widen", + description="加宽字符串中的每个字符。", + parameters="") +async def widen(context): + """ Widens every character in a string. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + wide_map = dict((i, i + 0xFEE0) for i in range(0x21, 0x7F)) + wide_map[0x20] = 0x3000 + result = str(message).translate(wide_map) + reply = await context.get_reply_message() + await context.edit(result) + if reply: + if reply.sender.is_self: + try: + await reply.edit(result) + except MessageNotModifiedError: + await context.edit("此消息已被加宽。") + return + await context.delete() + + +@listener(outgoing=True, command="fox", + description="使用狐狸来让您的消息看起来不那么完整", + parameters="") +async def fox(context): + """ Makes a fox scratch your message. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + result = corrupt(" ".join(message).lower()) + await edit_reply(result, context) + + +@listener(outgoing=True, command="owo", + description="将消息转换为OwO。", + parameters="") +async def owo(context): + """ Makes messages become OwO. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + result = owoify(message) + await edit_reply(result, context) + + +@listener(outgoing=True, command="flip", + description="翻转消息。", + parameters="") +async def flip(context): + """ Flip flops the message. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + result = message[::-1] + await edit_reply(result, context) + + +@listener(outgoing=True, command="ship", + description="生成随机基友,也支持指定目标。", + parameters=" ") +async def ship(context): + """ Ship randomly generated members. """ + await context.edit("生成基友中 . . .") + if len(context.parameter) == 0: + users = [] + async for user in context.client.iter_participants(context.chat_id): + users.append(user) + target_1 = choice(users) + target_2 = choice(users) + if len(users) == 1: + target_1 = users[0] + target_2 = await context.client.get_me() + elif len(context.parameter) == 1: + users = [] + user_expression = int(context.parameter[0]) if context.parameter[0].isnumeric() else context.parameter[0] + try: + target_1 = await context.client.get_entity(user_expression) + except BaseException: + await context.edit("出错了呜呜呜 ~ 获取用户时出错。") + return + async for user in context.client.iter_participants(context.chat_id): + users.append(user) + target_2 = choice(users) + elif len(context.parameter) == 2: + user_expression_1 = int(context.parameter[0]) if context.parameter[0].isnumeric() else context.parameter[0] + user_expression_2 = int(context.parameter[1]) if context.parameter[1].isnumeric() else context.parameter[1] + try: + target_1 = await context.client.get_entity(user_expression_1) + target_2 = await context.client.get_entity(user_expression_2) + except BaseException: + await context.edit("出错了呜呜呜 ~ 获取用户时出错。") + return + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + await context.edit(f"**恭喜两位(((**\n" + f"[{target_1.first_name}](tg://user?id={target_1.id}) + " + f"[{target_2.first_name}](tg://user?id={target_2.id}) = ❤️") + + +@listener(outgoing=True, command="rng", + description="生成具有特定长度的随机字符串。", + parameters="") +async def rng(context): + """ Generates a random string with a specific length. """ + if len(context.parameter) == 0: + await context.edit(await random_gen("A-Za-z0-9")) + return + if len(context.parameter) == 1: + try: + await context.edit(await random_gen("A-Za-z0-9", int(context.parameter[0]))) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + await context.edit("出错了呜呜呜 ~ 无效的参数。") + + +@listener(outgoing=True, command="aaa", + description="发送一条包含 a 和 A 的消息", + parameters="") +async def aaa(context): + """ Saves a few presses of the A and shift key. """ + if len(context.parameter) == 0: + await context.edit(await random_gen("Aa")) + return + if len(context.parameter) == 1: + try: + await context.edit(await random_gen("Aa", int(context.parameter[0]))) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + await context.edit("出错了呜呜呜 ~ 无效的参数。") + + +@listener(outgoing=True, command="asciiart", + description="为指定的字符串生成ASCII文字。", + parameters="") +async def asciiart(context): + """ Generates ASCII art of specified string. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + result = await execute(f"figlet -f {module_dir}/assets/graffiti.flf '{message}'") + await context.edit(f"```\n{result}\n```") + + +@listener(outgoing=True, command="tuxsay", + description="生成一条看起来像企鹅说话的 ASCII 艺术消息", + parameters="") +async def tuxsay(context): + """ Generates ASCII art of Tux saying a specific message. """ + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + result = cow.Tux().milk(message) + await context.edit(f"```\n{result}\n```") + + +@listener(outgoing=True, command="coin", + description="扔硬币。") +async def coin(context): + """ Throws a coin. """ + await context.edit("扔硬币中 . . .") + await sleep(.5) + outcomes = ['A'] * 5 + ['B'] * 5 + ['C'] * 1 + result = choice(outcomes) + count = 0 + while count <= 3: + await context.edit("`.` . .") + await sleep(.3) + await context.edit(". `.` .") + await sleep(.3) + await context.edit(". . `.`") + await sleep(.3) + count += 1 + if result == "C": + await context.edit("我丢了硬币") + elif result == "B": + await context.edit("Tails!") + elif result == "A": + await context.edit("Heads!") + + +def mocker(text, diversity_bias=0.5, random_seed=None): + """ Randomizes case in a string. """ + if diversity_bias < 0 or diversity_bias > 1: + raise ValueError('diversity_bias must be between the inclusive range [0,1]') + seed(random_seed) + out = '' + last_was_upper = True + swap_chance = 0.5 + for c in text: + if c.isalpha(): + if random() < swap_chance: + last_was_upper = not last_was_upper + swap_chance = 0.5 + c = c.upper() if last_was_upper else c.lower() + swap_chance += (1 - swap_chance) * diversity_bias + out += c + return out + + +def corrupt(text): + """ Summons fox to scratch strings. """ + num_accents_up = (1, 3) + num_accents_down = (1, 3) + num_accents_middle = (1, 2) + max_accents_per_letter = 3 + dd = ['̖', ' ̗', ' ̘', ' ̙', ' ̜', ' ̝', ' ̞', ' ̟', ' ̠', ' ̤', ' ̥', ' ̦', ' ̩', ' ̪', ' ̫', ' ̬', ' ̭', ' ̮', + ' ̯', ' ̰', ' ̱', ' ̲', ' ̳', ' ̹', ' ̺', ' ̻', ' ̼', ' ͅ', ' ͇', ' ͈', ' ͉', ' ͍', ' ͎', ' ͓', ' ͔', ' ͕', + ' ͖', ' ͙', ' ͚', ' ', ] + du = [' ̍', ' ̎', ' ̄', ' ̅', ' ̿', ' ̑', ' ̆', ' ̐', ' ͒', ' ͗', ' ͑', ' ̇', ' ̈', ' ̊', ' ͂', ' ̓', ' ̈́', ' ͊', + ' ͋', ' ͌', ' ̃', ' ̂', ' ̌', ' ͐', ' ́', ' ̋', ' ̏', ' ̽', ' ̉', ' ͣ', ' ͤ', ' ͥ', ' ͦ', ' ͧ', ' ͨ', ' ͩ', + ' ͪ', ' ͫ', ' ͬ', ' ͭ', ' ͮ', ' ͯ', ' ̾', ' ͛', ' ͆', ' ̚', ] + dm = [' ̕', ' ̛', ' ̀', ' ́', ' ͘', ' ̡', ' ̢', ' ̧', ' ̨', ' ̴', ' ̵', ' ̶', ' ͜', ' ͝', ' ͞', ' ͟', ' ͠', ' ͢', + ' ̸', ' ̷', ' ͡', ] + letters = list(text) + new_letters = [] + + for letter in letters: + a = letter + + if not a.isalpha(): + new_letters.append(a) + continue + + num_accents = 0 + num_u = randint(num_accents_up[0], num_accents_up[1]) + num_d = randint(num_accents_down[0], num_accents_down[1]) + num_m = randint(num_accents_middle[0], num_accents_middle[1]) + while num_accents < max_accents_per_letter and num_u + num_m + num_d != 0: + rand_int = randint(0, 2) + if rand_int == 0: + if num_u > 0: + a = a.strip() + du[randrange(0, len(du))].strip() + num_accents += 1 + num_u -= 1 + elif rand_int == 1: + if num_d > 0: + a = a.strip() + dd[randrange(0, len(dd))].strip() + num_d -= 1 + num_accents += 1 + else: + if num_m > 0: + a = a.strip() + dm[randrange(0, len(dm))].strip() + num_m -= 1 + num_accents += 1 + + new_letters.append(a) + + new_word = ''.join(new_letters) + return new_word + + +async def edit_reply(result, context): + reply = await context.get_reply_message() + await context.edit(result) + if reply: + if reply.sender.is_self: + await reply.edit(result) + await context.delete() diff --git a/pagermaid/modules/help.py b/pagermaid/modules/help.py new file mode 100644 index 0000000..b8d3728 --- /dev/null +++ b/pagermaid/modules/help.py @@ -0,0 +1,22 @@ +""" The help module. """ + +from pagermaid import help_messages +from pagermaid.listener import listener + + +@listener(outgoing=True, command="help", + description="显示命令列表或单个命令的帮助。", + parameters="<命令>") +async def help(context): + """ The help command,""" + if context.arguments: + if context.arguments in help_messages: + await context.edit(str(help_messages[context.arguments])) + else: + await context.edit("无效的参数") + else: + result = "**命令列表: \n**" + for command in sorted(help_messages, reverse=False): + result += "`" + str(command) + result += "`, " + await context.edit(result[:-2] + "\n**发送 \"-help <命令>\" 以查看特定命令的帮助。**") diff --git a/pagermaid/modules/message.py b/pagermaid/modules/message.py new file mode 100644 index 0000000..f9930f5 --- /dev/null +++ b/pagermaid/modules/message.py @@ -0,0 +1,126 @@ +""" Message related utilities. """ + +from telethon.tl.functions.messages import DeleteChatUserRequest +from telethon.tl.functions.channels import LeaveChannelRequest +from telethon.errors.rpcerrorlist import ChatIdInvalidError +from distutils2.util import strtobool +from pagermaid import bot, log, config +from pagermaid.listener import listener + + +@listener(outgoing=True, command="userid", + description="查询您回复消息的发送者的用户ID。") +async def userid(context): + """ Query the UserID of the sender of the message you replied to. """ + message = await context.get_reply_message() + if message: + if not message.forward: + user_id = message.sender.id + if message.sender.username: + target = "@" + message.sender.username + else: + try: + target = "**" + message.sender.first_name + "**" + except TypeError: + target = "**" + "死号" + "**" + + else: + user_id = message.forward.sender.id + if message.forward.sender.username: + target = "@" + message.forward.sender.username + else: + target = "*" + message.forward.sender.first_name + "*" + await context.edit( + f"**Username:** {target} \n" + f"**UserID:** `{user_id}`" + ) + else: + await context.edit("出错了呜呜呜 ~ 无法获取目标消息的信息。") + + +@listener(outgoing=True, command="chatid", + description="查询当前会话的 chatid 。") +async def chatid(context): + """ Queries the chatid of the chat you are in. """ + await context.edit("ChatID: `" + str(context.chat_id) + "`") + + +@listener(outgoing=True, command="log", + description="转发一条消息到日志。", + parameters="") +async def log(context): + """ Forwards a message into log group """ + if strtobool(config['log']): + if context.reply_to_msg_id: + reply_msg = await context.get_reply_message() + await reply_msg.forward_to(int(config['log_chatid'])) + elif context.arguments: + await log(context.arguments) + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + await context.edit("已记录。") + else: + await context.edit("出错了呜呜呜 ~ 日志记录已禁用。") + + +@listener(outgoing=True, command="leave", + description="说 再见 然后离开会话。") +async def leave(context): + """ It leaves you from the group. """ + if context.is_group: + await context.edit("贵群真是浪费我的时间,再见。") + try: + await bot(DeleteChatUserRequest(chat_id=context.chat_id, + user_id=context.sender_id + )) + except ChatIdInvalidError: + await bot(LeaveChannelRequest(chatid)) + else: + await context.edit("出错了呜呜呜 ~ 当前聊天不是群聊。") + + +@listener(outgoing=True, command="meter2feet", + description="将米转换为英尺。", + parameters="") +async def meter2feet(context): + """ Convert meter to feet. """ + if not len(context.parameter) == 1: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + meter = float(context.parameter[0]) + feet = meter / .3048 + await context.edit(f"将 {str(meter)} 米装换为了 {str(feet)} 英尺。") + + +@listener(outgoing=True, command="feet2meter", + description="将英尺转换为米。", + parameters="") +async def feet2meter(context): + """ Convert feet to meter. """ + if not len(context.parameter) == 1: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + feet = float(context.parameter[0]) + meter = feet * .3048 + await context.edit(f"将 {str(feet)} 英尺转换为了 {str(meter)} 米。") + + +@listener(outgoing=True, command="source", + description="显示原始 PagerMaid git 存储库的URL。") +async def source(context): + """ Outputs the git repository URL. """ + await context.edit("https://git.stykers.moe/scm/~stykers/pagermaid.git") + + +@listener(outgoing=True, command="site", + description="显示原始 PagerMaid 项目主页的URL。") +async def site(context): + """ Outputs the site URL. """ + await context.edit("https://katonkeyboard.moe/pagermaid.html") + +@listener(outgoing=True, command="sources", + description="显示 PagerMaid-Modify 存储库的URL。") +async def sources(context): + """ Outputs the repository URL. """ + await context.edit("https://github.com/xtaodada/PagerMaid-Modify/") \ No newline at end of file diff --git a/pagermaid/modules/plugin.py b/pagermaid/modules/plugin.py new file mode 100644 index 0000000..245277c --- /dev/null +++ b/pagermaid/modules/plugin.py @@ -0,0 +1,148 @@ +""" PagerMaid module to manage plugins. """ + +from os import remove, rename, chdir, path +from os.path import exists +from shutil import copyfile, move +from glob import glob +from pagermaid import log, working_dir +from pagermaid.listener import listener +from pagermaid.utils import upload_attachment +from pagermaid.modules import plugin_list as active_plugins, __list_plugins + + +@listener(outgoing=True, command="plugin", diagnostics=False, + description="用于管理安装到 PagerMaid 的插件。", + parameters="{status|install|remove|enable|disable|upload} <插件名称/文件>") +async def plugin(context): + if len(context.parameter) > 2 or len(context.parameter) == 0: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + reply = await context.get_reply_message() + plugin_directory = f"{working_dir}/plugins/" + if context.parameter[0] == "install": + if len(context.parameter) == 1: + await context.edit("安装插件中 . . .") + if reply: + file_path = await context.client.download_media(reply) + else: + file_path = await context.download_media() + if file_path is None or not file_path.endswith('.py'): + await context.edit("出错了呜呜呜 ~ 无法从附件获取插件文件。") + try: + remove(str(file_path)) + except FileNotFoundError: + pass + return + if exists(f"{plugin_directory}{file_path}"): + remove(f"{plugin_directory}{file_path}") + move(file_path, plugin_directory) + elif exists(f"{plugin_directory}{file_path}.disabled"): + remove(f"{plugin_directory}{file_path}.disabled") + move(file_path, f"{plugin_directory}{file_path}.disabled") + else: + move(file_path, plugin_directory) + await context.edit(f"插件 {path.basename(file_path)[:-3]} 已安装,PagerMaid 正在重新启动。") + await log(f"成功安装插件 {path.basename(file_path)[:-3]}.") + await context.client.disconnect() + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + elif context.parameter[0] == "remove": + if len(context.parameter) == 2: + if exists(f"{plugin_directory}{context.parameter[1]}.py"): + remove(f"{plugin_directory}{context.parameter[1]}.py") + await context.edit(f"成功删除插件 {context.parameter[1]}, PagerMaid 正在重新启动。") + await log(f"删除插件 {context.parameter[1]}.") + await context.client.disconnect() + elif exists(f"{plugin_directory}{context.parameter[1]}.py.disabled"): + remove(f"{plugin_directory}{context.parameter[1]}.py.disabled") + await context.edit(f"已删除的插件 {context.parameter[1]}.") + await log(f"已删除的插件 {context.parameter[1]}.") + elif "/" in context.parameter[1]: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + else: + await context.edit("出错了呜呜呜 ~ 指定的插件不存在。") + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + elif context.parameter[0] == "status": + if len(context.parameter) == 1: + inactive_plugins = sorted(__list_plugins()) + disabled_plugins = [] + if not len(inactive_plugins) == 0: + for target_plugin in active_plugins: + inactive_plugins.remove(target_plugin) + chdir("plugins/") + for target_plugin in glob(f"*.py.disabled"): + disabled_plugins += [f"{target_plugin[:-12]}"] + chdir("../") + active_plugins_string = "" + inactive_plugins_string = "" + disabled_plugins_string = "" + for target_plugin in active_plugins: + active_plugins_string += f"{target_plugin}, " + active_plugins_string = active_plugins_string[:-2] + for target_plugin in inactive_plugins: + inactive_plugins_string += f"{target_plugin}, " + inactive_plugins_string = inactive_plugins_string[:-2] + for target_plugin in disabled_plugins: + disabled_plugins_string += f"{target_plugin}, " + disabled_plugins_string = disabled_plugins_string[:-2] + if len(active_plugins) == 0: + active_plugins_string = "`没有运行中的插件。`" + if len(inactive_plugins) == 0: + inactive_plugins_string = "`没有加载失败的插件。`" + if len(disabled_plugins) == 0: + disabled_plugins_string = "`没有关闭的插件`" + output = f"**插件列表**\n" \ + f"运行中: {active_plugins_string}\n" \ + f"已关闭: {disabled_plugins_string}\n" \ + f"加载失败: {inactive_plugins_string}" + await context.edit(output) + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + elif context.parameter[0] == "enable": + if len(context.parameter) == 2: + if exists(f"{plugin_directory}{context.parameter[1]}.py.disabled"): + rename(f"{plugin_directory}{context.parameter[1]}.py.disabled", + f"{plugin_directory}{context.parameter[1]}.py") + await context.edit(f"插件 {context.parameter[1]} 已启用,PagerMaid 正在重新启动。") + await log(f"已启用 {context.parameter[1]}.") + await context.client.disconnect() + else: + await context.edit("出错了呜呜呜 ~ 指定的插件不存在。") + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + elif context.parameter[0] == "disable": + if len(context.parameter) == 2: + if exists(f"{plugin_directory}{context.parameter[1]}.py") is True: + rename(f"{plugin_directory}{context.parameter[1]}.py", + f"{plugin_directory}{context.parameter[1]}.py.disabled") + await context.edit(f"插件 {context.parameter[1]} 已被禁用,PagerMaid 正在重新启动。") + await log(f"已关闭插件 {context.parameter[1]}.") + await context.client.disconnect() + else: + await context.edit("出错了呜呜呜 ~ 指定的插件不存在。") + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + elif context.parameter[0] == "upload": + if len(context.parameter) == 2: + file_name = f"{context.parameter[1]}.py" + reply_id = None + if reply: + reply_id = reply.id + if exists(f"{plugin_directory}{file_name}"): + copyfile(f"{plugin_directory}{file_name}", file_name) + elif exists(f"{plugin_directory}{file_name}.disabled"): + copyfile(f"{plugin_directory}{file_name}.disabled", file_name) + if exists(file_name): + await context.edit("上传插件中 . . .") + await upload_attachment(file_name, + context.chat_id, reply_id, + caption=f"PagerMaid {context.parameter[1]} plugin.") + remove(file_name) + await context.delete() + else: + await context.edit("出错了呜呜呜 ~ 指定的插件不存在。") + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + else: + await context.edit("出错了呜呜呜 ~ 无效的参数。") diff --git a/pagermaid/modules/prune.py b/pagermaid/modules/prune.py new file mode 100644 index 0000000..3e26b24 --- /dev/null +++ b/pagermaid/modules/prune.py @@ -0,0 +1,82 @@ +""" Module to automate message deletion. """ + +from asyncio import sleep +from telethon.errors.rpcbaseerrors import BadRequestError +from pagermaid import log +from pagermaid.listener import listener + + +@listener(outgoing=True, command="prune", + description="从您回复的消息开始删除所有内容。") +async def prune(context): + """ Purge every single message after the message you replied to. """ + if not context.reply_to_msg_id: + await context.edit("出错了呜呜呜 ~ 没有回复的消息。") + return + input_chat = await context.get_input_chat() + messages = [] + count = 0 + async for message in context.client.iter_messages(input_chat, min_id=context.reply_to_msg_id): + messages.append(message) + count += 1 + messages.append(context.reply_to_msg_id) + if len(messages) == 100: + await context.client.delete_messages(input_chat, messages) + messages = [] + + if messages: + await context.client.delete_messages(input_chat, messages) + await log(f"批量删除了 {str(count)} 条消息。") + notification = await send_prune_notify(context, count) + await sleep(.5) + await notification.delete() + + +@listener(outgoing=True, command="selfprune", + description="删除您发送的特定数量的消息。", + parameters="") +async def selfprune(context): + """ Deletes specific amount of messages you sent. """ + if not len(context.parameter) == 1: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + try: + count = int(context.parameter[0]) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + count_buffer = 0 + async for message in context.client.iter_messages(context.chat_id, from_user="me"): + if count_buffer == count: + break + await message.delete() + count_buffer += 1 + await log(f"批量删除了自行发送的 {str(count)} 条消息。") + notification = await send_prune_notify(context, count) + await sleep(.5) + await notification.delete() + + +@listener(outgoing=True, command="delete", + description="删除您回复的消息。") +async def delete(context): + """ Deletes the message you replied to. """ + target = await context.get_reply_message() + if context.reply_to_msg_id: + try: + await target.delete() + await context.delete() + await log("删除了一条消息。") + except BadRequestError: + await context.edit("出错了呜呜呜 ~ 缺少删除此消息的权限。") + else: + await context.delete() + + +async def send_prune_notify(context, count): + return await context.client.send_message( + context.chat_id, + "删除了 " + + str(count) + + " 条消息。" + ) diff --git a/pagermaid/modules/qr.py b/pagermaid/modules/qr.py new file mode 100644 index 0000000..57028db --- /dev/null +++ b/pagermaid/modules/qr.py @@ -0,0 +1,56 @@ +""" QR Code related utilities. """ + +from os import remove +from pyqrcode import create +from pyzbar.pyzbar import decode +from PIL import Image +from pagermaid import log +from pagermaid.listener import listener +from pagermaid.utils import obtain_message, upload_attachment + + +@listener(outgoing=True, command="genqr", + description="生成包含文字内容的 QR Code 。", + parameters="") +async def genqr(context): + """ Generate QR codes. """ + reply_id = context.reply_to_msg_id + try: + message = await obtain_message(context) + except ValueError: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + await context.edit("生成QR码。") + try: + create(message, error='L', mode='binary').png('qr.webp', scale=6) + except UnicodeEncodeError: + await context.edit("出错了呜呜呜 ~ 目标消息中的字符无效。") + return + await upload_attachment("qr.webp", context.chat_id, reply_id) + remove("qr.webp") + await context.delete() + await log(f"为 `{message}` 生成了一张 QR 码。") + + +@listener(outgoing=True, command="parseqr", + description="将回复的消息文件解析为 QR码 并输出结果。") +async def parseqr(context): + """ Parse attachment of replied message as a QR Code and output results. """ + success = False + target_file_path = await context.client.download_media( + await context.get_reply_message() + ) + if not target_file_path: + await context.edit("出错了呜呜呜 ~ 回复的消息中没有附件。") + return + try: + message = str(decode(Image.open(target_file_path))[0].data)[2:][:-1] + success = True + await context.edit(f"**内容: **\n" + f"`{message}`") + except IndexError: + await context.edit("出错了呜呜呜 ~ 回复的附件不是 QR 码。") + message = None + if success: + await log(f"已解析一张带有 QR 码的消息,内容: `{message}`.") + remove(target_file_path) diff --git a/pagermaid/modules/status.py b/pagermaid/modules/status.py new file mode 100644 index 0000000..040ffce --- /dev/null +++ b/pagermaid/modules/status.py @@ -0,0 +1,210 @@ +""" PagerMaid module that contains utilities related to system status. """ + +from os import remove, popen +from datetime import datetime +from speedtest import Speedtest +from telethon import functions +from platform import python_version, uname +from wordcloud import WordCloud +from telethon import version as telethon_version +from sys import platform +from re import sub +from pathlib import Path +from pagermaid import log, config, redis_status +from pagermaid.utils import execute, upload_attachment +from pagermaid.listener import listener + + +@listener(outgoing=True, command="sysinfo", + description="通过 neofetch 检索系统信息。") +async def sysinfo(context): + """ Retrieve system information via neofetch. """ + await context.edit("加载系统信息中 . . .") + result = await execute("neofetch --config none --stdout") + await context.edit(f"`{result}`") + + +@listener(outgoing=True, command="fortune", + description="读取 fortune cookies 信息。") +async def fortune(context): + """ Reads a fortune cookie. """ + result = await execute("fortune") + if result == "/bin/sh: fortune: command not found": + await context.edit("`出错了呜呜呜 ~ 此系统上没有 fortune cookies`") + return + await context.edit(result) + + +@listener(outgoing=True, command="fbcon", + description="拍摄当前绑定的帧缓冲控制台的屏幕截图。") +async def tty(context): + """ Screenshots a TTY and prints it. """ + await context.edit("拍摄帧缓冲控制台的屏幕截图中 . . .") + reply_id = context.message.reply_to_msg_id + result = await execute("fbdump | magick - image.png") + if result == "/bin/sh: fbdump: command not found": + await context.edit("出错了呜呜呜 ~ 此系统上没有 fbdump") + remove("image.png") + return + if result == "/bin/sh: convert: command not found": + await context.edit("出错了呜呜呜 ~ ImageMagick 在该系统上不存在。") + remove("image.png") + return + if result == "Failed to open /dev/fb0: Permission denied": + await context.edit("出错了呜呜呜 ~ 运行 PagerMaid 的用户不在视频组中。") + remove("image.png") + return + if not await upload_attachment("image.png", context.chat_id, reply_id, + caption="绑定的帧缓冲区的屏幕截图。", + preview=False, document=False): + await context.edit("出错了呜呜呜 ~ 由于发生意外错误,导致文件生成失败。") + return + await context.delete() + remove("image.png") + await log("Screenshot of binded framebuffer console taken.") + + +@listener(outgoing=True, command="status", + description="输出 PagerMaid 的状态。") +async def status(context): + database = "Connected" if redis_status() else "Disconnected" + await context.edit( + f"**PagerMaid 状态** \n" + f"主机名: `{uname().node}` \n" + f"主机平台: `{platform}` \n" + f"Kernel 版本: `{uname().release}` \n" + f"Python 版本: `{python_version()}` \n" + f"Library 版本: `{telethon_version.__version__}` \n" + f"数据库状态: `{'Connected' if redis_status() else 'Disconnected'}`" + ) + + +@listener(outgoing=True, command="speedtest", + description="执行 speedtest 脚本并输出您的互联网速度。") +async def speedtest(context): + """ Tests internet speed using speedtest. """ + await context.edit("执行测试脚本 . . .") + test = Speedtest() + test.get_best_server() + test.download() + test.upload() + test.results.share() + result = test.results.dict() + await context.edit( + f"**Speedtest** \n" + f"Upload: `{unit_convert(result['upload'])}` \n" + f"Download: `{unit_convert(result['download'])}` \n" + f"Latency: `{result['ping']}` \n" + f"Timestamp: `{result['timestamp']}`" + ) + + +@listener(outgoing=True, command="connection", + description="显示 PagerMaid 和 Telegram 之间的连接信息。") +async def connection(context): + """ Displays connection information between PagerMaid and Telegram. """ + datacenter = await context.client(functions.help.GetNearestDcRequest()) + await context.edit( + f"**连接信息** \n" + f"国家: `{datacenter.country}` \n" + f"连接到的数据中心: `{datacenter.this_dc}` \n" + f"最近的数据中心: `{datacenter.nearest_dc}`" + ) + + +@listener(outgoing=True, command="ping", + description="计算 PagerMaid 和 Telegram 之间的延迟。") +async def ping(context): + """ Calculates latency between PagerMaid and Telegram. """ + start = datetime.now() + await context.edit("Pong!") + end = datetime.now() + duration = (end - start).microseconds / 1000 + await context.edit(f"Pong!|{duration}") + + +@listener(outgoing=True, command="topcloud", + description="生成资源占用的词云。") +async def topcloud(context): + """ Generates a word cloud of resource-hungry processes. """ + await context.edit("生成图片中 . . .") + command_list = [] + if not Path('/usr/bin/top').is_symlink(): + output = str(await execute("top -b -n 1")).split("\n")[7:] + else: + output = str(await execute("top -b -n 1")).split("\n")[4:] + for line in output[:-1]: + line = sub(r'\s+', ' ', line).strip() + fields = line.split(" ") + try: + if fields[11].count("/") > 0: + command = fields[11].split("/")[0] + else: + command = fields[11] + + cpu = float(fields[8].replace(",", ".")) + mem = float(fields[9].replace(",", ".")) + + if command != "top": + command_list.append((command, cpu, mem)) + except BaseException: + pass + command_dict = {} + for command, cpu, mem in command_list: + if command in command_dict: + command_dict[command][0] += cpu + command_dict[command][1] += mem + else: + command_dict[command] = [cpu + 1, mem + 1] + + resource_dict = {} + + for command, [cpu, mem] in command_dict.items(): + resource_dict[command] = (cpu ** 2 + mem ** 2) ** 0.5 + + width, height = None, None + try: + width, height = ((popen("xrandr | grep '*'").read()).split()[0]).split("x") + width = int(width) + height = int(height) + except BaseException: + pass + if not width or not height: + width = int(config['width']) + height = int(config['height']) + background = config['background'] + margin = int(config['margin']) + + cloud = WordCloud( + background_color=background, + width=width - 2 * int(margin), + height=height - 2 * int(margin) + ).generate_from_frequencies(resource_dict) + + cloud.to_file("cloud.png") + await context.edit("上传图片中 . . .") + await context.client.send_file( + context.chat_id, + "cloud.png", + reply_to=None, + caption="正在运行的进程。" + ) + remove("cloud.png") + await context.delete() + await log("生成了一张资源占用的词云。") + + +def unit_convert(byte): + """ Converts byte into readable formats. """ + power = 2 ** 10 + zero = 0 + units = { + 0: '', + 1: 'Kb/s', + 2: 'Mb/s', + 3: 'Gb/s', + 4: 'Tb/s'} + while byte > power: + byte /= power + zero += 1 + return f"{round(byte, 2)} {units[zero]}" diff --git a/pagermaid/modules/sticker.py b/pagermaid/modules/sticker.py new file mode 100644 index 0000000..9099928 --- /dev/null +++ b/pagermaid/modules/sticker.py @@ -0,0 +1,184 @@ +""" PagerMaid module to handle sticker collection. """ + +from urllib import request +from io import BytesIO +from telethon.tl.types import DocumentAttributeFilename, MessageMediaPhoto +from PIL import Image +from math import floor +from pagermaid import bot +from pagermaid.listener import listener + + +@listener(outgoing=True, command="sticker", + description="收集图像/贴纸作为贴纸,指定表情符号以设置自定义表情符号。", + parameters="<表情>") +async def sticker(context): + """ Fetches images/stickers and add them to your pack. """ + user = await bot.get_me() + if not user.username: + user.username = user.first_name + message = await context.get_reply_message() + custom_emoji = False + animated = False + emoji = "" + await context.edit("收集贴纸中 . . .") + if message and message.media: + if isinstance(message.media, MessageMediaPhoto): + photo = BytesIO() + photo = await bot.download_media(message.photo, photo) + elif "image" in message.media.document.mime_type.split('/'): + photo = BytesIO() + await context.edit("下载图片中 . . .") + await bot.download_file(message.media.document, photo) + if (DocumentAttributeFilename(file_name='sticker.webp') in + message.media.document.attributes): + emoji = message.media.document.attributes[1].alt + custom_emoji = True + elif (DocumentAttributeFilename(file_name='AnimatedSticker.tgs') in + message.media.document.attributes): + emoji = message.media.document.attributes[0].alt + custom_emoji = True + animated = True + photo = 1 + else: + await context.edit("`出错了呜呜呜 ~ 不支持此文件类型。`") + return + else: + await context.edit("`出错了呜呜呜 ~ 请回复带有图片/贴纸的消息。`") + return + + if photo: + split_strings = context.text.split() + if not custom_emoji: + emoji = "👀" + pack = 1 + if len(split_strings) == 3: + pack = split_strings[2] + emoji = split_strings[1] + elif len(split_strings) == 2: + if split_strings[1].isnumeric(): + pack = int(split_strings[1]) + else: + emoji = split_strings[1] + + pack_name = f"{user.username}_{pack}" + pack_title = f"@{user.username} 的私藏 ({pack})" + command = '/newpack' + file = BytesIO() + + if not animated: + await context.edit("调整图像大小中 . . .") + image = await resize_image(photo) + file.name = "sticker.png" + image.save(file, "PNG") + else: + pack_name += "_animated" + pack_title += " (animated)" + command = '/newanimated' + + response = request.urlopen( + request.Request(f'http://t.me/addstickers/{pack_name}')) + http_response = response.read().decode("utf8").split('\n') + + if " A Telegram user has created the Sticker Set." not in \ + http_response: + async with bot.conversation('Stickers') as conversation: + await conversation.send_message('/addsticker') + await conversation.get_response() + await bot.send_read_acknowledge(conversation.chat_id) + await conversation.send_message(pack_name) + chat_response = await conversation.get_response() + while chat_response.text == "Whoa! That's probably enough stickers for one pack, give it a break. \ +A pack can't have more than 120 stickers at the moment.": + pack += 1 + pack_name = f"{user.username}_{pack}" + pack_title = f"@{user.username} 的私藏 ({pack})" + await context.edit("Switching to pack " + str(pack) + + " since previous pack is full . . .") + await conversation.send_message(pack_name) + chat_response = await conversation.get_response() + if chat_response.text == "Invalid pack selected.": + await add_sticker(conversation, command, pack_title, pack_name, animated, message, + context, file, emoji) + await context.edit( + f"Sticker has been added to [this](t.me/addstickers/{pack_name}) alternative pack.", + parse_mode='md') + return + await upload_sticker(animated, message, context, file, conversation) + await conversation.get_response() + await conversation.send_message(emoji) + await bot.send_read_acknowledge(conversation.chat_id) + await conversation.get_response() + await conversation.send_message('/done') + await conversation.get_response() + await bot.send_read_acknowledge(conversation.chat_id) + else: + await context.edit("贴纸包不存在,正在创建 . . .") + async with bot.conversation('Stickers') as conversation: + await add_sticker(conversation, command, pack_title, pack_name, animated, message, + context, file, emoji) + + await context.edit( + f"这张贴纸已经被添加到 [这个](t.me/addstickers/{pack_name}) 贴纸包。", + parse_mode='md') + + +async def add_sticker(conversation, command, pack_title, pack_name, animated, message, context, file, emoji): + await conversation.send_message(command) + await conversation.get_response() + await bot.send_read_acknowledge(conversation.chat_id) + await conversation.send_message(pack_title) + await conversation.get_response() + await bot.send_read_acknowledge(conversation.chat_id) + await upload_sticker(animated, message, context, file, conversation) + await conversation.get_response() + await conversation.send_message(emoji) + await bot.send_read_acknowledge(conversation.chat_id) + await conversation.get_response() + await conversation.send_message("/publish") + if animated: + await conversation.get_response() + await conversation.send_message(f"<{pack_title}>") + await conversation.get_response() + await bot.send_read_acknowledge(conversation.chat_id) + await conversation.send_message("/skip") + await bot.send_read_acknowledge(conversation.chat_id) + await conversation.get_response() + await conversation.send_message(pack_name) + await bot.send_read_acknowledge(conversation.chat_id) + await conversation.get_response() + await bot.send_read_acknowledge(conversation.chat_id) + + +async def upload_sticker(animated, message, context, file, conversation): + if animated: + await bot.forward_messages( + 'Stickers', [message.id], context.chat_id) + else: + file.seek(0) + await context.edit("上传图片中 . . .") + await conversation.send_file(file, force_document=True) + + +async def resize_image(photo): + image = Image.open(photo) + maxsize = (512, 512) + if (image.width and image.height) < 512: + size1 = image.width + size2 = image.height + if image.width > image.height: + scale = 512 / size1 + size1new = 512 + size2new = size2 * scale + else: + scale = 512 / size2 + size1new = size1 * scale + size2new = 512 + size1new = floor(size1new) + size2new = floor(size2new) + size_new = (size1new, size2new) + image = image.resize(size_new) + else: + image.thumbnail(maxsize) + + return image diff --git a/pagermaid/modules/system.py b/pagermaid/modules/system.py new file mode 100644 index 0000000..f429894 --- /dev/null +++ b/pagermaid/modules/system.py @@ -0,0 +1,148 @@ +""" System related utilities for PagerMaid to integrate into the system. """ + +from platform import node +from getpass import getuser +from os import geteuid +from requests import head +from requests.exceptions import MissingSchema, InvalidURL, ConnectionError +from pagermaid import log +from pagermaid.listener import listener +from pagermaid.utils import attach_log, execute + + +@listener(outgoing=True, command="sh", + description="在 Telegram 上执行 Shell 命令。", + parameters="<命令>") +async def sh(context): + """ Use the command-line from Telegram. """ + user = getuser() + command = context.arguments + hostname = node() + if context.is_channel and not context.is_group: + await context.edit("`出错了呜呜呜 ~ 当前配置禁止在频道中执行 Shell 命令。`") + return + + if not command: + await context.edit("`出错了呜呜呜 ~ 无效的参数。`") + return + + if geteuid() == 0: + await context.edit( + f"`{user}`@{hostname} ~" + f"\n> `#` {command}" + ) + else: + await context.edit( + f"`{user}`@{hostname} ~" + f"\n> `$` {command}" + ) + + result = await execute(command) + + if result: + if len(result) > 4096: + await attach_log(result, context.chat_id, "output.log", context.id) + return + + if geteuid() == 0: + await context.edit( + f"`{user}`@{hostname} ~" + f"\n> `#` {command}" + f"\n`{result}`" + ) + else: + await context.edit( + f"`{user}`@{hostname} ~" + f"\n> `$` {command}" + f"\n`{result}`" + ) + else: + return + await log(f"在 Shell 中执行命令 `{command}`") + + +@listener(outgoing=True, command="restart", diagnostics=False, + description="使 PagerMaid 重新启动") +async def restart(context): + """ To re-execute PagerMaid. """ + if not context.text[0].isalpha(): + await context.edit("尝试重新启动系统。") + await log("PagerMaid-Modify 重新启动。") + await context.client.disconnect() + + +@listener(outgoing=True, command="trace", + description="跟踪 URL 的重定向。", + parameters="") +async def trace(context): + """ Trace URL redirects. """ + url = context.arguments + reply = await context.get_reply_message() + if reply: + url = reply.text + if url: + if url.startswith("https://") or url.startswith("http://"): + pass + else: + url = "https://" + url + await context.edit("跟踪重定向中 . . .") + result = str("") + for url in url_tracer(url): + count = 0 + if result: + result += " ↴\n" + url + else: + result = url + if count == 128: + result += "\n\n出错了呜呜呜 ~ 超过128次重定向,正在中止!" + break + if result: + if len(result) > 4096: + await context.edit("输出超出限制,正在附加文件。") + await attach_log(result, context.chat_id, "output.log", context.id) + return + await context.edit( + "重定向:\n" + f"{result}" + ) + await log(f"Traced redirects of {context.arguments}.") + else: + await context.edit( + "出错了呜呜呜 ~ 发出HTTP请求时出了点问题。" + ) + else: + await context.edit("无效的参数。") + + +@listener(outgoing=True, command="contact", + description="向 Kat 发送消息。", + parameters="") +async def contact(context): + """ Sends a message to Kat. """ + await context.edit("`对话已打开,请单击 `[here](tg://user?id=503691334)` 进入.`", + parse_mode="markdown") + message = "Hi, I would like to report something about PagerMaid." + if context.arguments: + message = context.arguments + await context.client.send_message( + 503691334, + message + ) + + +def url_tracer(url): + """ Method to trace URL redirects. """ + while True: + yield url + try: + response = head(url) + except MissingSchema: + break + except InvalidURL: + break + except ConnectionError: + break + if 300 < response.status_code < 400: + url = response.headers['location'] + else: + break diff --git a/pagermaid/modules/update.py b/pagermaid/modules/update.py new file mode 100644 index 0000000..a27fc4a --- /dev/null +++ b/pagermaid/modules/update.py @@ -0,0 +1,106 @@ +""" Pulls in the new version of PagerMaid from the git server. """ + +from os import remove +from git import Repo +from git.exc import GitCommandError, InvalidGitRepositoryError, NoSuchPathError +from pagermaid import log +from pagermaid.listener import listener +from pagermaid.utils import execute + + +@listener(outgoing=True, command="update", + description="从远程来源检查更新,并将其安装到 PagerMaid-Modify。", + parameters="") +async def update(context): + if len(context.parameter) > 1: + await context.edit("无效的参数。") + return + await context.edit("正在检查远程源以进行更新 . . .") + parameter = None + if len(context.parameter) == 1: + parameter = context.parameter[0] + repo_url = 'https://github.com/xtaodada/PagerMaid-Modify.git' + + try: + repo = Repo() + except NoSuchPathError as exception: + await context.edit(f"出错了呜呜呜 ~ 目录 {exception} 不存在。") + return + except InvalidGitRepositoryError: + await context.edit(f"此 PagerMaid-Modify 实例不是从源安装," + f" 请通过您的本机软件包管理器进行升级。") + return + except GitCommandError as exception: + await context.edit(f'出错了呜呜呜 ~ 收到了来自 git 的错误: `{exception}`') + return + + active_branch = repo.active_branch.name + if not await branch_check(active_branch): + await context.edit( + f"出错了呜呜呜 ~ 该分支未维护: {active_branch}.") + return + + try: + repo.create_remote('upstream', repo_url) + except BaseException: + pass + + upstream_remote = repo.remote('upstream') + upstream_remote.fetch(active_branch) + changelog = await changelog_gen(repo, f'HEAD..upstream/{active_branch}') + + if not changelog: + await context.edit(f"`PagerMaid-Modify 在分支 ` **{active_branch}**` 中已是最新。`") + return + + if parameter != "true": + changelog_str = f'**找到分支 {active_branch} 的更新.\n\n更新日志:**\n`{changelog}`' + if len(changelog_str) > 4096: + await context.edit("更新日志太长,正在附加文件。") + file = open("output.log", "w+") + file.write(changelog_str) + file.close() + await context.client.send_file( + context.chat_id, + "output.log", + reply_to=context.id, + ) + remove("output.log") + else: + await context.edit(changelog_str + "\n**执行 \"-update true\" 来安装更新。**") + return + + await context.edit('找到更新,正在拉取 . . .') + + try: + upstream_remote.pull(active_branch) + await execute("pip3 install -r requirements.txt --upgrade") + await execute("pip3 install -r requirements.txt") + await log("PagerMaid-Modify 已更新。") + await context.edit( + '更新成功,PagerMaid-Modify 正在重新启动。' + ) + await context.client.disconnect() + except GitCommandError: + upstream_remote.git.reset('--hard') + await log("PagerMaid-Modify 更新失败。") + await context.edit( + '更新时出现错误,PagerMaid-Modify 正在重新启动。' + ) + await context.client.disconnect() + + +async def changelog_gen(repo, diff): + result = '' + d_form = "%d/%m/%y" + for c in repo.iter_commits(diff): + result += f'•[{c.committed_datetime.strftime(d_form)}]: {c.summary} <{c.author}>\n' + return result + + +async def branch_check(branch): + official = ['master', 'staging'] + for k in official: + if k == branch: + return 1 + return diff --git a/pagermaid/static/favicon.ico b/pagermaid/static/favicon.ico new file mode 100644 index 0000000..5dc5c0a Binary files /dev/null and b/pagermaid/static/favicon.ico differ diff --git a/pagermaid/static/images/icon.png b/pagermaid/static/images/icon.png new file mode 100644 index 0000000..1c9de9f Binary files /dev/null and b/pagermaid/static/images/icon.png differ diff --git a/pagermaid/static/images/user.jpg b/pagermaid/static/images/user.jpg new file mode 100644 index 0000000..35082e5 Binary files /dev/null and b/pagermaid/static/images/user.jpg differ diff --git a/pagermaid/static/style.css b/pagermaid/static/style.css new file mode 100644 index 0000000..4ebcc63 --- /dev/null +++ b/pagermaid/static/style.css @@ -0,0 +1,151 @@ +html, body { + font-family: 'Roboto', 'Helvetica', sans-serif; + } + .demo-avatar { + width: 48px; + height: 48px; + border-radius: 24px; + } + .demo-layout .mdl-layout__header .mdl-layout__drawer-button { + color: rgba(0, 0, 0, 0.54); + } + .mdl-layout__drawer .avatar { + margin-bottom: 16px; + } + .demo-drawer { + border: none; + } + /* iOS Safari specific workaround */ + .demo-drawer .mdl-menu__container { + z-index: -1; + } + .demo-drawer .demo-navigation { + z-index: -2; + } + /* END iOS Safari specific workaround */ + .demo-drawer .mdl-menu .mdl-menu__item { + display: flex; + align-items: center; + } + .demo-drawer-header { + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: 16px; + height: 151px; + } + .demo-avatar-dropdown { + display: flex; + position: relative; + flex-direction: row; + align-items: center; + width: 100%; + } + + .demo-navigation { + flex-grow: 1; + } + .demo-layout .demo-navigation .mdl-navigation__link { + display: flex !important; + flex-direction: row; + align-items: center; + color: rgba(255, 255, 255, 0.56); + font-weight: 500; + } + .demo-layout .demo-navigation .mdl-navigation__link:hover { + background-color: #00BCD4; + color: #37474F; + } + .demo-navigation .mdl-navigation__link .material-icons { + font-size: 24px; + color: rgba(255, 255, 255, 0.56); + margin-right: 32px; + } + + .demo-content { + max-width: 1080px; + } + + .w-100 { + width: 100%; +} + +.w-50 { + width: 50%; +} + +.spacer-radio { + padding: 20px 0; +} + + .demo-cards { + align-items: flex-start; + align-content: flex-start; + } + .demo-cards .demo-separator { + height: 32px; + } + .demo-cards .mdl-card__title.mdl-card__title { + color: white; + font-size: 24px; + font-weight: 400; + } + .demo-cards ul { + padding: 0; + } + .demo-cards h3 { + font-size: 1em; + } + .demo-cards .mdl-card__actions a { + color: #00BCD4; + text-decoration: none; + } + + .demo-options h3 { + margin: 0; + } + .demo-options .mdl-checkbox__box-outline { + border-color: rgba(255, 255, 255, 0.89); + } + .demo-options ul { + margin: 0; + list-style-type: none; + } + .demo-options li { + margin: 4px 0; + } + .demo-options .material-icons { + color: rgba(255, 255, 255, 0.89); + } + .demo-options .mdl-card__actions { + height: 64px; + display: flex; + box-sizing: border-box; + align-items: center; + } + + .mdl-card__supporting-text { + width: 100%; + box-sizing: border-box; +} + +.mdl-data-table tbody tr:nth-child(2n) { + background-color: rgba(0,0,0,.04); +} + +.mdl-card, .mdl-card__supporting-text { + overflow: unset; +} + + +.mdl-selectfield.mdl-js-selectfield { + width: 100%; +} + +.login-content { + height: 100%; + justify-content: center; + align-items: center; + box-sizing: border-box; +} \ No newline at end of file diff --git a/pagermaid/templates/includes/navbar.html b/pagermaid/templates/includes/navbar.html new file mode 100644 index 0000000..d59e5a7 --- /dev/null +++ b/pagermaid/templates/includes/navbar.html @@ -0,0 +1,36 @@ +
+
+ icon +
+ PagerMaid +
+ +
    +
  • Profile
  • +
  • Logout
  • +
+
+
+ +
\ No newline at end of file diff --git a/pagermaid/templates/pages/404.html b/pagermaid/templates/pages/404.html new file mode 100644 index 0000000..65a9aa9 --- /dev/null +++ b/pagermaid/templates/pages/404.html @@ -0,0 +1,37 @@ + + +No such file or directory + + +
+
+
+

No such file or directory

+

Seems like a part of PagerMaid is missing or you have followed a bad link. If PagerMaid took you here please report this error.

+

Go back Home.

+
+
+
diff --git a/pagermaid/templates/pages/500.html b/pagermaid/templates/pages/500.html new file mode 100644 index 0000000..d46e709 --- /dev/null +++ b/pagermaid/templates/pages/500.html @@ -0,0 +1,37 @@ + + +Internal Server Error + + +
+
+
+

Internal Server Error

+

Seems like some part of PagerMaid decided to crash, please report this to Kat.

+

Go back Home.

+
+
+
diff --git a/pagermaid/templates/pages/index.html b/pagermaid/templates/pages/index.html new file mode 100644 index 0000000..87aa8c4 --- /dev/null +++ b/pagermaid/templates/pages/index.html @@ -0,0 +1,124 @@ + + + + + + + + PagerMaid + + + + + + + +
+
+
+ Dashboard +
+
+{% include 'includes/navbar.html' %} +
+
+ +
+
+

Status

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Hostname{{ hostname }}
Platform{{ platform }}
Kernel Version{{ kernel }}
Python Version{{ python }}
Library Version{{ telethon }}
Database Status{{ redis }}
+
+
+
+
+

Memory

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ Total available + +
+
{{ memory_available }} MiB / {{ memory_total }} MiB ({{ memory_available_percentage }}%)
Free +
+
{{ memory_free }} MiB / {{ memory_total }} MiB ({{ memory_free_percentage }}%)
Buffered +
+
{{ memory_buffered }} MiB / {{ memory_total }} MiB ({{ memory_buffered_percentage }}%)
Cached +
+
{{ memory_cached }} MiB / {{ memory_total }} MiB ({{ memory_cached_percentage }}%)
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/pagermaid/templates/pages/login.html b/pagermaid/templates/pages/login.html new file mode 100644 index 0000000..c1be1cb --- /dev/null +++ b/pagermaid/templates/pages/login.html @@ -0,0 +1,58 @@ + + + + + + + + PagerMaid Login + + + + + + + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/pagermaid/templates/pages/profile.html b/pagermaid/templates/pages/profile.html new file mode 100644 index 0000000..0d19532 --- /dev/null +++ b/pagermaid/templates/pages/profile.html @@ -0,0 +1,63 @@ + + + + + + + + PagerMaid + + + + + + + + + +
+
+
+ Profile +
+
+{% include 'includes/navbar.html' %} +
+
+ +
+
+

Profile

+
+
+
+
+ + {{ form.full_name(placeholder="Full Name", class="mdl-textfield__input") }} +
+
+ + {{ form.username(placeholder="Username", class="mdl-textfield__input") }} +
+
+ + {{ form.email(placeholder="Email Address", class="mdl-textfield__input") }} +
+
+ + {{ form.password(placeholder="Password", class="mdl-textfield__input") }} +
+ +
+
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pagermaid/templates/pages/settings.html b/pagermaid/templates/pages/settings.html new file mode 100644 index 0000000..eca6dbe --- /dev/null +++ b/pagermaid/templates/pages/settings.html @@ -0,0 +1,75 @@ + + + + + + + + PagerMaid + + + + + + + + + +
+
+
+ Settings +
+
+{% include 'includes/navbar.html' %} +
+
+ +
+
+

Settings

+
+
+
+
+ + +
+ +
+ + +
+ + + +
+ + +
+ + +
+
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pagermaid/templates/pages/setup.html b/pagermaid/templates/pages/setup.html new file mode 100644 index 0000000..45e9cfc --- /dev/null +++ b/pagermaid/templates/pages/setup.html @@ -0,0 +1,64 @@ + + + + + + + + PagerMaid Setup + + + + + + + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/pagermaid/utils.py b/pagermaid/utils.py new file mode 100644 index 0000000..d4ea43d --- /dev/null +++ b/pagermaid/utils.py @@ -0,0 +1,136 @@ +""" Libraries for python modules. """ + +from os import remove +from os.path import exists +from emoji import get_emoji_regexp +from random import choice +from json import load as load_json +from re import sub, IGNORECASE +from asyncio import create_subprocess_shell +from asyncio.subprocess import PIPE +from youtube_dl import YoutubeDL +from pagermaid import module_dir, bot + + +async def upload_attachment(file_path, chat_id, reply_id, caption=None, preview=None, document=None): + """ Uploads a local attachment file. """ + if not exists(file_path): + return False + try: + await bot.send_file( + chat_id, + file_path, + reply_to=reply_id, + caption=caption, + link_preview=preview, + force_document=document + ) + except BaseException as exception: + raise exception + return True + + +async def execute(command, pass_error=True): + """ Executes command and returns output, with the option of enabling stderr. """ + executor = await create_subprocess_shell( + command, + stdout=PIPE, + stderr=PIPE + ) + + stdout, stderr = await executor.communicate() + if pass_error: + result = str(stdout.decode().strip()) \ + + str(stderr.decode().strip()) + else: + result = str(stdout.decode().strip()) + return result + + +async def attach_log(plaintext, chat_id, file_name, reply_id=None, caption=None): + """ Attach plaintext as logs. """ + file = open(file_name, "w+") + file.write(plaintext) + file.close() + await bot.send_file( + chat_id, + file_name, + reply_to=reply_id, + caption=caption + ) + remove(file_name) + + +async def obtain_message(context): + """ Obtains a message from either the reply message or command arguments. """ + reply = await context.get_reply_message() + message = context.arguments + if reply and not message: + message = reply.text + if not message: + raise ValueError("出错了呜呜呜 ~ 没有成功获取到消息!") + return message + + +async def random_gen(selection, length=64): + if not isinstance(length, int): + raise ValueError("出错了呜呜呜 ~ 长度必须是整数!") + return await execute(f"head -c 65536 /dev/urandom | tr -dc {selection} | head -c {length} ; echo \'\'") + + +async def fetch_youtube_audio(url, chat_id, reply_id): + """ Extracts and uploads audio from YouTube video. """ + youtube_dl_options = { + 'format': 'bestaudio/best', + 'outtmpl': "audio.%(ext)s", + 'postprocessors': [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192', + }], + } + YoutubeDL(youtube_dl_options).download([url]) + if not exists("audio.mp3"): + return False + await bot.send_file( + chat_id, + "audio.mp3", + reply_to=reply_id + ) + remove("audio.mp3") + return True + + +def owoify(text): + """ Converts your text to OwO """ + smileys = [';;w;;', '^w^', '>w<', 'UwU', '(・`ω´・)', '(´・ω・`)'] + with open(f"{module_dir}/assets/replacements.json") as fp: + replacements = load_json(fp) + for expression in replacements: + replacement = replacements[expression] + text = sub(expression, replacement, text, flags=IGNORECASE) + words = text.split() + first_letter = words[0][0] + letter_stutter = f"{first_letter}-{first_letter.lower()}-{first_letter.lower()}" + if len(words[0]) > 1: + words[0] = letter_stutter + words[0][1:] + else: + words[0] = letter_stutter + text = " ".join(words) + text = text.replace('L', 'W').replace('l', 'w') + text = text.replace('R', 'W').replace('r', 'w') + text = '! {}'.format(choice(smileys)).join(text.rsplit('!', 1)) + text = '? OwO'.join(text.rsplit('?', 1)) + text = '. {}'.format(choice(smileys)).join(text.rsplit('.', 1)) + text = f"{text} desu" + for v in ['a', 'o', 'u', 'A', 'O', 'U']: + if 'n{}'.format(v) in text: + text = text.replace('n{}'.format(v), 'ny{}'.format(v)) + if 'N{}'.format(v) in text: + text = text.replace('N{}'.format(v), 'N{}{}'.format('Y' if v.isupper() else 'y', v)) + return text + + +def clear_emojis(target): + """ Removes all Emojis from provided string """ + return get_emoji_regexp().sub(u'', target) diff --git a/plugins/README b/plugins/README new file mode 100644 index 0000000..7389e3b --- /dev/null +++ b/plugins/README @@ -0,0 +1 @@ +Refer to DEVELOPMENT.md for plugin development documentation. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..66afeff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,32 @@ +psutil +pyqrcode +pypng +pyzbar +emoji +youtube_dl +pyyaml +redis +coloredlogs +requests +pytz +cowpy +googletrans +beautifulsoup4 +gtts +gtts-token +wordcloud +telethon +pillow +python-magic +pygments +distutils2-py3 +speedtest-cli +gitpython +werkzeug +flask +flask_sqlalchemy +flask_login +flask_bcrypt +flask_wtf +wtforms +cheroot \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..663a998 --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +""" Packaging of PagerMaid. """ + +from setuptools import setup, find_packages + +with open("README.md", "r") as fh: + long_description = fh.read() +with open("requirements.txt", "r") as fp: + install_requires = fp.read() + +setup( + name="pagermaid", + version="2020.2.post13", + author="Stykers", + author_email="stykers@stykers.moe", + description="A telegram utility daemon and plugin framework.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://katonkeyboard.moe/pagermaid.html", + packages=find_packages(), + entry_points={ + 'console_scripts': [ + 'pagermaid=pagermaid:__main__' + ] + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Operating System :: Unix" + ], + python_requires=">=3.6", + install_requires=install_requires, + include_package_data=True +) diff --git a/some-plugins/autorespond.py b/some-plugins/autorespond.py new file mode 100644 index 0000000..d8be9d6 --- /dev/null +++ b/some-plugins/autorespond.py @@ -0,0 +1,45 @@ +""" Pagermaid autorespond plugin. """ + +from telethon.events import StopPropagation +from pagermaid import persistent_vars, log +from pagermaid.listener import listener + +persistent_vars.update({'autorespond': {'enabled': False, 'message': None, 'amount': 0}}) + + +@listener(outgoing=True, command="autorespond", + description="启用自动回复。", + parameters="") +async def autorespond(context): + """ Enables the auto responder. """ + message = "我还在睡觉... ZzZzZzZzZZz" + if context.arguments: + message = context.arguments + await context.edit("成功启用自动响应器。") + await log(f"启用自动响应器,将自动回复 `{message}`.") + persistent_vars.update({'autorespond': {'enabled': True, 'message': message, 'amount': 0}}) + raise StopPropagation + + +@listener(outgoing=True) +async def disable_responder(context): + if persistent_vars['autorespond']['enabled']: + await log(f"禁用自动响应器。 在闲置期间 {persistent_vars['autorespond']['amount']}" + f" 条消息被自动回复") + persistent_vars.update({'autorespond': {'enabled': False, 'message': None, 'amount': 0}}) + + +@listener(incoming=True) +async def private_autorespond(context): + if persistent_vars['autorespond']['enabled']: + if context.is_private and not (await context.get_sender()).bot: + persistent_vars['autorespond']['amount'] += 1 + await context.reply(persistent_vars['autorespond']['message']) + + +@listener(incoming=True) +async def mention_autorespond(context): + if persistent_vars['autorespond']['enabled']: + if context.message.mentioned and not (await context.get_sender()).bot: + persistent_vars['autorespond']['amount'] += 1 + await context.reply(persistent_vars['autorespond']['message']) diff --git a/some-plugins/yt-dl.py b/some-plugins/yt-dl.py new file mode 100644 index 0000000..762c5bd --- /dev/null +++ b/some-plugins/yt-dl.py @@ -0,0 +1,51 @@ +""" Pagermaid plugin base. """ + +from os import remove +from os.path import exists +from youtube_dl import YoutubeDL +from re import compile as regex_compile +from pagermaid import bot, log +from pagermaid.listener import listener + + +@listener(outgoing=True, command="ytdl", + description="YouTube downloader.", + parameters=".") +async def ytdl(context): + url = context.arguments + reply = await context.get_reply_message() + reply_id = None + await context.edit("正在拉取视频 . . .") + if reply: + reply_id = reply.id + if url is None: + await context.edit("出错了呜呜呜 ~ 无效的参数。") + return + + youtube_pattern = regex_compile(r"^(http(s)?://)?((w){3}.)?youtu(be|.be)?(\.com)?/.+") + if youtube_pattern.match(url): + if not await fetch_youtube_video(url, context.chat_id, reply_id): + await context.edit("出错了呜呜呜 ~ 视频下载失败。") + await log(f"已拉取UTB视频,地址: {url}.") + + +async def fetch_youtube_video(url, chat_id, reply_id): + """ Extracts and uploads YouTube video. """ + youtube_dl_options = { + 'format': 'bestvideo[height=720]+bestaudio/best', + 'outtmpl': "video.%(ext)s", + 'postprocessors': [{ + 'key': 'FFmpegVideoConvertor', + 'preferedformat': 'mp4' + }] + } + YoutubeDL(youtube_dl_options).download([url]) + if not exists("video.mp4"): + return False + await bot.send_file( + chat_id, + "video.mp4", + reply_to=reply_id + ) + remove("video.mp4") + return True diff --git a/utils/docker.sh b/utils/docker.sh new file mode 100644 index 0000000..9d078e6 --- /dev/null +++ b/utils/docker.sh @@ -0,0 +1,130 @@ +#!/bin/sh + +welcome() { + echo "" + echo "Welcome to PagerMaid docker installer." + echo "The installation process will begin" + echo "in 5 seconds, if you wish to cancel," + echo "please abort within 5 seconds." + echo "" + sleep 5 +} + +docker_check() { + echo "Checking for docker . . ." + if command -v docker; + then + echo "Docker appears to be present, moving on . . ." + else + echo "Docker is not installed on this system, please" + echo "install docker and add yourself to the docker" + echo "group and re-run this script." + exit 1 + fi +} + +git_check() { + echo "Checking for git . . ." + if command -v git; + then + echo "Git appears to be present, moving on . . ." + else + echo "Git is not installed on this system, please" + echo "install git and re-run this script." + exit 1 + fi +} + +access_check() { + echo "Testing for docker access . . ." + if [ -w /var/run/docker.sock ] + then + echo "This user can access docker, moving on . . ." + else + echo "This user has no access to docker, or docker is" + echo "not running. Please add yourself to the docker" + echo "group or run the script as superuser." + exit 1 + fi +} + +download_repo() { + echo "Downloading repository . . ." + rm -rf /tmp/pagermaid + git clone https://git.stykers.moe/scm/~stykers/pagermaid.git /tmp/pagermaid + cd /tmp/pagermaid || exit +} + +configure() { + config_file=config.yml + echo "Generating config file . . ." + cp config.gen.yml config.yml + printf "Please enter application API Key: " + read -r api_key <&1 + sed -i "s/KEY_HERE/$api_key/" $config_file + printf "Please enter application API Hash: " + read -r api_hash <&1 + sed -i "s/HASH_HERE/$api_hash/" $config_file + printf "Please enter application language (Example: en): " + read -r application_language <&1 + sed -i "s/en/$application_language/" $config_file + printf "Please enter application region (Example: United States): " + read -r application_region <&1 + sed -i "s/United States/$application_region/" $config_file + printf "Enable logging? [Y/n] " + read -r logging_confirmation <&1 + case $logging_confirmation in + [yY][eE][sS]|[yY]) + printf "Please enter your logging group/channel ChatID (press Enter if you want to log into Kat): " + read -r log_chatid <&1 + if [ -z "$log_chatid" ] + then + echo "Setting log target to Kat." + else + sed -i "s/503691334/$log_chatid/" $config_file + fi + sed -i "s/log: False/log: True/" $config_file + ;; + [nN][oO]|[nN]) + echo "Moving on . . ." + ;; + *) + echo "Invalid choice . . ." + exit 1 + ;; + esac +} + +build_docker() { + printf "Please enter the name of the PagerMaid container: " + read -r container_name <&1 + echo "Building docker image . . ." + docker rm -f "$container_name" > /dev/null 2>&1 + docker build - --force-rm --no-cache -t pagermaid_"$container_name < Dockerfile.persistant" +} + +start_docker() { + echo "Starting docker container . . ." + echo "After logging in, press Ctrl + C to make the container restart in background mode." + sleep 3 + docker run -it --restart=always --name="$container_name" --hostname="$container_name" pagermaid_"$container_name" <&1 +} + +cleanup() { + echo "Cleaning up . . ." + rm -rf /tmp/pagermaid +} + +start_installation() { + welcome + docker_check + git_check + access_check + download_repo + configure + build_docker + start_docker + cleanup +} + +start_installation diff --git a/utils/entrypoint.sh b/utils/entrypoint.sh new file mode 100644 index 0000000..a784843 --- /dev/null +++ b/utils/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh +redis-server --daemonize yes +. /pagermaid/venv/bin/activate +/usr/bin/env python3 -m pagermaid diff --git a/utils/mksession.py b/utils/mksession.py new file mode 100644 index 0000000..dda417b --- /dev/null +++ b/utils/mksession.py @@ -0,0 +1,9 @@ +from telethon import TelegramClient +from yaml import load, FullLoader + +config = load(open(r"config.yml"), Loader=FullLoader) +api_key = config['api_key'] +api_hash = config['api_hash'] + +bot = TelegramClient('pagermaid', api_key, api_hash) +bot.start() diff --git a/utils/pagermaid b/utils/pagermaid new file mode 100644 index 0000000..b05e278 --- /dev/null +++ b/utils/pagermaid @@ -0,0 +1,4 @@ +#!/bin/bash +source venv/bin/activate; +/usr/bin/env python3 -m pagermaid + diff --git a/utils/pagermaid-pip.service b/utils/pagermaid-pip.service new file mode 100644 index 0000000..29cab4a --- /dev/null +++ b/utils/pagermaid-pip.service @@ -0,0 +1,14 @@ +[Unit] +Description=PagerMaid telegram utility daemon +After=network.target + +[Install] +WantedBy=multi-user.target + +[Service] +Type=simple +User=pagermaid +Group=pagermaid +WorkingDirectory=/var/lib/pagermaid +ExecStart=/var/lib/pagermaid/venv/lib/python3.8/site-packages/pagermaid/assets/pagermaid +Restart=always \ No newline at end of file diff --git a/utils/pagermaid.service b/utils/pagermaid.service new file mode 100644 index 0000000..06b0fd9 --- /dev/null +++ b/utils/pagermaid.service @@ -0,0 +1,14 @@ +[Unit] +Description=PagerMaid telegram utility daemon +After=network.target + +[Install] +WantedBy=multi-user.target + +[Service] +Type=simple +User=pagermaid +Group=pagermaid +WorkingDirectory=/var/lib/pagermaid +ExecStart=/var/lib/pagermaid/utils/pagermaid +Restart=always \ No newline at end of file diff --git a/utils/run b/utils/run new file mode 100644 index 0000000..e2ff960 --- /dev/null +++ b/utils/run @@ -0,0 +1,4 @@ +#!/bin/sh +cd /var/lib/pagermaid || exit +. venv/bin/activate +exec chpst -u pagermaid:pagermaid:wheel:tty:video /usr/bin/env pagermaid \ No newline at end of file diff --git a/utils/start.sh b/utils/start.sh new file mode 100644 index 0000000..0c5fbdd --- /dev/null +++ b/utils/start.sh @@ -0,0 +1,7 @@ +#!/bin/sh +while true; do + clear; + python3 -m pagermaid; + echo 'Restarting...'; + sleep 1; +done