📝 添加中文翻译,添加部分插件

This commit is contained in:
xtaodada 2020-02-19 23:31:39 +08:00
parent 13522d069c
commit 6c82ef608b
No known key found for this signature in database
GPG Key ID: 39EFACA711DF5D8C
66 changed files with 5681 additions and 2 deletions

78
DEVELOPMENT.md Normal file
View File

@ -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="<name>")
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.

42
Dockerfile Normal file
View File

@ -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"]

44
Dockerfile.persistant Normal file
View File

@ -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"]

76
INSTALL.md Normal file
View File

@ -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 启用。

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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 <http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include LICENSE
include pagermaid/assets/*

View File

@ -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 <message> 并点击链接进入通过 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`

46
config.gen.yml Normal file
View File

@ -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"

13
docker-compose.gen.yml Normal file
View File

@ -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

99
pagermaid/__init__.py Normal file
View File

@ -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
)

41
pagermaid/__main__.py Normal file
View File

@ -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()

Binary file not shown.

View File

@ -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

View File

@ -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}"

View File

@ -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"

View File

@ -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 \@
|__|_| /@
\/ @@
@
____ @
/ \ @
| | \@
|___| /@
\/ @@
@
____ @
/ _ \ @
( <_> )@
\____/ @
@@
@
______ @
\____ \ @
| |_> >@
| __/ @
|__| @@
@
______@
/ ____/@
< <_| |@
\__ |@
|__|@@
@
_______ @
\_ __ \@
| | \/@
|__| @
@@
@
______@
/ ___/@
\___ \ @
/____ >@
\/ @@
__ @
_/ |_ @
\ __\@
| | @
|__| @
@@
@
__ __ @
| | \@
| | /@
|____/ @
@@
@
___ __@
\ \/ /@
\ / @
\_/ @
@@
@
__ _ __@
\ \/ \/ /@
\ / @
\/\_/ @
@@
@
___ ___@
\ \/ /@
> < @
/__/\_ \@
\/@@
@
___.__.@
< | |@
\___ |@
/ ____|@
\/ @@
@
________@
\___ /@
/ / @
/_____ \@
\/@@
$ ___$@
$/ / $@
$\ \ $@
$< < $@
$/ / $@
$\_\_$@@
$._.$@
$| |$@
$|_|$@
$|-|$@
$| |$@
$|_|$@@
$___ $@
$ \ \$@
$ / /$@
$ > >$@
$ \ \$@
$_/_/$@@
$ ___ $@
$/ _ \_/\$@
$\/ \___/$@
$ $@
$ $@
$ $@@
@
@
@
@
@
@@
@
@
@
@
@
@@
@
@
@
@
@
@@
@
@
@
@
@
@@
@
@
@
@
@
@@
@
@
@
@
@
@@
@
@
@
@
@
@@

View File

@ -0,0 +1,4 @@
#!/bin/bash
source venv/bin/activate;
/usr/bin/env python3 -m pagermaid

View File

@ -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"}

View File

@ -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()

View File

@ -0,0 +1,5 @@
""" PagerMaid web interface startup. """
from pagermaid import logs
logs.info("出错了呜呜呜 ~ 此模块不应直接运行。")

View File

@ -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()])

View File

@ -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

View File

@ -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')

92
pagermaid/listener.py Normal file
View File

@ -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

View File

@ -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"]

View File

@ -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="<username>")
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="<first name> <last name>")
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="<string>")
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="<username>")
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)

View File

@ -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="<true|false|status>")
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="<true|false|status>")
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()

View File

@ -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="<string>,<string> <image>")
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="<string>")
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

View File

@ -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

View File

@ -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="<language>")
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="<string>")
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="<query>")
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="<url>")
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__()

354
pagermaid/modules/fun.py Normal file
View File

@ -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="<message>")
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="<message>")
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="<message>")
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="<message>")
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="<message>")
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="<message>")
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="<message>")
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="<username> <username>")
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="<length>")
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="<integer>")
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="<string>")
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="<message>")
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()

22
pagermaid/modules/help.py Normal file
View File

@ -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 <命令>\" 以查看特定命令的帮助。**")

View File

@ -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="<string>")
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="<meters>")
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="<feet>")
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/")

148
pagermaid/modules/plugin.py Normal file
View File

@ -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("出错了呜呜呜 ~ 无效的参数。")

View File

@ -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="<integer>")
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)
+ " 条消息。"
)

56
pagermaid/modules/qr.py Normal file
View File

@ -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="<string>")
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)

210
pagermaid/modules/status.py Normal file
View File

@ -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]}"

View File

@ -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 <strong>Telegram</strong> user has created the <strong>Sticker&nbsp;Set</strong>." 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

148
pagermaid/modules/system.py Normal file
View File

@ -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="<url>")
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="<message>")
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

106
pagermaid/modules/update.py Normal file
View File

@ -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="<boolean>")
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

151
pagermaid/static/style.css Normal file
View File

@ -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;
}

View File

@ -0,0 +1,36 @@
<div class="demo-drawer mdl-layout__drawer mdl-color--blue-grey-900 mdl-color-text--blue-grey-50">
<header class="demo-drawer-header">
<img src="static/images/icon.png" class="demo-avatar" alt="icon">
<div class="demo-avatar-dropdown">
<span>PagerMaid</span>
<div class="mdl-layout-spacer"></div>
<button id="accbtn" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon">
<i class="material-icons" role="presentation">arrow_drop_down</i>
<span class="visuallyhidden">Options</span>
</button>
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" for="accbtn">
<li class="mdl-menu__item" onclick="location.href='profile';">Profile</li>
<li class="mdl-menu__item" onclick="location.href='logout';">Logout</li>
</ul>
</div>
</header>
<nav class="demo-navigation mdl-navigation mdl-color--blue-grey-800">
<a class="mdl-navigation__link" href="/">
<i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">home</i>Dashboard
</a>
<a class="mdl-navigation__link" href="settings">
<i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">settings</i>
Settings
</a>
<a class="mdl-navigation__link" href="logout">
<i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">logout</i>
Logout
</a>
<div class="mdl-layout-spacer"></div>
<a class="mdl-navigation__link" href="https://katonkeyboard.moe/pagermaid.html" target="_blank">
<i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">help_outline</i>
<span class="visuallyhidden">Help</span>
</a>
</nav>
</div>

View File

@ -0,0 +1,37 @@
<!doctype html>
<title>No such file or directory</title>
<style>
body {
text-align: center;
padding: 150px;
background-color: #424242;
background-size: cover;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
}
.content {
background-color: rgba(255, 255, 255, 0.75);
background-size: 100%;
color: inherit;
padding: 1px 100px 10px 100px;
border-radius: 15px;
}
h1 { font-size: 40pt;}
body { font: 20px Helvetica, sans-serif; color: #333; }
article { display: block; text-align: left; width: 75%; margin: 0 auto; }
a:hover { color: #333; text-decoration: none; }
</style>
<article>
<div class="background">
<div class="content">
<h1>No such file or directory</h1>
<p>Seems like a part of PagerMaid is missing or you have followed a bad link. If PagerMaid took you here please report this error.</p>
<p>Go back <span style="color: #000000;"><strong><a style="color: #000000;" href="/">Home</a></strong></span>.</p>
</div>
</div>
</article>

View File

@ -0,0 +1,37 @@
<!doctype html>
<title>Internal Server Error</title>
<style>
body {
text-align: center;
padding: 150px;
background-color: #424242;
background-size: cover;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
}
.content {
background-color: rgba(255, 255, 255, 0.75);
background-size: 100%;
color: inherit;
padding: 1px 100px 10px 100px;
border-radius: 15px;
}
h1 { font-size: 40pt;}
body { font: 20px Helvetica, sans-serif; color: #333; }
article { display: block; text-align: left; width: 75%; margin: 0 auto; }
a:hover { color: #333; text-decoration: none; }
</style>
<article>
<div class="background">
<div class="content">
<h1>Internal Server Error</h1>
<p>Seems like some part of PagerMaid decided to crash, please report this to Kat.</p>
<p>Go back <span style="color: #000000;"><strong><a style="color: #000000;" href="/">Home</a></strong></span>.</p>
</div>
</div>
</article>

View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PagerMaid</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.min.css" />
<link rel="stylesheet" href="style.css" />
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
</head>
<body>
<div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-header">
<header class="demo-header mdl-layout__header mdl-color--grey-100 mdl-color-text--grey-600">
<div class="mdl-layout__header-row">
<span class="mdl-layout-title">Dashboard</span>
</div>
</header>
{% include 'includes/navbar.html' %}
<main class="mdl-layout__content mdl-color--grey-100">
<div class="mdl-grid demo-content">
<div
class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--4-col mdl-cell--4-col-tablet mdl-cell--12-col-desktop">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Status</h2>
</div>
<div class="mdl-card__supporting-text">
<table class="mdl-data-table w-100 mdl-js-data-table">
<!--<thead>
<tr>
<th class="mdl-data-table__cell&#45;&#45;non-numeric">Node</th>
<th>Value</th>
</tr>
</thead>-->
<tbody>
<tr>
<td class="mdl-data-table__cell--non-numeric">Hostname</td>
<td>{{ hostname }}</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Platform</td>
<td>{{ platform }}</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Kernel Version</td>
<td>{{ kernel }}</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Python Version</td>
<td>{{ python }}</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Library Version</td>
<td>{{ telethon }}</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric">Database Status</td>
<td>{{ redis }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--4-col mdl-cell--4-col-tablet mdl-cell--12-col-desktop">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Memory</h2>
</div>
<div class="mdl-card__supporting-text">
<table class="mdl-data-table w-100 mdl-js-data-table">
<tbody>
<tr>
<td class="mdl-data-table__cell--non-numeric mdl-typography--font-bold">
Total available
</td>
<td>
<div class="mdl-progress mdl-js-progress" data-progress="{{ memory_available_percentage }}"></div>
</td>
<td>{{ memory_available }} MiB / {{ memory_total }} MiB ({{ memory_available_percentage }}%)</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric mdl-typography--font-bold">Free</td>
<td>
<div class="mdl-progress mdl-js-progress" data-progress="{{ memory_free_percentage }}"></div>
</td>
<td>{{ memory_free }} MiB / {{ memory_total }} MiB ({{ memory_free_percentage }}%)</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric mdl-typography--font-bold">Buffered</td>
<td>
<div class="mdl-progress mdl-js-progress" data-progress="{{ memory_buffered_percentage }}"></div>
</td>
<td>{{ memory_buffered }} MiB / {{ memory_total }} MiB ({{ memory_buffered_percentage }}%)</td>
</tr>
<tr>
<td class="mdl-data-table__cell--non-numeric mdl-typography--font-bold">Cached</td>
<td>
<div class="mdl-progress mdl-js-progress" data-progress="{{ memory_cached_percentage }}"></div>
</td>
<td>{{ memory_cached }} MiB / {{ memory_total }} MiB ({{ memory_cached_percentage }}%)</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
<script>
document.querySelectorAll('.mdl-progress').forEach(function(el) {
el.addEventListener('mdl-componentupgraded', function () {
var progress = parseInt(this.dataset.progress || 0);
this.MaterialProgress.setProgress(progress <= 100 ? progress : 100);
});
})
</script>
</body>
</html>

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PagerMaid Login</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.min.css" />
<link rel="stylesheet" href="style.css" />
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
</head>
<body>
<div class="demo-layout mdl-layout mdl-js-layout">
<main class="mdl-layout__content mdl-color--grey-800">
<div class="mdl-grid login-content">
<div class="mdl-layout-spacer"></div>
<div
class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--4-col mdl-cell--4-col-tablet mdl-cell--4-col-desktop">
<div class="mdl-card__title">
{% if msg %}
<h2 class="mdl-card__title-text">{{ msg }}</h2>
{% else %}
<h2 class="mdl-card__title-text">Authentication</h2>
{% endif %}
</div>
<div class="mdl-card__supporting-text">
<form method="post" action="">
{{ form.hidden_tag() }}
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Username</label>
{{ form.username(placeholder="Username", class="mdl-textfield__input") }}
</div>
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Password</label>
{{ form.password(placeholder="Password", class="mdl-textfield__input") }}
</div>
<button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Login
</button>
</form>
</div>
</div>
<div class="mdl-layout-spacer"></div>
</div>
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PagerMaid</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.min.css" />
<link rel="stylesheet" href="style.css" />
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<script src="https://raw.githubusercontent.com/meyvn/mdl-selectfield/master/mdl-selectfield.min.js" defer></script>
<link rel="stylesheet" href="https://raw.githubusercontent.com/meyvn/mdl-selectfield/master/mdl-selectfield.min.css">
</head>
<body>
<div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-header">
<header class="demo-header mdl-layout__header mdl-color--grey-100 mdl-color-text--grey-600">
<div class="mdl-layout__header-row">
<span class="mdl-layout-title">Profile</span>
</div>
</header>
{% include 'includes/navbar.html' %}
<main class="mdl-layout__content mdl-color--grey-100">
<div class="mdl-grid demo-content">
<div
class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--4-col mdl-cell--4-col-tablet mdl-cell--12-col-desktop">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Profile</h2>
</div>
<div class="mdl-card__supporting-text">
<form action="">
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Full Name</label>
{{ form.full_name(placeholder="Full Name", class="mdl-textfield__input") }}
</div>
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Username</label>
{{ form.username(placeholder="Username", class="mdl-textfield__input") }}
</div>
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Email Address</label>
{{ form.email(placeholder="Email Address", class="mdl-textfield__input") }}
</div>
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Password</label>
{{ form.password(placeholder="Password", class="mdl-textfield__input") }}
</div>
<button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Save
</button>
</form>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PagerMaid</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.min.css" />
<link rel="stylesheet" href="style.css" />
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<script src="https://raw.githubusercontent.com/meyvn/mdl-selectfield/master/mdl-selectfield.min.js" defer></script>
<link rel="stylesheet" href="https://raw.githubusercontent.com/meyvn/mdl-selectfield/master/mdl-selectfield.min.css">
</head>
<body>
<div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-header">
<header class="demo-header mdl-layout__header mdl-color--grey-100 mdl-color-text--grey-600">
<div class="mdl-layout__header-row">
<span class="mdl-layout-title">Settings</span>
</div>
</header>
{% include 'includes/navbar.html' %}
<main class="mdl-layout__content mdl-color--grey-100">
<div class="mdl-grid demo-content">
<div
class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--4-col mdl-cell--4-col-tablet mdl-cell--12-col-desktop">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Settings</h2>
</div>
<div class="mdl-card__supporting-text">
<form action="">
<div class="mdl-textfield mdl-js-textfield w-100">
<input class="mdl-textfield__input" type="text" id="full-name">
<label class="mdl-textfield__label" for="full-name">Full Name</label>
</div>
<div class="mdl-textfield mdl-js-textfield w-100">
<textarea class="mdl-textfield__input" type="text" rows="3" id="description"></textarea>
<label class="mdl-textfield__label" for="description">Description</label>
</div>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="switch-1">
<input type="checkbox" id="switch-1" class="mdl-switch__input" checked>
<span class="mdl-switch__label">Toggle?</span>
</label>
<div class="spacer-radio">
<label class="mdl-radio mdl-js-radio mdl-js-ripple-effect w-50" for="option-1">
<input type="radio" id="option-1" class="mdl-radio__button" name="options" value="1"
checked>
<span class="mdl-radio__label">First</span>
</label>
<label class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="option-2">
<input type="radio" id="option-2" class="mdl-radio__button" name="options"
value="2">
<span class="mdl-radio__label">Second</span>
</label>
</div>
<button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Save
</button>
</form>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PagerMaid Setup</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.min.css" />
<link rel="stylesheet" href="style.css" />
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
</head>
<body>
<div class="demo-layout mdl-layout mdl-js-layout">
<main class="mdl-layout__content mdl-color--grey-800">
<div class="mdl-grid login-content">
<div class="mdl-layout-spacer"></div>
<div
class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--4-col mdl-cell--4-col-tablet mdl-cell--4-col-desktop">
<div class="mdl-card__title">
{% if msg %}
<h2 class="mdl-card__title-text">{{ msg }}</h2>
{% else %}
<h2 class="mdl-card__title-text">Setup</h2>
{% endif %}
</div>
<div class="mdl-card__supporting-text">
<form method="post" action="">
{{ form.hidden_tag() }}
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Full Name</label>
{{ form.full_name(placeholder="Full Name", class="mdl-textfield__input") }}
</div>
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Username</label>
{{ form.username(placeholder="Username", class="mdl-textfield__input") }}
</div>
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Email Address</label>
{{ form.email(placeholder="Email Address", class="mdl-textfield__input") }}
</div>
<div class="mdl-textfield mdl-js-textfield w-100">
<label class="mdl-textfield__label">Password</label>
{{ form.password(placeholder="Password", class="mdl-textfield__input") }}
</div>
<button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Confirm
</button>
</form>
</div>
</div>
<div class="mdl-layout-spacer"></div>
</div>
</main>
</div>
</body>
</html>

136
pagermaid/utils.py Normal file
View File

@ -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)

1
plugins/README Normal file
View File

@ -0,0 +1 @@
Refer to DEVELOPMENT.md for plugin development documentation.

32
requirements.txt Normal file
View File

@ -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

33
setup.py Normal file
View File

@ -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
)

View File

@ -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="<message>")
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'])

51
some-plugins/yt-dl.py Normal file
View File

@ -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="<url>.")
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

130
utils/docker.sh Normal file
View File

@ -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

4
utils/entrypoint.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
redis-server --daemonize yes
. /pagermaid/venv/bin/activate
/usr/bin/env python3 -m pagermaid

9
utils/mksession.py Normal file
View File

@ -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()

4
utils/pagermaid Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
source venv/bin/activate;
/usr/bin/env python3 -m pagermaid

View File

@ -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

14
utils/pagermaid.service Normal file
View File

@ -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

4
utils/run Normal file
View File

@ -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

7
utils/start.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/sh
while true; do
clear;
python3 -m pagermaid;
echo 'Restarting...';
sleep 1;
done