✨ Initial commit
8
.deepsource.toml
Normal file
@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "python"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
runtime_version = "3.x.x"
|
34
.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
### Code Editor ###
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# Pycharm
|
||||
*.iws
|
||||
.idea/
|
||||
out/
|
||||
|
||||
### Python ###
|
||||
|
||||
# Environments
|
||||
env/
|
||||
venv/
|
||||
cache/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.pyo
|
||||
*.pyc
|
||||
|
||||
### Customize ###
|
||||
|
||||
config/config.json
|
||||
**_test.html
|
||||
test_**.html
|
||||
logs/
|
||||
/resources/*/*/test/
|
661
LICENSE
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are 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.
|
||||
|
||||
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.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
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 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 work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero 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 Affero 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 Affero 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 Affero 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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
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 AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
55
README.md
Normal file
@ -0,0 +1,55 @@
|
||||
<h1 align="center">TGPaimonBot</h1>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/badge/python-3.8%2B-blue">
|
||||
<img src="https://img.shields.io/badge/works%20on-my%20machine-brightgreen">
|
||||
<img src="https://img.shields.io/badge/status-%E5%92%95%E5%92%95%E5%92%95-blue">
|
||||
<a href="https://www.codacy.com/gh/luoshuijs/TGPaimonBot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=luoshuijs/TGPaimonBot&utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/810a80be4cbe4b7284ab7634941423c4"/></a>
|
||||
</div>
|
||||
|
||||
## 简介
|
||||
|
||||
基于 [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) 的 Paimon BOT
|
||||
|
||||
项目仅供学习交流使用,严禁用于任何商业用途和非法行为
|
||||
|
||||
## 需求
|
||||
|
||||
### 环境需求
|
||||
|
||||
- Python 3.8+
|
||||
- MySQL
|
||||
- Redis
|
||||
|
||||
#### 环境需求需要的注意事项
|
||||
|
||||
因为上游 `genshin.py` 的原因 本项目 python 最低版本为 3.8
|
||||
|
||||
### 模块需求
|
||||
|
||||
#### 模块需求需要的注意事项
|
||||
|
||||
`python-telegram-bot` 需要预览版本 即 `20.0a2`
|
||||
|
||||
出现相关的 `telegram` 模块导入的 `ImportError` 错误需要你手动执行 `pip install python-telegram-bot==20.0a2`
|
||||
|
||||
请注意你的 python 是否安装 `aiohttp` ( `genshin.py` 的依赖)
|
||||
|
||||
如果 `aiohttp` 版本大于 `4.0.0a1`
|
||||
会导致 `redis` 和 `aiohttp` 的依赖 `async-timeout` 版本冲突进而运行代码导致 `TypeError` 异常
|
||||
|
||||
解决上面版本冲突导致的错误需要你手动执行 `pip install aiohttp==3.8.1`
|
||||
|
||||
如果出现模块导入错误请打开 issue 联系开发者 这可能由于上游为预览版本 部分类名称改变导致的问题
|
||||
|
||||
## 其他说明
|
||||
|
||||
这个项目目前正在扩展,加入更多原神相关娱乐和信息查询功能,敬请期待。
|
||||
|
||||
## Thanks
|
||||
|
||||
| Nickname | Introduce |
|
||||
| :----------------------------------------------------------: | -------------------------------- |
|
||||
| [原神抽卡全机制总结](https://www.bilibili.com/read/cv10468091) | 本项目抽卡模拟器使用的逻辑 |
|
||||
| [西风驿站](https://bbs.mihoyo.com/ys/collection/307224) | 本项目攻略图图源 |
|
||||
| [Yunzai-Bot](https://github.com/Le-niao/Yunzai-Bot) | 本项使用的抽卡图片和前端资源来源 |
|
14
app/admin/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
from app.admin.cache import BotAdminCache
|
||||
from app.admin.repositories import BotAdminRepository
|
||||
from app.admin.service import BotAdminService
|
||||
from utils.app.manager import listener_service
|
||||
from utils.mysql import MySQL
|
||||
from utils.redisdb import RedisDB
|
||||
|
||||
|
||||
@listener_service()
|
||||
def create_bot_admin_service(mysql: MySQL, redis: RedisDB):
|
||||
_cache = BotAdminCache(redis)
|
||||
_repository = BotAdminRepository(mysql)
|
||||
_service = BotAdminService(_repository, _cache)
|
||||
return _service
|
38
app/admin/cache.py
Normal file
@ -0,0 +1,38 @@
|
||||
from typing import List
|
||||
|
||||
from utils.redisdb import RedisDB
|
||||
|
||||
|
||||
class BotAdminCache:
|
||||
def __init__(self, redis: RedisDB):
|
||||
self.client = redis.client
|
||||
self.qname = "bot:admin"
|
||||
|
||||
async def get_list(self):
|
||||
return [int(str_data) for str_data in await self.client.lrange(self.qname, 0, -1)]
|
||||
|
||||
async def set_list(self, str_list: List[int], ttl: int = -1):
|
||||
await self.client.ltrim(self.qname, 1, 0)
|
||||
await self.client.lpush(self.qname, *str_list)
|
||||
if ttl != -1:
|
||||
await self.client.expire(self.qname, ttl)
|
||||
count = await self.client.llen(self.qname)
|
||||
return count
|
||||
|
||||
|
||||
class GroupAdminCache:
|
||||
def __init__(self, redis: RedisDB):
|
||||
self.client = redis.client
|
||||
self.qname = "group:admin_list"
|
||||
|
||||
async def get_chat_admin(self, chat_id: int):
|
||||
qname = f"{self.qname}:{chat_id}"
|
||||
return [int(str_id) for str_id in await self.client.lrange(qname, 0, -1)]
|
||||
|
||||
async def set_chat_admin(self, chat_id: int, admin_list: List[int]):
|
||||
qname = f"{self.qname}:{chat_id}"
|
||||
await self.client.ltrim(qname, 1, 0)
|
||||
await self.client.lpush(qname, *admin_list)
|
||||
await self.client.expire(qname, 60)
|
||||
count = await self.client.llen(qname)
|
||||
return count
|
37
app/admin/repositories.py
Normal file
@ -0,0 +1,37 @@
|
||||
from typing import List
|
||||
|
||||
from utils.mysql import MySQL
|
||||
|
||||
|
||||
class BotAdminRepository:
|
||||
def __init__(self, mysql: MySQL):
|
||||
self.mysql = mysql
|
||||
|
||||
async def delete_by_user_id(self, user_id: int):
|
||||
query = """
|
||||
DELETE FROM `admin`
|
||||
WHERE user_id=%s;
|
||||
"""
|
||||
query_args = (user_id,)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
||||
|
||||
async def add_by_user_id(self, user_id: int):
|
||||
query = """
|
||||
INSERT INTO `admin`
|
||||
(user_id)
|
||||
VALUES
|
||||
(%s)
|
||||
"""
|
||||
query_args = (user_id,)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
||||
|
||||
async def get_by_user_id(self) -> List[int]:
|
||||
query = """
|
||||
SELECT user_id
|
||||
FROM `admin`
|
||||
"""
|
||||
query_args = ()
|
||||
data = await self.mysql.execute_and_fetchall(query, query_args)
|
||||
if len(data) == 0:
|
||||
return []
|
||||
return [i[0] for i in data]
|
60
app/admin/service.py
Normal file
@ -0,0 +1,60 @@
|
||||
from typing import List
|
||||
|
||||
from pymysql import IntegrityError
|
||||
from telegram import Bot
|
||||
|
||||
from app.admin.cache import BotAdminCache, GroupAdminCache
|
||||
from app.admin.repositories import BotAdminRepository
|
||||
from config import config
|
||||
from logger import Log
|
||||
|
||||
|
||||
class BotAdminService:
|
||||
def __init__(self, repository: BotAdminRepository, cache: BotAdminCache):
|
||||
self._repository = repository
|
||||
self._cache = cache
|
||||
|
||||
async def get_admin_list(self) -> List[int]:
|
||||
admin_list = await self._cache.get_list()
|
||||
if len(admin_list) == 0:
|
||||
admin_list = await self._repository.get_by_user_id()
|
||||
for config_admin in config.ADMINISTRATORS:
|
||||
admin_list.append(config_admin["user_id"])
|
||||
await self._cache.set_list(admin_list)
|
||||
return admin_list
|
||||
|
||||
async def add_admin(self, user_id: int) -> bool:
|
||||
try:
|
||||
await self._repository.add_by_user_id(user_id)
|
||||
except IntegrityError as error:
|
||||
Log.warning(f"{user_id} 已经存在数据库 \n", error)
|
||||
admin_list = await self._repository.get_by_user_id()
|
||||
for config_admin in config.ADMINISTRATORS:
|
||||
admin_list.append(config_admin["user_id"])
|
||||
await self._cache.set_list(admin_list)
|
||||
return True
|
||||
|
||||
async def delete_admin(self, user_id: int) -> bool:
|
||||
try:
|
||||
await self._repository.delete_by_user_id(user_id)
|
||||
except ValueError:
|
||||
return False
|
||||
admin_list = await self._repository.get_by_user_id()
|
||||
for config_admin in config.ADMINISTRATORS:
|
||||
admin_list.append(config_admin["user_id"])
|
||||
await self._cache.set_list(admin_list)
|
||||
return True
|
||||
|
||||
|
||||
class GroupAdminService:
|
||||
def __init__(self, cache: GroupAdminCache):
|
||||
self._cache = cache
|
||||
|
||||
async def get_admins(self, bot: Bot, chat_id: int, extra_user: List[int]) -> List[int]:
|
||||
admin_id_list = await self._cache.get_chat_admin(chat_id)
|
||||
if len(admin_id_list) == 0:
|
||||
admin_list = await bot.get_chat_administrators(chat_id)
|
||||
admin_id_list = [admin.user.id for admin in admin_list]
|
||||
await self._cache.set_chat_admin(chat_id, admin_id_list)
|
||||
admin_id_list += extra_user
|
||||
return admin_id_list
|
0
app/cookies/__init__.py
Normal file
79
app/cookies/repositories.py
Normal file
@ -0,0 +1,79 @@
|
||||
import ujson
|
||||
|
||||
from model.base import ServiceEnum
|
||||
from utils.error import NotFoundError
|
||||
from utils.mysql import MySQL
|
||||
|
||||
|
||||
class CookiesRepository:
|
||||
def __init__(self, mysql: MySQL):
|
||||
self.mysql = mysql
|
||||
|
||||
async def update_cookie(self, user_id: int, cookies: str, default_service: ServiceEnum):
|
||||
if default_service == ServiceEnum.HYPERION:
|
||||
query = """
|
||||
UPDATE `mihoyo_cookie`
|
||||
SET cookie=%s
|
||||
WHERE user_id=%s;
|
||||
"""
|
||||
elif default_service == ServiceEnum.HOYOLAB:
|
||||
query = """
|
||||
UPDATE `hoyoverse_cookie`
|
||||
SET cookie=%s
|
||||
WHERE user_id=%s;
|
||||
"""
|
||||
else:
|
||||
raise DefaultServiceNotFoundError(default_service.name)
|
||||
query_args = (cookies, user_id)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
||||
|
||||
async def set_cookie(self, user_id: int, cookies: str, default_service: ServiceEnum):
|
||||
if default_service == ServiceEnum.HYPERION:
|
||||
query = """
|
||||
INSERT INTO `mihoyo_cookie`
|
||||
(user_id,cookie)
|
||||
VALUES
|
||||
(%s,%s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
cookie=VALUES(cookie);
|
||||
"""
|
||||
elif default_service == ServiceEnum.HOYOLAB:
|
||||
query = """
|
||||
INSERT INTO `hoyoverse_cookie`
|
||||
(user_id,cookie)
|
||||
VALUES
|
||||
(%s,%s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
cookie=VALUES(cookie);
|
||||
"""
|
||||
else:
|
||||
raise DefaultServiceNotFoundError(default_service.name)
|
||||
query_args = (user_id, cookies)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
||||
|
||||
async def read_cookies(self, user_id, default_service: ServiceEnum) -> dict:
|
||||
if default_service == ServiceEnum.HYPERION:
|
||||
query = """
|
||||
SELECT cookie
|
||||
FROM `mihoyo_cookie`
|
||||
WHERE user_id=%s;
|
||||
"""
|
||||
elif default_service == ServiceEnum.HOYOLAB:
|
||||
query = """
|
||||
SELECT cookie
|
||||
FROM `hoyoverse_cookie`
|
||||
WHERE user_id=%s;;
|
||||
"""
|
||||
else:
|
||||
raise DefaultServiceNotFoundError(default_service.name)
|
||||
query_args = (user_id,)
|
||||
data = await self.mysql.execute_and_fetchall(query, query_args)
|
||||
if len(data) == 0:
|
||||
return {}
|
||||
(cookies,) = data
|
||||
return ujson.loads(cookies)
|
||||
|
||||
|
||||
class DefaultServiceNotFoundError(NotFoundError):
|
||||
entity_name: str = "ServiceEnum"
|
||||
entity_value_name: str = "default_service"
|
16
app/cookies/service.py
Normal file
@ -0,0 +1,16 @@
|
||||
from app.cookies.repositories import CookiesRepository
|
||||
from model.base import ServiceEnum
|
||||
|
||||
|
||||
class CookiesService:
|
||||
def __init__(self, user_repository: CookiesRepository) -> None:
|
||||
self._repository: CookiesRepository = user_repository
|
||||
|
||||
async def update_cookie(self, user_id: int, cookies: str, default_service: ServiceEnum):
|
||||
await self._repository.update_cookie(user_id, cookies, default_service)
|
||||
|
||||
async def set_cookie(self, user_id: int, cookies: str, default_service: ServiceEnum):
|
||||
await self._repository.set_cookie(user_id, cookies, default_service)
|
||||
|
||||
async def read_cookies(self, user_id: int, default_service: ServiceEnum):
|
||||
return await self._repository.read_cookies(user_id, default_service)
|
0
app/game/__init__.py
Normal file
22
app/game/cache.py
Normal file
@ -0,0 +1,22 @@
|
||||
from typing import List
|
||||
|
||||
from utils.redisdb import RedisDB
|
||||
|
||||
|
||||
class GameStrategyCache:
|
||||
def __init__(self, redis: RedisDB, ttl: int = 3600):
|
||||
self.client = redis.client
|
||||
self.qname = "game:strategy"
|
||||
self.ttl = ttl
|
||||
|
||||
async def get_url_list(self, character_name: str):
|
||||
qname = f"{self.qname}:{character_name}"
|
||||
return [str(str_data, encoding="utf-8") for str_data in await self.client.lrange(qname, 0, -1)]
|
||||
|
||||
async def set_url_list(self, character_name: str, str_list: List[str]):
|
||||
qname = f"{self.qname}:{character_name}"
|
||||
await self.client.ltrim(qname, 1, 0)
|
||||
await self.client.lpush(qname, *str_list)
|
||||
await self.client.expire(qname, self.ttl)
|
||||
count = await self.client.llen(qname)
|
||||
return count
|
46
app/game/service.py
Normal file
@ -0,0 +1,46 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from app.game.cache import GameStrategyCache
|
||||
from model.apihelper.hyperion import Hyperion
|
||||
|
||||
|
||||
class GameStrategyService:
|
||||
|
||||
def __init__(self, cache: GameStrategyCache, collections: Optional[List[int]] = None):
|
||||
self._cache = cache
|
||||
self._hyperion = Hyperion()
|
||||
if collections is None:
|
||||
self._collections = [839176, 839179, 839181]
|
||||
else:
|
||||
self._collections = collections
|
||||
|
||||
async def _get_strategy_from_hyperion(self, collection_id: int, character_name: str) -> int:
|
||||
post_id: int = -1
|
||||
post_full_in_collection = await self._hyperion.get_post_full_in_collection(collection_id)
|
||||
if post_full_in_collection.error:
|
||||
return post_id
|
||||
for post_data in post_full_in_collection.data["posts"]:
|
||||
topics = post_data["topics"]
|
||||
for topic in topics:
|
||||
if character_name == topic["name"]:
|
||||
post_id = int(post_data["post"]["post_id"])
|
||||
break
|
||||
if post_id != -1:
|
||||
break
|
||||
return post_id
|
||||
|
||||
async def get_strategy(self, character_name: str) -> str:
|
||||
cache = await self._cache.get_url_list(character_name)
|
||||
if len(cache) >= 1:
|
||||
return cache[-1]
|
||||
|
||||
for collection_id in self._collections:
|
||||
post_id = await self._get_strategy_from_hyperion(collection_id, character_name)
|
||||
if post_id != -1:
|
||||
break
|
||||
else:
|
||||
return ""
|
||||
|
||||
artwork_info = await self._hyperion.get_artwork_info(2, post_id)
|
||||
await self._cache.set_url_list(character_name, artwork_info.results.image_url_list)
|
||||
return artwork_info.results.image_url_list[0]
|
0
app/quiz/__init__.py
Normal file
18
app/quiz/base.py
Normal file
@ -0,0 +1,18 @@
|
||||
from typing import List
|
||||
|
||||
from app.quiz.models import Question, Answer
|
||||
|
||||
|
||||
def CreatQuestionFromSQLData(data: tuple) -> List[Question]:
|
||||
temp_list = []
|
||||
for temp_data in data:
|
||||
(question_id, text) = temp_data
|
||||
temp_list.append(Question(question_id, text))
|
||||
return temp_list
|
||||
|
||||
def CreatAnswerFromSQLData(data: tuple) -> List[Answer]:
|
||||
temp_list = []
|
||||
for temp_data in data:
|
||||
(answer_id, question_id, is_correct, text) = temp_data
|
||||
temp_list.append(Answer(answer_id, question_id, is_correct, text))
|
||||
return temp_list
|
63
app/quiz/cache.py
Normal file
@ -0,0 +1,63 @@
|
||||
from typing import List
|
||||
|
||||
import ujson
|
||||
|
||||
from app.quiz.models import Question, Answer
|
||||
from utils.redisdb import RedisDB
|
||||
|
||||
|
||||
class QuizCache:
|
||||
|
||||
def __init__(self, redis: RedisDB):
|
||||
self.client = redis.client
|
||||
self.question_qname = "quiz:question"
|
||||
self.answer_qname = "quiz:answer"
|
||||
|
||||
async def get_all_question(self) -> List[Question]:
|
||||
temp_list = []
|
||||
qname = self.question_qname + "id_list"
|
||||
data_list = [self.question_qname + f":{question_id}" for question_id in
|
||||
await self.client.lrange(qname, 0, -1)]
|
||||
data = await self.client.mget(data_list)
|
||||
for i in data:
|
||||
temp_list.append(Question.de_json(ujson.loads(i)))
|
||||
return temp_list
|
||||
|
||||
async def get_all_question_id_list(self) -> List[str]:
|
||||
qname = self.question_qname + ":id_list"
|
||||
return await self.client.lrange(qname, 0, -1)
|
||||
|
||||
async def get_one_question(self, question_id: int) -> Question:
|
||||
qname = f"{self.question_qname}:{question_id}"
|
||||
data = await self.client.get(qname)
|
||||
return Question.de_json(ujson.loads(data))
|
||||
|
||||
async def get_one_answer(self, answer_id: int) -> str:
|
||||
qname = f"{self.answer_qname}:{answer_id}"
|
||||
return await self.client.get(qname)
|
||||
|
||||
async def add_question(self, question_list: List[Question] = None):
|
||||
for question in question_list:
|
||||
await self.client.set(f"{self.question_qname}:{question.question_id}", str(question))
|
||||
question_id_list = [question.question_id for question in question_list]
|
||||
await self.client.lpush(f"{self.question_qname}:id_list", *question_id_list)
|
||||
return await self.client.llen(f"{self.question_qname}:id_list")
|
||||
|
||||
async def del_all_question(self):
|
||||
keys = await self.client.keys(f"{self.question_qname}*")
|
||||
if keys is not None:
|
||||
for key in keys:
|
||||
await self.client.delete(key)
|
||||
|
||||
async def del_all_answer(self):
|
||||
keys = await self.client.keys(f"{self.answer_qname}*")
|
||||
if keys is not None:
|
||||
for key in keys:
|
||||
await self.client.delete(key)
|
||||
|
||||
async def add_answer(self, answer_list: List[Answer] = None):
|
||||
for answer in answer_list:
|
||||
await self.client.set(f"{self.answer_qname}:{answer.answer_id}", str(answer))
|
||||
answer_id_list = [answer.answer_id for answer in answer_list]
|
||||
await self.client.lpush(f"{self.answer_qname}:id_list", *answer_id_list)
|
||||
return await self.client.llen(f"{self.answer_qname}:id_list")
|
50
app/quiz/models.py
Normal file
@ -0,0 +1,50 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from model.baseobject import BaseObject
|
||||
from model.types import JSONDict
|
||||
|
||||
|
||||
class Answer(BaseObject):
|
||||
def __init__(self, answer_id: int = 0, question_id: int = 0, is_correct: bool = True, text: str = ""):
|
||||
"""Answer类
|
||||
|
||||
:param answer_id: 答案ID
|
||||
:param question_id: 与之对应的问题ID
|
||||
:param is_correct: 该答案是否正确
|
||||
:param text: 答案文本
|
||||
"""
|
||||
self.answer_id = answer_id
|
||||
self.question_id = question_id
|
||||
self.text = text
|
||||
self.is_correct = is_correct
|
||||
|
||||
__slots__ = ("answer_id", "question_id", "text", "is_correct")
|
||||
|
||||
|
||||
class Question(BaseObject):
|
||||
def __init__(self, question_id: int = 0, text: str = "", answers: List[Answer] = None):
|
||||
"""Question类
|
||||
|
||||
:param question_id: 问题ID
|
||||
:param text: 问题文本
|
||||
:param answers: 答案列表
|
||||
"""
|
||||
self.question_id = question_id
|
||||
self.text = text
|
||||
self.answers = [] if answers is None else answers
|
||||
|
||||
def to_dict(self) -> JSONDict:
|
||||
data = super().to_dict()
|
||||
if self.answers:
|
||||
data["sub_item"] = [e.to_dict() for e in self.answers]
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict]) -> Optional["Question"]:
|
||||
data = cls._parse_data(data)
|
||||
if not data:
|
||||
return None
|
||||
data["sub_item"] = Answer.de_list(data.get("sub_item"))
|
||||
return cls(**data)
|
||||
|
||||
__slots__ = ("question_id", "text", "answers")
|
83
app/quiz/repositories.py
Normal file
@ -0,0 +1,83 @@
|
||||
from typing import List
|
||||
|
||||
from app.quiz.base import CreatQuestionFromSQLData, CreatAnswerFromSQLData
|
||||
from app.quiz.models import Question, Answer
|
||||
from utils.mysql import MySQL
|
||||
|
||||
|
||||
class QuizRepository:
|
||||
def __init__(self, mysql: MySQL):
|
||||
self.mysql = mysql
|
||||
|
||||
async def get_question_list(self) -> List[Question]:
|
||||
query = """
|
||||
SELECT id,question
|
||||
FROM `question`
|
||||
"""
|
||||
query_args = ()
|
||||
data = await self.mysql.execute_and_fetchall(query, query_args)
|
||||
return CreatQuestionFromSQLData(data)
|
||||
|
||||
async def get_answer_form_question_id(self, question_id: int) -> List[Answer]:
|
||||
query = """
|
||||
SELECT id,question_id,is_correct,answer
|
||||
FROM `answer`
|
||||
WHERE question_id=%s;
|
||||
"""
|
||||
query_args = (question_id,)
|
||||
data = await self.mysql.execute_and_fetchall(query, query_args)
|
||||
return CreatAnswerFromSQLData(data)
|
||||
|
||||
async def add_question(self, question: str):
|
||||
query = """
|
||||
INSERT INTO `question`
|
||||
(question)
|
||||
VALUES
|
||||
(%s)
|
||||
"""
|
||||
query_args = (question,)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
||||
|
||||
async def get_question(self, question: str) -> Question:
|
||||
query = """
|
||||
SELECT id,question
|
||||
FROM `question`
|
||||
WHERE question=%s;
|
||||
"""
|
||||
query_args = (question,)
|
||||
data = await self.mysql.execute_and_fetchall(query, query_args)
|
||||
return CreatQuestionFromSQLData(data)[0]
|
||||
|
||||
async def add_answer(self, question_id: int, is_correct: int, answer: str):
|
||||
query = """
|
||||
INSERT INTO `answer`
|
||||
(question_id,is_correct,answer)
|
||||
VALUES
|
||||
(%s,%s,%s)
|
||||
"""
|
||||
query_args = (question_id, is_correct, answer)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
||||
|
||||
async def delete_question(self, question_id: int):
|
||||
query = """
|
||||
DELETE FROM `question`
|
||||
WHERE id=%s;
|
||||
"""
|
||||
query_args = (question_id,)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
||||
|
||||
async def delete_answer(self, answer_id: int):
|
||||
query = """
|
||||
DELETE FROM `answer`
|
||||
WHERE id=%s;
|
||||
"""
|
||||
query_args = (answer_id,)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
||||
|
||||
async def delete_admin(self, user_id: int):
|
||||
query = """
|
||||
DELETE FROM `admin`
|
||||
WHERE user_id=%s;
|
||||
"""
|
||||
query_args = (user_id,)
|
||||
await self.mysql.execute_and_fetchall(query, query_args)
|
49
app/quiz/service.py
Normal file
@ -0,0 +1,49 @@
|
||||
from typing import List
|
||||
|
||||
import ujson
|
||||
|
||||
from app.quiz.cache import QuizCache
|
||||
from app.quiz.models import Question
|
||||
from app.quiz.repositories import QuizRepository
|
||||
|
||||
|
||||
class QuizService:
|
||||
def __init__(self, repository: QuizRepository, cache: QuizCache):
|
||||
self._repository = repository
|
||||
self._cache = cache
|
||||
|
||||
async def get_quiz(self) -> List[Question]:
|
||||
"""从数据库获取问题列表
|
||||
:return:
|
||||
"""
|
||||
question_list = await self._repository.get_question_list()
|
||||
for question in question_list:
|
||||
question_id = question.question_id
|
||||
answers = await self._repository.get_answer_form_question_id(question_id)
|
||||
question.answers = answers
|
||||
return question_list
|
||||
|
||||
async def save_quiz(self, data: Question):
|
||||
await self._repository.get_question(data.text)
|
||||
question = await self._repository.get_question(data.text)
|
||||
for answers in data.answers:
|
||||
await self._repository.add_answer(question.question_id, answers.is_correct, answers.text)
|
||||
|
||||
async def refresh_quiz(self) -> int:
|
||||
question_list = await self.get_quiz()
|
||||
await self._cache.del_all_question()
|
||||
question_count = await self._cache.add_question(question_list)
|
||||
await self._cache.del_all_answer()
|
||||
for question in question_list:
|
||||
await self._cache.add_answer(question.answers)
|
||||
return question_count
|
||||
|
||||
async def get_question_id_list(self) -> List[int]:
|
||||
return [int(question_id) for question_id in await self._cache.get_all_question_id_list()]
|
||||
|
||||
async def get_answer(self, answer_id: int):
|
||||
data = await self._cache.get_one_answer(answer_id)
|
||||
return ujson.loads(data)
|
||||
|
||||
async def get_question(self, question_id: int) -> Question:
|
||||
return await self._cache.get_one_question(question_id)
|
9
app/template/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from app.template.service import TemplateService
|
||||
from utils.aiobrowser import AioBrowser
|
||||
from utils.app.manager import listener_service
|
||||
|
||||
|
||||
@listener_service()
|
||||
def create_template_service(browser: AioBrowser):
|
||||
_service = TemplateService(browser)
|
||||
return _service
|
70
app/template/service.py
Normal file
@ -0,0 +1,70 @@
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from jinja2 import PackageLoader, Environment, Template
|
||||
from playwright.async_api import ViewportSize
|
||||
|
||||
from config import config
|
||||
from logger import Log
|
||||
from utils.aiobrowser import AioBrowser
|
||||
|
||||
|
||||
class TemplateService:
|
||||
def __init__(self, browser: AioBrowser, template_package_name: str = "resources", cache_dir_name: str = "cache"):
|
||||
self._browser = browser
|
||||
self._template_package_name = template_package_name
|
||||
self._current_dir = os.getcwd()
|
||||
self._output_dir = os.path.join(self._current_dir, cache_dir_name)
|
||||
if not os.path.exists(self._output_dir):
|
||||
os.mkdir(self._output_dir)
|
||||
self._jinja2_env = {}
|
||||
self._jinja2_template = {}
|
||||
|
||||
def get_template(self, package_path: str, template_name: str, auto_escape: bool = True) -> Template:
|
||||
if config.DEBUG:
|
||||
# DEBUG下 禁止复用 方便查看和修改模板
|
||||
loader = PackageLoader(self._template_package_name, package_path)
|
||||
jinja2_env = Environment(loader=loader, enable_async=True, autoescape=auto_escape)
|
||||
jinja2_template = jinja2_env.get_template(template_name)
|
||||
else:
|
||||
jinja2_env: Environment = self._jinja2_env.get(package_path)
|
||||
jinja2_template: Template = self._jinja2_template.get(package_path + template_name)
|
||||
if jinja2_env is None:
|
||||
loader = PackageLoader(self._template_package_name, package_path)
|
||||
jinja2_env = Environment(loader=loader, enable_async=True, autoescape=auto_escape)
|
||||
jinja2_template = jinja2_env.get_template(template_name)
|
||||
self._jinja2_env[package_path] = jinja2_env
|
||||
self._jinja2_template[package_path + template_name] = jinja2_template
|
||||
return jinja2_template
|
||||
|
||||
async def render(self, template_path: str, template_name: str, template_data: dict,
|
||||
viewport: ViewportSize, full_page: bool = True, auto_escape: bool = True,
|
||||
evaluate: Optional[str] = None) -> bytes:
|
||||
"""
|
||||
模板渲染成图片
|
||||
:param template_path: 模板目录
|
||||
:param template_name: 模板文件名
|
||||
:param template_data: 模板数据
|
||||
:param viewport: 截图大小
|
||||
:param full_page: 是否长截图
|
||||
:param auto_escape: 是否自动转义
|
||||
:param evaluate: 页面加载后运行的 js
|
||||
:return:
|
||||
"""
|
||||
start_time = time.time()
|
||||
template = self.get_template(template_path, template_name, auto_escape)
|
||||
template_data["res_path"] = f"file://{self._current_dir}"
|
||||
html = await template.render_async(**template_data)
|
||||
Log.debug(f"{template_name} 模板渲染使用了 {str(time.time() - start_time)}")
|
||||
browser = await self._browser.get_browser()
|
||||
start_time = time.time()
|
||||
page = await browser.new_page(viewport=viewport)
|
||||
await page.goto(f"file://{template.filename}")
|
||||
await page.set_content(html, wait_until="networkidle")
|
||||
if evaluate:
|
||||
await page.evaluate(evaluate)
|
||||
png_data = await page.screenshot(full_page=full_page)
|
||||
await page.close()
|
||||
Log.debug(f"{template_name} 图片渲染使用了 {str(time.time() - start_time)}")
|
||||
return png_data
|
11
app/user/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
from app.user.repositories import UserRepository
|
||||
from app.user.services import UserService
|
||||
from utils.app.manager import listener_service
|
||||
from utils.mysql import MySQL
|
||||
|
||||
|
||||
@listener_service()
|
||||
def create_user_service(mysql: MySQL):
|
||||
_repository = UserRepository(mysql)
|
||||
_service = UserService(_repository)
|
||||
return _service
|
11
app/user/models.py
Normal file
@ -0,0 +1,11 @@
|
||||
from model.base import ServiceEnum
|
||||
from model.baseobject import BaseObject
|
||||
|
||||
|
||||
class User(BaseObject):
|
||||
def __init__(self, user_id: int = 0, yuanshen_game_uid: int = 0, genshin_game_uid: int = 0,
|
||||
default_service: ServiceEnum = ServiceEnum.NULL):
|
||||
self.user_id = user_id
|
||||
self.yuanshen_game_uid = yuanshen_game_uid
|
||||
self.genshin_game_uid = genshin_game_uid
|
||||
self.default_service = default_service
|
26
app/user/repositories.py
Normal file
@ -0,0 +1,26 @@
|
||||
from app.user.models import User
|
||||
from model.base import ServiceEnum
|
||||
from utils.error import NotFoundError
|
||||
from utils.mysql import MySQL
|
||||
|
||||
|
||||
class UserRepository:
|
||||
def __init__(self, mysql: MySQL):
|
||||
self.mysql = mysql
|
||||
|
||||
async def get_by_user_id(self, user_id: int) -> User:
|
||||
query = """
|
||||
SELECT user_id,mihoyo_game_uid,hoyoverse_game_uid,service
|
||||
FROM `user`
|
||||
WHERE user_id=%s;"""
|
||||
query_args = (user_id,)
|
||||
data = await self.mysql.execute_and_fetchall(query, query_args)
|
||||
if len(data) == 0:
|
||||
raise UserNotFoundError(user_id)
|
||||
(user_id, yuanshen_game_uid, genshin_game_uid, default_service) = data
|
||||
return User(user_id, yuanshen_game_uid, genshin_game_uid, ServiceEnum(default_service))
|
||||
|
||||
|
||||
class UserNotFoundError(NotFoundError):
|
||||
entity_name: str = "User"
|
||||
entity_value_name: str = "id"
|
15
app/user/services.py
Normal file
@ -0,0 +1,15 @@
|
||||
from app.user.models import User
|
||||
from app.user.repositories import UserRepository
|
||||
|
||||
|
||||
class UserService:
|
||||
|
||||
def __init__(self, user_repository: UserRepository) -> None:
|
||||
self._repository: UserRepository = user_repository
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> User:
|
||||
"""从数据库获取用户信息
|
||||
:param user_id:用户ID
|
||||
:return:
|
||||
"""
|
||||
return await self._repository.get_by_user_id(user_id)
|
0
app/wiki/__init__.py
Normal file
21
app/wiki/cache.py
Normal file
@ -0,0 +1,21 @@
|
||||
import ujson
|
||||
|
||||
from utils.redisdb import RedisDB
|
||||
|
||||
|
||||
class WikiCache:
|
||||
def __init__(self, redis: RedisDB):
|
||||
self.client = redis.client
|
||||
self.qname = "wiki"
|
||||
|
||||
async def refresh_info_cache(self, key_name: str, info):
|
||||
qname = f"{self.qname}:{key_name}"
|
||||
await self.client.set(qname, ujson.dumps(info))
|
||||
|
||||
async def del_one(self, key_name: str):
|
||||
qname = f"{self.qname}:{key_name}"
|
||||
await self.client.delete(qname)
|
||||
|
||||
async def get_one(self, key_name: str) -> str:
|
||||
qname = f"{self.qname}:{key_name}"
|
||||
return await self.client.get(qname)
|
117
app/wiki/service.py
Normal file
@ -0,0 +1,117 @@
|
||||
import asyncio
|
||||
|
||||
import ujson
|
||||
|
||||
from app.wiki.cache import WikiCache
|
||||
from logger import Log
|
||||
from model.wiki.characters import Characters
|
||||
from model.wiki.weapons import Weapons
|
||||
|
||||
|
||||
class WikiService:
|
||||
|
||||
def __init__(self, cache: WikiCache):
|
||||
self._cache = cache
|
||||
"""
|
||||
Redis 在这里的作用是作为持久化
|
||||
"""
|
||||
self.weapons = Weapons()
|
||||
self.characters = Characters()
|
||||
self._characters_list = []
|
||||
self._characters_name_list = []
|
||||
self._weapons_name_list = []
|
||||
self._weapons_list = []
|
||||
self.first_run = True
|
||||
|
||||
async def refresh_weapon(self):
|
||||
weapon_url_list = await self.weapons.get_all_weapon_url()
|
||||
Log.info(f"一共找到 {len(weapon_url_list)} 把武器信息")
|
||||
weapons_list = []
|
||||
task_list = []
|
||||
for index, weapon_url in enumerate(weapon_url_list):
|
||||
task_list.append(self.weapons.get_weapon_info(weapon_url))
|
||||
# weapon_info = await self.weapons.get_weapon_info(weapon_url)
|
||||
if index % 5 == 0:
|
||||
result_list = await asyncio.gather(*task_list)
|
||||
weapons_list.extend(result for result in result_list if isinstance(result, dict))
|
||||
task_list.clear()
|
||||
if index % 10 == 0 and index != 0:
|
||||
Log.info(f"现在已经获取到 {index} 把武器信息")
|
||||
result_list = await asyncio.gather(*task_list)
|
||||
weapons_list.extend(result for result in result_list if isinstance(result, dict))
|
||||
|
||||
Log.info("写入武器信息到Redis")
|
||||
self._weapons_list = weapons_list
|
||||
await self._cache.del_one("weapon")
|
||||
await self._cache.refresh_info_cache("weapon", weapons_list)
|
||||
|
||||
async def refresh_characters(self):
|
||||
characters_url_list = await self.characters.get_all_characters_url()
|
||||
Log.info(f"一共找到 {len(characters_url_list)} 个角色信息")
|
||||
characters_list = []
|
||||
task_list = []
|
||||
for index, characters_url in enumerate(characters_url_list):
|
||||
task_list.append(self.characters.get_characters(characters_url))
|
||||
if index % 5 == 0:
|
||||
result_list = await asyncio.gather(*task_list)
|
||||
characters_list.extend(result for result in result_list if isinstance(result, dict))
|
||||
task_list.clear()
|
||||
if index % 10 == 0 and index != 0:
|
||||
Log.info(f"现在已经获取到 {index} 个角色信息")
|
||||
result_list = await asyncio.gather(*task_list)
|
||||
characters_list.extend(result for result in result_list if isinstance(result, dict))
|
||||
|
||||
Log.info("写入角色信息到Redis")
|
||||
self._characters_list = characters_list
|
||||
await self._cache.del_one("characters")
|
||||
await self._cache.refresh_info_cache("characters", characters_list)
|
||||
|
||||
async def refresh_wiki(self):
|
||||
"""
|
||||
用于把Redis的缓存全部加载进Python
|
||||
:return:
|
||||
"""
|
||||
Log.info("正在重新获取Wiki")
|
||||
Log.info("正在重新获取武器信息")
|
||||
await self.refresh_weapon()
|
||||
Log.info("正在重新获取角色信息")
|
||||
await self.refresh_characters()
|
||||
Log.info("刷新成功")
|
||||
|
||||
async def init(self):
|
||||
"""
|
||||
用于把Redis的缓存全部加载进Python
|
||||
:return:
|
||||
"""
|
||||
if self.first_run:
|
||||
weapon_dict = await self._cache.get_one("weapon")
|
||||
self._weapons_list = ujson.loads(weapon_dict)
|
||||
for weapon in self._weapons_list:
|
||||
self._weapons_name_list.append(weapon["name"])
|
||||
characters_dict = await self._cache.get_one("characters")
|
||||
self._characters_list = ujson.loads(characters_dict)
|
||||
for characters in self._characters_list:
|
||||
self._characters_name_list.append(characters["name"])
|
||||
self.first_run = False
|
||||
|
||||
async def get_weapons(self, name: str):
|
||||
await self.init()
|
||||
if len(self._weapons_list) == 0:
|
||||
return {}
|
||||
return next((weapon for weapon in self._weapons_list if weapon["name"] == name), {})
|
||||
|
||||
async def get_weapons_name_list(self) -> list:
|
||||
await self.init()
|
||||
return self._weapons_name_list
|
||||
|
||||
async def get_weapons_list(self) -> list:
|
||||
await self.init()
|
||||
return self._weapons_list
|
||||
|
||||
async def get_characters_list(self) -> list:
|
||||
await self.init()
|
||||
return self._characters_list
|
||||
|
||||
async def get_characters_name_list(self) -> list:
|
||||
await self.init()
|
||||
return self._characters_name_list
|
29
config.py
Normal file
@ -0,0 +1,29 @@
|
||||
import os
|
||||
|
||||
import ujson
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
project_path = os.path.dirname(__file__)
|
||||
config_file = os.path.join(project_path, './config', 'config.json')
|
||||
if not os.path.exists(config_file):
|
||||
config_file = os.path.join(project_path, './config', 'config.example.json')
|
||||
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
self._config_json: dict = ujson.load(f)
|
||||
|
||||
self.DEBUG = self.get_config("debug")
|
||||
if not isinstance(self.DEBUG, bool):
|
||||
self.DEBUG = False
|
||||
self.ADMINISTRATORS = self.get_config("administrators")
|
||||
self.MYSQL = self.get_config("mysql")
|
||||
self.REDIS = self.get_config("redis")
|
||||
self.TELEGRAM = self.get_config("telegram")
|
||||
self.FUNCTION = self.get_config("function")
|
||||
|
||||
def get_config(self, name: str):
|
||||
return self._config_json.get(name, {})
|
||||
|
||||
|
||||
config = Config()
|
38
config/config.json.example
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"mysql": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 3306,
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
},
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
"database": 0
|
||||
},
|
||||
"telegram": {
|
||||
"token": "",
|
||||
"notice": {
|
||||
"ERROR": {
|
||||
"name": "",
|
||||
"chat_id":
|
||||
}
|
||||
},
|
||||
"channel": {
|
||||
"POST": [
|
||||
{
|
||||
"name": "",
|
||||
"chat_id":
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"administrators":
|
||||
[
|
||||
{
|
||||
"username": "",
|
||||
"user_id":
|
||||
}
|
||||
]
|
||||
}
|
38
jobs/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# jobs 目录
|
||||
|
||||
## 说明
|
||||
|
||||
改目录存放 BOT 的工作队列、注册和具体实现
|
||||
|
||||
## 基础代码
|
||||
|
||||
``` python
|
||||
import datetime
|
||||
|
||||
from telegram.ext import CallbackContext
|
||||
|
||||
from jobs.base import RunDailyHandler
|
||||
from logger import Log
|
||||
from utils.job.manager import listener_jobs_class
|
||||
|
||||
@listener_jobs_class()
|
||||
class JobTest:
|
||||
|
||||
@classmethod
|
||||
def build_jobs(cls) -> list:
|
||||
test = cls()
|
||||
# 注册每日执行任务
|
||||
# 执行时间为21点45分
|
||||
return [
|
||||
RunDailyHandler(test.test, datetime.time(21, 45, 00), name="测试Job")
|
||||
]
|
||||
|
||||
async def test(self, context: CallbackContext):
|
||||
Log.info("测试Job[OK]")
|
||||
```
|
||||
|
||||
### 注意
|
||||
|
||||
jobs 模块下的类必须提供 `build_jobs` 类方法作为构建相应处理程序给 `handle.py`
|
||||
|
||||
只需在构建的类前加上 `@listener_jobs_class()` 修饰器即可
|
202
jobs/base.py
Normal file
@ -0,0 +1,202 @@
|
||||
import datetime
|
||||
from typing import Union, Tuple
|
||||
|
||||
from telegram.ext import CallbackContext
|
||||
|
||||
from model.types import JSONDict, Func
|
||||
|
||||
|
||||
class BaseJobHandler:
|
||||
pass
|
||||
|
||||
|
||||
class RunDailyHandler:
|
||||
def __init__(self, callback: Func, time: datetime.time, days: Tuple[int, ...] = tuple(range(7)),
|
||||
data: object = None, name: str = None, chat_id: int = None, user_id: int = None,
|
||||
job_kwargs: JSONDict = None, ):
|
||||
"""Creates a new :class:`Job` that runs on a daily basis and adds it to the queue.
|
||||
|
||||
Note:
|
||||
For a note about DST, please see the documentation of `APScheduler`_.
|
||||
|
||||
.. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
|
||||
#daylight-saving-time-behavior
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
||||
(:obj:`datetime.time.tzinfo`) is :obj:`None`, the default timezone of the bot will
|
||||
be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
|
||||
run (where ``0-6`` correspond to sunday - saturday). By default, the job will run
|
||||
every day.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Changed day of the week mapping of 0-6 from monday-sunday to sunday-saturday.
|
||||
data (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.data` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Renamed the parameter ``context`` to :paramref:`data`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
"""
|
||||
# 复制文档
|
||||
self.job_kwargs = job_kwargs
|
||||
self.user_id = user_id
|
||||
self.chat_id = chat_id
|
||||
self.name = name
|
||||
self.data = data
|
||||
self.days = days
|
||||
self.time = time
|
||||
self.callback = callback
|
||||
|
||||
@property
|
||||
def get_kwargs(self) -> dict:
|
||||
kwargs = {
|
||||
"callback": self.callback,
|
||||
"time": self.time,
|
||||
"days": self.days,
|
||||
"data": self.data,
|
||||
"name": self.name,
|
||||
"chat_id": self.chat_id,
|
||||
"user_id": self.callback,
|
||||
"job_kwargs": self.job_kwargs,
|
||||
}
|
||||
return kwargs
|
||||
|
||||
|
||||
class RunRepeatingHandler:
|
||||
|
||||
def __init__(self, callback: Func, interval: Union[float, datetime.timedelta],
|
||||
first: Union[float, datetime.timedelta, datetime.datetime, datetime.time] = None,
|
||||
last: Union[float, datetime.timedelta, datetime.datetime, datetime.time] = None,
|
||||
context: object = None, name: str = None, chat_id: int = None, user_id: int = None,
|
||||
job_kwargs: JSONDict = None):
|
||||
"""Creates a new :class:`Job` instance that runs at specified intervals and adds it to the
|
||||
queue.
|
||||
|
||||
Note:
|
||||
For a note about DST, please see the documentation of `APScheduler`_.
|
||||
|
||||
.. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
|
||||
#daylight-saving-time-behavior
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`): The interval in which
|
||||
the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted
|
||||
as seconds.
|
||||
first (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
||||
:obj:`datetime.datetime` | :obj:`datetime.time`, optional):
|
||||
Time in or at which the job should run. This parameter will be interpreted
|
||||
depending on its type.
|
||||
|
||||
* :obj:`int` or :obj:`float` will be interpreted as "seconds from now" in which the
|
||||
job should run.
|
||||
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
|
||||
job should run.
|
||||
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
|
||||
which the job should run. If the timezone (:attr:`datetime.datetime.tzinfo`) is
|
||||
:obj:`None`, the default timezone of the bot will be used.
|
||||
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
|
||||
job should run. This could be either today or, if the time has already passed,
|
||||
tomorrow. If the timezone (:attr:`datetime.time.tzinfo`) is :obj:`None`, the
|
||||
default timezone of the bot will be used, which is UTC unless
|
||||
:attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
Defaults to :paramref:`interval`
|
||||
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
||||
:obj:`datetime.datetime` | :obj:`datetime.time`, optional):
|
||||
Latest possible time for the job to run. This parameter will be interpreted
|
||||
depending on its type. See :paramref:`first` for details.
|
||||
|
||||
If :paramref:`last` is :obj:`datetime.datetime` or :obj:`datetime.time` type
|
||||
and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be
|
||||
assumed, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
Defaults to :obj:`None`.
|
||||
data (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.data` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Renamed the parameter ``context`` to :paramref:`data`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
"""
|
||||
# 复制文档
|
||||
self.callback = callback
|
||||
self.interval = interval
|
||||
self.first = first
|
||||
self.last = last
|
||||
self.context = context
|
||||
self.name = name
|
||||
self.chat_id = chat_id
|
||||
self.user_id = user_id
|
||||
self.job_kwargs = job_kwargs
|
||||
|
||||
@property
|
||||
def get_kwargs(self) -> dict:
|
||||
kwargs = {
|
||||
"callback": self.callback,
|
||||
"interval": self.interval,
|
||||
"first": self.first,
|
||||
"last": self.last,
|
||||
"context": self.context,
|
||||
"name": self.name,
|
||||
"chat_id": self.chat_id,
|
||||
"user_id": self.callback,
|
||||
"job_kwargs": self.job_kwargs,
|
||||
}
|
||||
return kwargs
|
||||
|
||||
|
||||
class BaseJob:
|
||||
|
||||
@staticmethod
|
||||
def remove_job_if_exists(name: str, context: CallbackContext) -> bool:
|
||||
current_jobs = context.job_queue.get_jobs_by_name(name)
|
||||
context.job_queue.run_repeating()
|
||||
if not current_jobs:
|
||||
return False
|
||||
for job in current_jobs:
|
||||
job.schedule_removal()
|
||||
return True
|
61
logger.py
Normal file
@ -0,0 +1,61 @@
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
import colorlog
|
||||
|
||||
from config import config
|
||||
|
||||
current_path = os.path.realpath(os.getcwd())
|
||||
log_path = os.path.join(current_path, "logs")
|
||||
if not os.path.exists(log_path):
|
||||
os.mkdir(log_path)
|
||||
log_file_name = os.path.join(log_path, "log.log")
|
||||
|
||||
log_colors_config = {
|
||||
"DEBUG": "cyan",
|
||||
"INFO": "green",
|
||||
"WARNING": "yellow",
|
||||
"ERROR": "red",
|
||||
"CRITICAL": "red",
|
||||
}
|
||||
|
||||
|
||||
class Logger:
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger("TGPaimonBot")
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.CRITICAL)
|
||||
if config.DEBUG:
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
self.logger.setLevel(logging.INFO)
|
||||
self.formatter = colorlog.ColoredFormatter(
|
||||
"%(log_color)s[%(asctime)s] [%(levelname)s] - %(message)s", log_colors=log_colors_config)
|
||||
self.formatter2 = logging.Formatter("[%(asctime)s] [%(levelname)s] - %(message)s")
|
||||
fh = RotatingFileHandler(filename=log_file_name, maxBytes=1024 * 1024 * 5, backupCount=5,
|
||||
encoding="utf-8")
|
||||
fh.setFormatter(self.formatter2)
|
||||
root_logger.addHandler(fh)
|
||||
|
||||
ch = colorlog.StreamHandler()
|
||||
ch.setFormatter(self.formatter)
|
||||
root_logger.addHandler(ch)
|
||||
|
||||
def getLogger(self):
|
||||
return self.logger
|
||||
|
||||
def debug(self, msg, exc_info=None):
|
||||
self.logger.debug(msg=msg, exc_info=exc_info)
|
||||
|
||||
def info(self, msg, exc_info=None):
|
||||
self.logger.info(msg=msg, exc_info=exc_info)
|
||||
|
||||
def warning(self, msg, exc_info=None):
|
||||
self.logger.warning(msg=msg, exc_info=exc_info)
|
||||
|
||||
def error(self, msg, exc_info=None):
|
||||
self.logger.error(msg=msg, exc_info=exc_info)
|
||||
|
||||
|
||||
Log = Logger()
|
94
main.py
Normal file
@ -0,0 +1,94 @@
|
||||
import asyncio
|
||||
from warnings import filterwarnings
|
||||
|
||||
import pytz
|
||||
from telegram.ext import Application, Defaults
|
||||
from telegram.warnings import PTBUserWarning
|
||||
|
||||
from config import config
|
||||
from logger import Log
|
||||
from utils.aiobrowser import AioBrowser
|
||||
from utils.app.manager import AppsManager
|
||||
from utils.job.register import register_job
|
||||
from utils.mysql import MySQL
|
||||
from utils.plugins.register import register_plugin_handlers
|
||||
from utils.redisdb import RedisDB
|
||||
|
||||
# 无视相关警告
|
||||
# 该警告说明在官方GITHUB的WIKI中Frequently Asked Questions里的What do the per_* settings in ConversationHandler do?
|
||||
filterwarnings(action="ignore", message=r".*CallbackQueryHandler", category=PTBUserWarning)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
Log.info("正在启动项目")
|
||||
|
||||
# 初始化数据库
|
||||
Log.info("初始化数据库")
|
||||
mysql = MySQL(host=config.MYSQL["host"], user=config.MYSQL["user"], password=config.MYSQL["password"],
|
||||
port=config.MYSQL["port"], database=config.MYSQL["database"])
|
||||
|
||||
# 初始化Redis缓存
|
||||
Log.info("初始化Redis缓存")
|
||||
redis = RedisDB(host=config.REDIS["host"], port=config.REDIS["port"], db=config.REDIS["database"])
|
||||
|
||||
# 初始化Playwright
|
||||
Log.info("初始化Playwright")
|
||||
browser = AioBrowser()
|
||||
|
||||
# 传入服务并启动
|
||||
Log.info("正在启动服务")
|
||||
apps = AppsManager(mysql, redis, browser)
|
||||
apps.refresh_list("./app/*")
|
||||
apps.import_module()
|
||||
apps.add_service()
|
||||
|
||||
# 构建BOT
|
||||
Log.info("构建BOT")
|
||||
|
||||
defaults = Defaults(tzinfo=pytz.timezone("Asia/Shanghai"))
|
||||
|
||||
application = Application\
|
||||
.builder()\
|
||||
.token(config.TELEGRAM["token"])\
|
||||
.defaults(defaults)\
|
||||
.build()
|
||||
|
||||
register_plugin_handlers(application)
|
||||
|
||||
register_job(application)
|
||||
|
||||
# 启动BOT
|
||||
try:
|
||||
Log.info("BOT已经启动 开始处理命令")
|
||||
# BOT 在退出后默认关闭LOOP 这时候得让LOOP不要关闭
|
||||
application.run_polling(close_loop=False)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
except Exception as exc:
|
||||
Log.info("BOT执行过程中出现错误")
|
||||
raise exc
|
||||
finally:
|
||||
Log.info("项目收到退出命令 BOT停止处理并退出")
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
# 需要关闭数据库连接
|
||||
Log.info("正在关闭数据库连接")
|
||||
loop.run_until_complete(mysql.wait_closed())
|
||||
# 关闭Redis连接
|
||||
Log.info("正在关闭Redis连接")
|
||||
loop.run_until_complete(redis.close())
|
||||
# 关闭playwright
|
||||
Log.info("正在关闭Playwright")
|
||||
loop.run_until_complete(browser.close())
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
except Exception as exc:
|
||||
Log.error("关闭必要连接时出现错误 \n", exc)
|
||||
Log.info("正在关闭loop")
|
||||
# 关闭LOOP
|
||||
loop.close()
|
||||
Log.info("项目已经已结束")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
5
metadata/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# metadata 目录说明
|
||||
|
||||
| FileName | Introduce |
|
||||
| :----------: | ------------- |
|
||||
| shortname.py | 记录短名称MAP |
|
158
metadata/shortname.py
Normal file
@ -0,0 +1,158 @@
|
||||
roles = {
|
||||
10000003: ["琴", "Jean", "jean", "团长", "代理团长", "琴团长", "蒲公英骑士"],
|
||||
10000006: ["丽莎", "Lisa", "lisa", "图书管理员", "图书馆管理员", "蔷薇魔女"],
|
||||
10000005: ["空", "男主", "男主角", "龙哥", "空哥", "旅行者", "卑鄙的外乡人", "荣誉骑士", "爷", "风主", "岩主", "雷主", "履刑者", "抽卡不歪真君"],
|
||||
10000007: ["荧", "女主", "女主角", "莹", "萤", "黄毛阿姨", "荧妹"],
|
||||
10000014: ["芭芭拉", "Barbara", "barbara", "巴巴拉", "拉粑粑", "拉巴巴", "内鬼", "加湿器", "闪耀偶像", "偶像"],
|
||||
10000015: ["凯亚", "Kaeya", "kaeya", "盖亚", "凯子哥", "凯鸭", "矿工", "矿工头子", "骑兵队长", "凯子", "凝冰渡海真君"],
|
||||
10000016: ["迪卢克", "diluc", "Diluc", "卢姥爷", "姥爷", "卢老爷", "卢锅巴", "正义人", "正e人", "正E人", "卢本伟", "暗夜英雄", "卢卢伯爵",
|
||||
"落魄了", "落魄了家人们"],
|
||||
10000020: ["雷泽", "razor", "Razor", "狼少年", "狼崽子", "狼崽", "卢皮卡", "小狼", "小狼狗"],
|
||||
10000021: ["安柏", "Amber", "amber", "安伯", "兔兔伯爵", "飞行冠军", "侦查骑士", "点火姬", "点火机", "打火机", "打火姬", ],
|
||||
10000022: ["温迪", "Venti", "venti", "温蒂", "风神", "卖唱的", "巴巴托斯", "巴巴脱丝", "芭芭托斯", "芭芭脱丝", "干点正事", "不干正事", "吟游诗人",
|
||||
"诶嘿", "唉嘿", "摸鱼", ],
|
||||
10000023: ["香菱", "Xiangling", "xiangling", "香玲", "锅巴", "厨师", "万民堂厨师", "香师傅"],
|
||||
10000024: ["北斗", "Beidou", "beidou", "大姐头", "大姐", "无冕的龙王", "龙王"],
|
||||
10000025: ["行秋", "Xingqiu", "xingqiu", "秋秋人", "秋妹妹", "书呆子", "水神", "飞云商会二少爷"],
|
||||
10000026: ["魈", "Xiao", "xiao", "杏仁豆腐", "打桩机", "插秧", "三眼五显仙人", "三眼五显真人", "降魔大圣", "护法夜叉", "快乐风男", "无聊", "靖妖傩舞",
|
||||
"矮子仙人", "三点五尺仙人", "跳跳虎"],
|
||||
10000027: ["凝光", "Ningguang", "ningguang", "富婆", "天权星"],
|
||||
10000029: ["可莉", "Klee", "klee", "嘟嘟可", "火花骑士", "蹦蹦炸弹", "炸鱼", "放火烧山", "放火烧山真君", "蒙德最强战力", "逃跑的太阳", "啦啦啦", "哒哒哒",
|
||||
"炸弹人", "禁闭室", ],
|
||||
10000030: ["钟离", "Zhongli", "zhongli", "摩拉克斯", "岩王爷", "岩神", "钟师傅", "天动万象", "岩王帝君", "未来可期", "帝君", "拒收病婿"],
|
||||
10000031: ["菲谢尔", "Fischl", "fischl", "皇女", "小艾米", "小艾咪", "奥兹", "断罪皇女", "中二病", "中二少女", "中二皇女", "奥兹发射器"],
|
||||
10000032: ["班尼特", "Bennett", "bennett", "点赞哥", "点赞", "倒霉少年", "倒霉蛋", "霹雳闪雷真君", "班神", "班爷", "倒霉", "火神", "六星真神"],
|
||||
10000033: ["达达利亚", "Tartaglia", "tartaglia", "Childe", "childe", "Ajax", "ajax", "达达鸭", "达达利鸭", "公子", "玩具销售员",
|
||||
"玩具推销员", "钱包", "鸭鸭", "愚人众末席"],
|
||||
10000034: ["诺艾尔", "Noelle", "noelle", "女仆", "高达", "岩王帝姬"],
|
||||
10000035: ["七七", "Qiqi", "qiqi", "僵尸", "肚饿真君", "度厄真君"],
|
||||
10000036: ["重云", "Chongyun", "chongyun", "纯阳之体", "冰棍"],
|
||||
10000037: ["甘雨", "Ganyu", "ganyu", "椰羊", "椰奶", "王小美"],
|
||||
10000038: ["阿贝多", "Albedo", "albedo", "可莉哥哥", "升降机", "升降台", "电梯", "白垩之子", "贝爷", "白垩", "阿贝少", "花呗多", "阿贝夕",
|
||||
"abd", "阿师傅"],
|
||||
10000039: ["迪奥娜", "Diona", "diona", "迪欧娜", "dio", "dio娜", "冰猫", "猫猫", "猫娘", "喵喵", "调酒师"],
|
||||
10000041: ["莫娜", "Mona", "mona", "穷鬼", "穷光蛋", "穷", "莫纳", "占星术士", "占星师", "讨龙真君", "半部讨龙真君", "阿斯托洛吉斯·莫娜·梅姬斯图斯"],
|
||||
10000042: ["刻晴", "Keqing", "keqing", "刻情", "氪晴", "刻师傅", "刻师父", "牛杂", "牛杂师傅", "斩尽牛杂", "免疫", "免疫免疫", "屁斜剑法",
|
||||
"玉衡星", "阿晴", "啊晴"],
|
||||
10000043: ["砂糖", "Sucrose", "sucrose", "雷莹术士", "雷萤术士", "雷荧术士"],
|
||||
10000044: ["辛焱", "Xinyan", "xinyan", "辛炎", "黑妹", "摇滚"],
|
||||
10000045: ["罗莎莉亚", "Rosaria", "rosaria", "罗莎莉娅", "白色史莱姆", "白史莱姆", "修女", "罗莎利亚", "罗莎利娅", "罗沙莉亚", "罗沙莉娅", "罗沙利亚",
|
||||
"罗沙利娅", "萝莎莉亚", "萝莎莉娅", "萝莎利亚", "萝莎利娅", "萝沙莉亚", "萝沙莉娅", "萝沙利亚", "萝沙利娅"],
|
||||
10000046: ["胡桃", "Hu Tao", "hu tao", "HuTao", "hutao", "Hutao", "胡淘", "往生堂堂主", "火化", "抬棺的", "蝴蝶", "核桃", "堂主",
|
||||
"胡堂主", "雪霁梅香"],
|
||||
10000047: ["枫原万叶", "Kaedehara Kazuha", "Kazuha", "kazuha", "万叶", "叶天帝", "天帝", "叶师傅"],
|
||||
10000048: ["烟绯", "Yanfei", "yanfei", "烟老师", "律师", "罗翔"],
|
||||
10000051: ["优菈", "Eula", "eula", "优拉", "尤拉", "尤菈", "浪花骑士", "记仇", "劳伦斯"],
|
||||
10000002: ["神里绫华", "Kamisato Ayaka", "Ayaka", "ayaka", "神里", "绫华", "神里凌华", "凌华", "白鹭公主", "神里大小姐"],
|
||||
10000049: ["宵宫", "Yoimiya", "yoimiya", "霄宫", "烟花", "肖宫", "肖工", "绷带女孩"],
|
||||
10000052: ["雷电将军", "Raiden Shogun", "Raiden", "raiden", "雷神", "将军", "雷军", "巴尔", "阿影", "影", "巴尔泽布", "煮饭婆", "奶香一刀",
|
||||
"无想一刀", "宅女"],
|
||||
10000053: ["早柚", "Sayu", "sayu", "小狸猫", "狸猫", "忍者"],
|
||||
10000054: ["珊瑚宫心海", "Sangonomiya Kokomi", "Kokomi", "kokomi", "心海", "军师", "珊瑚宫", "书记", "观赏鱼", "水母", "鱼", "美人鱼"],
|
||||
10000056: ["九条裟罗", "Kujou Sara", "Sara", "sara", "九条", "九条沙罗", "裟罗", "沙罗", "天狗"],
|
||||
10000062: ["埃洛伊", "Aloy", "aloy"],
|
||||
10000050: ["托马", "Thoma", "thoma", "家政官", "太郎丸", "地头蛇", "男仆", "拖马"],
|
||||
10000055: ["五郎", "Gorou", "gorou", "柴犬", "土狗", "希娜", "希娜小姐"],
|
||||
10000057: ["荒泷一斗", "Arataki Itto", "Itto", "itto", "荒龙一斗", "荒泷天下第一斗", "一斗", "一抖", "荒泷", "1斗", "牛牛", "斗子哥", "牛子哥",
|
||||
"牛子", "孩子王", "斗虫", "巧乐兹", "放牛的"],
|
||||
10000058: ["八重神子", "Yae Miko", "Miko", "miko", "八重", "神子", "狐狸", "想得美哦", "巫女", "屑狐狸", "骚狐狸", "八重宫司", "婶子", "小八"],
|
||||
10000059: ["鹿野院平藏", "shikanoin heizou", "Heizou", "heizou", "heizo", "鹿野苑", "鹿野院", "平藏", "鹿野苑平藏"],
|
||||
10000060: ["夜兰", "Yelan", "yelan", "夜阑", "叶澜", "腋兰", "夜天后"],
|
||||
10000063: ["申鹤", "Shenhe", "shenhe", "神鹤", "小姨", "小姨子", "审鹤"],
|
||||
10000064: ["云堇", "Yun Jin", "yunjin", "yun jin", "云瑾", "云先生", "云锦", "神女劈观"],
|
||||
10000065: ["久岐忍", "Kuki Shinobu", "Kuki", "kuki", "Shinobu", "shinobu", "97忍", "小忍", "久歧忍", "97", "茄忍", "阿忍", "忍姐"],
|
||||
10000066: ["神里绫人", "Kamisato Ayato", "Ayato", "ayato", "绫人", "神里凌人", "凌人", "0人", "神人", "零人", "大舅哥"],
|
||||
}
|
||||
weapons = {
|
||||
"磐岩结绿": ["绿箭", "绿剑"],
|
||||
"斫峰之刃": ["斫峰", "盾剑"],
|
||||
"无工之剑": ["蜈蚣", "蜈蚣大剑", "无工大剑", "盾大剑", "无工"],
|
||||
"贯虹之槊": ["贯虹", "岩枪", "盾枪"],
|
||||
"赤角石溃杵": ["赤角", "石溃杵"],
|
||||
"尘世之锁": ["尘世锁", "尘世", "盾书", "锁"],
|
||||
|
||||
"终末嗟叹之诗": ["终末", "终末弓", "叹气弓", "乐团弓"],
|
||||
"松籁响起之时": ["松籁", "乐团大剑", "松剑"],
|
||||
"苍古自由之誓": ["苍古", "乐团剑"],
|
||||
|
||||
"渔获": ["鱼叉"],
|
||||
"衔珠海皇": ["海皇", "咸鱼剑", "咸鱼大剑"],
|
||||
|
||||
"匣里日月": ["日月"],
|
||||
"匣里灭辰": ["灭辰"],
|
||||
"匣里龙吟": ["龙吟"],
|
||||
|
||||
"天空之翼": ["天空弓"],
|
||||
"天空之刃": ["天空剑"],
|
||||
"天空之卷": ["天空书", "厕纸"],
|
||||
"天空之脊": ["天空枪", "薄荷枪"],
|
||||
"天空之傲": ["天空大剑"],
|
||||
"四风原典": ["四风"],
|
||||
|
||||
"试作斩岩": ["斩岩"],
|
||||
"试作星镰": ["星镰"],
|
||||
"试作金珀": ["金珀"],
|
||||
"试作古华": ["古华"],
|
||||
"试作澹月": ["澹月"],
|
||||
|
||||
"千岩长枪": ["千岩枪"],
|
||||
"千岩古剑": ["千岩剑", "千岩大剑"],
|
||||
|
||||
"暗巷闪光": ["暗巷剑"],
|
||||
"暗巷猎手": ["暗巷弓"],
|
||||
|
||||
"阿莫斯之弓": ["阿莫斯", "ams", "痛苦弓"],
|
||||
"雾切之回光": ["雾切"],
|
||||
"飞雷之弦振": ["飞雷", "飞雷弓"],
|
||||
"薙草之稻光": ["薙草", "稻光", "薙草稻光", "马尾枪", "马尾", "薙刀"],
|
||||
"神乐之真意": ["神乐", "真意"],
|
||||
"狼的末路": ["狼末"],
|
||||
"护摩之杖": ["护摩", "护摩枪", "护膜"],
|
||||
"和璞鸢": ["鸟枪", "绿枪"],
|
||||
"风鹰剑": ["风鹰"],
|
||||
"冬极白星": ["冬极"],
|
||||
"不灭月华": ["月华"],
|
||||
"波乱月白经津": ["波乱", "月白", "波乱月白", "经津", "波波津"],
|
||||
"若水": ["麒麟弓", "Aqua", "aqua"],
|
||||
|
||||
"昭心": ["糟心"],
|
||||
"幽夜华尔兹": ["幽夜", "幽夜弓", "华尔兹", "皇女弓"],
|
||||
"雪葬的星银": ["雪葬", "星银", "雪葬星银", "雪山大剑"],
|
||||
"喜多院十文字": ["喜多院", "十文字"],
|
||||
"万国诸海图谱": ["万国", "万国诸海"],
|
||||
"天目影打刀": ["天目刀", "天目"],
|
||||
"破魔之弓": ["破魔弓"],
|
||||
"曚云之月": ["曚云弓"],
|
||||
"流月针": ["针"],
|
||||
"流浪乐章": ["赌狗书", "赌狗乐章", "赌狗"],
|
||||
"桂木斩长正": ["桂木", "斩长正"],
|
||||
"腐殖之剑": ["腐殖", "腐殖剑"],
|
||||
"风花之颂": ["风花弓"],
|
||||
"证誓之明瞳": ["证誓", "明瞳", "证誓明瞳"],
|
||||
"嘟嘟可故事集": ["嘟嘟可"],
|
||||
"辰砂之纺锤": ["辰砂", "辰砂纺锤", "纺锤"],
|
||||
"白辰之环": ["白辰", "白辰环"],
|
||||
|
||||
"决斗之枪": ["决斗枪", "决斗", "月卡枪"],
|
||||
"螭骨剑": ["螭骨", "丈育剑", "离骨剑", "月卡大剑"],
|
||||
"黑剑": ["月卡剑"],
|
||||
"苍翠猎弓": ["绿弓", "月卡弓"],
|
||||
|
||||
"讨龙英杰谭": ["讨龙"],
|
||||
"神射手之誓": ["脚气弓", "神射手"],
|
||||
"黑缨枪": ["史莱姆枪"]
|
||||
}
|
||||
|
||||
|
||||
def roleToName(shortname: str) -> str:
|
||||
if not shortname:
|
||||
return shortname
|
||||
for value in roles.values():
|
||||
for i in value:
|
||||
if i == shortname:
|
||||
return value[0]
|
||||
return shortname
|
||||
|
||||
|
||||
def weaponToName(shortname: str) -> str:
|
||||
return next((key for key, value in weapons.items() if shortname == key or shortname in value), shortname)
|
27
model/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# model 目录说明
|
||||
|
||||
## apihelpe 模块
|
||||
|
||||
用于获取米忽悠BBS的数据写的请求模块
|
||||
|
||||
## wiki 模块
|
||||
|
||||
### 计划
|
||||
|
||||
当成设计之初为考虑摆脱并消除第三方 `metadata` 数据结发生变化的影响
|
||||
|
||||
### 选择
|
||||
|
||||
关于选择那个Wiki数据获取我考虑了很久,还是选择了国外的蜜蜂网,原因还是有两个方面
|
||||
|
||||
页面形式几乎固定 未来不会发生变化
|
||||
|
||||
最主要还是有参考代码 极大减少了我工作量
|
||||
|
||||
毕竟如果从零开始真的头顶很凉 〒▽〒
|
||||
|
||||
### 感谢
|
||||
|
||||
| Nickname | Contribution |
|
||||
| :--------------------------------------------------------: | -------------------- |
|
||||
| [Crawler-ghhw](https://github.com/DGP-Studio/Crawler-ghhw) | 本项目参考的爬虫代码 |
|
54
model/apihelper/artifact.py
Normal file
@ -0,0 +1,54 @@
|
||||
from base64 import b64encode
|
||||
from random import choice
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def get_format_sub_item(artifact_attr: dict):
|
||||
return "".join(f'{i["name"]:\u3000<6} | {i["value"]}\n' for i in artifact_attr["sub_item"])
|
||||
|
||||
|
||||
def get_comment(get_rate_num):
|
||||
data = {"1": ["破玩意谁能用啊,谁都用不了吧", "喂了吧,这东西做狗粮还能有点用", "抽卡有保底,圣遗物没有下限",
|
||||
"未来可期呢(笑)", "你出门一定很安全", "你是不是得罪米哈游了?", "……宁就是班尼特本特?",
|
||||
"丢人!你给我退出提瓦特(", "不能说很糟糕,只能说特别不好"],
|
||||
"2": ["淡如清泉镇的圣水,莫得提升", "你怎么不强化啊?", "嗯嗯嗯好好好可以可以可以挺好挺好(敷衍)",
|
||||
"这就是日常,下一个", "洗洗还能吃(bushi)", "下次一定行……?", "派蒙平静地点了个赞",
|
||||
"不知道该说什么,就当留个纪念吧"],
|
||||
"3": ["不能说有质变,只能说有提升", "过渡用的话没啥问题,大概", "再努努力吧", "嗯,差不多能用",
|
||||
"这很合理", "达成成就“合格圣遗物”", "嗯,及格了,过渡用挺好的", "中规中矩,有待提升"],
|
||||
"4": ["以普遍理性而论,很好", "算是个很不戳的圣遗物了!", "很好,很有精神!", "再努努力,超越一下自己",
|
||||
"感觉可以戴着它大杀四方了", "这就是大佬背包里的平均水平吧", "先锁上呗,这波不亏", "达成成就“高分圣遗物”",
|
||||
"这波对输出有很大提升啊(认真)", "我也想拥有这种分数的圣遗物(切实)"],
|
||||
"5": ["多吃点好的,出门注意安全", "晒吧,欧不可耻,只是可恨", "没啥好说的,让我自闭一会", "达成成就“高分圣遗物”",
|
||||
"怕不是以后开宝箱只能开出卷心菜", "吃了吗?没吃的话,吃我一拳", "我觉得这个游戏有问题", "这合理吗",
|
||||
"这东西没啥用,给我吧(柠檬)", "??? ????"]}
|
||||
try:
|
||||
data_ = int(float(get_rate_num))
|
||||
except ValueError:
|
||||
data_ = 0
|
||||
if data_ == 100:
|
||||
return choice(data["5"])
|
||||
return choice(data[str(data_ // 20 + 1)])
|
||||
|
||||
|
||||
class ArtifactOcrRate:
|
||||
OCR_URL = "https://api.genshin.pub/api/v1/app/ocr"
|
||||
RATE_URL = "https://api.genshin.pub/api/v1/relic/rate"
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.client = httpx.AsyncClient(headers=self.HEADERS)
|
||||
|
||||
async def get_artifact_attr(self, photo_byte):
|
||||
b64_str = b64encode(photo_byte).decode()
|
||||
req = await self.client.post(self.OCR_URL, json={"image": b64_str}, timeout=8)
|
||||
return req
|
||||
|
||||
async def rate_artifact(self, artifact_attr: dict):
|
||||
req = await self.client.post(self.RATE_URL, json=artifact_attr, timeout=8)
|
||||
return req
|
158
model/apihelper/base.py
Normal file
@ -0,0 +1,158 @@
|
||||
import imghdr
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ArtworkImage:
|
||||
|
||||
def __init__(self, art_id: int, page: int = 0, is_error: bool = False, data: bytes = b""):
|
||||
"""
|
||||
:param art_id: 插画ID
|
||||
:param page: 页数
|
||||
:param is_error: 插画是否有问题
|
||||
:param data: 插画数据
|
||||
"""
|
||||
self.art_id = art_id
|
||||
self.data = data
|
||||
self.is_error = is_error
|
||||
if not is_error:
|
||||
self.format: str = imghdr.what(None, self.data)
|
||||
self.page = page
|
||||
|
||||
|
||||
class BaseResponseData:
|
||||
def __init__(self, response=None, error_message: str = ""):
|
||||
"""
|
||||
:param response: 相应
|
||||
:param error_message: 错误信息
|
||||
"""
|
||||
if response is None:
|
||||
self.error: bool = True
|
||||
self.message: str = error_message
|
||||
return
|
||||
self.response: dict = response
|
||||
self.code = response["retcode"]
|
||||
if self.code == 0:
|
||||
self.error = False
|
||||
else:
|
||||
self.error = True
|
||||
self.message = response["message"]
|
||||
self.data = response["data"]
|
||||
|
||||
|
||||
class Stat:
|
||||
def __init__(self, view_num: int = 0, reply_num: int = 0, like_num: int = 0, bookmark_num: int = 0,
|
||||
forward_num: int = 0):
|
||||
self.forward_num = forward_num # 关注数
|
||||
self.bookmark_num = bookmark_num # 收藏数
|
||||
self.like_num = like_num # 喜欢数
|
||||
self.reply_num = reply_num # 回复数
|
||||
self.view_num = view_num # 观看数
|
||||
|
||||
|
||||
class ArtworkInfo:
|
||||
def __init__(self, post_id: int = 0, subject: str = "", tags=None,
|
||||
image_url_list=None, stat: Stat = None, uid: int = 0, created_at: int = 0):
|
||||
"""
|
||||
:param post_id: post_id
|
||||
:param subject: 标题
|
||||
:param tags: 标签
|
||||
:param image_url_list: 图片URL列表
|
||||
:param stat: 统计
|
||||
:param uid: 用户UID
|
||||
:param created_at: 创建时间
|
||||
"""
|
||||
if tags is None:
|
||||
self.tags = []
|
||||
else:
|
||||
self.tags = tags
|
||||
if image_url_list is None:
|
||||
self.image_url_list = []
|
||||
else:
|
||||
self.image_url_list = image_url_list
|
||||
self.Stat = stat
|
||||
self.created_at = created_at
|
||||
self.uid = uid
|
||||
self.subject = subject
|
||||
self.post_id = post_id
|
||||
|
||||
|
||||
class HyperionResponse:
|
||||
"""
|
||||
:param response: 相应
|
||||
:param error_message: 错误信息
|
||||
"""
|
||||
|
||||
def __init__(self, response=None, error_message: str = ""):
|
||||
if response is None:
|
||||
self.error: bool = True
|
||||
self.message: str = error_message
|
||||
return
|
||||
self.response: dict = response
|
||||
self.code = response["retcode"]
|
||||
if self.code == 0:
|
||||
self.error = False
|
||||
else:
|
||||
if self.code == 1102:
|
||||
self.message = "作品不存在"
|
||||
self.error = True
|
||||
return
|
||||
if response["data"] is None:
|
||||
self.error = True
|
||||
self.message: str = response["message"]
|
||||
if self.error:
|
||||
return
|
||||
try:
|
||||
self._data_post = response["data"]["post"]
|
||||
post = self._data_post["post"] # 投稿信息
|
||||
post_id = post["post_id"]
|
||||
subject = post["subject"] # 介绍,类似title标题
|
||||
created_at = post["created_at"] # 创建时间
|
||||
user = self._data_post["user"] # 用户数据
|
||||
uid = user["uid"] # 用户ID
|
||||
topics = self._data_post["topics"] # 存放 Tag
|
||||
image_list = self._data_post["image_list"] # image_list
|
||||
except (AttributeError, TypeError) as err:
|
||||
self.error: bool = True
|
||||
self.message: str = err
|
||||
return
|
||||
topics_list = []
|
||||
image_url_list = []
|
||||
for topic in topics:
|
||||
topics_list.append(topic["name"])
|
||||
for image in image_list:
|
||||
image_url_list.append(image["url"])
|
||||
self.post_id = post["post_id"]
|
||||
self.user_id = user["uid"]
|
||||
self.created_at = post["created_at"]
|
||||
stat = Stat(view_num=self._data_post["stat"]["view_num"],
|
||||
reply_num=self._data_post["stat"]["reply_num"],
|
||||
like_num=self._data_post["stat"]["like_num"],
|
||||
bookmark_num=self._data_post["stat"]["bookmark_num"],
|
||||
forward_num=self._data_post["stat"]["forward_num"],
|
||||
)
|
||||
self.results = ArtworkInfo(
|
||||
subject=subject,
|
||||
created_at=created_at,
|
||||
uid=uid,
|
||||
stat=stat,
|
||||
tags=topics_list,
|
||||
post_id=post_id,
|
||||
image_url_list=image_url_list
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
"""
|
||||
:return: 是否错误
|
||||
"""
|
||||
return self.error
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
:return: 插画连接数量
|
||||
"""
|
||||
return len(self.results.image_url_list)
|
||||
|
||||
|
||||
class ServiceEnum(Enum):
|
||||
HYPERION = 1
|
||||
HOYOLAB = 2
|
29
model/apihelper/gacha.py
Normal file
@ -0,0 +1,29 @@
|
||||
import httpx
|
||||
|
||||
from .base import BaseResponseData
|
||||
|
||||
|
||||
class GachaInfo:
|
||||
GACHA_LIST_URL = "https://webstatic.mihoyo.com/hk4e/gacha_info/cn_gf01/gacha/list.json"
|
||||
GACHA_INFO_URL = "https://webstatic.mihoyo.com/hk4e/gacha_info/cn_gf01/%s/zh-cn.json"
|
||||
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " \
|
||||
"Chrome/90.0.4430.72 Safari/537.36"
|
||||
|
||||
def __init__(self):
|
||||
self.headers = {
|
||||
'User-Agent': self.USER_AGENT,
|
||||
}
|
||||
self.client = httpx.AsyncClient(headers=self.headers)
|
||||
|
||||
async def get_gacha_list_info(self) -> BaseResponseData:
|
||||
req = await self.client.get(self.GACHA_LIST_URL)
|
||||
if req.is_error:
|
||||
return BaseResponseData(error_message="请求错误")
|
||||
return BaseResponseData(req.json())
|
||||
|
||||
async def get_gacha_info(self, gacha_id: str) -> dict:
|
||||
req = await self.client.get(self.GACHA_INFO_URL % gacha_id)
|
||||
if req.is_error:
|
||||
return {}
|
||||
return req.json()
|
63
model/apihelper/helpers.py
Normal file
@ -0,0 +1,63 @@
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import uuid
|
||||
|
||||
RECOGNIZE_SERVER = {
|
||||
"1": "cn_gf01",
|
||||
"2": "cn_gf01",
|
||||
"5": "cn_qd01",
|
||||
"6": "os_usa",
|
||||
"7": "os_euro",
|
||||
"8": "os_asia",
|
||||
"9": "os_cht",
|
||||
}
|
||||
|
||||
|
||||
def get_device_id(name: str) -> str:
|
||||
return str(uuid.uuid3(uuid.NAMESPACE_URL, name)).replace('-', '').upper()
|
||||
|
||||
|
||||
def md5(text: str) -> str:
|
||||
_md5 = hashlib.md5()
|
||||
_md5.update(text.encode())
|
||||
return _md5.hexdigest()
|
||||
|
||||
|
||||
def random_text(num: int) -> str:
|
||||
return ''.join(random.sample(string.ascii_lowercase + string.digits, num))
|
||||
|
||||
|
||||
def timestamp() -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def get_ds(salt: str = "", web: int = 1) -> str:
|
||||
if salt == "":
|
||||
if web == 1:
|
||||
salt = "h8w582wxwgqvahcdkpvdhbh2w9casgfl"
|
||||
elif web == 2:
|
||||
salt = "h8w582wxwgqvahcdkpvdhbh2w9casgfl"
|
||||
elif web == 3:
|
||||
salt = "fd3ykrh7o1j54g581upo1tvpam0dsgtf"
|
||||
i = str(timestamp())
|
||||
r = random_text(6)
|
||||
c = md5("salt=" + salt + "&t=" + i + "&r=" + r)
|
||||
return f"{i},{r},{c}"
|
||||
|
||||
|
||||
def get_recognize_server(uid: int) -> str:
|
||||
server = RECOGNIZE_SERVER.get(str(uid)[0])
|
||||
if server:
|
||||
return server
|
||||
else:
|
||||
raise TypeError(f"UID {uid} isn't associated with any recognize server")
|
||||
|
||||
|
||||
def get_headers():
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36",
|
||||
}
|
||||
return headers
|
96
model/apihelper/hoyolab.py
Normal file
@ -0,0 +1,96 @@
|
||||
from httpx import AsyncClient
|
||||
|
||||
from .base import BaseResponseData
|
||||
from .helpers import get_ds, get_device_id, get_recognize_server
|
||||
|
||||
|
||||
class Genshin:
|
||||
SIGN_INFO_URL = "https://hk4e-api-os.hoyoverse.com/event/sol/info"
|
||||
SIGN_URL = "https://hk4e-api-os.hoyoverse.com/event/sol/sign"
|
||||
SIGN_HOME_URL = "https://hk4e-api-os.hoyoverse.com/event/sol/home"
|
||||
|
||||
APP_VERSION = "2.11.1"
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
|
||||
REFERER = "https://webstatic.hoyoverse.com"
|
||||
ORIGIN = "https://webstatic.hoyoverse.com"
|
||||
|
||||
ACT_ID = "e202102251931481"
|
||||
DS_SALT = "6cqshh5dhw73bzxn20oexa9k516chk7s"
|
||||
|
||||
def __init__(self):
|
||||
self.headers = {
|
||||
"Origin": self.ORIGIN,
|
||||
'DS': get_ds(self.DS_SALT),
|
||||
'x-rpc-app_version': self.APP_VERSION,
|
||||
'User-Agent': self.USER_AGENT,
|
||||
'x-rpc-client_type': '5', # 1为ios 2为安卓 4为pc_web 5为mobile_web
|
||||
'Referer': self.REFERER,
|
||||
'x-rpc-device_id': get_device_id(self.USER_AGENT)}
|
||||
self.client = AsyncClient(headers=self.headers)
|
||||
|
||||
async def is_sign(self, uid: int, region: str = "", cookies: dict = None, lang: str = 'zh-cn'):
|
||||
"""
|
||||
检查是否签到
|
||||
:param lang: 语言
|
||||
:param uid: 游戏UID
|
||||
:param region: 服务器
|
||||
:param cookies: cookie
|
||||
:return:
|
||||
"""
|
||||
if region == "":
|
||||
region = get_recognize_server(uid)
|
||||
params = {
|
||||
"act_id": self.ACT_ID,
|
||||
"region": region,
|
||||
"uid": uid,
|
||||
"lang": lang
|
||||
}
|
||||
req = await self.client.get(self.SIGN_INFO_URL, params=params, cookies=cookies)
|
||||
if req.is_error:
|
||||
return BaseResponseData(error_message="请求错误")
|
||||
return BaseResponseData(req.json())
|
||||
|
||||
async def sign(self, uid: int, region: str = "", cookies: dict = None, lang: str = 'zh-cn'):
|
||||
"""
|
||||
执行签到
|
||||
:param lang:
|
||||
:param uid: 游戏UID
|
||||
:param region: 服务器
|
||||
:param cookies: cookie
|
||||
:return:
|
||||
"""
|
||||
if region == "":
|
||||
region = get_recognize_server(uid)
|
||||
data = {
|
||||
"act_id": self.ACT_ID,
|
||||
"region": region,
|
||||
"uid": uid,
|
||||
"lang": lang
|
||||
}
|
||||
req = await self.client.post(self.SIGN_URL, json=data, cookies=cookies)
|
||||
if req.is_error:
|
||||
return BaseResponseData(error_message="签到失败")
|
||||
return BaseResponseData(req.json())
|
||||
|
||||
async def get_sign_give(self, cookies: dict = None, lang: str = 'zh-cn'):
|
||||
"""
|
||||
返回今日签到信息
|
||||
:param lang:
|
||||
:param cookies:
|
||||
:return:
|
||||
"""
|
||||
params = {
|
||||
"act_id": self.ACT_ID,
|
||||
"lang": lang
|
||||
}
|
||||
req = await self.client.get(self.SIGN_HOME_URL, params=params, cookies=cookies)
|
||||
if req.is_error:
|
||||
return
|
||||
return BaseResponseData(req.json())
|
||||
|
||||
async def __aenter__(self):
|
||||
pass
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await self.client.aclose()
|
251
model/apihelper/hyperion.py
Normal file
@ -0,0 +1,251 @@
|
||||
import asyncio
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
import httpx
|
||||
from httpx import AsyncClient
|
||||
|
||||
from .base import HyperionResponse, ArtworkImage, BaseResponseData
|
||||
from .helpers import get_ds, get_device_id
|
||||
|
||||
|
||||
class Hyperion:
|
||||
"""
|
||||
米忽悠bbs相关API请求
|
||||
该名称来源于米忽悠的安卓BBS包名结尾,考虑到大部分重要的功能确实是在移动端实现了
|
||||
"""
|
||||
|
||||
POST_FULL_URL = "https://bbs-api.mihoyo.com/post/wapi/getPostFull"
|
||||
POST_FULL_IN_COLLECTION_URL = "https://bbs-api.mihoyo.com/post/wapi/getPostFullInCollection"
|
||||
GET_NEW_LIST_URL = "https://bbs-api.mihoyo.com/post/wapi/getNewsList"
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " \
|
||||
"Chrome/90.0.4430.72 Safari/537.36"
|
||||
|
||||
def __init__(self):
|
||||
self.client = httpx.AsyncClient(headers=self.get_headers())
|
||||
|
||||
@staticmethod
|
||||
def extract_post_id(text: str) -> int:
|
||||
"""
|
||||
:param text:
|
||||
# https://bbs.mihoyo.com/ys/article/8808224
|
||||
# https://m.bbs.mihoyo.com/ys/article/8808224
|
||||
:return: post_id
|
||||
"""
|
||||
rgx = re.compile(r"(?:bbs\.)?mihoyo\.com/[^.]+/article/(?P<article_id>\d+)")
|
||||
matches = rgx.search(text)
|
||||
if matches is None:
|
||||
return -1
|
||||
entries = matches.groupdict()
|
||||
if entries is None:
|
||||
return -1
|
||||
try:
|
||||
art_id = int(entries.get('article_id'))
|
||||
except (IndexError, ValueError, TypeError):
|
||||
return -1
|
||||
return art_id
|
||||
|
||||
def get_headers(self, referer: str = "https://bbs.mihoyo.com/"):
|
||||
return {
|
||||
"User-Agent": self.USER_AGENT,
|
||||
"Referer": referer
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_list_url_params(forum_id: int, is_good: bool = False, is_hot: bool = False,
|
||||
page_size: int = 20) -> dict:
|
||||
params = {
|
||||
"forum_id": forum_id,
|
||||
"gids": 2,
|
||||
"is_good": is_good,
|
||||
"is_hot": is_hot,
|
||||
"page_size": page_size,
|
||||
"sort_type": 1
|
||||
}
|
||||
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
def get_images_params(resize: int = 600, quality: int = 80, auto_orient: int = 0, interlace: int = 1,
|
||||
images_format: str = "jpg"):
|
||||
"""
|
||||
image/resize,s_600/quality,q_80/auto-orient,0/interlace,1/format,jpg
|
||||
:param resize: 图片大小
|
||||
:param quality: 图片质量
|
||||
:param auto_orient: 自适应
|
||||
:param interlace: 未知
|
||||
:param images_format: 图片格式
|
||||
:return:
|
||||
"""
|
||||
params = f"image/resize,s_{resize}/quality,q_{quality}/auto-orient," \
|
||||
f"{auto_orient}/interlace,{interlace}/format,{images_format}"
|
||||
return {"x-oss-process": params}
|
||||
|
||||
async def get_post_full_in_collection(self, collection_id: int, gids: int = 2, order_type=1) -> BaseResponseData:
|
||||
params = {
|
||||
"collection_id": collection_id,
|
||||
"gids": gids,
|
||||
"order_type": order_type
|
||||
}
|
||||
response = await self.client.get(url=self.POST_FULL_IN_COLLECTION_URL, params=params)
|
||||
if response.is_error:
|
||||
return BaseResponseData(error_message="请求错误")
|
||||
return BaseResponseData(response.json())
|
||||
|
||||
async def get_artwork_info(self, gids: int, post_id: int, read: int = 1) -> HyperionResponse:
|
||||
params = {
|
||||
"gids": gids,
|
||||
"post_id": post_id,
|
||||
"read": read
|
||||
}
|
||||
response = await self.client.get(self.POST_FULL_URL, params=params)
|
||||
if response.is_error:
|
||||
return HyperionResponse(error_message="请求错误")
|
||||
return HyperionResponse(response.json())
|
||||
|
||||
async def get_post_full_info(self, gids: int, post_id: int, read: int = 1) -> BaseResponseData:
|
||||
params = {
|
||||
"gids": gids,
|
||||
"post_id": post_id,
|
||||
"read": read
|
||||
}
|
||||
response = await self.client.get(self.POST_FULL_URL, params=params)
|
||||
if response.is_error:
|
||||
return BaseResponseData(error_message="请求错误")
|
||||
return BaseResponseData(response.json())
|
||||
|
||||
async def get_images_by_post_id(self, gids: int, post_id: int) -> List[ArtworkImage]:
|
||||
artwork_info = await self.get_artwork_info(gids, post_id)
|
||||
if artwork_info.error:
|
||||
return []
|
||||
urls = artwork_info.results.image_url_list
|
||||
art_list = []
|
||||
task_list = [
|
||||
self.download_image(artwork_info.post_id, urls[page], page) for page in range(len(urls))
|
||||
]
|
||||
result_list = await asyncio.gather(*task_list)
|
||||
for result in result_list:
|
||||
if isinstance(result, ArtworkImage):
|
||||
art_list.append(result)
|
||||
|
||||
def take_page(elem: ArtworkImage):
|
||||
return elem.page
|
||||
|
||||
art_list.sort(key=take_page)
|
||||
return art_list
|
||||
|
||||
async def download_image(self, art_id: int, url: str, page: int = 0) -> ArtworkImage:
|
||||
response = await self.client.get(url, params=self.get_images_params(resize=2000), timeout=5)
|
||||
if response.is_error:
|
||||
return ArtworkImage(art_id, page, True)
|
||||
return ArtworkImage(art_id, page, data=response.content)
|
||||
|
||||
async def get_new_list(self, gids: int, type_id: int, page_size: int = 20):
|
||||
"""
|
||||
?gids=2&page_size=20&type=3
|
||||
:return:
|
||||
"""
|
||||
params = {
|
||||
"gids": gids,
|
||||
"page_size": page_size,
|
||||
"type": type_id
|
||||
}
|
||||
response = await self.client.get(url=self.GET_NEW_LIST_URL, params=params)
|
||||
if response.is_error:
|
||||
return BaseResponseData(error_message="请求错误")
|
||||
return BaseResponseData(response.json())
|
||||
|
||||
async def close(self):
|
||||
await self.client.aclose()
|
||||
|
||||
|
||||
class YuanShen:
|
||||
SIGN_INFO_URL = "https://api-takumi.mihoyo.com/event/bbs_sign_reward/info"
|
||||
SIGN_URL = "https://api-takumi.mihoyo.com/event/bbs_sign_reward/sign"
|
||||
SIGN_HOME_URL = "https://api-takumi.mihoyo.com/event/bbs_sign_reward/home"
|
||||
|
||||
APP_VERSION = "2.3.0"
|
||||
USER_AGENT = "Mozilla/5.0 (Linux; Android 9; Unspecified Device) AppleWebKit/537.36 (KHTML, like Gecko) " \
|
||||
"Version/4.0 Chrome/39.0.0.0 Mobile Safari/537.36 miHoYoBBS/2.3.0"
|
||||
REFERER = "https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?" \
|
||||
"bbs_auth_required=true&act_id=e202009291139501&utm_source=hyperion&utm_medium=mys&utm_campaign=icon"
|
||||
ORIGIN = "https://webstatic.mihoyo.com"
|
||||
|
||||
ACT_ID = "e202009291139501"
|
||||
DS_SALT = "h8w582wxwgqvahcdkpvdhbh2w9casgfl"
|
||||
|
||||
def __init__(self):
|
||||
self.headers = {
|
||||
"Origin": self.ORIGIN,
|
||||
'DS': get_ds(self.DS_SALT),
|
||||
'x-rpc-app_version': self.APP_VERSION,
|
||||
'User-Agent': self.USER_AGENT,
|
||||
'x-rpc-client_type': '5', # 1为ios 2为安卓 4为pc_web 5为mobile_web
|
||||
'Referer': self.REFERER,
|
||||
'x-rpc-device_id': get_device_id(self.USER_AGENT)}
|
||||
self.client = AsyncClient(headers=self.headers)
|
||||
|
||||
async def is_sign(self, uid: int, region: str = "cn_gf01", cookies: dict = None):
|
||||
"""
|
||||
检查是否签到
|
||||
:param uid: 游戏UID
|
||||
:param region: 服务器
|
||||
:param cookies: cookie
|
||||
:return:
|
||||
"""
|
||||
params = {
|
||||
"act_id": self.ACT_ID,
|
||||
"region": region,
|
||||
"uid": uid
|
||||
}
|
||||
req = await self.client.get(self.SIGN_INFO_URL, params=params, cookies=cookies)
|
||||
if req.is_error:
|
||||
return BaseResponseData(error_message="请求错误")
|
||||
return BaseResponseData(req.json())
|
||||
|
||||
async def sign(self, uid: int, region: str = "cn_gf01", cookies: dict = None):
|
||||
"""
|
||||
执行签到
|
||||
:param uid: 游戏UID
|
||||
:param region: 服务器
|
||||
:param cookies: cookie
|
||||
:return:
|
||||
"""
|
||||
data = {
|
||||
"act_id": self.ACT_ID,
|
||||
"region": region,
|
||||
"uid": uid
|
||||
}
|
||||
req = await self.client.post(self.SIGN_URL, json=data, cookies=cookies)
|
||||
if req.is_error:
|
||||
return BaseResponseData(error_message="签到失败")
|
||||
return BaseResponseData(req.json())
|
||||
|
||||
async def get_sign_give(self, cookies: dict = None):
|
||||
"""
|
||||
返回今日签到信息
|
||||
:param cookies:
|
||||
:return:
|
||||
"""
|
||||
params = {
|
||||
"act_id": self.ACT_ID
|
||||
}
|
||||
req = await self.client.get(self.SIGN_HOME_URL, params=params, cookies=cookies)
|
||||
if req.is_error:
|
||||
return
|
||||
return BaseResponseData(req.json())
|
||||
|
||||
async def __aenter__(self):
|
||||
"""
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
"""
|
||||
:param exc_type:
|
||||
:param exc:
|
||||
:param tb:
|
||||
:return:
|
||||
"""
|
||||
await self.client.aclose()
|
1725
model/apihelper/metadata/CharactersMap.json
Normal file
387
model/apihelper/metadata/NameTextMapHash.json
Normal file
@ -0,0 +1,387 @@
|
||||
{
|
||||
"20848859": "黑岩斩刀",
|
||||
"33330467": "元素熟练",
|
||||
"37147251": "匣里日月",
|
||||
"43015699": "待定",
|
||||
"54857595": "止水息雷",
|
||||
"83115355": "被怜爱的少女",
|
||||
"85795635": "专注",
|
||||
"88505754": "枫原万叶",
|
||||
"135182203": "止水息雷",
|
||||
"147298547": "流浪大地的乐团",
|
||||
"156294403": "沉沦之心",
|
||||
"160493219": "暗铁剑",
|
||||
"168956722": "七七",
|
||||
"197755235": "贯虹之槊",
|
||||
"212557731": "祭雷之人",
|
||||
"240385755": "破浪",
|
||||
"246984427": "踏火息雷",
|
||||
"262428003": "祭冰之人",
|
||||
"270124867": "护国的无垢之心",
|
||||
"287454963": "祭风之人",
|
||||
"288666635": "无垢之心",
|
||||
"302691299": "琥珀玥",
|
||||
"303155515": "离簇不归",
|
||||
"310247243": "神乐之真意",
|
||||
"334242634": "申鹤",
|
||||
"339931171": "乘胜追击",
|
||||
"342097547": "辰砂之纺锤",
|
||||
"346510395": "衔珠海皇",
|
||||
"368014203": "斩裂晴空的龙脊",
|
||||
"391273955": "斫断黑翼的利齿",
|
||||
"411685275": "钢轮弓",
|
||||
"479076483": "冷刃",
|
||||
"481755219": "黑岩刺枪",
|
||||
"486287579": "余热",
|
||||
"500612819": "「旗杆」",
|
||||
"500987603": "(test)穿模测试",
|
||||
"506630267": "顺风而行",
|
||||
"514784907": "踏火止水",
|
||||
"521221323": "护国的无垢之心",
|
||||
"540938627": "掠食者",
|
||||
"566772267": "御伽大王御伽话",
|
||||
"577103787": "能量沐浴",
|
||||
"578575283": "流月针",
|
||||
"597991835": "白夜皓月",
|
||||
"613846163": "降世",
|
||||
"618786571": "钺矛",
|
||||
"623494555": "摧坚",
|
||||
"623534363": "西风秘典",
|
||||
"630452219": "樱之斋宫",
|
||||
"646100491": "千岩诀·同心",
|
||||
"650049651": "风花之颂",
|
||||
"655825874": "云堇",
|
||||
"656120259": "神射手之誓",
|
||||
"680510411": "白影剑",
|
||||
"688991243": "息灾",
|
||||
"693354267": "尘世之锁",
|
||||
"697277554": "烟绯",
|
||||
"716252627": "千岩长枪",
|
||||
"729851187": "冰之川与雪之砂",
|
||||
"735056795": "西风大剑",
|
||||
"807607555": "天空之卷",
|
||||
"824949859": "嘟嘟!大冒险",
|
||||
"828711395": "阿莫斯之弓",
|
||||
"836208539": "炊金",
|
||||
"850802171": "白铁大剑",
|
||||
"855894507": "战狂",
|
||||
"862591315": "苍白之火",
|
||||
"877751435": "宗室大剑",
|
||||
"902264035": "风鹰剑",
|
||||
"902282051": "收割",
|
||||
"909145139": "护国的无垢之心",
|
||||
"930640955": "钟剑",
|
||||
"933076627": "冰风迷途的勇士",
|
||||
"942758755": "专注",
|
||||
"944332883": "斫峰之刃",
|
||||
"949506483": "海洋的胜利",
|
||||
"968378595": "西风之鹰的抗争",
|
||||
"968893378": "班尼特",
|
||||
"991968139": "非时之梦·常世灶食",
|
||||
"1006042610": "神里绫华",
|
||||
"1021898539": "弹弓",
|
||||
"1021947690": "魈",
|
||||
"1028735635": "抗争的践行之歌",
|
||||
"1053433018": "砂糖",
|
||||
"1072884907": "万国诸海图谱",
|
||||
"1075647299": "松籁响起之时",
|
||||
"1082448331": "微光的海渊民",
|
||||
"1089950259": "天空之傲",
|
||||
"1097898243": "沉重",
|
||||
"1103732675": "幸运儿",
|
||||
"1113306282": "莫娜",
|
||||
"1114777131": "和弦",
|
||||
"1119368259": "旅程",
|
||||
"1130996346": "香菱",
|
||||
"1133599347": "矢志不忘",
|
||||
"1148024603": "「渔获」",
|
||||
"1154009435": "试作星镰",
|
||||
"1163263227": "流浪乐章",
|
||||
"1163616891": "霜葬",
|
||||
"1182966603": "佣兵重剑",
|
||||
"1186209435": "赌徒",
|
||||
"1212345779": "角斗士的终幕礼",
|
||||
"1217552947": "白刃流转",
|
||||
"1240067179": "西风猎弓",
|
||||
"1319974859": "激励",
|
||||
"1321135667": "匣里龙吟",
|
||||
"1337666507": "千岩牢固",
|
||||
"1344953075": "顺风而行",
|
||||
"1345343763": "磐岩结绿",
|
||||
"1383639611": "奇迹",
|
||||
"1388004931": "飞天御剑",
|
||||
"1390797107": "白缨枪",
|
||||
"1404688115": "别离的思念之歌",
|
||||
"1406746947": "异世界行记",
|
||||
"1414366819": "金璋皇极",
|
||||
"1437658243": "螭骨剑",
|
||||
"1438974835": "逆飞的流星",
|
||||
"1455107995": "四风原典",
|
||||
"1468367538": "迪奥娜",
|
||||
"1479961579": "铁影阔剑",
|
||||
"1483922610": "九条裟罗",
|
||||
"1485303435": "注能之刺",
|
||||
"1492752155": "气定神闲",
|
||||
"1499235563": "乘胜追击",
|
||||
"1499817443": "苍翠之风",
|
||||
"1516554699": "石英大剑",
|
||||
"1522029867": "踏火息雷",
|
||||
"1524173875": "炽烈的炎之魔女",
|
||||
"1533656818": "旅行者",
|
||||
"1541919827": "染血的骑士道",
|
||||
"1545992315": "「正义」",
|
||||
"1558036915": "辰砂往生录",
|
||||
"1562601179": "翠绿之影",
|
||||
"1588620330": "神里绫人",
|
||||
"1595734083": "(test)穿模测试",
|
||||
"1600275315": "波乱月白经津",
|
||||
"1608953539": "黎明神剑",
|
||||
"1610242915": "传承",
|
||||
"1628928163": "风花之愿",
|
||||
"1632377563": "渡过烈火的贤人",
|
||||
"1651985379": "极昼的先兆者",
|
||||
"1660598451": "岩藏之胤",
|
||||
"1675686363": "祭礼大剑",
|
||||
"1706534267": "有话直说",
|
||||
"1722706579": "止水融冰",
|
||||
"1745286795": "名士振舞",
|
||||
"1745712907": "驭浪的海祇民",
|
||||
"1751039235": "昔日宗室之仪",
|
||||
"1756609915": "海染砗磲",
|
||||
"1771603299": "金璋皇极",
|
||||
"1773425155": "降临之剑",
|
||||
"1789612403": "回响",
|
||||
"1820235315": "无矢之歌",
|
||||
"1836628747": "叛逆的守护者",
|
||||
"1860795787": "曚云之月",
|
||||
"1864015138": "刻晴",
|
||||
"1873342283": "平息鸣雷的尊者",
|
||||
"1890163363": "不灭月华",
|
||||
"1901973075": "冬极白星",
|
||||
"1921418842": "诺艾尔",
|
||||
"1932742643": "灭却之戒法",
|
||||
"1934830979": "无尽的渴慕",
|
||||
"1940821986": "久岐忍",
|
||||
"1940919994": "胡桃",
|
||||
"1966438658": "安柏",
|
||||
"1982136171": "专注",
|
||||
"1990641987": "祭礼剑",
|
||||
"1990820123": "天目影打刀",
|
||||
"1991707099": "试作斩岩",
|
||||
"1997709467": "和璞鸢",
|
||||
"2006422931": "千岩古剑",
|
||||
"2009975571": "(test)穿模测试",
|
||||
"2017033267": "气定神闲",
|
||||
"2025598051": "顺风而行",
|
||||
"2040573235": "悠古的磐岩",
|
||||
"2060049099": "祭火之人",
|
||||
"2108574027": "碎石",
|
||||
"2109571443": "专注",
|
||||
"2125206395": "银剑",
|
||||
"2149411851": "金璋皇极",
|
||||
"2172529947": "乘胜追击",
|
||||
"2176134843": "专注",
|
||||
"2190368347": "决",
|
||||
"2191797987": "冒险家",
|
||||
"2195665683": "祭礼残章",
|
||||
"2242027395": "黑檀弓",
|
||||
"2276480763": "绝缘之旗印",
|
||||
"2279290283": "魔导绪论",
|
||||
"2297485451": "速射弓斗",
|
||||
"2312640651": "气定神闲",
|
||||
"2317820211": "注能之针",
|
||||
"2322648115": "粉碎",
|
||||
"2324146259": "白辰之环",
|
||||
"2340970067": "历练的猎弓",
|
||||
"2359799475": "恶王丸",
|
||||
"2364208851": "行者之心",
|
||||
"2365025043": "街巷游侠",
|
||||
"2375993851": "宗室长剑",
|
||||
"2383998915": "驭浪的海祇民",
|
||||
"2384519283": "弹弓",
|
||||
"2388785242": "早柚",
|
||||
"2400012995": "祭礼弓",
|
||||
"2410593283": "无锋剑",
|
||||
"2417717595": "暗巷猎手",
|
||||
"2425414923": "落霞",
|
||||
"2433755451": "揭旗的叛逆之歌",
|
||||
"2440850563": "回响长天的诗歌",
|
||||
"2466140362": "温迪",
|
||||
"2469300579": "乘胜追击",
|
||||
"2470306939": "飞雷御执",
|
||||
"2474354867": "西风剑",
|
||||
"2476346187": "踏火止水",
|
||||
"2491797315": "喜多院十文字",
|
||||
"2504399314": "宵宫",
|
||||
"2512309395": "如雷的盛怒",
|
||||
"2521338131": "试作金珀",
|
||||
"2534304035": "雾切御腰物",
|
||||
"2539208459": "证誓之明瞳",
|
||||
"2546254811": "华馆梦醒形骸记",
|
||||
"2556914683": "绝弦",
|
||||
"2587614459": "忍冬之果",
|
||||
"2614170427": "飞天大御剑",
|
||||
"2646367730": "北斗",
|
||||
"2664629131": "匣里灭辰",
|
||||
"2666951267": "训练大剑",
|
||||
"2673337443": "注能之矢",
|
||||
"2679781122": "甘雨",
|
||||
"2684365579": "登场乐",
|
||||
"2705029563": "口袋魔导书",
|
||||
"2713453234": "八重神子",
|
||||
"2719832059": "(test)穿模测试",
|
||||
"2743659331": "激流",
|
||||
"2749508387": "金璋皇极",
|
||||
"2749853923": "腐殖之剑",
|
||||
"2753539619": "雪葬的星银",
|
||||
"2764598579": "流放者",
|
||||
"2792766467": "无工之剑",
|
||||
"2796697027": "新手长枪",
|
||||
"2832648187": "宗室长弓",
|
||||
"2834803571": "金璋皇极",
|
||||
"2848374378": "夜兰",
|
||||
"2853296811": "穿刺高天的利齿",
|
||||
"2871793795": "锐利",
|
||||
"2876340530": "重云",
|
||||
"2890909531": "武人",
|
||||
"2893964243": "飞矢传书",
|
||||
"2915865819": "渊中霞彩",
|
||||
"2918525947": "飞雷之弦振",
|
||||
"2935286715": "宗室猎枪",
|
||||
"2947140987": "暗巷闪光",
|
||||
"2949448555": "苍古自由之誓",
|
||||
"2963220587": "翡玉法球",
|
||||
"3001782875": "气定神闲",
|
||||
"3018479371": "船歌",
|
||||
"3024507506": "雷电将军",
|
||||
"3063488107": "强力攻击",
|
||||
"3068316954": "荒泷一斗",
|
||||
"3070169307": "铁尖枪",
|
||||
"3079462611": "驭浪的海祇民",
|
||||
"3090373787": "暗巷的酒与诗",
|
||||
"3097441915": "以理服人",
|
||||
"3112448011": "决心",
|
||||
"3112679155": "终末嗟叹之诗",
|
||||
"3156385731": "昭心",
|
||||
"3169209451": "弓藏",
|
||||
"3192689683": "霜葬",
|
||||
"3221566250": "琴",
|
||||
"3235324891": "护摩之杖",
|
||||
"3252085691": "顺风而行",
|
||||
"3258658763": "嗜魔",
|
||||
"3265161211": "注能之锋",
|
||||
"3273999011": "黑岩绯玉",
|
||||
"3277782506": "菲谢尔",
|
||||
"3302787771": "霜葬",
|
||||
"3305772819": "奔袭战术",
|
||||
"3314157803": "克柔",
|
||||
"3337185491": "浅濑之弭",
|
||||
"3337249451": "过载",
|
||||
"3339083250": "可莉",
|
||||
"3344622722": "丽莎",
|
||||
"3364338659": "无边际的眷顾",
|
||||
"3371922315": "神樱神游神乐舞",
|
||||
"3378007475": "黑岩长剑",
|
||||
"3400133546": "五郎",
|
||||
"3406113971": "顺风而行",
|
||||
"3421967235": "吃虎鱼刀",
|
||||
"3439749859": "苍翠猎弓",
|
||||
"3443142923": "龙脊长枪",
|
||||
"3447737235": "黑岩战弓",
|
||||
"3456986819": "嘟嘟可故事集",
|
||||
"3465493459": "精准",
|
||||
"3500935003": "讨龙英杰谭",
|
||||
"3535784755": "勇士之心",
|
||||
"3541083923": "角斗士",
|
||||
"3555115602": "托马",
|
||||
"3584825427": "学徒笔记",
|
||||
"3587062891": "千岩诀·同心",
|
||||
"3587621259": "笛剑",
|
||||
"3600623979": "猎弓",
|
||||
"3608180322": "迪卢克",
|
||||
"3618167299": "学士",
|
||||
"3625393819": "试作澹月",
|
||||
"3626268211": "来歆余响",
|
||||
"3673792067": "旅行剑",
|
||||
"3684723963": "雨裁",
|
||||
"3689108098": "埃洛伊",
|
||||
"3717667418": "优菈",
|
||||
"3717849275": "薙草之稻光",
|
||||
"3719372715": "甲级宝珏",
|
||||
"3722933411": "试作古华",
|
||||
"3755004051": "西风长枪",
|
||||
"3762437019": "(test)穿模测试",
|
||||
"3775299170": "芭芭拉",
|
||||
"3782508715": "游医",
|
||||
"3796702635": "变化万端",
|
||||
"3796905611": "黑剑",
|
||||
"3816664530": "旅行者",
|
||||
"3827789435": "宗室秘法录",
|
||||
"3832443723": "不屈",
|
||||
"3836188467": "无羁的朱赤之蝶",
|
||||
"3847143266": "达达利亚",
|
||||
"3862787418": "钟离",
|
||||
"3890292467": "教官",
|
||||
"3898539027": "浮游四方的灵云",
|
||||
"3914045794": "珊瑚宫心海",
|
||||
"3914951691": "赤角石溃杵",
|
||||
"3933622347": "天空之翼",
|
||||
"3949653579": "幽夜华尔兹",
|
||||
"3966753539": "洗濯诸类之形",
|
||||
"3975746731": "鸦羽弓",
|
||||
"3995710363": "狼的末路",
|
||||
"3996017211": "收割",
|
||||
"3999792907": "祭水之人",
|
||||
"4000770243": "街巷伏击",
|
||||
"4022012131": "乘胜追击",
|
||||
"4049410651": "决斗之枪",
|
||||
"4055003299": "天空之刃",
|
||||
"4060235987": "日月辉",
|
||||
"4080317355": "勇气",
|
||||
"4082302819": "守护之心",
|
||||
"4090429643": "沐浴龙血的剑",
|
||||
"4103022435": "铁蜂刺",
|
||||
"4103766499": "黑缨枪",
|
||||
"4108620722": "阿贝多",
|
||||
"4113638323": "昭理的鸢之枪",
|
||||
"4119663210": "凯亚",
|
||||
"4122509083": "断浪长鳍",
|
||||
"4124851547": "雾切之回光",
|
||||
"4127888970": "凝光",
|
||||
"4137694339": "(test)竿测试",
|
||||
"4139294531": "信使",
|
||||
"4144069251": "追忆之注连",
|
||||
"4158505619": "天空之脊",
|
||||
"4160147242": "雷泽",
|
||||
"4162981171": "(test)穿模测试",
|
||||
"4186179883": "破魔之弓",
|
||||
"4193089947": "桂木斩长正",
|
||||
"4197635682": "行秋",
|
||||
"4226083179": "名士振舞",
|
||||
"4230231107": "若水",
|
||||
"4245213187": "注能之卷",
|
||||
"4258047555": "极夜二重奏",
|
||||
"4260733330": "罗莎莉亚",
|
||||
"4267718859": "反曲弓",
|
||||
"4273845410": "辛焱",
|
||||
"4275754179": "如狼般狩猎者",
|
||||
"FIGHT_PROP_MAX_HP": "生命值上限",
|
||||
"FIGHT_PROP_ATTACK": "攻击力",
|
||||
"FIGHT_PROP_DEFENSE": "防御力",
|
||||
"FIGHT_PROP_ELEMENT_MASTERY": "元素精通",
|
||||
"FIGHT_PROP_CRITICAL": "暴击率",
|
||||
"FIGHT_PROP_CRITICAL_HURT": "暴击伤害",
|
||||
"FIGHT_PROP_HEAL_ADD": "治疗加成",
|
||||
"FIGHT_PROP_HEALED_ADD": "受治疗加成",
|
||||
"FIGHT_PROP_CHARGE_EFFICIENCY": "元素充能效率",
|
||||
"FIGHT_PROP_SHIELD_COST_MINUS_RATIO": "护盾强效",
|
||||
"FIGHT_PROP_FIRE_ADD_HURT": "火元素伤害加成",
|
||||
"FIGHT_PROP_WATER_ADD_HURT": "水元素伤害加成",
|
||||
"FIGHT_PROP_GRASS_ADD_HURT": "草元素伤害加成",
|
||||
"FIGHT_PROP_ELEC_ADD_HURT": "雷元素伤害加成",
|
||||
"FIGHT_PROP_WIND_ADD_HURT": "风元素伤害加成",
|
||||
"FIGHT_PROP_ICE_ADD_HURT": "冰元素伤害加成",
|
||||
"FIGHT_PROP_ROCK_ADD_HURT": "岩元素伤害加成",
|
||||
"FIGHT_PROP_PHYSICAL_ADD_HURT": "物理伤害加成",
|
||||
"level": "等级"
|
||||
}
|
31
model/apihelper/metadata/ReliquaryNameMap.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"FIGHT_PROP_BASE_ATTACK": "基础攻击力",
|
||||
"FIGHT_PROP_BASE_DEFENSE": "基础防御力",
|
||||
"FIGHT_PROP_BASE_HP": "基础血量",
|
||||
"FIGHT_PROP_ATTACK": "攻击力",
|
||||
"FIGHT_PROP_ATTACK_PERCENT": "百分比攻击力",
|
||||
"FIGHT_PROP_HP": "血量",
|
||||
"FIGHT_PROP_HP_PERCENT": "百分比血量",
|
||||
"FIGHT_PROP_DEFENSE": "防御力",
|
||||
"FIGHT_PROP_DEFENSE_PERCENT": "百分比防御力",
|
||||
"FIGHT_PROP_ELEMENT_MASTERY": "元素精通",
|
||||
"FIGHT_PROP_CRITICAL": "暴击率",
|
||||
"FIGHT_PROP_CRITICAL_HURT": "暴击伤害",
|
||||
"FIGHT_PROP_CHARGE_EFFICIENCY": "元素充能效率",
|
||||
"FIGHT_PROP_FIRE_SUB_HURT": "火元素抗性",
|
||||
"FIGHT_PROP_ELEC_SUB_HURT": "雷元素抗性",
|
||||
"FIGHT_PROP_ICE_SUB_HURT": "冰元素抗性",
|
||||
"FIGHT_PROP_WATER_SUB_HURT": "水元素抗性",
|
||||
"FIGHT_PROP_WIND_SUB_HURT": "风元素抗性",
|
||||
"FIGHT_PROP_ROCK_SUB_HURT": "岩元素抗性",
|
||||
"FIGHT_PROP_GRASS_SUB_HURT": "草元素抗性",
|
||||
"FIGHT_PROP_FIRE_ADD_HURT": "火元素伤害加成",
|
||||
"FIGHT_PROP_ELEC_ADD_HURT": "雷元素伤害加成",
|
||||
"FIGHT_PROP_ICE_ADD_HURT": "冰元素伤害加成",
|
||||
"FIGHT_PROP_WATER_ADD_HURT": "水元素伤害加成",
|
||||
"FIGHT_PROP_WIND_ADD_HURT": "风元素伤害加成",
|
||||
"FIGHT_PROP_ROCK_ADD_HURT": "岩元素伤害加成",
|
||||
"FIGHT_PROP_GRASS_ADD_HURT": "草元素伤害加成",
|
||||
"FIGHT_PROP_PHYSICAL_ADD_HURT": "物理伤害加成",
|
||||
"FIGHT_PROP_HEAL_ADD": "治疗加成"
|
||||
}
|
156
model/apihelper/playercards.py
Normal file
@ -0,0 +1,156 @@
|
||||
import os
|
||||
from typing import Union, Optional
|
||||
|
||||
import httpx
|
||||
import ujson
|
||||
|
||||
from model.base import GameItem
|
||||
from model.game.artifact import ArtifactInfo
|
||||
from model.game.character import CharacterInfo, CharacterValueInfo
|
||||
from model.game.fetter import FetterInfo
|
||||
from model.game.skill import Skill
|
||||
from model.game.talent import Talent
|
||||
from model.game.weapon import WeaponInfo
|
||||
from .helpers import get_headers
|
||||
|
||||
|
||||
class PlayerCardsAPI:
|
||||
UI_URL = "https://enka.shinshin.moe/ui/"
|
||||
|
||||
def __init__(self):
|
||||
self.client = httpx.AsyncClient(headers=get_headers())
|
||||
project_path = os.path.dirname(__file__)
|
||||
characters_map_file = os.path.join(project_path, "metadata", "CharactersMap.json")
|
||||
name_text_map_hash_file = os.path.join(project_path, "metadata", "NameTextMapHash.json")
|
||||
reliquary_name_map_file = os.path.join(project_path, "metadata", "ReliquaryNameMap.json")
|
||||
with open(characters_map_file, "r", encoding="utf-8") as f:
|
||||
self._characters_map_json: dict = ujson.load(f)
|
||||
with open(name_text_map_hash_file, "r", encoding="utf-8") as f:
|
||||
self._name_text_map_hash_json: dict = ujson.load(f)
|
||||
with open(reliquary_name_map_file, "r", encoding="utf-8") as f:
|
||||
self._reliquary_name_map_json: dict = ujson.load(f)
|
||||
|
||||
def get_characters_name(self, item_id: Union[int, str]) -> str:
|
||||
if isinstance(item_id, int):
|
||||
item_id = str(item_id)
|
||||
characters = self.get_characters(item_id)
|
||||
name_text_map_hash = characters.get("NameTextMapHash", "-1")
|
||||
return self.get_text(str(name_text_map_hash))
|
||||
|
||||
def get_characters(self, item_id: Union[int, str]) -> dict:
|
||||
if isinstance(item_id, int):
|
||||
item_id = str(item_id)
|
||||
return self._characters_map_json.get(item_id, {})
|
||||
|
||||
def get_text(self, hash_value: Union[int, str]) -> str:
|
||||
if isinstance(hash_value, int):
|
||||
hash_value = str(hash_value)
|
||||
return self._name_text_map_hash_json.get(hash_value, "")
|
||||
|
||||
def get_reliquary_name(self, reliquary: str) -> str:
|
||||
return self._reliquary_name_map_json[reliquary]
|
||||
|
||||
async def get_data(self, uid: Union[str, int]):
|
||||
url = f"https://enka.shinshin.moe/u/{uid}/__data.json"
|
||||
response = await self.client.get(url)
|
||||
return response
|
||||
|
||||
def data_handler(self, avatar_data: dict, avatar_id: int) -> CharacterInfo:
|
||||
artifact_list = []
|
||||
|
||||
weapon_info: Optional[WeaponInfo] = None
|
||||
|
||||
equip_list = avatar_data["equipList"] # 圣遗物和武器相关
|
||||
fetter_info = avatar_data["fetterInfo"] # 好感等级
|
||||
fight_prop_map = avatar_data["fightPropMap"] # 属性
|
||||
# inherent_proud_skill_list = avatar_data["inherentProudSkillList"] # 不知道
|
||||
prop_map = avatar_data["propMap"] # 角色等级 其他信息
|
||||
# proud_skill_extra_level_map = avatar_data["proudSkillExtraLevelMap"] # 不知道
|
||||
# skill_depot_id = avatar_data["skillDepotId"] # 不知道
|
||||
skill_level_map = avatar_data["skillLevelMap"] # 技能等级
|
||||
|
||||
# 角色等级
|
||||
character_level = prop_map['4001']['val']
|
||||
|
||||
# 角色姓名
|
||||
character_name = self.get_characters_name(avatar_id)
|
||||
characters_data = self.get_characters(avatar_id)
|
||||
|
||||
# 圣遗物和武器
|
||||
for equip in equip_list:
|
||||
if "reliquary" in equip: # 圣遗物
|
||||
flat = equip["flat"]
|
||||
reliquary = equip["reliquary"]
|
||||
reliquary_main_stat = flat["reliquaryMainstat"]
|
||||
reliquary_sub_stats = flat['reliquarySubstats']
|
||||
sub_item = []
|
||||
for reliquary_sub in reliquary_sub_stats:
|
||||
sub_item.append(GameItem(name=self.get_reliquary_name(reliquary_sub["appendPropId"]),
|
||||
item_type=reliquary_sub["appendPropId"], value=reliquary_sub["statValue"]))
|
||||
main_item = GameItem(name=self.get_reliquary_name(reliquary_main_stat["mainPropId"]),
|
||||
item_type=reliquary_main_stat["mainPropId"],
|
||||
value=reliquary_main_stat["statValue"])
|
||||
name = self.get_text(flat["nameTextMapHash"])
|
||||
artifact_list.append(ArtifactInfo(item_id=equip["itemId"], name=name, star=flat["rankLevel"],
|
||||
level=reliquary["level"] - 1, main_item=main_item, sub_item=sub_item))
|
||||
if "weapon" in equip: # 武器
|
||||
flat = equip["flat"]
|
||||
weapon_data = equip["weapon"]
|
||||
# 防止未精炼
|
||||
if 'promoteLevel' in weapon_data:
|
||||
weapon_level = weapon_data['promoteLevel'] - 1
|
||||
else:
|
||||
weapon_level = 0
|
||||
if 'affixMap' in weapon_data:
|
||||
affix = list(weapon_data['affixMap'].values())[0] + 1
|
||||
else:
|
||||
|
||||
affix = 1
|
||||
reliquary_main_stat = flat["weaponStats"][0]
|
||||
reliquary_sub_stats = flat['weaponStats'][1]
|
||||
sub_item = GameItem(name=self.get_reliquary_name(reliquary_main_stat["appendPropId"]),
|
||||
item_type=reliquary_sub_stats["appendPropId"],
|
||||
value=reliquary_sub_stats["statValue"])
|
||||
main_item = GameItem(name=self.get_reliquary_name(reliquary_main_stat["appendPropId"]),
|
||||
item_type=reliquary_main_stat["appendPropId"],
|
||||
value=reliquary_main_stat["statValue"])
|
||||
weapon_name = self.get_text(flat["nameTextMapHash"])
|
||||
weapon_info = WeaponInfo(item_id=equip["itemId"], name=weapon_name, star=flat["rankLevel"],
|
||||
level=weapon_level, main_item=main_item, sub_item=sub_item, affix=affix)
|
||||
|
||||
# 好感度
|
||||
fetter = FetterInfo(fetter_info["expLevel"])
|
||||
|
||||
# 基础数值处理
|
||||
for i in range(40, 47):
|
||||
if fight_prop_map[str(i)] > 0:
|
||||
dmg_bonus = fight_prop_map[str(i)]
|
||||
break
|
||||
else:
|
||||
dmg_bonus = 0
|
||||
|
||||
base_value = CharacterValueInfo(fight_prop_map["2000"], fight_prop_map["1"], fight_prop_map["2001"],
|
||||
fight_prop_map["4"], fight_prop_map["2002"], fight_prop_map["7"],
|
||||
fight_prop_map["28"], fight_prop_map["20"], fight_prop_map["22"],
|
||||
fight_prop_map["23"], fight_prop_map["26"], fight_prop_map["27"],
|
||||
fight_prop_map["29"], fight_prop_map["30"], dmg_bonus)
|
||||
|
||||
# 技能处理
|
||||
skill_list = []
|
||||
skills = characters_data["Skills"]
|
||||
for skill_id in skill_level_map:
|
||||
skill_list.append(Skill(skill_id, name=skill_level_map[skill_id], icon=skills[skill_id]))
|
||||
|
||||
# 命座处理
|
||||
talent_list = []
|
||||
consts = characters_data["Consts"]
|
||||
if 'talentIdList' in avatar_data:
|
||||
talent_id_list = avatar_data["talentIdList"]
|
||||
for index, value in enumerate(talent_id_list):
|
||||
talent_list.append(Talent(talent_id_list[index], icon=consts[index]))
|
||||
|
||||
element = characters_data["Element"]
|
||||
icon = characters_data["SideIconName"]
|
||||
character_info = CharacterInfo(character_name, element, character_level, fetter, base_value, weapon_info,
|
||||
artifact_list, skill_list, talent_list, icon)
|
||||
return character_info
|
62
model/base.py
Normal file
@ -0,0 +1,62 @@
|
||||
import imghdr
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
from model.baseobject import BaseObject
|
||||
|
||||
|
||||
class Stat:
|
||||
def __init__(self, view_num: int = 0, reply_num: int = 0, like_num: int = 0, bookmark_num: int = 0,
|
||||
forward_num: int = 0):
|
||||
self.forward_num = forward_num # 关注数
|
||||
self.bookmark_num = bookmark_num # 收藏数
|
||||
self.like_num = like_num # 喜欢数
|
||||
self.reply_num = reply_num # 回复数
|
||||
self.view_num = view_num # 观看数
|
||||
|
||||
|
||||
class ArtworkInfo:
|
||||
|
||||
def __init__(self):
|
||||
self.user_id: int = 0
|
||||
self.artwork_id: int = 0 # 作品ID
|
||||
self.site = ""
|
||||
self.title: str = "" # 标题
|
||||
self.origin_url: str = ""
|
||||
self.site_name: str = ""
|
||||
self.tags: list = []
|
||||
self.stat: Stat = Stat()
|
||||
self.create_timestamp: int = 0
|
||||
self.info = None
|
||||
|
||||
|
||||
class ArtworkImage:
|
||||
|
||||
def __init__(self, art_id: int, page: int = 0, is_error: bool = False, data: bytes = b""):
|
||||
self.art_id = art_id
|
||||
self.data = data
|
||||
self.is_error = is_error
|
||||
if not is_error:
|
||||
self.format: str = imghdr.what(None, self.data)
|
||||
self.page = page
|
||||
|
||||
|
||||
class ServiceEnum(Enum):
|
||||
"""
|
||||
该名称来源于米忽悠的安卓BBS包名结尾,考虑到大部分重要的功能确实是在移动端实现了
|
||||
"""
|
||||
NULL = None
|
||||
HYPERION = 1 # 米忽悠国服
|
||||
HOYOLAB = 2 # 米忽悠国际服
|
||||
|
||||
|
||||
class GameItem(BaseObject):
|
||||
def __init__(self, item_id: int = 0, name: str = "", item_type: Union[Enum, str, int] = "",
|
||||
value: Union[Enum, str, int, bool, float] = 0):
|
||||
self.item_id = item_id
|
||||
self.name = name # 名称
|
||||
self.type = item_type # 类型
|
||||
self.value = value # 数值
|
||||
|
||||
__slots__ = ("name", "type", "value", "item_id")
|
||||
|
103
model/baseobject.py
Normal file
@ -0,0 +1,103 @@
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from typing import Dict, Union, Optional, List
|
||||
|
||||
from model.types import JSONDict
|
||||
|
||||
|
||||
class BaseObject:
|
||||
"""
|
||||
大多数数据对象的基础类型
|
||||
"""
|
||||
|
||||
def __new__(cls, *args: object, **kwargs: object) -> "BaseObject":
|
||||
instance = super().__new__(cls)
|
||||
return instance
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.to_dict())
|
||||
|
||||
def __getitem__(self, item: str) -> object:
|
||||
try:
|
||||
return getattr(self, item)
|
||||
except AttributeError as exc:
|
||||
raise KeyError(
|
||||
f"Objects of type {self.__class__.__name__} don't have an attribute called "
|
||||
f"`{item}`."
|
||||
) from exc
|
||||
|
||||
def __getstate__(self) -> Dict[str, Union[str, object]]:
|
||||
return self._get_attrs(include_private=True, recursive=False)
|
||||
|
||||
def __setstate__(self, state: dict) -> None:
|
||||
for key, val in state.items():
|
||||
setattr(self, key, val)
|
||||
|
||||
def __deepcopy__(self, memodict: dict = None):
|
||||
if memodict is None:
|
||||
memodict = {}
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls) # 创建新实例
|
||||
attrs = self._get_attrs(include_private=True) # 获取其所有属性
|
||||
|
||||
for k in attrs: # 在DeepCopy对象中设置属性
|
||||
setattr(result, k, deepcopy(getattr(self, k), memodict))
|
||||
return result
|
||||
|
||||
# 添加插槽可减少内存使用,并允许更快的属性访问
|
||||
__slots__ = ()
|
||||
|
||||
def _get_attrs(self, include_private: bool = False, recursive: bool = False, ) -> Dict[str, Union[str, object]]:
|
||||
data = {}
|
||||
if not recursive:
|
||||
try:
|
||||
# __dict__ 具有来自超类的属性,因此无需在下面的for循环中输入
|
||||
data.update(self.__dict__)
|
||||
except AttributeError:
|
||||
pass
|
||||
# 我们希望使用self获取类的所有属性,但如果使用 self.__slots__ ,仅包括该类本身使用的属性,而不是它的超类
|
||||
# 因此,我们得到它的MRO,然后再得到它们的属性
|
||||
# 使用“[:-1]”切片排除了“object”类
|
||||
for cls in self.__class__.__mro__[:-1]:
|
||||
for key in cls.__slots__: # 忽略 属性已定义
|
||||
if not include_private and key.startswith("_"):
|
||||
continue
|
||||
|
||||
value = getattr(self, key, None)
|
||||
if value is not None:
|
||||
if recursive and hasattr(value, "to_dict"):
|
||||
data[key] = value.to_dict()
|
||||
else:
|
||||
data[key] = value
|
||||
elif not recursive:
|
||||
data[key] = value
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]:
|
||||
return None if data is None else data.copy()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict]):
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if cls == BaseObject:
|
||||
return cls()
|
||||
return cls(**data)
|
||||
|
||||
@classmethod
|
||||
def de_list(cls, data: Optional[List[JSONDict]]) -> List:
|
||||
if not data:
|
||||
return []
|
||||
|
||||
return [cls.de_json(d) for d in data]
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
def to_dict(self) -> JSONDict:
|
||||
return self._get_attrs(recursive=True)
|
67
model/gacha/banner.py
Normal file
@ -0,0 +1,67 @@
|
||||
from enum import Enum
|
||||
|
||||
from model.gacha.common import ItemParamData
|
||||
|
||||
|
||||
class BannerType(Enum):
|
||||
STANDARD = 1
|
||||
EVENT = 2
|
||||
WEAPON = 3
|
||||
|
||||
|
||||
class GachaBanner:
|
||||
def __init__(self):
|
||||
self.gachaType: int = 0
|
||||
self.scheduleId: int = 0
|
||||
self.prefabPath: str = ""
|
||||
self.previewPrefabPath: str = ""
|
||||
self.titlePath: str = ""
|
||||
self.costItemId = 0
|
||||
self.costItemAmount = 1
|
||||
self.costItemId10 = 0
|
||||
self.costItemAmount10 = 10
|
||||
self.beginTime: int = 0
|
||||
self.endTime: int = 0
|
||||
self.sortId: int = 0
|
||||
self.rateUpItems4 = {}
|
||||
self.rateUpItems5 = {}
|
||||
self.fallbackItems3 = {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302,
|
||||
15304}
|
||||
self.fallbackItems4Pool1 = {1014, 1020, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045,
|
||||
1048, 1053, 1055, 1056, 1064}
|
||||
self.fallbackItems4Pool2 = {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402,
|
||||
14403, 14409, 15401, 15402, 15403, 15405}
|
||||
self.fallbackItems5Pool1 = {1003, 1016, 1042, 1035, 1041}
|
||||
self.fallbackItems5Pool2 = {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502}
|
||||
self.removeC6FromPool = False
|
||||
self.autoStripRateUpFromFallback = True
|
||||
self.weights4 = {{1, 510}, {8, 510}, {10, 10000}}
|
||||
self.weights5 = {{1, 75}, {73, 150}, {90, 10000}}
|
||||
self.poolBalanceWeights4 = {{1, 255}, {17, 255}, {21, 10455}}
|
||||
self.poolBalanceWeights5 = {{1, 30}, {147, 150}, {181, 10230}}
|
||||
self.eventChance4 = 50
|
||||
self.eventChance5 = 50
|
||||
self.bannerType = BannerType.STANDARD
|
||||
self.rateUpItems1 = {}
|
||||
self.rateUpItems2 = {}
|
||||
self.eventChance = -1
|
||||
self.costItem = 0
|
||||
|
||||
def getGachaType(self):
|
||||
return self.gachaType
|
||||
|
||||
def getCost(self, numRolls: int):
|
||||
"""
|
||||
获取消耗的Item
|
||||
:param numRolls:
|
||||
:return:
|
||||
"""
|
||||
if numRolls == 1:
|
||||
return ItemParamData()
|
||||
elif numRolls == 10:
|
||||
return ItemParamData(self.costItemId10 if self.costItemId10 > 0 else self.getCostItem(),
|
||||
self.costItemAmount10)
|
||||
return ItemParamData()
|
||||
|
||||
def getCostItem(self):
|
||||
return self.costItem if self.costItem > 0 else self.costItemId
|
4
model/gacha/common.py
Normal file
@ -0,0 +1,4 @@
|
||||
class ItemParamData:
|
||||
def __init__(self, item_id: int = 0, count: int = 0):
|
||||
self.id: int = item_id
|
||||
self.count: int = count
|
7
model/gacha/manager.py
Normal file
@ -0,0 +1,7 @@
|
||||
class GachaManager:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def DoPulls(self, player_id: int, gacha_type: int, times: int):
|
||||
pass
|
51
model/game/artifact.py
Normal file
@ -0,0 +1,51 @@
|
||||
from enum import Enum
|
||||
from typing import Union, Optional, List
|
||||
|
||||
from model.base import GameItem
|
||||
from model.baseobject import BaseObject
|
||||
from model.types import JSONDict
|
||||
|
||||
|
||||
class ArtifactInfo(BaseObject):
|
||||
"""
|
||||
圣遗物信息
|
||||
"""
|
||||
|
||||
def __init__(self, item_id: int = 0, name: str = "", level: int = 0, main_item: Optional[GameItem] = None,
|
||||
pos: Union[Enum, str] = "", star: int = 1, sub_item: Optional[List[GameItem]] = None, icon: str = ""):
|
||||
"""
|
||||
:param item_id: item_id
|
||||
:param name: 圣遗物名字
|
||||
:param level: 圣遗物等级
|
||||
:param main_item: 主词条
|
||||
:param pos: 圣遗物类型
|
||||
:param star: 星级
|
||||
:param sub_item: 副词条
|
||||
:param icon: 图片
|
||||
"""
|
||||
self.icon = icon
|
||||
self.item_id = item_id
|
||||
self.level = level
|
||||
self.main_item = main_item
|
||||
self.name = name
|
||||
self.pos = pos
|
||||
self.star = star
|
||||
self.sub_item: List[GameItem] = []
|
||||
if sub_item is not None:
|
||||
self.sub_item = sub_item
|
||||
|
||||
def to_dict(self) -> JSONDict:
|
||||
data = super().to_dict()
|
||||
if self.sub_item:
|
||||
data["sub_item"] = [e.to_dict() for e in self.sub_item]
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict]) -> Optional["ArtifactInfo"]:
|
||||
data = cls._parse_data(data)
|
||||
if not data:
|
||||
return None
|
||||
data["sub_item"] = GameItem.de_list(data.get("sub_item"))
|
||||
return cls(**data)
|
||||
|
||||
__slots__ = ("name", "type", "value", "pos", "star", "sub_item", "main_item", "level", "item_id", "icon")
|
123
model/game/character.py
Normal file
@ -0,0 +1,123 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from model.baseobject import BaseObject
|
||||
from model.game.artifact import ArtifactInfo
|
||||
from model.game.fetter import FetterInfo
|
||||
from model.game.skill import Skill
|
||||
from model.game.talent import Talent
|
||||
from model.game.weapon import WeaponInfo
|
||||
from model.types import JSONDict
|
||||
|
||||
|
||||
class CharacterValueInfo(BaseObject):
|
||||
"""角色数值信息
|
||||
"""
|
||||
|
||||
def __init__(self, hp: float = 0, base_hp: float = 0, atk: float = 0, base_atk: float = 0,
|
||||
def_value: float = 0, base_def: float = 0, elemental_mastery: float = 0, crit_rate: float = 0,
|
||||
crit_dmg: float = 0, energy_recharge: float = 0, heal_bonus: float = 0, healed_bonus: float = 0,
|
||||
physical_dmg_sub: float = 0, physical_dmg_bonus: float = 0, dmg_bonus: float = 0):
|
||||
"""
|
||||
:param hp: 生命值
|
||||
:param base_hp: 基础生命值
|
||||
:param atk: 攻击力
|
||||
:param base_atk: 基础攻击力
|
||||
:param def_value: 防御力
|
||||
:param base_def: 基础防御力
|
||||
:param elemental_mastery: 元素精通
|
||||
:param crit_rate: 暴击率
|
||||
:param crit_dmg: 暴击伤害
|
||||
:param energy_recharge: 充能效率
|
||||
:param heal_bonus: 治疗
|
||||
:param healed_bonus: 受治疗
|
||||
:param physical_dmg_sub: 物理伤害加成
|
||||
:param physical_dmg_bonus: 物理伤害抗性
|
||||
:param dmg_bonus: 伤害加成
|
||||
"""
|
||||
self.dmg_bonus = dmg_bonus
|
||||
self.physical_dmg_bonus = physical_dmg_bonus
|
||||
self.physical_dmg_sub = physical_dmg_sub
|
||||
self.healed_bonus = healed_bonus
|
||||
self.heal_bonus = heal_bonus
|
||||
self.energy_recharge = energy_recharge
|
||||
self.crit_dmg = crit_dmg
|
||||
self.crit_rate = crit_rate
|
||||
self.elemental_mastery = elemental_mastery
|
||||
self.base_def = base_def
|
||||
self.def_value = def_value
|
||||
self.base_atk = base_atk
|
||||
self.atk = atk
|
||||
self.base_hp = base_hp
|
||||
self.hp = hp
|
||||
|
||||
@property
|
||||
def add_hp(self) -> float:
|
||||
return self.hp - self.base_hp
|
||||
|
||||
@property
|
||||
def add_atk(self) -> float:
|
||||
return self.atk - self.base_atk
|
||||
|
||||
@property
|
||||
def add_def(self) -> float:
|
||||
return self.def_value - self.base_def
|
||||
|
||||
__slots__ = (
|
||||
"hp", "base_hp", "atk", "base_atk", "def_value", "base_def", "elemental_mastery", "crit_rate", "crit_dmg",
|
||||
"energy_recharge", "dmg_bonus", "physical_dmg_bonus", "physical_dmg_sub", "healed_bonus",
|
||||
"heal_bonus")
|
||||
|
||||
|
||||
class CharacterInfo(BaseObject):
|
||||
"""角色信息
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "", elementl: str = 0, level: int = 0, fetter: Optional[FetterInfo] = None,
|
||||
base_value: Optional[CharacterValueInfo] = None, weapon: Optional[WeaponInfo] = None,
|
||||
artifact: Optional[List[ArtifactInfo]] = None, skill: Optional[List[Skill]] = None,
|
||||
talent: Optional[List[Talent]] = None, icon: str = ""):
|
||||
"""
|
||||
:param name: 角色名字
|
||||
:param level: 角色等级
|
||||
:param elementl: 属性
|
||||
:param fetter: 好感度
|
||||
:param base_value: 基础数值
|
||||
:param weapon: 武器
|
||||
:param artifact: 圣遗物
|
||||
:param skill: 技能
|
||||
:param talent: 命座
|
||||
:param icon: 角色图片
|
||||
"""
|
||||
self.icon = icon
|
||||
self.elementl = elementl
|
||||
self.talent = talent
|
||||
self.skill = skill
|
||||
self.artifact = artifact
|
||||
self.weapon = weapon
|
||||
self.base_value = base_value
|
||||
self.fetter = fetter
|
||||
self.level = level
|
||||
self.name = name
|
||||
|
||||
def to_dict(self) -> JSONDict:
|
||||
data = super().to_dict()
|
||||
if self.artifact:
|
||||
data["artifact"] = [e.to_dict() for e in self.artifact]
|
||||
if self.artifact:
|
||||
data["skill"] = [e.to_dict() for e in self.skill]
|
||||
if self.artifact:
|
||||
data["talent"] = [e.to_dict() for e in self.talent]
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict]) -> Optional["CharacterInfo"]:
|
||||
data = cls._parse_data(data)
|
||||
if not data:
|
||||
return None
|
||||
data["artifact"] = ArtifactInfo.de_list(data.get("sub_item"))
|
||||
data["skill"] = Skill.de_list(data.get("sub_item"))
|
||||
data["talent"] = Talent.de_list(data.get("sub_item"))
|
||||
return cls(**data)
|
||||
|
||||
__slots__ = (
|
||||
"name", "level", "level", "fetter", "base_value", "weapon", "artifact", "skill", "talent", "elementl", "icon")
|
15
model/game/fetter.py
Normal file
@ -0,0 +1,15 @@
|
||||
from model.baseobject import BaseObject
|
||||
|
||||
|
||||
class FetterInfo(BaseObject):
|
||||
"""
|
||||
好感度信息
|
||||
"""
|
||||
|
||||
def __init__(self, level: int = 0):
|
||||
"""
|
||||
:param level: 等级
|
||||
"""
|
||||
self.level = level
|
||||
|
||||
__slots__ = ("level",)
|
21
model/game/skill.py
Normal file
@ -0,0 +1,21 @@
|
||||
from model.baseobject import BaseObject
|
||||
|
||||
|
||||
class Skill(BaseObject):
|
||||
"""
|
||||
技能信息
|
||||
"""
|
||||
|
||||
def __init__(self, skill_id: int = 0, name: str = "", level: int = 0, icon: str = ""):
|
||||
"""
|
||||
:param skill_id: 技能ID
|
||||
:param name: 技能名称
|
||||
:param level: 技能等级
|
||||
:param icon: 技能图标
|
||||
"""
|
||||
self.icon = icon
|
||||
self.level = level
|
||||
self.name = name
|
||||
self.skill_id = skill_id
|
||||
|
||||
__slots__ = ("skill_id", "name", "level", "icon")
|
19
model/game/talent.py
Normal file
@ -0,0 +1,19 @@
|
||||
from model.baseobject import BaseObject
|
||||
|
||||
|
||||
class Talent(BaseObject):
|
||||
"""
|
||||
命座
|
||||
"""
|
||||
|
||||
def __init__(self, talent_id: int = 0, name: str = "", icon: str = ""):
|
||||
"""
|
||||
:param talent_id: 命座ID
|
||||
:param name: 命座名字
|
||||
:param icon: 图标
|
||||
"""
|
||||
self.icon = icon
|
||||
self.name = name
|
||||
self.talent_id = talent_id
|
||||
|
||||
__slots__ = ("talent_id", "name", "icon")
|
36
model/game/weapon.py
Normal file
@ -0,0 +1,36 @@
|
||||
from enum import Enum
|
||||
from typing import Union, Optional
|
||||
|
||||
from model.base import GameItem
|
||||
from model.baseobject import BaseObject
|
||||
|
||||
|
||||
class WeaponInfo(BaseObject):
|
||||
"""武器信息
|
||||
"""
|
||||
|
||||
def __init__(self, item_id: int = 0, name: str = "", level: int = 0, main_item: Optional[GameItem] = None,
|
||||
affix: int = 0, pos: Union[Enum, str] = "", star: int = 1, sub_item: Optional[GameItem] = None,
|
||||
icon: str = ""):
|
||||
"""
|
||||
:param item_id: item_id
|
||||
:param name: 武器名字
|
||||
:param level: 武器等级
|
||||
:param main_item: 主词条
|
||||
:param affix: 精炼等级
|
||||
:param pos: 武器类型
|
||||
:param star: 星级
|
||||
:param sub_item: 副词条
|
||||
:param icon: 图片
|
||||
"""
|
||||
self.affix = affix
|
||||
self.icon = icon
|
||||
self.item_id = item_id
|
||||
self.level = level
|
||||
self.main_item = main_item
|
||||
self.name = name
|
||||
self.pos = pos
|
||||
self.star = star
|
||||
self.sub_item = sub_item
|
||||
|
||||
__slots__ = ("name", "type", "value", "pos", "star", "sub_item", "main_item", "level", "item_id", "icon", "affix")
|
5
model/types.py
Normal file
@ -0,0 +1,5 @@
|
||||
from typing import Dict, Any, Callable, TypeVar
|
||||
|
||||
JSONDict = Dict[str, Any]
|
||||
|
||||
Func = TypeVar("Func", bound=Callable[..., Any])
|
189
model/wiki/characters.py
Normal file
@ -0,0 +1,189 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from .helpers import get_headers
|
||||
|
||||
|
||||
class Characters:
|
||||
CHARACTERS_LIST_URL = "https://genshin.honeyhunterworld.com/db/char/characters/?lang=CHS"
|
||||
ROOT_URL = "https://genshin.honeyhunterworld.com"
|
||||
|
||||
def __init__(self):
|
||||
self.client = httpx.AsyncClient(headers=get_headers())
|
||||
|
||||
async def _get_soup(self, url: str) -> Optional[BeautifulSoup]:
|
||||
request = await self.client.get(url)
|
||||
return BeautifulSoup(request.text, "lxml")
|
||||
|
||||
async def get_all_characters_url(self):
|
||||
url_list = []
|
||||
soup = await self._get_soup(self.CHARACTERS_LIST_URL)
|
||||
character_list = soup.find_all('div', {'class': 'char_sea_cont'})
|
||||
for character in character_list:
|
||||
name = character.find("span", {"class": "sea_charname"}).text
|
||||
if "旅行者" in name:
|
||||
continue
|
||||
character_link = self.ROOT_URL + character.a['href']
|
||||
url_list.append(character_link)
|
||||
return url_list
|
||||
|
||||
def get_characters_info_template(self):
|
||||
characters_info_dict = {
|
||||
"name": "",
|
||||
"title": "",
|
||||
"rarity": 0,
|
||||
"element": {"name": "", "icon": ""},
|
||||
"description": "",
|
||||
"constellations": {},
|
||||
"skills": {
|
||||
"normal_attack": self.get_skills_info_template(),
|
||||
"skill_e": self.get_skills_info_template(),
|
||||
"skill_q": self.get_skills_info_template(),
|
||||
"skill_replace": self.get_skills_info_template(),
|
||||
},
|
||||
"gacha_splash": ""
|
||||
}
|
||||
return characters_info_dict
|
||||
|
||||
@staticmethod
|
||||
def get_skills_info_template():
|
||||
skills_info_dict = {
|
||||
"icon": "",
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
return skills_info_dict
|
||||
|
||||
async def get_characters(self, url: str):
|
||||
characters_info_dict = self.get_characters_info_template()
|
||||
soup = await self._get_soup(url)
|
||||
main_content = soup.find("div", {'class': 'wrappercont'})
|
||||
char_name = main_content.find('div', {'class': 'custom_title'}).text
|
||||
characters_info_dict["name"] = char_name
|
||||
# 基础信息
|
||||
char_info_table = main_content.find('table', {'class': 'item_main_table'}).find_all('tr')
|
||||
for char_info_item in char_info_table:
|
||||
content = char_info_item.find_all('td')
|
||||
if content[0].text == "Title":
|
||||
char_title = content[1].text
|
||||
characters_info_dict["title"] = char_title
|
||||
if content[0].text == "Allegiance":
|
||||
char_allegiance = content[1].text
|
||||
characters_info_dict["allegiance"] = char_allegiance
|
||||
if content[0].text == "Rarity":
|
||||
char_rarity = len(content[1].find_all('div', {'class': 'sea_char_stars_wrap'}))
|
||||
characters_info_dict["rarity"] = char_rarity
|
||||
if content[0].text == "Element":
|
||||
char_element_icon_url = self.ROOT_URL + content[1].find('img')['data-src'].replace("_35", "")
|
||||
characters_info_dict["element"]["icon"] = char_element_icon_url
|
||||
if content[0].text == "Astrolabe Name":
|
||||
char_astrolabe_name = content[1].text
|
||||
if content[0].text == "In-game Description":
|
||||
char_description = content[1].text
|
||||
characters_info_dict["description"] = char_description
|
||||
|
||||
# 角色属性表格 咕咕咕
|
||||
skill_dmg_wrapper = main_content.find('div', {'class': 'skilldmgwrapper'}).find_all('tr')
|
||||
|
||||
# 命之座
|
||||
constellations_title = main_content.find('span', {'class': 'item_secondary_title'}, string="Constellations")
|
||||
constellations_table = constellations_title.findNext('table', {'class': 'item_main_table'}).find_all('tr')
|
||||
constellations_list = []
|
||||
constellations_list_index = 0
|
||||
for index, value in enumerate(constellations_table):
|
||||
# 判断第一行
|
||||
if index % 2 == 0:
|
||||
constellations_dict = {
|
||||
"icon": "",
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
constellations_list.append(constellations_dict)
|
||||
icon_url = self.ROOT_URL + value.find_all('img', {'class': 'itempic'})[-1]['data-src']
|
||||
constellations_name = value.find_all('a', href=re.compile("/db/skill"))[-1].text
|
||||
constellations_list[constellations_list_index]["icon"] = icon_url
|
||||
constellations_list[constellations_list_index]["name"] = constellations_name
|
||||
if index % 2 == 1:
|
||||
constellations_description = value.find('div', {'class': 'skill_desc_layout'}).text
|
||||
constellations_list[constellations_list_index]["description"] = constellations_description
|
||||
constellations_list_index += 1
|
||||
|
||||
characters_info_dict["constellations"] = constellations_list
|
||||
|
||||
# 技能
|
||||
skills_title = main_content.find('span', string='Attack Talents')
|
||||
|
||||
# 普攻
|
||||
normal_attack_area = skills_title.find_next_sibling()
|
||||
normal_attack_info = normal_attack_area.find_all('tr')
|
||||
normal_attack_icon = self.ROOT_URL + normal_attack_info[0].find('img', {'class': 'itempic'})['data-src']
|
||||
normal_attack_name = normal_attack_info[0].find('a', href=re.compile('/db/skill/')).text
|
||||
normal_attack_desc = normal_attack_info[1].find('div', {'class': 'skill_desc_layout'}).text.replace(" ", "\n")
|
||||
normal_attack = characters_info_dict["skills"]["normal_attack"]
|
||||
normal_attack["icon"] = normal_attack_icon
|
||||
normal_attack["name"] = normal_attack_name
|
||||
normal_attack["description"] = normal_attack_desc
|
||||
|
||||
normal_attack_table_area = normal_attack_area.find_next_sibling()
|
||||
# normal_attack_table = normal_attack_table_area.find_all('tr')
|
||||
|
||||
skill_e_area = normal_attack_table_area.find_next_sibling()
|
||||
skill_e_info = skill_e_area.find_all('tr')
|
||||
skill_e_icon = self.ROOT_URL + skill_e_info[0].find('img', {'class': 'itempic'})['data-src']
|
||||
skill_e_name = skill_e_info[0].find('a', href=re.compile('/db/skill/')).text
|
||||
skill_e_desc = skill_e_info[1].find('div', {'class': 'skill_desc_layout'}).text.replace(" ", "\n")
|
||||
skill_e = characters_info_dict["skills"]["skill_e"]
|
||||
skill_e["icon"] = skill_e_icon
|
||||
skill_e["name"] = skill_e_name
|
||||
skill_e["description"] = skill_e_desc
|
||||
|
||||
skill_e_table_area = skill_e_area.find_next_sibling()
|
||||
# skillE_table = skillE_table_area.find_all('tr')
|
||||
|
||||
load_another_talent_q: bool = False
|
||||
if char_name in ("神里绫华", "莫娜"):
|
||||
load_another_talent_q = True
|
||||
|
||||
skill_q_area = skill_e_table_area.find_next_sibling()
|
||||
skill_q_info = skill_q_area.find_all('tr')
|
||||
skill_q_icon = self.ROOT_URL + skill_q_info[0].find('img', {'class': 'itempic'})['data-src']
|
||||
skill_q_name = skill_q_info[0].find('a', href=re.compile('/db/skill/')).text
|
||||
skill_q_desc = skill_q_info[1].find('div', {'class': 'skill_desc_layout'}).text.replace(" ", "\n")
|
||||
skill_q_table_area = skill_q_area.find_next_sibling()
|
||||
# skill_q_table = skill_q_table_area.find_all('tr')
|
||||
|
||||
if load_another_talent_q:
|
||||
skill_replace = characters_info_dict["skills"]["skill_replace"]
|
||||
skill_replace["icon"] = skill_q_icon
|
||||
skill_replace["name"] = skill_q_name
|
||||
skill_replace["description"] = skill_q_desc
|
||||
else:
|
||||
skill_q = characters_info_dict["skills"]["skill_q"]
|
||||
skill_q["icon"] = skill_q_icon
|
||||
skill_q["name"] = skill_q_name
|
||||
skill_q["description"] = skill_q_desc
|
||||
|
||||
if load_another_talent_q:
|
||||
skill_q2_area = skill_q_table_area.find_next_sibling()
|
||||
skill_q2_info = skill_q2_area.find_all('tr')
|
||||
skill_q2_icon = self.ROOT_URL + skill_q2_info[0].find('img', {'class': 'itempic'})['data-src']
|
||||
skill_q2_name = skill_q2_info[0].find('a', href=re.compile('/db/skill/')).text
|
||||
skill_q2_desc = skill_q2_info[1].find('div', {'class': 'skill_desc_layout'}).text.replace(" ", "\n")
|
||||
skill_q2 = characters_info_dict["skills"]["skill_q"]
|
||||
skill_q2["icon"] = skill_q2_icon
|
||||
skill_q2["name"] = skill_q2_name
|
||||
skill_q2["description"] = skill_q2_desc
|
||||
|
||||
# 角色图片
|
||||
char_pic_area = main_content.find('span', string='Character Gallery').find_next_sibling()
|
||||
all_char_pic = char_pic_area.find("div", {"class": "gallery_cont"})
|
||||
|
||||
gacha_splash_text = all_char_pic.find("span", {"class": "gallery_cont_span"}, string="Gacha Splash")
|
||||
gacha_splash_pic_url = self.ROOT_URL + gacha_splash_text.previous_element.previous_element["data-src"].replace(
|
||||
"_70", "")
|
||||
characters_info_dict["gacha_splash"] = gacha_splash_pic_url
|
||||
|
||||
return characters_info_dict
|
27
model/wiki/helpers.py
Normal file
@ -0,0 +1,27 @@
|
||||
import re
|
||||
|
||||
ID_RGX = re.compile(r"/db/[^.]+_(?P<id>\d+)")
|
||||
|
||||
|
||||
def get_headers():
|
||||
headers = {
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;"
|
||||
"q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36",
|
||||
"referer": "https://genshin.honeyhunterworld.com/db/char/hutao/?lang=CHS",
|
||||
}
|
||||
return headers
|
||||
|
||||
|
||||
def get_id_form_url(url: str):
|
||||
matches = ID_RGX.search(url)
|
||||
if matches is None:
|
||||
return -1
|
||||
entries = matches.groupdict()
|
||||
if entries is None:
|
||||
return -1
|
||||
try:
|
||||
return int(entries.get('id'))
|
||||
except (IndexError, ValueError, TypeError):
|
||||
return -1
|
85
model/wiki/metadata/ascension.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"504": {
|
||||
"city": "Mondstadt",
|
||||
"name": "高塔孤王",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_504.png"
|
||||
},
|
||||
"524": {
|
||||
"city": "Mondstadt",
|
||||
"name": "凛风奔狼",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_524.png"
|
||||
},
|
||||
"544": {
|
||||
"city": "Mondstadt",
|
||||
"name": "狮牙斗士",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_544.png"
|
||||
},
|
||||
"514": {
|
||||
"city": "Liyue",
|
||||
"name": "孤云寒林",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"Key": "Weapon_Guyun",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_514.png"
|
||||
},
|
||||
"534": {
|
||||
"city": "Liyue",
|
||||
"name": "雾海云间",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_534.png"
|
||||
},
|
||||
"554": {
|
||||
"city": "Liyue",
|
||||
"name": "漆黑陨铁",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_554.png"
|
||||
},
|
||||
"564": {
|
||||
"city": "Inazuma",
|
||||
"name": "远海夷地",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"Key": "Weapon_DistantSea",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_564.png"
|
||||
},
|
||||
"574": {
|
||||
"city": "Inazuma",
|
||||
"name": "鸣神御灵",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_574.png"
|
||||
},
|
||||
"584": {
|
||||
"city": "Inazuma",
|
||||
"name": "今昔剧画",
|
||||
"star": {
|
||||
"value": 5,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
|
||||
},
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_584.png"
|
||||
}
|
||||
}
|
92
model/wiki/metadata/elite.json
Normal file
@ -0,0 +1,92 @@
|
||||
{
|
||||
"63": {
|
||||
"name": "号角",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_Horn",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_63.png"
|
||||
},
|
||||
"73": {
|
||||
"name": "地脉",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_LeyLine",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_73.png"
|
||||
},
|
||||
"83": {
|
||||
"name": "混沌",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_Chaos",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_83.png"
|
||||
},
|
||||
"93": {
|
||||
"name": "雾虚",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_MistGrass",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_93.png"
|
||||
},
|
||||
"103": {
|
||||
"name": "祭刀",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_SacrificialKnife",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_103.png"
|
||||
},
|
||||
"143": {
|
||||
"name": "骨片",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_BoneShard",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_143.png"
|
||||
},
|
||||
"153": {
|
||||
"name": "刻像",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_Statuette",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_153.png"
|
||||
},
|
||||
"173": {
|
||||
"name": "混沌2",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_Chaos2",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_173.png"
|
||||
},
|
||||
"176": {
|
||||
"name": "隐兽",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_Concealed",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_176.png"
|
||||
},
|
||||
"183": {
|
||||
"name": "棱镜",
|
||||
"star": {
|
||||
"value": 4,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
|
||||
},
|
||||
"key": "Elite_Prism",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_183.png"
|
||||
}
|
||||
}
|
83
model/wiki/metadata/monster.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"23": {
|
||||
"name": "史莱姆",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"key": "Monster_Slime",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_23.png"
|
||||
},
|
||||
"33": {
|
||||
"name": "面具",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"Key": "Monster_Mask",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_33.png"
|
||||
},
|
||||
"43": {
|
||||
"name": "绘卷",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"Key": "Monster_Scroll",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_43.png"
|
||||
},
|
||||
"53": {
|
||||
"name": "箭簇",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"Key": "Monster_Scroll",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_43.png"
|
||||
},
|
||||
"113": {
|
||||
"name": "徽记",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"Key": "Monster_Insignia",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_113.png"
|
||||
},
|
||||
"123": {
|
||||
"name": "鸦印",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"Key": "Monster_RavenInsignia",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_123.png"
|
||||
},
|
||||
"133": {
|
||||
"name": "花蜜",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"Key": "Monster_Nectar",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_133.png"
|
||||
},
|
||||
"163": {
|
||||
"name": "刀镡",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"Key": "Monster_Handguard",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_163.png"
|
||||
},
|
||||
"187": {
|
||||
"name": "浮游",
|
||||
"star": {
|
||||
"value": 3,
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
|
||||
},
|
||||
"Key": "Monster_Spectral",
|
||||
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_187.png"
|
||||
}
|
||||
}
|
210
model/wiki/weapons.py
Normal file
@ -0,0 +1,210 @@
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
import ujson
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from .helpers import get_headers, get_id_form_url
|
||||
|
||||
|
||||
class WeaponType(Enum):
|
||||
Sword = "sword" # 单手剑
|
||||
Claymore = "claymore" # 双手剑
|
||||
PoleArm = "polearm" # 长柄武器
|
||||
Bow = "bow" # 弓
|
||||
Catalyst = "catalyst" # 法器
|
||||
|
||||
|
||||
class Weapons:
|
||||
IGNORE_WEAPONS_ID = [
|
||||
"1001", "1101", "1406",
|
||||
"2001", "2101", "2204", "2406", "2407",
|
||||
"3001", "3101", "3204", "3404",
|
||||
"4001", "4101", "4201", "4403", "4405", "4406",
|
||||
"5001", "5101", "5201", "5404", "5404", "5405",
|
||||
] # 忽略的武器包括一星、二星武器,beta表格内无名武器,未上架到正服的武器
|
||||
|
||||
# 根地址
|
||||
ROOT_URL = "https://genshin.honeyhunterworld.com"
|
||||
|
||||
TEXT_MAPPING = {
|
||||
"Type": "类型",
|
||||
"Rarity": "Rarity",
|
||||
"Base Attack": "基础攻击力"
|
||||
}
|
||||
|
||||
WEAPON_TYPE_MAPPING = {
|
||||
"Sword": "https://genshin.honeyhunterworld.com/img/skills/s_33101.png", # 单手剑
|
||||
"Claymore": "https://genshin.honeyhunterworld.com/img/skills/s_163101.png", # 双手剑
|
||||
"Polearm": "https://genshin.honeyhunterworld.com/img/skills/s_233101.png", # 长枪
|
||||
"Bow": "https://genshin.honeyhunterworld.com/img/skills/s_213101.png", # 弓箭
|
||||
"Catalyst": "https://genshin.honeyhunterworld.com/img/skills/s_43101.png", # 法器
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.client = httpx.AsyncClient(headers=get_headers())
|
||||
project_path = os.path.dirname(__file__)
|
||||
characters_file = os.path.join(project_path, "metadata", "ascension.json")
|
||||
monster_file = os.path.join(project_path, "metadata", "monster.json")
|
||||
elite_file = os.path.join(project_path, "metadata", "elite.json")
|
||||
with open(characters_file, "r", encoding="utf-8") as f:
|
||||
self._ascension_json: dict = ujson.load(f)
|
||||
with open(monster_file, "r", encoding="utf-8") as f:
|
||||
self._monster_json: dict = ujson.load(f)
|
||||
with open(elite_file, "r", encoding="utf-8") as f:
|
||||
self._elite_json: dict = ujson.load(f)
|
||||
|
||||
async def _get_soup(self, url: str) -> Optional[BeautifulSoup]:
|
||||
request = await self.client.get(url)
|
||||
return BeautifulSoup(request.text, "lxml")
|
||||
|
||||
async def get_weapon_url_list(self, weapon_type: WeaponType):
|
||||
weapon_url_list = []
|
||||
url = self.ROOT_URL + f"/db/weapon/{weapon_type.value}/?lang=CHS"
|
||||
soup = await self._get_soup(url)
|
||||
weapon_table = soup.find("span", {"class": "item_secondary_title"},
|
||||
string="Released (Codex) Weapons").find_next_sibling()
|
||||
weapon_table_rows = weapon_table.find_all("tr")
|
||||
for weapon_table_row in weapon_table_rows:
|
||||
content = weapon_table_row.find_all("td")[2]
|
||||
if content.find("a") is not None:
|
||||
weapon_url = self.ROOT_URL + content.find("a")["href"]
|
||||
weapon_id = str(get_id_form_url(weapon_url))
|
||||
if weapon_id not in self.IGNORE_WEAPONS_ID:
|
||||
weapon_url_list.append(weapon_url)
|
||||
return weapon_url_list
|
||||
|
||||
async def get_all_weapon_url(self):
|
||||
all_weapon_url = []
|
||||
temp_data = await self.get_weapon_url_list(WeaponType.Bow)
|
||||
all_weapon_url.extend(temp_data)
|
||||
temp_data = await self.get_weapon_url_list(WeaponType.Sword)
|
||||
all_weapon_url.extend(temp_data)
|
||||
temp_data = await self.get_weapon_url_list(WeaponType.PoleArm)
|
||||
all_weapon_url.extend(temp_data)
|
||||
temp_data = await self.get_weapon_url_list(WeaponType.Catalyst)
|
||||
all_weapon_url.extend(temp_data)
|
||||
temp_data = await self.get_weapon_url_list(WeaponType.Claymore)
|
||||
all_weapon_url.extend(temp_data)
|
||||
return all_weapon_url
|
||||
|
||||
@staticmethod
|
||||
def get_weapon_info_template():
|
||||
weapon_info_dict = {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"source_img": "",
|
||||
"atk":
|
||||
{
|
||||
"min": 0,
|
||||
"max": 999999,
|
||||
"name": "基础攻击力"
|
||||
},
|
||||
"secondary":
|
||||
{
|
||||
"min": 0.1,
|
||||
"max": 999999.9,
|
||||
"name": ""
|
||||
},
|
||||
"star":
|
||||
{
|
||||
"value": -1,
|
||||
"icon": ""
|
||||
},
|
||||
"type":
|
||||
{
|
||||
"name": "",
|
||||
"icon": ""
|
||||
},
|
||||
"passive_ability":
|
||||
{
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
materials_dict = {
|
||||
"name": "",
|
||||
"star": {
|
||||
"value": 0,
|
||||
"icon": ""
|
||||
},
|
||||
"city": "",
|
||||
"icon": ""
|
||||
}
|
||||
weapon_info_dict["materials"] = {
|
||||
"ascension": materials_dict,
|
||||
"elite": materials_dict,
|
||||
"monster": materials_dict,
|
||||
}
|
||||
return weapon_info_dict
|
||||
|
||||
async def get_weapon_info(self, url: str):
|
||||
weapon_info_dict = self.get_weapon_info_template()
|
||||
soup = await self._get_soup(url)
|
||||
weapon_content = soup.find("div", {"class": "wrappercont"})
|
||||
data = weapon_content.find("div", {"class": "data_cont_wrapper", "style": "display: block"})
|
||||
weapon_info = data.find("table", {"class": "item_main_table"})
|
||||
weapon_name = weapon_content.find("div", {"class": "custom_title"}).text.replace("-", "").replace(" ", "")
|
||||
weapon_info_dict["name"] = weapon_name
|
||||
weapon_info_row = weapon_info.find_all("tr")
|
||||
for weapon_info_ in weapon_info_row:
|
||||
content = weapon_info_.find_all("td")
|
||||
if len(content) == 3: # 第一行会有三个td,其中一个td是武器图片
|
||||
weapon_info_dict["source_img"] = self.ROOT_URL + content[0].find("img",
|
||||
{"class": "itempic lazy"})["data-src"]
|
||||
weapon_info_dict["type"]["name"] = content[2].text
|
||||
weapon_info_dict["type"]["icon"] = self.get_weapon_type(content[2].text)
|
||||
elif len(content) == 2:
|
||||
if content[0].text == "Rarity":
|
||||
weapon_info_dict["star"]["value"] = len(
|
||||
content[1].find_all("div", {"class": "sea_char_stars_wrap"}))
|
||||
elif content[0].text == "Special (passive) Ability":
|
||||
weapon_info_dict["passive_ability"]["name"] = content[1].text
|
||||
elif content[0].text == "Special (passive) Ability Description":
|
||||
weapon_info_dict["passive_ability"]["description"] = content[1].text
|
||||
elif content[0].text == "In-game Description":
|
||||
weapon_info_dict["description"] = content[1].text
|
||||
elif content[0].text == "Secondary Stat":
|
||||
weapon_info_dict["secondary"]["name"] = content[1].text
|
||||
|
||||
stat_table = data.find("span", {"class": "item_secondary_title"},
|
||||
string=" Stat Progression ").find_next_sibling()
|
||||
stat_table_row = stat_table.find_all("tr")
|
||||
for stat_table_ in stat_table_row:
|
||||
content = stat_table_.find_all("td")
|
||||
# 通过等级判断
|
||||
if content[0].text == "1":
|
||||
weapon_info_dict["atk"]["min"] = int(content[1].text)
|
||||
weapon_info_dict["secondary"]["min"] = float(content[2].text)
|
||||
elif content[0].text == "80+":
|
||||
item_hrefs = content[3].find_all("a")
|
||||
for item_href in item_hrefs:
|
||||
item_id = get_id_form_url(item_href["href"])
|
||||
ascension = self.get_ascension(str(item_id))
|
||||
if ascension.get("name") is not None:
|
||||
weapon_info_dict["materials"]["ascension"] = ascension
|
||||
monster = self.get_monster(str(item_id))
|
||||
if monster.get("name") is not None:
|
||||
weapon_info_dict["materials"]["monster"] = monster
|
||||
elite = self.get_elite(str(item_id))
|
||||
if elite.get("name") is not None:
|
||||
weapon_info_dict["materials"]["elite"] = elite
|
||||
elif content[0].text == "90":
|
||||
weapon_info_dict["atk"]["max"] = int(content[1].text)
|
||||
weapon_info_dict["secondary"]["max"] = float(content[2].text)
|
||||
|
||||
return weapon_info_dict
|
||||
|
||||
def get_ascension(self, item_id: str):
|
||||
return self._ascension_json.get(item_id, {})
|
||||
|
||||
def get_monster(self, item_id: str):
|
||||
return self._monster_json.get(item_id, {})
|
||||
|
||||
def get_elite(self, item_id: str):
|
||||
return self._elite_json.get(item_id, {})
|
||||
|
||||
def get_weapon_type(self, weapon_type: str):
|
||||
return self.WEAPON_TYPE_MAPPING.get(weapon_type, "")
|
52
plugins/README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# plugins 目录
|
||||
|
||||
## 说明
|
||||
|
||||
该目录仅限处理交互层和业务层数据交换的任务
|
||||
|
||||
如有任何新业务接口,请转到 `service` 目录添加
|
||||
|
||||
如有任何API请求接口,请转到 `model` 目录添加
|
||||
|
||||
## 基础代码
|
||||
|
||||
``` python
|
||||
from telegram.ext import CommandHandler, CallbackContext
|
||||
|
||||
from logger import Log
|
||||
from utils.decorators.error import error_callable
|
||||
from utils.decorators.restricts import restricts
|
||||
from utils.plugins.manager import listener_plugins_class
|
||||
|
||||
@listener_plugins_class()
|
||||
class Example:
|
||||
|
||||
@classmethod
|
||||
def create_handlers(cls):
|
||||
example = cls()
|
||||
return [CommandHandler('example', example.command_start)]
|
||||
|
||||
@error_callable
|
||||
@restricts()
|
||||
async def command_start(self, update: Update, context: CallbackContext) -> None:
|
||||
user = update.effective_user
|
||||
Log.info(f"用户 {user.full_name}[{user.id}] 发出example命令")
|
||||
await message.reply_text("Example")
|
||||
|
||||
```
|
||||
|
||||
### 注意
|
||||
|
||||
plugins 模块下的类必须提供 `create_handlers` 类方法作为构建相应处理程序给 `handle.py`
|
||||
|
||||
在函数注册为命令处理过程(如 `CommandHandler` )需要添加 `error_callable` 修饰器作为错误统一处理
|
||||
|
||||
如果套引用服务,参数需要声明需要引用服务的类型,并且添加 `inject` 修饰器
|
||||
|
||||
必要的函数必须捕获异常后通知用户或者直接抛出异常
|
||||
|
||||
入口函数必须使用 `@restricts()` 修饰器 预防洪水攻击
|
||||
|
||||
只需在构建的类前加上 `@listener_plugins_class()` 修饰器即可向程序注册插件
|
||||
|
||||
**注意:`@restricts()` 修饰器带参,必须带括号,否则会出现调用错误**
|
112
plugins/abyss.py
Normal file
@ -0,0 +1,112 @@
|
||||
from genshin import Client
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import CommandHandler, MessageHandler, filters, CallbackContext
|
||||
|
||||
from app.cookies.service import CookiesService
|
||||
from app.template.service import TemplateService
|
||||
from app.user import UserService
|
||||
from app.user.repositories import UserNotFoundError
|
||||
from logger import Log
|
||||
from plugins.base import BasePlugins
|
||||
from utils.app.inject import inject
|
||||
from utils.helpers import get_genshin_client, url_to_file
|
||||
from utils.plugins.manager import listener_plugins_class
|
||||
|
||||
|
||||
@listener_plugins_class()
|
||||
class Abyss(BasePlugins):
|
||||
|
||||
@classmethod
|
||||
def create_handlers(cls) -> list:
|
||||
abyss = cls()
|
||||
return [
|
||||
CommandHandler("abyss", abyss.command_start, block=False),
|
||||
MessageHandler(filters.Regex(r"^深渊数据查询(.*)"), abyss.command_start, block=True)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _get_role_star_bg(value: int):
|
||||
if value == 4:
|
||||
return "./background/roleStarBg4.png"
|
||||
elif value == 5:
|
||||
return "./background/roleStarBg5.png"
|
||||
else:
|
||||
raise ValueError("错误的数据")
|
||||
|
||||
async def _get_abyss_data(self, client: Client) -> dict:
|
||||
uid = client.uid
|
||||
spiral_abyss_info = await client.get_spiral_abyss(uid)
|
||||
if not spiral_abyss_info.unlocked:
|
||||
raise ValueError("unlocked is false")
|
||||
ranks = spiral_abyss_info.ranks
|
||||
if len(spiral_abyss_info.ranks.most_kills) == 0:
|
||||
raise ValueError("本次深渊旅行者还没挑战呢")
|
||||
abyss_data = {
|
||||
"uid": uid,
|
||||
"max_floor": spiral_abyss_info.max_floor,
|
||||
"total_battles": spiral_abyss_info.total_battles,
|
||||
"total_stars": spiral_abyss_info.total_stars,
|
||||
"most_played_list": [],
|
||||
"most_kills": {
|
||||
"icon": await url_to_file(ranks.most_kills[0].icon),
|
||||
"value": ranks.most_kills[0].value,
|
||||
},
|
||||
"strongest_strike": {
|
||||
"icon": await url_to_file(ranks.strongest_strike[0].icon),
|
||||
"value": ranks.strongest_strike[0].value
|
||||
},
|
||||
"most_damage_taken": {
|
||||
"icon": await url_to_file(ranks.most_damage_taken[0].icon),
|
||||
"value": ranks.most_damage_taken[0].value
|
||||
},
|
||||
"most_bursts_used": {
|
||||
"icon": await url_to_file(ranks.most_bursts_used[0].icon),
|
||||
"value": ranks.most_bursts_used[0].value
|
||||
},
|
||||
"most_skills_used": {
|
||||
"icon": await url_to_file(ranks.most_skills_used[0].icon),
|
||||
"value": ranks.most_skills_used[0].value
|
||||
}
|
||||
}
|
||||
# most_kills
|
||||
most_played_list = ranks.most_played
|
||||
for most_played in most_played_list:
|
||||
temp = {
|
||||
"icon": await url_to_file(most_played.icon),
|
||||
"value": most_played.value,
|
||||
"background": self._get_role_star_bg(most_played.rarity)
|
||||
}
|
||||
abyss_data["most_played_list"].append(temp)
|
||||
return abyss_data
|
||||
|
||||
@inject
|
||||
async def command_start(self, update: Update, context: CallbackContext, user_service: UserService,
|
||||
cookies_service: CookiesService, template_service: TemplateService) -> None:
|
||||
user = update.effective_user
|
||||
message = update.message
|
||||
Log.info(f"用户 {user.full_name}[{user.id}] 查深渊挑战命令请求")
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
try:
|
||||
client = await get_genshin_client(user.id, user_service, cookies_service)
|
||||
abyss_data = await self._get_abyss_data(client)
|
||||
except UserNotFoundError:
|
||||
reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号")
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30)
|
||||
self._add_delete_message_job(context, message.chat_id, message.message_id, 30)
|
||||
return
|
||||
except ValueError as exc:
|
||||
if "unlocked is false" in str(exc):
|
||||
await message.reply_text("本次深渊旅行者还没挑战呢,咕咕咕~~~")
|
||||
return
|
||||
if "本次深渊旅行者还没挑战呢" in str(exc):
|
||||
await message.reply_text("本次深渊旅行者还没挑战呢,咕咕咕~~~")
|
||||
return
|
||||
raise exc
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||
png_data = await template_service.render('genshin/abyss', "abyss.html", abyss_data,
|
||||
{"width": 690, "height": 504}, full_page=False)
|
||||
await message.reply_photo(png_data, filename=f"abyss_{user.id}.png",
|
||||
allow_sending_without_reply=True)
|
||||
return
|
80
plugins/admin.py
Normal file
@ -0,0 +1,80 @@
|
||||
from telegram import Update
|
||||
from telegram.error import BadRequest, Forbidden
|
||||
from telegram.ext import CallbackContext, CommandHandler
|
||||
|
||||
from app.admin import BotAdminService
|
||||
from logger import Log
|
||||
from utils.app.inject import inject
|
||||
from utils.decorators.admins import bot_admins_rights_check
|
||||
from utils.plugins.manager import listener_plugins_class
|
||||
|
||||
|
||||
@listener_plugins_class()
|
||||
class Admin:
|
||||
"""有关BOT ADMIN处理"""
|
||||
|
||||
@classmethod
|
||||
def create_handlers(cls) -> list:
|
||||
admin = cls()
|
||||
return [
|
||||
CommandHandler("add_admin", admin.add_admin, block=False),
|
||||
CommandHandler("del_admin", admin.del_admin, block=False),
|
||||
CommandHandler("leave_chat", admin.leave_chat, block=False),
|
||||
]
|
||||
|
||||
@bot_admins_rights_check
|
||||
@inject
|
||||
async def add_admin(self, update: Update, _: CallbackContext, bot_admin_service: BotAdminService):
|
||||
message = update.message
|
||||
reply_to_message = message.reply_to_message
|
||||
if reply_to_message is None:
|
||||
await message.reply_text("请回复对应消息")
|
||||
else:
|
||||
admin_list = await bot_admin_service.get_admin_list()
|
||||
if reply_to_message.from_user.id in admin_list:
|
||||
await message.reply_text("该用户已经存在管理员列表")
|
||||
else:
|
||||
await bot_admin_service.add_admin(reply_to_message.from_user.id)
|
||||
await message.reply_text("添加成功")
|
||||
|
||||
@bot_admins_rights_check
|
||||
@inject
|
||||
async def del_admin(self, update: Update, _: CallbackContext, bot_admin_service: BotAdminService):
|
||||
message = update.message
|
||||
reply_to_message = message.reply_to_message
|
||||
admin_list = await bot_admin_service.get_admin_list()
|
||||
if reply_to_message is None:
|
||||
await message.reply_text("请回复对应消息")
|
||||
else:
|
||||
if reply_to_message.from_user.id in admin_list:
|
||||
await bot_admin_service.delete_admin(reply_to_message.from_user.id)
|
||||
await message.reply_text("删除成功")
|
||||
else:
|
||||
await message.reply_text("该用户不存在管理员列表")
|
||||
|
||||
@bot_admins_rights_check
|
||||
async def leave_chat(self, update: Update, context: CallbackContext):
|
||||
message = update.message
|
||||
try:
|
||||
args = message.text.split()
|
||||
if len(args) >= 2:
|
||||
chat_id = int(args[1])
|
||||
else:
|
||||
await message.reply_text("输入错误")
|
||||
return
|
||||
except ValueError as error:
|
||||
Log.error("获取 chat_id 发生错误! 错误信息为 \n", error)
|
||||
await message.reply_text("输入错误")
|
||||
return
|
||||
try:
|
||||
try:
|
||||
char = await context.bot.get_chat(chat_id)
|
||||
await message.reply_text(f"正在尝试退出群 {char.title}[{char.id}]")
|
||||
except (BadRequest, Forbidden):
|
||||
pass
|
||||
await context.bot.leave_chat(chat_id)
|
||||
except (BadRequest, Forbidden) as error:
|
||||
Log.error(f"退出 chat_id[{chat_id}] 发生错误! 错误信息为 \n", error)
|
||||
await message.reply_text(f"退出 chat_id[{chat_id}] 发生错误! 错误信息为 {str(error)}")
|
||||
return
|
||||
await message.reply_text(f"退出 chat_id[{chat_id}] 成功!")
|
83
plugins/base.py
Normal file
@ -0,0 +1,83 @@
|
||||
import datetime
|
||||
from typing import Callable
|
||||
|
||||
from telegram import Update, ReplyKeyboardRemove
|
||||
from telegram.error import BadRequest
|
||||
from telegram.ext import CallbackContext, ConversationHandler, filters
|
||||
|
||||
from app.admin import BotAdminService
|
||||
from logger import Log
|
||||
from utils.app.inject import inject
|
||||
|
||||
|
||||
async def clean_message(context: CallbackContext, chat_id: int, message_id: int) -> bool:
|
||||
try:
|
||||
await context.bot.delete_message(chat_id=chat_id, message_id=message_id)
|
||||
return True
|
||||
except BadRequest as error:
|
||||
if "not found" in str(error):
|
||||
Log.warning(f"定时删除消息 chat_id[{chat_id}] message_id[{message_id}]失败 消息不存在")
|
||||
elif "Message can't be deleted" in str(error):
|
||||
Log.warning(f"定时删除消息 chat_id[{chat_id}] message_id[{message_id}]失败 消息无法删除 可能是没有授权")
|
||||
else:
|
||||
Log.warning(f"定时删除消息 chat_id[{chat_id}] message_id[{message_id}]失败 \n", error)
|
||||
return False
|
||||
|
||||
|
||||
def add_delete_message_job(context: CallbackContext, chat_id: int, message_id: int,
|
||||
delete_seconds: int = 60):
|
||||
context.job_queue.scheduler.add_job(clean_message, "date",
|
||||
id=f"{chat_id}|{message_id}|auto_clean_message",
|
||||
name=f"{chat_id}|{message_id}|auto_clean_message",
|
||||
args=[context, chat_id, message_id],
|
||||
run_date=context.job_queue._tz_now() + datetime.timedelta(
|
||||
seconds=delete_seconds), replace_existing=True)
|
||||
|
||||
|
||||
class BasePlugins:
|
||||
|
||||
@staticmethod
|
||||
async def cancel(update: Update, _: CallbackContext) -> int:
|
||||
await update.message.reply_text("退出命令", reply_markup=ReplyKeyboardRemove())
|
||||
return ConversationHandler.END
|
||||
|
||||
@staticmethod
|
||||
async def _clean(context: CallbackContext, chat_id: int, message_id: int) -> bool:
|
||||
return await clean_message(context, chat_id, message_id)
|
||||
|
||||
@staticmethod
|
||||
def _add_delete_message_job(context: CallbackContext, chat_id: int, message_id: int,
|
||||
delete_seconds: int = 60):
|
||||
return add_delete_message_job(context, chat_id, message_id, delete_seconds)
|
||||
|
||||
|
||||
class NewChatMembersHandler:
|
||||
def __init__(self, auth_callback: Callable):
|
||||
self.auth_callback = auth_callback
|
||||
|
||||
@inject
|
||||
async def new_member(self, update: Update, context: CallbackContext, bot_admin_service: BotAdminService) -> None:
|
||||
message = update.message
|
||||
chat = message.chat
|
||||
from_user = message.from_user
|
||||
quit_status = False
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
for user in message.new_chat_members:
|
||||
if user.id == context.bot.id:
|
||||
if from_user is not None:
|
||||
Log.info(f"用户 {from_user.full_name}[{from_user.id}] 在群 {chat.title}[{chat.id}] 邀请BOT")
|
||||
admin_list = await bot_admin_service.get_admin_list()
|
||||
if from_user.id in admin_list:
|
||||
await context.bot.send_message(message.chat_id,
|
||||
'感谢邀请小派蒙到本群!请使用 /help 查看咱已经学会的功能。')
|
||||
else:
|
||||
quit_status = True
|
||||
else:
|
||||
Log.info(f"未知用户 在群 {chat.title}[{chat.id}] 邀请BOT")
|
||||
quit_status = True
|
||||
if quit_status:
|
||||
Log.warning("不是管理员邀请!退出群聊。")
|
||||
await context.bot.send_message(message.chat_id, "派蒙不想进去!不是旅行者的邀请!")
|
||||
await context.bot.leave_chat(chat.id)
|
||||
await self.auth_callback(update, context)
|
||||
|
88
plugins/errorhandler.py
Normal file
@ -0,0 +1,88 @@
|
||||
import html
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
import ujson
|
||||
from telegram import Update, ReplyKeyboardRemove, Message
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.error import BadRequest
|
||||
from telegram.ext import CallbackContext
|
||||
|
||||
from config import config
|
||||
from logger import Log
|
||||
|
||||
try:
|
||||
notice_chat_id = config.TELEGRAM["notice"]["ERROR"]["chat_id"]
|
||||
except KeyError as error:
|
||||
Log.warning("错误通知Chat_id获取失败或未配置,BOT发生致命错误时不会收到通知 错误信息为\n", error)
|
||||
notice_chat_id = None
|
||||
|
||||
|
||||
async def error_handler(update: object, context: CallbackContext) -> None:
|
||||
"""
|
||||
记录错误并发送消息通知开发人员。
|
||||
Log the error and send a telegram message to notify the developer.
|
||||
"""
|
||||
Log.error(msg="处理函数时发生异常:", exc_info=context.error)
|
||||
|
||||
if notice_chat_id is None:
|
||||
return
|
||||
|
||||
tb_list = traceback.format_exception(None, context.error, context.error.__traceback__)
|
||||
tb_string = ''.join(tb_list)
|
||||
|
||||
update_str = update.to_dict() if isinstance(update, Update) else str(update)
|
||||
text_1 = (
|
||||
f'<b>处理函数时发生异常</b> \n'
|
||||
f'Exception while handling an update \n'
|
||||
f'<pre>update = {html.escape(ujson.dumps(update_str, indent=2, ensure_ascii=False))}'
|
||||
'</pre>\n\n'
|
||||
f'<pre>context.chat_data = {html.escape(str(context.chat_data))}</pre>\n\n'
|
||||
f'<pre>context.user_data = {html.escape(str(context.user_data))}</pre>\n\n'
|
||||
)
|
||||
text_2 = (
|
||||
f'<pre>{html.escape(tb_string)}</pre>'
|
||||
)
|
||||
try:
|
||||
if 'make sure that only one bot instance is running' in tb_string:
|
||||
Log.error("其他机器人在运行,请停止!")
|
||||
return
|
||||
await context.bot.send_message(notice_chat_id, text_1, parse_mode=ParseMode.HTML)
|
||||
await context.bot.send_message(notice_chat_id, text_2, parse_mode=ParseMode.HTML)
|
||||
except BadRequest as exc:
|
||||
if 'too long' in str(exc):
|
||||
text = (
|
||||
f'<b>处理函数时发生异常,traceback太长导致无法发送,但已写入日志</b> \n'
|
||||
f'<code>{html.escape(str(context.error))}</code>'
|
||||
)
|
||||
try:
|
||||
await context.bot.send_message(notice_chat_id, text, parse_mode=ParseMode.HTML)
|
||||
except BadRequest:
|
||||
text = (
|
||||
'<b>处理函数时发生异常,traceback太长导致无法发送,但已写入日志</b> \n')
|
||||
try:
|
||||
await context.bot.send_message(notice_chat_id, text, parse_mode=ParseMode.HTML)
|
||||
except BadRequest as exc_1:
|
||||
Log.error("处理函数时发生异常", exc_1)
|
||||
effective_user = update.effective_user
|
||||
try:
|
||||
message: Optional[Message] = None
|
||||
if update.callback_query is not None:
|
||||
message = update.callback_query.message
|
||||
if update.message is not None:
|
||||
message = update.message
|
||||
if update.edited_message is not None:
|
||||
message = update.edited_message
|
||||
if message is not None:
|
||||
chat = message.chat
|
||||
Log.info(f"尝试通知用户 {effective_user.full_name}[{effective_user.id}] "
|
||||
f"在 {chat.full_name}[{chat.id}]"
|
||||
f"的 update_id[{update.update_id}] 错误信息")
|
||||
text = f"派蒙这边发生了点问题无法处理!\n" \
|
||||
f"如果当前有对话请发送 /cancel 退出对话。\n" \
|
||||
f"错误信息为 <code>{html.escape(str(context.error))}</code>"
|
||||
await context.bot.send_message(message.chat_id, text, reply_markup=ReplyKeyboardRemove(),
|
||||
parse_mode=ParseMode.HTML)
|
||||
except BadRequest as exc:
|
||||
Log.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为 {str(exc)}")
|
||||
pass
|
53
plugins/help.py
Normal file
@ -0,0 +1,53 @@
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.error import BadRequest
|
||||
from telegram.ext import CommandHandler, CallbackContext
|
||||
|
||||
from app.template.service import TemplateService
|
||||
from config import config
|
||||
from logger import Log
|
||||
from utils.app.inject import inject
|
||||
from utils.decorators.error import error_callable
|
||||
from utils.decorators.restricts import restricts
|
||||
from utils.plugins.manager import listener_plugins_class
|
||||
|
||||
|
||||
@listener_plugins_class()
|
||||
class Help:
|
||||
"""
|
||||
帮助
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.help_png = None
|
||||
self.file_id = None
|
||||
|
||||
@classmethod
|
||||
def create_handlers(cls) -> list:
|
||||
_help = cls()
|
||||
return [
|
||||
CommandHandler("help", _help.command_start, block=False),
|
||||
]
|
||||
|
||||
@error_callable
|
||||
@restricts()
|
||||
@inject
|
||||
async def command_start(self, update: Update, _: CallbackContext, template_service: TemplateService) -> None:
|
||||
message = update.message
|
||||
user = update.effective_user
|
||||
Log.info(f"用户 {user.full_name}[{user.id}] 发出help命令")
|
||||
if self.file_id is None or config.DEBUG:
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
help_png = await template_service.render('bot/help', "help.html", {}, {"width": 768, "height": 768})
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||
reply_photo = await message.reply_photo(help_png, filename="help.png", allow_sending_without_reply=True)
|
||||
photo = reply_photo.photo[0]
|
||||
self.file_id = photo.file_id
|
||||
else:
|
||||
try:
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||
await message.reply_photo(self.file_id, allow_sending_without_reply=True)
|
||||
except BadRequest as error:
|
||||
self.file_id = None
|
||||
Log.error("发送图片失败,尝试清空已经保存的file_id,错误信息为", error)
|
||||
await message.reply_text("发送图片失败", allow_sending_without_reply=True)
|
44
plugins/start.py
Normal file
@ -0,0 +1,44 @@
|
||||
from telegram import Update, ReplyKeyboardRemove
|
||||
from telegram.ext import CallbackContext
|
||||
from telegram.helpers import escape_markdown
|
||||
|
||||
from utils.decorators.restricts import restricts
|
||||
|
||||
|
||||
@restricts()
|
||||
async def start(update: Update, context: CallbackContext) -> None:
|
||||
user = update.effective_user
|
||||
message = update.message
|
||||
args = context.args
|
||||
if args is not None:
|
||||
if len(args) >= 1:
|
||||
if args[0] == "inline_message":
|
||||
await message.reply_markdown_v2(f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}\n"
|
||||
f"{escape_markdown('发送 /help 命令即可查看命令帮助')}")
|
||||
return
|
||||
await update.message.reply_markdown_v2(f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}")
|
||||
|
||||
|
||||
@restricts()
|
||||
async def help_command(update: Update, _: CallbackContext) -> None:
|
||||
await update.message.reply_text("前面的区域,以后再来探索吧!")
|
||||
|
||||
|
||||
@restricts()
|
||||
async def unknown_command(update: Update, _: CallbackContext) -> None:
|
||||
await update.message.reply_text("前面的区域,以后再来探索吧!")
|
||||
|
||||
|
||||
@restricts()
|
||||
async def emergency_food(update: Update, _: CallbackContext) -> None:
|
||||
await update.message.reply_text("派蒙才不是应急食品!")
|
||||
|
||||
|
||||
@restricts()
|
||||
async def ping(update: Update, _: CallbackContext) -> None:
|
||||
await update.message.reply_text("online! ヾ(✿゚▽゚)ノ")
|
||||
|
||||
|
||||
@restricts()
|
||||
async def reply_keyboard_remove(update: Update, _: CallbackContext) -> None:
|
||||
await update.message.reply_text("移除远程键盘成功", reply_markup=ReplyKeyboardRemove())
|
20
requirements.txt
Normal file
@ -0,0 +1,20 @@
|
||||
redis>=4.3.3
|
||||
ujson>=5.3.0
|
||||
genshin>=1.2.1
|
||||
aiomysql>=0.1.1
|
||||
colorlog~=6.6.0
|
||||
numpy~=1.22.3
|
||||
httpx==0.23.0
|
||||
asyncio>=3.4.3
|
||||
jinja2>=3.1.2
|
||||
aiofiles>=0.8.0
|
||||
playwright==1.22.0
|
||||
pymysql>=0.9.3
|
||||
beautifulsoup4>=4.11.1
|
||||
pyppeteer~=1.0.2
|
||||
lxml>=4.9.0
|
||||
fakeredis>=1.8.1
|
||||
aiohttp<=3.8.1
|
||||
python-telegram-bot==20.0a2
|
||||
pytz>=2021.3
|
||||
Pillow
|
15
resources/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# resource 目录说明
|
||||
|
||||
用于给用户交互的前端界面,使用 `Jinja2` 渲染模板后,
|
||||
再使用无头浏览器 `playwright` 截图发送给用户
|
||||
|
||||
## background 来源
|
||||
|
||||
原神官方
|
||||
|
||||
## 使用的styles
|
||||
|
||||
| ProjectName | Contribution |
|
||||
|:-------------------------------------------------:|------------------|
|
||||
| [tailwindcss](https://tailwindcss.com/) | 本项目使用的CSS框架 |
|
||||
| [fontawesome](https://fontawesome.dashgame.com/) | 一套绝佳的图标字体库和CSS框架 |
|
BIN
resources/background/horizontal/原神1周年.png
Normal file
After Width: | Height: | Size: 6.0 MiB |
BIN
resources/background/vertical/不动鸣神泡影断灭.jpg
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
resources/background/vertical/原神1周年-2.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
resources/background/vertical/原神1周年.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
resources/background/vertical/薄樱初绽时八重神子.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
resources/background/vertical/薄樱初绽时雷电将军八重神子.png
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
resources/bot/help/background/1006.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/bot/help/background/2015.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
resources/bot/help/background/2020021114213984258.jpg
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
resources/bot/help/background/2020021114213984258.png
Normal file
After Width: | Height: | Size: 427 KiB |
BIN
resources/bot/help/background/genshin.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
90
resources/bot/help/help.css
Normal file
@ -0,0 +1,90 @@
|
||||
body {
|
||||
background-color: rgba(253, 253, 253, 0.75);
|
||||
}
|
||||
|
||||
#container {
|
||||
max-width: 768px;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: url(background/2020021114213984258.png) no-repeat center;
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
||||
background-size: 780px 120px;
|
||||
}
|
||||
|
||||
.command-title {
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
border: 1px solid #e0dad3;
|
||||
}
|
||||
|
||||
.command-title h1 {
|
||||
margin: 2px;
|
||||
background-color: #e0dad3;
|
||||
}
|
||||
|
||||
.command {
|
||||
flex: 1 1 0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 5px;
|
||||
margin: 10px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0dad3;
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
||||
|
||||
}
|
||||
|
||||
.command i {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.base-command {
|
||||
border: 1px solid #e0dad3;
|
||||
background-color: #f0ece8;
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.command-background {
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.command-background img {
|
||||
margin-left: 400px;
|
||||
margin-top: -10px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.genshin-background {
|
||||
opacity: 0.3;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.genshin-background img {
|
||||
margin-left: 150px;
|
||||
margin-top: 100px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.command-name {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.command-description {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.about {
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
border: 1px solid #e0dad3;
|
||||
}
|
||||
|
||||
.about h1 {
|
||||
margin: 2px;
|
||||
background-color: #e0dad3;
|
||||
}
|
144
resources/bot/help/help.html
Normal file
@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<link href="../../styles/tailwind.min.css" rel="stylesheet">
|
||||
<link href="../../styles/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="help.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mx-auto px-5 py-10" id="container">
|
||||
<div class="info p-6 flex flex-wrap mb-8 rounded-xl">
|
||||
<div class="info-name">
|
||||
<h1 class="text-4xl italic">TGPaimonBot</h1>
|
||||
<h1 class="text-2xl">使用说明</h1>
|
||||
</div>
|
||||
<div class="info-name text-1xl ml-60 pt-1">
|
||||
<p><i class="fa fa-address-card-o mr-2"></i>需要绑定Cookie</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-command pt-4 rounded-xl">
|
||||
<div class="command-background">
|
||||
<img src="background/2015.png">
|
||||
</div>
|
||||
<div class="genshin-background">
|
||||
<img src="background/genshin.png">
|
||||
</div>
|
||||
<div class="command-type px-1">
|
||||
<div class="command-title text-2xl">
|
||||
<h1>查询命令</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-list py-4 px-2">
|
||||
<div class="flex">
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">/weapon</div>
|
||||
<div class="command-description text-base ">查询武器</div>
|
||||
</div>
|
||||
<div class="command rounded-xl flex-1">
|
||||
<div class="command-name text-xl">/strategy</div>
|
||||
<div class="command-description text-base ">查询角色攻略</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="command">
|
||||
<div class="command-name text-xl ">
|
||||
/uid
|
||||
<i class="fa fa-address-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="name text-base ">查询玩家信息</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">
|
||||
/sign
|
||||
<i class="fa fa-address-card-o ml-2"></i></div>
|
||||
<div class="command-description text-base ">每日签到 | 查询</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">
|
||||
/dailynote
|
||||
<i class="fa fa-address-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="name text-base1">查询角色当前状态</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">
|
||||
/artifact_rate
|
||||
<div class="command-description text-base ">圣遗物评分</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">
|
||||
/ledger
|
||||
<i class="fa fa-address-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="name text-base1">查询角色当月旅行扎记</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">
|
||||
/abyss
|
||||
<i class="fa fa-address-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="name text-base1">查询当期深渊螺旋战绩</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-command pt-4 mb-8 rounded-xl">
|
||||
<div class="command-background">
|
||||
<img src="background/1006.png">
|
||||
</div>
|
||||
<div class="command-type px-1">
|
||||
<div class="command-title text-2xl">
|
||||
<h1>其他命令</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-list py-4 px-2">
|
||||
<div class="flex">
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">/gacha</div>
|
||||
<div class="name text-base1">抽卡模拟器(非洲人模拟器)</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">/quiz</div>
|
||||
<div class="command-description text-bas">派蒙的十万个为什么</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">/adduser</div>
|
||||
<div class="command-description text-base ">添加账号(请私聊BOT)</div>
|
||||
</div>
|
||||
<div class="command rounded-xl flex-1">
|
||||
<div class="command-name text-xl">/cancel</div>
|
||||
<div class="command-description text-base ">取消操作(解决一切玄学问题)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-command pt-4 mb-8 rounded-xl">
|
||||
<div class="command-type px-1">
|
||||
<div class="command-title text-2xl">
|
||||
<h1>inline 模式关键词</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-list py-4 px-2">
|
||||
<div class="flex">
|
||||
<div class="command">
|
||||
<div class="command-name text-xl">角色名</div>
|
||||
<div class="name text-base1">查询角色攻略</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="about">
|
||||
<h1>更多功能,还在咕咕咕!咕咕咕!</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
resources/fonts/fontawesome-webfont.woff2
Normal file
BIN
resources/fonts/seguiemj.ttf
Normal file
BIN
resources/fonts/tttgbnumber.ttf
Normal file
BIN
resources/fonts/汉仪文黑-85W.ttf
Normal file
83
resources/genshin/abyss/abyss.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-ch">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>abyss</title>
|
||||
<link type="text/css" href="../../styles/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" href="../../styles/public.css" rel="stylesheet">
|
||||
<style>
|
||||
#container {
|
||||
background: url("./background/lookback-bg.png");
|
||||
}
|
||||
|
||||
.character-icon {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.character-side-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mx-auto flex flex-col h-full bg-no-repeat bg-cover" id="container">
|
||||
<div class="title text-2xl my-4 text-yellow-500 mx-auto">深境螺旋</div>
|
||||
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-white bg-opacity-10">
|
||||
<div class="uid text-center mx-auto">UID {{uid}}</div>
|
||||
<div class="text-center mx-auto">最深抵达 {{max_floor}}</div>
|
||||
<div class="text-center mx-auto">战斗次数 {{total_battles}}</div>
|
||||
<div class="text-center mx-auto">获得星级 {{total_stars}}</div>
|
||||
</div>
|
||||
<div class="base-info flex flex-col px-20 py-1 text-black my-1">
|
||||
<div class="text-center mr-auto text-yellow-500">出战次数</div>
|
||||
<div class="mx-auto flex my-2">
|
||||
{% for most_played in most_played_list %}
|
||||
<div class="bg-white rounded-lg mx-2">
|
||||
<div class="character-icon rounded-lg bg-cover"
|
||||
style="background: url({{most_played.background}});background-size: cover;">
|
||||
<img src="{{most_played.icon}}" alt=""></div>
|
||||
<div class="text-center">{{most_played.value}}次</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col my-1">
|
||||
<div class="flex flex-col px-20 py-1 text-black my-1">
|
||||
<div class="text-center mr-auto text-yellow-500">出战次数</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-black bg-opacity-10 ">
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">最多击破数:{{most_kills.value}}</div>
|
||||
<img class="character-side-icon ml-auto" src="{{most_kills.icon}}" alt="">
|
||||
</div>
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">最强一击:{{strongest_strike.value}}</div>
|
||||
<img class="character-side-icon ml-auto" src="{{strongest_strike.icon}}" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-info flex flex-row px-20 py-1 my-1 text-white">
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">承受最多伤害:{{most_damage_taken.value}}</div>
|
||||
<img class="character-side-icon ml-auto" src="{{most_damage_taken.icon}}" alt="">
|
||||
</div>
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">元素爆发数:{{most_bursts_used.value}}</div>
|
||||
<img class="character-side-icon ml-auto" src="{{most_bursts_used.icon}}" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-black bg-opacity-10 ">
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">元素战技释放次数:{{most_skills_used.value}}</div>
|
||||
<img class="character-side-icon ml-auto" src="{{most_skills_used.icon}}" alt="">
|
||||
</div>
|
||||
<div class="text-center flex flex-row flex-1 mr-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-2"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
resources/genshin/abyss/background/lookback-bg.png
Normal file
After Width: | Height: | Size: 198 KiB |
BIN
resources/genshin/abyss/background/roleStarBg4.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
resources/genshin/abyss/background/roleStarBg5.png
Normal file
After Width: | Height: | Size: 15 KiB |
99
resources/genshin/abyss/example.html
Normal file
@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-ch">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>abyss</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
#container {
|
||||
background: url("./background/lookback-bg.png");
|
||||
}
|
||||
|
||||
.character-icon {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.character-side-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mx-auto flex flex-col h-full bg-no-repeat bg-cover" id="container">
|
||||
<div class="title text-2xl my-4 text-yellow-500 mx-auto">深境螺旋</div>
|
||||
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-white bg-opacity-10">
|
||||
<div class="uid text-center mx-auto">UID 1414514</div>
|
||||
<div class="text-center mx-auto">最深抵达 12-3</div>
|
||||
<div class="text-center mx-auto">战斗次数 12</div>
|
||||
<div class="text-center mx-auto">获得星级 36</div>
|
||||
</div>
|
||||
<div class="base-info flex flex-col px-20 py-1 text-black my-1">
|
||||
<div class="text-center mr-auto text-yellow-500">出战次数</div>
|
||||
<div class="mx-auto flex my-2">
|
||||
<div class="bg-white rounded-lg mx-2">
|
||||
<div class="character-icon rounded-lg bg-cover"
|
||||
style="background: url('./background/roleStarBg4.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">12次</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2">
|
||||
<div class="character-icon rounded-lg bg-cover"
|
||||
style="background: url('./background/roleStarBg4.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">12次</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2">
|
||||
<div class="character-icon rounded-lg bg-cover"
|
||||
style="background: url('./background/roleStarBg5.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">12次</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2">
|
||||
<div class="character-icon rounded-lg bg-cover"
|
||||
style="background: url('./background/roleStarBg5.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">12次</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col my-1">
|
||||
<div class="flex flex-col px-20 py-1 text-black my-1">
|
||||
<div class="text-center mr-auto text-yellow-500">出战次数</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-black bg-opacity-10 ">
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">最多击破数:21</div>
|
||||
<img class="character-side-icon ml-auto" src="./../../img/example/256x256.png" alt="">
|
||||
</div>
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">最强一击:21</div>
|
||||
<img class="character-side-icon ml-auto" src="./../../img/example/256x256.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-info flex flex-row px-20 py-1 my-1 text-white">
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">承受最多伤害:21</div>
|
||||
<img class="character-side-icon ml-auto" src="./../../img/example/256x256.png" alt="">
|
||||
</div>
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">元素爆发数:21</div>
|
||||
<img class="character-side-icon ml-auto" src="./../../img/example/256x256.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-black bg-opacity-10 ">
|
||||
<div class="text-center flex flex-row flex-1 mr-6">
|
||||
<div class="my-auto">元素战技释放次数:21</div>
|
||||
<img class="character-side-icon ml-auto" src="./../../img/example/256x256.png" alt="">
|
||||
</div>
|
||||
<div class="text-center flex flex-row flex-1 mr-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-2"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
121
resources/genshin/daily_note/daily_note.css
Normal file
@ -0,0 +1,121 @@
|
||||
@font-face {
|
||||
font-family: "tttgbnumber";
|
||||
src: url("../../fonts/tttgbnumber.ttf");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
font-family: "tttgbnumber", system-ui;
|
||||
transform: scale(1.5);
|
||||
transform-origin: 0 0;
|
||||
color: #1e1f20;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 400px;
|
||||
height: 365px;
|
||||
background: #f0eae3;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-bottom: 9px;
|
||||
color: #504c49;
|
||||
}
|
||||
|
||||
.title .id {
|
||||
flex: 1;
|
||||
line-height: 18px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.title .id:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 24px;
|
||||
border-radius: 1px;
|
||||
left: 0px;
|
||||
top: -3px;
|
||||
background: #d3bc8d;
|
||||
}
|
||||
|
||||
.title .day {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
border: 1px solid #dfd8d1;
|
||||
display: flex;
|
||||
height: 49px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.item .main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
background-color: #f5f1eb;
|
||||
position: relative;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
|
||||
.item .main .bg {
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background-size: 100% auto;
|
||||
background-image: url(./items/bg.png);
|
||||
}
|
||||
|
||||
.item .main .icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin: 11px 8px 0 8px;
|
||||
}
|
||||
|
||||
.item .main .info {
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.item .main .info .name {
|
||||
font-size: 14px;
|
||||
/* color: #5f5f5d; */
|
||||
line-height: 1;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.item .main .info .time {
|
||||
font-size: 12px;
|
||||
/* font-weight: 400; */
|
||||
color: #5f5f5d;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.item .right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96px;
|
||||
height: 100%;
|
||||
background-color: #ece3d8;
|
||||
font-size: 16px;
|
||||
color: #504c49;
|
||||
line-height: 55px;
|
||||
}
|
||||
|
||||
.item .right .red {
|
||||
color: #f24e4c;
|
||||
}
|
||||
|
129
resources/genshin/daily_note/daily_note.html
Normal file
@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" type="text/css" href="./daily_note.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="container">
|
||||
<div class="title">
|
||||
<div class="id">
|
||||
<span>ID:{{ uid }}</span>
|
||||
</div>
|
||||
<div class="day">
|
||||
<span>{{ day }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="main">
|
||||
<div class="bg"></div>
|
||||
<img class="icon" src="items/树脂.png" />
|
||||
<div class="info">
|
||||
<div class="name">原粹树脂</div>
|
||||
<div class="time">
|
||||
{% if resin_recovery_time %}
|
||||
将于{{ resin_recovery_time }} 全部恢复
|
||||
{% else %}
|
||||
树脂已完全恢复
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ current_resin }}/{{ max_resin }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="main">
|
||||
<div class="bg"></div>
|
||||
<img class="icon" src="items/洞天宝钱.png" />
|
||||
<div class="info">
|
||||
<div class="name">洞天宝钱</div>
|
||||
<div class="time">
|
||||
{% if realm_recovery_time %}
|
||||
预计{{ realm_recovery_time }}后达到上限
|
||||
{% else %}
|
||||
存储已满
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="{% if current_realm_currency/(max_realm_currency or 1) > 0.9 %}red{% endif %}">
|
||||
{{ current_realm_currency }}/{{ max_realm_currency }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="main">
|
||||
<div class="bg"></div>
|
||||
<img class="icon" src="items/委托.png" />
|
||||
<div class="info">
|
||||
<div class="name">每日委托任务</div>
|
||||
<div class="time">今日委托奖励{% if claimed_commission_reward %}已{% else %}未{% endif %}领取</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ completed_commissions }}/{{ max_commissions }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="main">
|
||||
<div class="bg"></div>
|
||||
<img class="icon" src="items/派遣.png" />
|
||||
<div class="info">
|
||||
<div class="name">探索派遣</div>
|
||||
<div class="time">
|
||||
{% if not expeditions %}尚未进行派遣
|
||||
{% elif remained_time %}将于{{ remained_time }}完成
|
||||
{% else %}派遣已完成{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ current_expeditions }}/{{ max_expeditions }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="main">
|
||||
<div class="bg"></div>
|
||||
<img class="icon" src="items/周本.png" />
|
||||
<div class="info">
|
||||
<div class="name">值得铭记的强敌</div>
|
||||
<div class="time">
|
||||
{% if remaining_resin_discounts<=0 %}周本已完成
|
||||
{% else %}周本树脂减半次数已用{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ remaining_resin_discounts }}/{{ max_resin_discounts }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="main">
|
||||
<div class="bg"></div>
|
||||
<img class="icon" src="items/参量质变仪.png" />
|
||||
<div class="info">
|
||||
<div class="name">参量质变仪</div>
|
||||
<div class="time">
|
||||
{% if transformer %}
|
||||
{% if transformer_ready %}已准备完成
|
||||
{% else %}{{ transformer_recovery_time }}后可使用{% endif %}
|
||||
{% else %}
|
||||
尚未获得
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="{% if (transformer and transformer_ready) %}red{% endif %}">
|
||||
{% if transformer %}{% if transformer_ready %}可使用{% else %}冷却中{% endif %}{% else %}尚未获得{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript"></script>
|
||||
</html>
|
BIN
resources/genshin/daily_note/items/bg.png
Normal file
After Width: | Height: | Size: 6.1 KiB |