Init
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
test.mjs
|
||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 80
|
||||
}
|
||||
661
LICENSE
Normal file
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) 2025 FurryR
|
||||
|
||||
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/>.
|
||||
43
eslint.config.mjs
Normal file
43
eslint.config.mjs
Normal file
@@ -0,0 +1,43 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import json from '@eslint/json'
|
||||
import markdown from '@eslint/markdown'
|
||||
import { defineConfig } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
|
||||
plugins: { js },
|
||||
extends: ['js/recommended']
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
|
||||
languageOptions: { globals: globals.node }
|
||||
},
|
||||
tseslint.configs.recommended,
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
plugins: { json },
|
||||
language: 'json/json',
|
||||
extends: ['json/recommended']
|
||||
},
|
||||
{
|
||||
files: ['**/*.jsonc'],
|
||||
plugins: { json },
|
||||
language: 'json/jsonc',
|
||||
extends: ['json/recommended']
|
||||
},
|
||||
{
|
||||
files: ['**/*.json5'],
|
||||
plugins: { json },
|
||||
language: 'json/json5',
|
||||
extends: ['json/recommended']
|
||||
},
|
||||
{
|
||||
files: ['**/*.md'],
|
||||
plugins: { markdown },
|
||||
language: 'markdown/gfm',
|
||||
extends: ['markdown/recommended']
|
||||
}
|
||||
])
|
||||
4913
package-lock.json
generated
Normal file
4913
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "lionheart",
|
||||
"version": "1.1.0",
|
||||
"description": "A client framework for maimai DX (SDGB)",
|
||||
"module": "dist/index.mjs",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"lint:type": "tsgo --noEmit",
|
||||
"lint:format": "prettier --check ./src/**/*.ts",
|
||||
"format": "prettier --write ./src",
|
||||
"lint": "eslint ./src",
|
||||
"lint:fix": "eslint ./src --fix"
|
||||
},
|
||||
"author": "FurryR",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
"md5": "^2.3.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"pako": "^2.1.0",
|
||||
"socks-proxy-agent": "^8.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@eslint/json": "^0.13.1",
|
||||
"@eslint/markdown": "^6.4.0",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20250607.1",
|
||||
"eslint": "^9.28.0",
|
||||
"globals": "^16.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript-eslint": "^8.33.0"
|
||||
}
|
||||
}
|
||||
476
src/client.ts
Normal file
476
src/client.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
import forge from 'node-forge'
|
||||
import md5 from 'md5'
|
||||
import * as Encryption from './encryption'
|
||||
import JSONBigInt from 'json-bigint'
|
||||
import type { Api as ApiTyping } from './typings/api'
|
||||
import { User } from './user'
|
||||
import pako from 'pako'
|
||||
import { Http } from '.'
|
||||
// import { request } from './utils/http'
|
||||
|
||||
const JSONParser = JSONBigInt({
|
||||
useNativeBigInt: true
|
||||
})
|
||||
|
||||
export interface CreateOption {
|
||||
url: string // 服务器地址(不用加 at. ai. 等前缀)
|
||||
codename: string // 游戏代号
|
||||
chipId: string // 基板 ID
|
||||
version: string // 游戏版本 (1.40, 1.50)
|
||||
encryption: {
|
||||
aime: Encryption.Aime.EncryptionParam
|
||||
maimai: Encryption.Maimai.EncryptionParam
|
||||
}
|
||||
fetch?: Client['fetch']
|
||||
}
|
||||
|
||||
export interface OptionImages {
|
||||
patch: URL[] // TODO: Single URL?
|
||||
options: URL[]
|
||||
}
|
||||
|
||||
export class Client {
|
||||
public cookies: Map<number, string> = new Map()
|
||||
|
||||
private _Obfuscator(apiName: string) {
|
||||
const ObfuscateParam = this.encryption.maimai.obfuscateParam
|
||||
return md5(apiName + ObfuscateParam, { encoding: 'hex' }).toLowerCase()
|
||||
}
|
||||
|
||||
private _generateUserAgent(apiName: string) {
|
||||
return `${this._Obfuscator(apiName)}#${this.shortChipId}`
|
||||
}
|
||||
|
||||
private _generateUserAgentWithId(apiName: string, userId: number) {
|
||||
return `${this._Obfuscator(apiName)}#${userId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Request an API from Maimai server.
|
||||
* @param apiName The name of the API to request, e.g. 'GetUserPreviewApi'.
|
||||
* @param data The data to send to the API, which should match the API's request format.
|
||||
* @param options Optional parameters, such as `userId` for generating user-specific User-Agent.
|
||||
* @param options.userId The user ID to include in the User-Agent header. If not provided, a generic User-Agent will be used.
|
||||
* @param options.maxTimeout The maximum timeout in seconds to wait for a successful response. Default is 60.
|
||||
* @return A promise that resolves to the API's response data, which should match the API's response format.
|
||||
* @throws An error if the API request fails or returns an unexpected response.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = await Client.create('example.com', 'SDEZ', 'A000-00000000000', '1.40');
|
||||
* const userId = 1234567890;
|
||||
* const preview = await client.request('GetUserPreviewApi', {
|
||||
* userId,
|
||||
* segaIdAuthKey: ''
|
||||
* });
|
||||
* console.log(preview);
|
||||
* ```
|
||||
* @template T The type of the API name, which should match the keys of `ApiTyping`.
|
||||
* @protected This API is for internal uses. Avoid using it directly in your code.
|
||||
*/
|
||||
public async request<T extends keyof ApiTyping>(
|
||||
apiName: T,
|
||||
data: ApiTyping[T][0],
|
||||
options?: { userId?: number; maxTimeout?: number }
|
||||
): Promise<ApiTyping[T][1]>
|
||||
|
||||
public async request(
|
||||
apiName: string,
|
||||
data: unknown,
|
||||
options?: { userId?: number; maxTimeout?: number }
|
||||
): Promise<unknown> {
|
||||
const realName = `${apiName}Maimai${this.region}`
|
||||
const endpoint = this.url.maimai + this._Obfuscator(realName)
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent': options?.userId
|
||||
? this._generateUserAgentWithId(realName, options.userId)
|
||||
: this._generateUserAgent(realName),
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Encoding': 'deflate',
|
||||
'Mai-Encoding': this.version,
|
||||
charset: 'UTF-8',
|
||||
expect: '100-continue'
|
||||
}
|
||||
if (options?.userId && this.cookies.has(options.userId)) {
|
||||
headers['Cookie'] = this.cookies.get(options.userId)!
|
||||
}
|
||||
const body = Encryption.Maimai.encrypt(
|
||||
pako.deflate(new TextEncoder().encode(JSONParser.stringify(data)))
|
||||
.buffer as ArrayBuffer,
|
||||
this.encryption.maimai
|
||||
) as Uint8Array<ArrayBuffer>
|
||||
let req = null
|
||||
let startTime = Date.now()
|
||||
while (1) {
|
||||
req = await this.fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
})
|
||||
if (req.status === 200) break
|
||||
if (Date.now() - startTime < (options?.maxTimeout ?? 10) * 1000) continue
|
||||
}
|
||||
if (!req || req.status !== 200)
|
||||
throw new Error(
|
||||
`${apiName} request failed: ${req?.status} ${req?.statusText}`
|
||||
)
|
||||
const setCookie = req.headers.get('set-cookie')
|
||||
if (setCookie && options?.userId) {
|
||||
this.cookies.set(options.userId, setCookie)
|
||||
}
|
||||
let v = await req.arrayBuffer()
|
||||
// fs.writeFileSync(`./${apiName}-${Date.now()}.bin`, Buffer.from(v))
|
||||
v = Encryption.Maimai.decrypt(v, this.encryption.maimai)
|
||||
.buffer as ArrayBuffer
|
||||
try {
|
||||
v = pako.inflate(v).buffer as ArrayBuffer
|
||||
} catch {
|
||||
// error handling intentionally omitted
|
||||
}
|
||||
// console.log(`${apiName} -> ${JSONParser.stringify(data)}`)
|
||||
const decrypted = new TextDecoder().decode(v)
|
||||
return JSONParser.parse(decrypted === '' ? 'null' : decrypted)
|
||||
}
|
||||
|
||||
async login(
|
||||
userId: number,
|
||||
token: string,
|
||||
options?: {
|
||||
isContinue?: boolean
|
||||
eventMode?: boolean
|
||||
readOnly?: boolean
|
||||
dateTime?: Date
|
||||
loginDateTime?: Date
|
||||
}
|
||||
) {
|
||||
const isAlreadyLogin = this.preview(userId, token).then(v => !!v?.isLogin)
|
||||
|
||||
const dateTime = Math.floor((options?.dateTime ?? this.created).getTime() / 1000)
|
||||
const loginDateTime = Math.floor((options?.loginDateTime ?? new Date()).getTime() / 1000)
|
||||
|
||||
const payload = {
|
||||
userId,
|
||||
accessCode: '',
|
||||
regionId: this.place.region.id,
|
||||
placeId: this.place.id,
|
||||
clientId: this.shortChipId,
|
||||
dateTime,
|
||||
loginDateTime,
|
||||
isContinue: !!options?.isContinue,
|
||||
genericFlag: +!!options?.eventMode,
|
||||
token
|
||||
}
|
||||
|
||||
const resp = options?.readOnly
|
||||
? { returnCode: 100, loginId: null, loginDateTime: 0, token: null }
|
||||
: await this.request('UserLoginApi', payload, { userId })
|
||||
if (resp?.returnCode === 102 || resp?.returnCode === 100) {
|
||||
return new User(
|
||||
userId,
|
||||
null,
|
||||
options?.dateTime ?? null,
|
||||
this,
|
||||
await isAlreadyLogin,
|
||||
resp.loginDateTime ? new Date(resp.loginDateTime * 1000) : (options?.loginDateTime ?? new Date()),
|
||||
resp?.token ?? token
|
||||
)
|
||||
} else if (resp?.returnCode !== 1) {
|
||||
throw new Error(`Login failed: ${resp?.returnCode}`)
|
||||
}
|
||||
|
||||
return new User(
|
||||
userId,
|
||||
resp.loginId,
|
||||
options?.dateTime ?? new Date(dateTime * 1000),
|
||||
this,
|
||||
true,
|
||||
options?.loginDateTime ?? new Date(loginDateTime * 1000),
|
||||
resp?.token ?? token
|
||||
)
|
||||
}
|
||||
|
||||
preview(userId: number, token: string) {
|
||||
return this.request(
|
||||
'GetUserPreviewApi',
|
||||
{
|
||||
userId,
|
||||
segaIdAuthKey: '',
|
||||
token,
|
||||
clientId: this.shortChipId
|
||||
},
|
||||
{
|
||||
userId
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async parseQR(qrCode: string): Promise<{ userId: number; token: string }> {
|
||||
const timestamp = qrCode.slice(8, 20)
|
||||
const data = qrCode.slice(20)
|
||||
|
||||
const json = (await this.fetch(
|
||||
`http://${this.url.aime}/wc_aime/api/get_data`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'WC_AIME_LIB'
|
||||
},
|
||||
body: JSONParser.stringify({
|
||||
chipID: this.chipId,
|
||||
openGameID: 'MAID',
|
||||
key: (() => {
|
||||
const key = forge.sha256.create()
|
||||
key.update(
|
||||
`${this.chipId}${timestamp}XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW`
|
||||
)
|
||||
return key.digest().toHex().toUpperCase()
|
||||
})(),
|
||||
qrCode: data,
|
||||
timestamp
|
||||
})
|
||||
}
|
||||
).then(v => v.json())) as {
|
||||
errorID: number
|
||||
userID: number
|
||||
token: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
if (json?.errorID !== 0) {
|
||||
throw new Error(`QR code parse failed: ${json?.errorID}`)
|
||||
}
|
||||
return {
|
||||
userId: json?.userID,
|
||||
token: json?.token
|
||||
}
|
||||
}
|
||||
|
||||
async getOpt(): Promise<OptionImages> {
|
||||
const response = await this.fetch(
|
||||
`http://${this.url.allnet}/net/delivery/instruction`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': `${this.codename};Windows/Lite`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: Encryption.Aime.encode(
|
||||
`title_id=${this.codename}&title_ver=${this.version}&client_id=${this.shortChipId}\r\n`,
|
||||
this.encryption.aime
|
||||
)
|
||||
}
|
||||
)
|
||||
.then(v => v.arrayBuffer())
|
||||
.then(v => Encryption.Aime.decode(v, this.encryption.aime))
|
||||
|
||||
type Configuration = {
|
||||
result: string
|
||||
uri: string
|
||||
}
|
||||
|
||||
// The URI may contain both an app download instruction URL
|
||||
// and an optional image download instruction URL.
|
||||
// These two URLs are concatenated together, with the optional image URL prefixed by |.
|
||||
// This means if only an optional image is available, it will have the format: |https://url.to/opt
|
||||
|
||||
const configuration: Configuration = Object.fromEntries(
|
||||
response.split('&').map(v => {
|
||||
const [key, value] = v.split('=')
|
||||
return [key, decodeURIComponent(value ?? '')]
|
||||
})
|
||||
) as Configuration
|
||||
|
||||
if (configuration.result !== '1') {
|
||||
throw new Error(`GetOpt failed: ${configuration.result}`)
|
||||
}
|
||||
|
||||
const uris = configuration.uri.split('|').filter(v => v !== '')
|
||||
const installUrls: string[] = []
|
||||
|
||||
for (const uri of uris) {
|
||||
const config = await this.fetch(uri).then(v => v.text())
|
||||
|
||||
// For each Patch Image delivery instruction (パッチイメージ配信指示書ソース),
|
||||
// there is one INSTALL1= URL,
|
||||
// which points to a *.app file.
|
||||
|
||||
// For each Option Image delivery instruction (オプションイメージ配信指示書ソース),
|
||||
// there is one INSTALL1= URL in the COMMON section,
|
||||
// as well as all historical options in the OPTIONAL section, start as INSTALL\d+=.
|
||||
|
||||
// We assume that the file extension of the patch image is always .app,
|
||||
// and the file extension of the option image is always .opt.
|
||||
// Therefore, we only need to extract the INSTALL\d+= URLs from the config, and seperate them later.
|
||||
|
||||
installUrls.push(
|
||||
...Array.from(config.matchAll(/INSTALL\d+=([^\r\n]+)/g)).map(
|
||||
match => match[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
return {
|
||||
patch: installUrls.filter(v => v.endsWith('.app')).map(v => new URL(v)),
|
||||
options: installUrls.filter(v => v.endsWith('.opt')).map(v => new URL(v))
|
||||
}
|
||||
}
|
||||
|
||||
static async create(options: CreateOption) {
|
||||
if (!options.fetch) {
|
||||
options.fetch = Http.makeRequest({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
}
|
||||
const response = await options
|
||||
.fetch(`http://at.${options.url}/net/initialize`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': `${options.codename};Windows/Lite`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: Encryption.Aime.encode(
|
||||
`title_id=${options.codename}&title_ver=${options.version}&client_id=${options.chipId
|
||||
.replace(/-/g, '')
|
||||
.slice(0, 11)}&token=${Math.floor(Math.random() * 4294967296)}\r\n`,
|
||||
options.encryption.aime
|
||||
)
|
||||
})
|
||||
.then(v => v.arrayBuffer())
|
||||
.then(v => Encryption.Aime.decode(v, options.encryption.aime))
|
||||
.then(v => v.slice(0, -2))
|
||||
|
||||
type Configuration = {
|
||||
result: string
|
||||
place_id: string
|
||||
uri1: string
|
||||
uri2: string
|
||||
name: string
|
||||
nickname: string
|
||||
setting: string
|
||||
region0: string
|
||||
region_name0: string
|
||||
region_name1: string
|
||||
region_name2: string
|
||||
region_name3: string
|
||||
country: string
|
||||
location_type: string
|
||||
utc_time: string
|
||||
client_timezone: string
|
||||
res_ver: string
|
||||
}
|
||||
|
||||
const configuration: Configuration = Object.fromEntries(
|
||||
response.split('&').map(v => {
|
||||
const [key, value] = v.split('=')
|
||||
return [key, decodeURIComponent(value ?? '')]
|
||||
})
|
||||
) as Configuration
|
||||
return new Client({
|
||||
url: {
|
||||
aime: 'ai.' + options.url,
|
||||
allnet: 'at.' + options.url,
|
||||
maimai: configuration.uri1 + 'Maimai2Servlet/'
|
||||
},
|
||||
chipId: options.chipId,
|
||||
version: options.version,
|
||||
codename: options.codename,
|
||||
created: new Date(),
|
||||
region: configuration.country === 'CHN' ? 'Chn' : 'Exp',
|
||||
placeInfo: {
|
||||
id: parseInt(configuration.place_id, 16),
|
||||
name: configuration.name,
|
||||
nickname: configuration.nickname,
|
||||
region: {
|
||||
id: parseInt(configuration.region0),
|
||||
name: [
|
||||
configuration.region_name0,
|
||||
configuration.region_name1,
|
||||
configuration.region_name2,
|
||||
configuration.region_name3
|
||||
]
|
||||
}
|
||||
},
|
||||
encryption: options.encryption,
|
||||
fetch: options.fetch
|
||||
})
|
||||
}
|
||||
|
||||
public readonly fetch: (
|
||||
url: string | URL,
|
||||
options?: {
|
||||
method?: 'GET' | 'POST'
|
||||
headers?: Record<string, string>
|
||||
body?: string | ArrayBuffer | Uint8Array<ArrayBuffer>
|
||||
}
|
||||
) => Promise<Response>
|
||||
public readonly url: {
|
||||
aime: string
|
||||
maimai: string
|
||||
allnet: string
|
||||
}
|
||||
public readonly chipId: string
|
||||
public readonly created: Date
|
||||
public readonly version: string
|
||||
public readonly region: 'Chn' | 'Exp'
|
||||
public readonly codename: string
|
||||
public readonly place: {
|
||||
id: number
|
||||
name: string
|
||||
nickname: string
|
||||
region: {
|
||||
id: number
|
||||
name: string[]
|
||||
}
|
||||
}
|
||||
public readonly encryption: {
|
||||
aime: Encryption.Aime.EncryptionParam
|
||||
maimai: Encryption.Maimai.EncryptionParam
|
||||
}
|
||||
constructor({
|
||||
url,
|
||||
chipId,
|
||||
created,
|
||||
version,
|
||||
region,
|
||||
codename,
|
||||
placeInfo,
|
||||
encryption,
|
||||
fetch
|
||||
}: {
|
||||
url: {
|
||||
aime: string
|
||||
maimai: string
|
||||
allnet: string
|
||||
}
|
||||
chipId: string
|
||||
created: Date
|
||||
version: string
|
||||
region: 'Chn' | 'Exp'
|
||||
codename: string
|
||||
placeInfo: {
|
||||
id: number
|
||||
name: string
|
||||
nickname: string
|
||||
region: {
|
||||
id: number
|
||||
name: string[]
|
||||
}
|
||||
}
|
||||
encryption: {
|
||||
aime: Encryption.Aime.EncryptionParam
|
||||
maimai: Encryption.Maimai.EncryptionParam
|
||||
}
|
||||
fetch: Client['fetch']
|
||||
}) {
|
||||
this.url = url
|
||||
this.chipId = chipId
|
||||
this.created = created
|
||||
this.version = version
|
||||
this.region = region
|
||||
this.codename = codename
|
||||
this.place = placeInfo
|
||||
this.encryption = encryption
|
||||
this.fetch = fetch
|
||||
}
|
||||
get shortChipId(): string {
|
||||
// A63E-01E12030000 -> A63E01E1203
|
||||
return this.chipId.replace(/-/g, '').slice(0, 11)
|
||||
}
|
||||
}
|
||||
99
src/containers/Character.ts
Normal file
99
src/containers/Character.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { UserCharacter } from '../typings/api'
|
||||
|
||||
import { values, entries } from '../utils/iterator'
|
||||
|
||||
// TODO: 添加 nextAwake 和 nextAwakePercent 属性 (getter)
|
||||
export interface Character {
|
||||
point: number // 累计点数
|
||||
level: number
|
||||
awakening: number
|
||||
}
|
||||
|
||||
export class CharacterSet {
|
||||
private _character: Map<number, Character> = new Map()
|
||||
private _modifiedCharacter: (Character & {
|
||||
characterId: number
|
||||
isNew: boolean
|
||||
})[] = []
|
||||
get(characterId: number): Character | undefined {
|
||||
const character = this._character.get(characterId)
|
||||
if (!character) return undefined
|
||||
return new Proxy<Character>(character, {
|
||||
set: (target, prop, value) => {
|
||||
if (
|
||||
['point', 'level', ' awakening', 'useCount'].includes(prop as string)
|
||||
) {
|
||||
Reflect.set(target, prop, value)
|
||||
const modifiedCharacter = this._modifiedCharacter.find(
|
||||
c => c.characterId === characterId
|
||||
)
|
||||
if (modifiedCharacter) {
|
||||
Reflect.set(modifiedCharacter, prop, value)
|
||||
} else {
|
||||
this._modifiedCharacter.push({
|
||||
characterId,
|
||||
awakening: target.awakening,
|
||||
level: target.level,
|
||||
point: target.point,
|
||||
isNew: false
|
||||
})
|
||||
}
|
||||
return Reflect.set(target, prop, value)
|
||||
}
|
||||
return false // 不允许修改其他属性
|
||||
},
|
||||
deleteProperty: () => false,
|
||||
isExtensible: () => false
|
||||
})
|
||||
}
|
||||
set(characterId: number, character: Partial<Character>) {
|
||||
const original = this._character.get(characterId)
|
||||
const chr: Character = {
|
||||
point: character?.point ?? 0,
|
||||
level: character?.level ?? 0,
|
||||
awakening: character?.awakening ?? 0
|
||||
}
|
||||
if (original) {
|
||||
Object.assign(original, chr)
|
||||
} else {
|
||||
this._character.set(characterId, {
|
||||
...chr
|
||||
})
|
||||
this._modifiedCharacter.push({
|
||||
characterId,
|
||||
...chr,
|
||||
isNew: true
|
||||
})
|
||||
}
|
||||
}
|
||||
export(): { value: UserCharacter; isNew: boolean }[] {
|
||||
return this._modifiedCharacter.map(v => ({
|
||||
value: {
|
||||
characterId: v.characterId,
|
||||
point: v.point,
|
||||
level: v.level,
|
||||
awakening: v.awakening,
|
||||
useCount: 0
|
||||
},
|
||||
isNew: v.isNew
|
||||
}))
|
||||
}
|
||||
keys() {
|
||||
return this._character.keys()
|
||||
}
|
||||
values() {
|
||||
return values(this)
|
||||
}
|
||||
entries() {
|
||||
return entries(this)
|
||||
}
|
||||
constructor(data: UserCharacter[]) {
|
||||
data.forEach(detail => {
|
||||
this._character.set(detail.characterId, {
|
||||
point: detail.point,
|
||||
level: detail.level,
|
||||
awakening: detail.awakening
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
95
src/containers/Item.ts
Normal file
95
src/containers/Item.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { UserItem, UserItemKind } from '../typings/api'
|
||||
|
||||
import { values, entries } from '../utils/iterator'
|
||||
|
||||
export interface Item {
|
||||
stock: number // 库存数量
|
||||
}
|
||||
|
||||
export function unpackPresent(itemId: number) {
|
||||
// 从 itemId 抽出 itemKind 和真正的 itemId。
|
||||
/**
|
||||
* 参照代码:
|
||||
* itemType = (ItemKind)(presentId / (long)ConstParameter.PresentKindConvert);
|
||||
* itemId = (int)(presentId % (long)ConstParameter.PresentKindConvert);
|
||||
* 其中:
|
||||
* public static int PresentKindConvert = 1000000;
|
||||
*/
|
||||
const kind = Math.floor(itemId / 1000000) as UserItemKind
|
||||
const id = itemId % 1000000
|
||||
return [kind, id]
|
||||
}
|
||||
|
||||
export function packPresent(itemKind: UserItemKind, itemId: number): number {
|
||||
return itemKind * 1000000 + itemId
|
||||
}
|
||||
|
||||
export class ItemSet {
|
||||
private _item: Map<number, Item> = new Map()
|
||||
private _modifiedItem: (Item & {
|
||||
id: number
|
||||
isNew: boolean
|
||||
})[] = []
|
||||
get(itemId: number): number | undefined {
|
||||
return this._item.get(itemId)?.stock
|
||||
}
|
||||
set(itemId: number, stock: number) {
|
||||
const original = this._item.get(itemId)
|
||||
const item: Item = {
|
||||
stock
|
||||
}
|
||||
if (original) {
|
||||
original.stock = stock
|
||||
const modifiedItem = this._modifiedItem.find(i => i.id === itemId)
|
||||
if (modifiedItem) {
|
||||
modifiedItem.stock = stock
|
||||
} else {
|
||||
this._modifiedItem.push({
|
||||
id: itemId,
|
||||
stock,
|
||||
isNew: false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this._item.set(itemId, {
|
||||
...item
|
||||
})
|
||||
this._modifiedItem.push({
|
||||
id: itemId,
|
||||
stock,
|
||||
isNew: true
|
||||
})
|
||||
}
|
||||
}
|
||||
export(): { value: UserItem; isNew: boolean }[] {
|
||||
return this._modifiedItem.map(v => ({
|
||||
value: {
|
||||
itemKind: this.present ? UserItemKind.Present : this.itemKind,
|
||||
itemId: this.present ? packPresent(this.itemKind, v.id) : v.id,
|
||||
stock: v.stock,
|
||||
isValid: true
|
||||
},
|
||||
isNew: v.isNew
|
||||
}))
|
||||
}
|
||||
keys() {
|
||||
return this._item.keys()
|
||||
}
|
||||
values() {
|
||||
return values(this)
|
||||
}
|
||||
entries() {
|
||||
return entries(this)
|
||||
}
|
||||
constructor(
|
||||
data: UserItem[],
|
||||
public readonly itemKind: UserItemKind,
|
||||
public readonly present: boolean
|
||||
) {
|
||||
data.forEach(detail => {
|
||||
this._item.set(detail.itemId, {
|
||||
stock: detail.stock
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
14
src/containers/Mission.ts
Normal file
14
src/containers/Mission.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { UserWeeklyData, UserMissionData } from '../typings/api'
|
||||
|
||||
export class Mission {
|
||||
public weekly: UserWeeklyData
|
||||
public mission: UserMissionData[]
|
||||
constructor(weekly: UserWeeklyData, mission: UserMissionData[]) {
|
||||
this.weekly = {
|
||||
beforeLoginWeek: weekly.beforeLoginWeek ?? '1900-01-01 00:00:00',
|
||||
lastLoginWeek: weekly.lastLoginWeek ?? '1900-01-01 00:00:00',
|
||||
friendBonusFlag: !!weekly.friendBonusFlag
|
||||
}
|
||||
this.mission = mission
|
||||
}
|
||||
}
|
||||
284
src/containers/Score.ts
Normal file
284
src/containers/Score.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import {
|
||||
MusicClearRankID,
|
||||
MusicDifficultyID,
|
||||
PlayComboFlagID,
|
||||
PlaySyncFlagID,
|
||||
UserMusicDetail
|
||||
} from '../typings/api'
|
||||
|
||||
import { values, entries } from '../utils/iterator'
|
||||
|
||||
export interface ScoreEntryInit {
|
||||
playCount: number
|
||||
achievement: number
|
||||
comboStatus: PlayComboFlagID
|
||||
syncStatus: PlaySyncFlagID
|
||||
deluxscoreMax: number
|
||||
extNum1: number // 理论值次数
|
||||
}
|
||||
|
||||
interface ScoreInternal {
|
||||
[MusicDifficultyID.Basic]?: ScoreEntryInit
|
||||
[MusicDifficultyID.Advanced]?: ScoreEntryInit
|
||||
[MusicDifficultyID.Expert]?: ScoreEntryInit
|
||||
[MusicDifficultyID.Master]?: ScoreEntryInit
|
||||
[MusicDifficultyID.ReMaster]?: ScoreEntryInit
|
||||
[MusicDifficultyID.Utage]?: ScoreEntryInit
|
||||
}
|
||||
|
||||
export interface ScoreEntry extends ScoreEntryInit {
|
||||
get scoreRank(): MusicClearRankID // calculated by achievement
|
||||
}
|
||||
|
||||
export interface Score {
|
||||
get [MusicDifficultyID.Basic](): ScoreEntry | undefined
|
||||
get [MusicDifficultyID.Advanced](): ScoreEntry | undefined
|
||||
get [MusicDifficultyID.Expert](): ScoreEntry | undefined
|
||||
get [MusicDifficultyID.Master](): ScoreEntry | undefined
|
||||
get [MusicDifficultyID.ReMaster](): ScoreEntry | undefined
|
||||
get [MusicDifficultyID.Utage](): ScoreEntry | undefined
|
||||
set [MusicDifficultyID.Basic](value: ScoreEntryInit)
|
||||
set [MusicDifficultyID.Advanced](value: ScoreEntryInit)
|
||||
set [MusicDifficultyID.Expert](value: ScoreEntryInit)
|
||||
set [MusicDifficultyID.Master](value: ScoreEntryInit)
|
||||
set [MusicDifficultyID.ReMaster](value: ScoreEntryInit)
|
||||
set [MusicDifficultyID.Utage](value: ScoreEntryInit)
|
||||
}
|
||||
|
||||
export function convertAchievementToScoreRank(
|
||||
achievement: number,
|
||||
utage?: boolean
|
||||
): MusicClearRankID {
|
||||
const allRanks = [
|
||||
MusicClearRankID.Rank_D,
|
||||
MusicClearRankID.Rank_C,
|
||||
MusicClearRankID.Rank_B,
|
||||
MusicClearRankID.Rank_BB,
|
||||
MusicClearRankID.Rank_BBB,
|
||||
MusicClearRankID.Rank_A,
|
||||
MusicClearRankID.Rank_AA,
|
||||
MusicClearRankID.Rank_AAA,
|
||||
MusicClearRankID.Rank_S,
|
||||
MusicClearRankID.Rank_SP,
|
||||
MusicClearRankID.Rank_SS,
|
||||
MusicClearRankID.Rank_SSP,
|
||||
MusicClearRankID.Rank_SSS
|
||||
] as const
|
||||
const border = [
|
||||
499999, // D: 0.0000% ~ 49.9999%
|
||||
599999, // C: 50.0000% ~ 59.9999%
|
||||
699999, // B: 60.0000% ~ 69.9999%
|
||||
749999, // BB: 70.0000% ~ 74.9999%
|
||||
799999, // BBB: 75.0000% ~ 79.9999%
|
||||
899999, // A: 80.0000% ~ 89.9999%
|
||||
939999, // AA: 90.0000% ~ 93.9999%
|
||||
969999, // AAA: 94.0000% ~ 96.9999%
|
||||
979999, // S: 97.0000% ~ 97.9999%
|
||||
989999, // S+: 98.0000% ~ 98.9999%
|
||||
994999, // SS: 99.0000% ~ 99.4999%
|
||||
999999, // SS+: 99.5000% ~ 99.9999%
|
||||
1004999 // SSS: 100.0000% ~ 104.9999%
|
||||
] as const
|
||||
let rank = -1
|
||||
for (let i = 0; i < border.length; i++) {
|
||||
const rankBorder = border[i] * (+!!utage + 1)
|
||||
if (achievement <= rankBorder) {
|
||||
rank = allRanks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (rank === -1 && achievement >= 1005000 && achievement <= 1010000) {
|
||||
rank = MusicClearRankID.Rank_SSSP
|
||||
}
|
||||
return rank
|
||||
}
|
||||
|
||||
export class ScoreSet {
|
||||
private _score: Map<number /* musicId */, ScoreInternal> = new Map()
|
||||
private _modifiedScore: (ScoreEntryInit & {
|
||||
musicId: number
|
||||
level: MusicDifficultyID
|
||||
isNew: boolean
|
||||
})[] = []
|
||||
static resettable(data: UserMusicDetail[]): [ScoreSet, () => void] {
|
||||
const scoreSet = new ScoreSet(data)
|
||||
return [scoreSet, () => (scoreSet._modifiedScore = [])]
|
||||
}
|
||||
constructor(data: UserMusicDetail[]) {
|
||||
for (const detail of data) {
|
||||
const score: ScoreInternal = this._score.get(detail.musicId) || {}
|
||||
const entry = {
|
||||
playCount: detail.playCount,
|
||||
achievement: detail.achievement,
|
||||
comboStatus: detail.comboStatus,
|
||||
syncStatus: detail.syncStatus,
|
||||
deluxscoreMax: detail.deluxscoreMax,
|
||||
extNum1: detail.extNum1
|
||||
}
|
||||
score[detail.level] = entry
|
||||
this._score.set(detail.musicId, score)
|
||||
}
|
||||
}
|
||||
get size(): number {
|
||||
return this._score.size
|
||||
}
|
||||
get(musicId: number): Score {
|
||||
let score = this._score.get(musicId)
|
||||
if (!score) {
|
||||
score = {}
|
||||
this._score.set(musicId, score)
|
||||
}
|
||||
return new Proxy<Score>(score as Score, {
|
||||
get: (target, prop) => {
|
||||
if (
|
||||
!Reflect.has(target, prop) ||
|
||||
!(Number(prop) in MusicDifficultyID)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
const entry = Reflect.get(target, prop) as ScoreEntryInit
|
||||
return new Proxy<ScoreEntry>(entry as ScoreEntry, {
|
||||
get: (entryTarget, entryProp) => {
|
||||
if (entryProp === 'scoreRank') {
|
||||
return convertAchievementToScoreRank(
|
||||
entryTarget.achievement,
|
||||
entryTarget.achievement > 1010000 &&
|
||||
Number(prop) === MusicDifficultyID.Utage
|
||||
)
|
||||
}
|
||||
return Reflect.get(entryTarget, entryProp)
|
||||
},
|
||||
set: (entryTarget, entryProp, value) => {
|
||||
if (
|
||||
[
|
||||
'playCount',
|
||||
'achievement',
|
||||
'comboStatus',
|
||||
'syncStatus',
|
||||
'deluxscoreMax',
|
||||
'extNum1'
|
||||
].includes(entryProp as string)
|
||||
) {
|
||||
Reflect.set(entryTarget, entryProp, value)
|
||||
const existingScore = this._modifiedScore.find(
|
||||
e => e.musicId === musicId && e.level === Number(prop)
|
||||
)
|
||||
if (existingScore) {
|
||||
Reflect.set(existingScore, entryProp, value)
|
||||
} else {
|
||||
this._modifiedScore.push({
|
||||
achievement: entryTarget.achievement,
|
||||
playCount: entryTarget.playCount,
|
||||
comboStatus: entryTarget.comboStatus,
|
||||
syncStatus: entryTarget.syncStatus,
|
||||
deluxscoreMax: entryTarget.deluxscoreMax,
|
||||
musicId: musicId,
|
||||
extNum1: entryTarget.extNum1,
|
||||
level: Number(prop),
|
||||
isNew: false
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
has: (_, entryProp) => {
|
||||
return [
|
||||
'playCount',
|
||||
'achievement',
|
||||
'comboStatus',
|
||||
'syncStatus',
|
||||
'deluxscoreMax',
|
||||
'scoreRank',
|
||||
'extNum1'
|
||||
].includes(entryProp as string)
|
||||
},
|
||||
isExtensible: () => false,
|
||||
ownKeys: () => [
|
||||
'playCount',
|
||||
'achievement',
|
||||
'comboStatus',
|
||||
'syncStatus',
|
||||
'deluxscoreMax',
|
||||
'scoreRank',
|
||||
'extNum1'
|
||||
]
|
||||
})
|
||||
},
|
||||
set: (target, prop, value) => {
|
||||
if (
|
||||
Object.values(MusicDifficultyID)
|
||||
.map(v => String(v))
|
||||
.includes(prop as string)
|
||||
) {
|
||||
const isNew = !Reflect.has(target, prop)
|
||||
value = {
|
||||
playCount: Number(value?.playCount || 0),
|
||||
achievement: Number(value?.achievement || 0),
|
||||
comboStatus: Number(value?.comboStatus || PlayComboFlagID.None),
|
||||
syncStatus: Number(value?.syncStatus || PlaySyncFlagID.None),
|
||||
deluxscoreMax: Number(value?.deluxscoreMax || 0),
|
||||
extNum1: Number(value?.extNum1 || 0)
|
||||
}
|
||||
Reflect.set(target, prop, value)
|
||||
const entry: ScoreEntryInit & {
|
||||
musicId: number
|
||||
level: MusicDifficultyID
|
||||
} = {
|
||||
...value,
|
||||
musicId: musicId,
|
||||
level: Number(prop)
|
||||
}
|
||||
const existingIndex = this._modifiedScore.findIndex(
|
||||
e => e.musicId === entry.musicId && e.level === entry.level
|
||||
)
|
||||
if (existingIndex !== -1) {
|
||||
this._modifiedScore[existingIndex] = Object.assign({}, entry, {
|
||||
isNew: this._modifiedScore[existingIndex].isNew || isNew
|
||||
})
|
||||
} else {
|
||||
this._modifiedScore.push({
|
||||
...entry,
|
||||
isNew
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
deleteProperty: () => false
|
||||
})
|
||||
}
|
||||
set(musicId: number, value: ScoreInternal) {
|
||||
const dummyScore = this.get(musicId)
|
||||
Object.assign(dummyScore, value)
|
||||
}
|
||||
keys() {
|
||||
return this._score.keys()
|
||||
}
|
||||
values() {
|
||||
return values(this)
|
||||
}
|
||||
export(): { value: UserMusicDetail; isNew: boolean }[] {
|
||||
return this._modifiedScore.map(entry => ({
|
||||
value: {
|
||||
musicId: entry.musicId,
|
||||
level: entry.level,
|
||||
playCount: entry.playCount,
|
||||
achievement: entry.achievement,
|
||||
comboStatus: entry.comboStatus,
|
||||
syncStatus: entry.syncStatus,
|
||||
deluxscoreMax: entry.deluxscoreMax,
|
||||
scoreRank: convertAchievementToScoreRank(
|
||||
entry.achievement,
|
||||
entry.achievement > 1010000 && entry.level === MusicDifficultyID.Utage
|
||||
),
|
||||
extNum1: entry.extNum1
|
||||
},
|
||||
isNew: entry.isNew
|
||||
}))
|
||||
}
|
||||
entries() {
|
||||
return entries(this)
|
||||
}
|
||||
}
|
||||
4
src/containers/index.ts
Normal file
4
src/containers/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './Score'
|
||||
export * from './Character'
|
||||
export * from './Item'
|
||||
export * from './Mission'
|
||||
45
src/encryption/aime.ts
Normal file
45
src/encryption/aime.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import forge from 'node-forge'
|
||||
|
||||
export interface EncryptionParam {
|
||||
key: string
|
||||
}
|
||||
|
||||
export function encode(
|
||||
data: string,
|
||||
options: EncryptionParam & {
|
||||
iv?: Uint8Array
|
||||
}
|
||||
): Uint8Array<ArrayBuffer> {
|
||||
const iv =
|
||||
(options?.iv as Uint8Array<ArrayBuffer>) ??
|
||||
crypto.getRandomValues(new Uint8Array(16))
|
||||
const cipher = forge.cipher.createCipher(
|
||||
'AES-CBC',
|
||||
forge.util.createBuffer(options.key)
|
||||
)
|
||||
cipher.start({ iv: forge.util.createBuffer(iv) })
|
||||
cipher.update(forge.util.createBuffer(data))
|
||||
cipher.finish()
|
||||
const encrypted = cipher.output.getBytes()
|
||||
return new Uint8Array([
|
||||
...iv,
|
||||
...Array.from(encrypted).map(v => v.charCodeAt(0))
|
||||
])
|
||||
}
|
||||
/**
|
||||
* @param {Uint8Array} data
|
||||
*/
|
||||
export function decode(data: ArrayBuffer, options: EncryptionParam): string {
|
||||
const iv = data.slice(0, 16)
|
||||
const decipher = forge.cipher.createDecipher(
|
||||
'AES-CBC',
|
||||
forge.util.createBuffer(options.key)
|
||||
)
|
||||
decipher.start({ iv: forge.util.createBuffer(iv) })
|
||||
decipher.update(forge.util.createBuffer(data.slice(16)))
|
||||
decipher.finish()
|
||||
const decrypted = decipher.output.getBytes()
|
||||
return new TextDecoder().decode(
|
||||
new Uint8Array(Array.from(decrypted).map(v => v.charCodeAt(0)))
|
||||
)
|
||||
}
|
||||
2
src/encryption/index.ts
Normal file
2
src/encryption/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as Aime from './aime'
|
||||
export * as Maimai from './maimai'
|
||||
39
src/encryption/maimai.ts
Normal file
39
src/encryption/maimai.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import forge from 'node-forge'
|
||||
|
||||
export interface EncryptionParam {
|
||||
key: string
|
||||
iv: string
|
||||
obfuscateParam: string
|
||||
}
|
||||
|
||||
export function encrypt(
|
||||
data: ArrayBuffer,
|
||||
options: EncryptionParam
|
||||
): Uint8Array {
|
||||
// aes encrypt
|
||||
const cipher = forge.cipher.createCipher(
|
||||
'AES-CBC',
|
||||
forge.util.createBuffer(options.key)
|
||||
)
|
||||
cipher.start({ iv: options.iv })
|
||||
cipher.update(forge.util.createBuffer(data))
|
||||
cipher.finish()
|
||||
return Uint8Array.from(
|
||||
Array.from(cipher.output.getBytes()).map(v => v.charCodeAt(0))
|
||||
)
|
||||
}
|
||||
export function decrypt(
|
||||
data: ArrayBuffer,
|
||||
options: EncryptionParam
|
||||
): Uint8Array {
|
||||
// aes decrypt
|
||||
const decipher = forge.cipher.createDecipher(
|
||||
'AES-CBC',
|
||||
forge.util.createBuffer(options.key)
|
||||
)
|
||||
decipher.start({ iv: options.iv })
|
||||
decipher.update(forge.util.createBuffer(data))
|
||||
decipher.finish()
|
||||
const decrypted = decipher.output.getBytes()
|
||||
return new Uint8Array(Array.from(decrypted).map(v => v.charCodeAt(0)))
|
||||
}
|
||||
9
src/index.ts
Normal file
9
src/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './encryption'
|
||||
export * from './client'
|
||||
export * from './user'
|
||||
export * as Http from './utils/http'
|
||||
export * as Date from './utils/date'
|
||||
export * as Event from './utils/event'
|
||||
export * as Iterator from './utils/iterator'
|
||||
export * as Api from './typings/api'
|
||||
export * from './containers'
|
||||
9
src/typings/api/GetUserActivityApi.ts
Normal file
9
src/typings/api/GetUserActivityApi.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { UserActivity } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userActivity: UserActivity
|
||||
}
|
||||
21
src/typings/api/GetUserCardApi.ts
Normal file
21
src/typings/api/GetUserCardApi.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface Request {
|
||||
userId: number
|
||||
nextIndex: number | bigint
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
export interface CardItem {
|
||||
cardId: number
|
||||
cardTypeId: number
|
||||
charaId: number
|
||||
mapId: number
|
||||
startDate: string
|
||||
endDate: string
|
||||
}
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
nextIndex: number | bigint
|
||||
userCardList: CardItem[] | null
|
||||
serialIdList: unknown[] | null // Unused?
|
||||
}
|
||||
22
src/typings/api/GetUserCharacterApi.ts
Normal file
22
src/typings/api/GetUserCharacterApi.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { UserCharacter } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
userCharacterList: UserCharacter[] | null
|
||||
// userCharacterList:
|
||||
// | {
|
||||
// characterId: number
|
||||
// point: number
|
||||
// useCount: number
|
||||
// level: number
|
||||
// nextAwake: number
|
||||
// nextAwakePercent: number
|
||||
// awakening: number // 0 = not awakened, 1 = first awakening, etc.
|
||||
// }[]
|
||||
// | null
|
||||
}
|
||||
17
src/typings/api/GetUserChargeApi.ts
Normal file
17
src/typings/api/GetUserChargeApi.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
userChargeList:
|
||||
| {
|
||||
chargeId: number
|
||||
stock: number
|
||||
purchaseDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
validDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
extNum1: number
|
||||
}[]
|
||||
| null
|
||||
}
|
||||
13
src/typings/api/GetUserCourseApi.ts
Normal file
13
src/typings/api/GetUserCourseApi.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UserCourse } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
nextIndex: number | bigint
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
nextIndex: number | bigint
|
||||
userCourseList: UserCourse[] | null
|
||||
}
|
||||
11
src/typings/api/GetUserDataApi.ts
Normal file
11
src/typings/api/GetUserDataApi.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { UserDetail } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userData: UserDetail
|
||||
banState: 0 | 1 | 2 // 0 = not banned, 1 = warning, 2 = banned
|
||||
}
|
||||
10
src/typings/api/GetUserExtendApi.ts
Normal file
10
src/typings/api/GetUserExtendApi.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { UserExtend } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userExtend: UserExtend
|
||||
}
|
||||
12
src/typings/api/GetUserFavoriteApi.ts
Normal file
12
src/typings/api/GetUserFavoriteApi.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { UserFavorite } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
itemKind: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
// NOTE: 无法参考来自 C# 的数据; typo?
|
||||
userFavorite: UserFavorite
|
||||
}
|
||||
20
src/typings/api/GetUserFavoriteItemApi.ts
Normal file
20
src/typings/api/GetUserFavoriteItemApi.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface Request {
|
||||
userId: number
|
||||
kind: number
|
||||
nextIndex: number | bigint
|
||||
maxCount: number
|
||||
isAllFavoriteItem: boolean
|
||||
}
|
||||
|
||||
export interface FavoriteItem {
|
||||
orderId: number
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
kind: number
|
||||
length: number
|
||||
nextIndex: number | bigint
|
||||
userFavoriteItemList: FavoriteItem[] | null
|
||||
}
|
||||
10
src/typings/api/GetUserGhostApi.ts
Normal file
10
src/typings/api/GetUserGhostApi.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { UserGhost } from './base/UserGhost'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userGhostList: UserGhost[]
|
||||
}
|
||||
15
src/typings/api/GetUserItemApi.ts
Normal file
15
src/typings/api/GetUserItemApi.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { UserItem, UserItemKind } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
nextIndex: number | bigint
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
nextIndex: number | bigint
|
||||
itemKind: UserItemKind
|
||||
userItemList: UserItem[] | null // Array of user items, can be null if no items are present
|
||||
}
|
||||
14
src/typings/api/GetUserLoginBonusApi.ts
Normal file
14
src/typings/api/GetUserLoginBonusApi.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { UserLoginBonus } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
nextIndex: number | bigint
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
nextIndex: number | bigint
|
||||
userLoginBonusList: UserLoginBonus[] | null
|
||||
}
|
||||
14
src/typings/api/GetUserMapApi.ts
Normal file
14
src/typings/api/GetUserMapApi.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { UserMap } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
nextIndex: number | bigint
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
nextIndex: number | bigint
|
||||
userMapList: UserMap[] | null
|
||||
}
|
||||
11
src/typings/api/GetUserMissionDataApi.ts
Normal file
11
src/typings/api/GetUserMissionDataApi.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { UserMissionData, UserWeeklyData } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number // 用户 ID
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userWeeklyData: UserWeeklyData
|
||||
userMissionDataList: UserMissionData[]
|
||||
}
|
||||
19
src/typings/api/GetUserMusicApi.ts
Normal file
19
src/typings/api/GetUserMusicApi.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { UserMusicDetail } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
nextIndex: number | bigint
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
interface ScoreItem {
|
||||
userMusicDetailList: UserMusicDetail[] | null
|
||||
length: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
nextIndex: number | bigint
|
||||
userMusicList: ScoreItem[] | null
|
||||
}
|
||||
10
src/typings/api/GetUserOptionApi.ts
Normal file
10
src/typings/api/GetUserOptionApi.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { UserOption } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userOption: UserOption
|
||||
}
|
||||
28
src/typings/api/GetUserPreviewApi.ts
Normal file
28
src/typings/api/GetUserPreviewApi.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface Request {
|
||||
userId: number
|
||||
segaIdAuthKey: string
|
||||
token: string
|
||||
clientId: string
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userName: string
|
||||
isLogin: boolean
|
||||
lastGameId: number | null
|
||||
lastRomVersion: string
|
||||
lastDataVersion: string
|
||||
lastLoginDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
lastPlayDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
playerRating: number
|
||||
nameplateId: number
|
||||
iconId: number
|
||||
trophyId: number
|
||||
isNetMember: 1 | 0 // 1 for true, 0 for false
|
||||
isInherit: boolean
|
||||
totalAwake: number
|
||||
dispRate: number
|
||||
dailyBonusDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
headPhoneVolume: number | null // Nullable
|
||||
banState: 0 | 1 | 2 // 0 = not banned, 1 = warning, 2 = banned
|
||||
}
|
||||
10
src/typings/api/GetUserRatingApi.ts
Normal file
10
src/typings/api/GetUserRatingApi.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { UserRating } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userRating: UserRating
|
||||
}
|
||||
16
src/typings/api/GetUserRecommendRateMusicApi.ts
Normal file
16
src/typings/api/GetUserRecommendRateMusicApi.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MusicDifficultyID } from './base/MusicDifficultyID'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
interface RecommendRateMusicItem {
|
||||
musicId: number
|
||||
level: MusicDifficultyID
|
||||
averageAchievement: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userRecommendRateMusicIdList: RecommendRateMusicItem[]
|
||||
}
|
||||
8
src/typings/api/GetUserRecommendSelectMusicApi.ts
Normal file
8
src/typings/api/GetUserRecommendSelectMusicApi.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
userRecommendSelectionMusicIdList: number[]
|
||||
}
|
||||
15
src/typings/api/GetUserRegionApi.ts
Normal file
15
src/typings/api/GetUserRegionApi.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface Request {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface RegionItem {
|
||||
regionId: number
|
||||
playCount: number
|
||||
created: string
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
userId: number
|
||||
length: number
|
||||
userRegionList: RegionItem[] | null
|
||||
}
|
||||
127
src/typings/api/UploadUserPlaylogListApi.ts
Normal file
127
src/typings/api/UploadUserPlaylogListApi.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { MusicClearRankID } from './base'
|
||||
import { MusicDifficultyID } from './base'
|
||||
import { PlayComboFlagID } from './base'
|
||||
import { PlaySyncFlagID } from './base'
|
||||
import { UdemaeID } from './base'
|
||||
|
||||
export interface Playlog {
|
||||
userId: number // 0
|
||||
orderId: number // 0,未使用
|
||||
playlogId: bigint
|
||||
version: number
|
||||
placeId: number
|
||||
placeName: string
|
||||
loginDate: number
|
||||
playDate: string // yyyy-mm-dd
|
||||
userPlayDate: string // yyyy-mm-dd hh:mm:ss.0
|
||||
type: number // 来自谱面文件, 未使用
|
||||
musicId: number
|
||||
level: MusicDifficultyID
|
||||
trackNo: number // starting from 1
|
||||
vsMode: number // 0 = 无, 1 = 友人对战, 2 = npc, 3 = 其它
|
||||
vsStatus: number // 0 = 无, 1 = 我方胜利, 2 = 对方胜利
|
||||
vsUserName: string // 友人对战时的对手名称
|
||||
vsUserRating: number // 对方 rating
|
||||
vsUserAchievement: number // 对方达成率
|
||||
vsUserGradeRank: number // 对方段位 (框: 待分析)
|
||||
vsRank: number // 对方 rank?
|
||||
playerNum: number // 玩家是几人模式
|
||||
playedUserId1: number // 玩家2的用户ID
|
||||
playedUserName1: string // 玩家2的名称
|
||||
playedMusicLevel1: MusicDifficultyID // 玩家2的音乐难度
|
||||
playedUserId2: number // 玩家3的用户ID
|
||||
playedUserName2: string // 玩家3的名称
|
||||
playedMusicLevel2: MusicDifficultyID // 玩家3的音乐难度
|
||||
playedUserId3: number // 玩家4的用户ID
|
||||
playedUserName3: string // 玩家4的名称
|
||||
playedMusicLevel3: MusicDifficultyID // 玩家4的音乐难度
|
||||
characterId1: number // 当前玩家 的 角色1 id
|
||||
characterAwakening1: number // 当前玩家 的 角色1 觉醒等级
|
||||
characterLevel1: number
|
||||
characterId2: number // 当前玩家 的 角色2 id
|
||||
characterAwakening2: number // 当前玩家 的 角色2 觉醒等级
|
||||
characterLevel2: number
|
||||
characterId3: number // 当前玩家 的 角色3 id
|
||||
characterAwakening3: number // 当前玩家 的 角色3 觉醒等级
|
||||
characterLevel3: number
|
||||
characterId4: number // 当前玩家 的 角色4 id
|
||||
characterAwakening4: number // 当前玩家 的 角色4 觉醒等级
|
||||
characterLevel4: number
|
||||
characterId5: number // 当前玩家 的 角色5 id
|
||||
characterAwakening5: number // 当前玩家 的 角色5 觉醒等级
|
||||
characterLevel5: number
|
||||
achievement: number // 达成率
|
||||
deluxscore: number // dx 分
|
||||
scoreRank: MusicClearRankID // 评级
|
||||
maxCombo: number // 最大连击
|
||||
totalCombo: number // 总连击
|
||||
maxSync: number // 最大同步
|
||||
totalSync: number // 总同步
|
||||
tapCriticalPerfect: number // tap critical perfect 数
|
||||
tapPerfect: number // tap perfect 数
|
||||
tapGreat: number // tap great 数
|
||||
tapGood: number // tap good 数
|
||||
tapMiss: number // tap miss 数
|
||||
holdCriticalPerfect: number // hold critical perfect 数
|
||||
holdPerfect: number // hold perfect 数
|
||||
holdGreat: number // hold great 数
|
||||
holdGood: number // hold good 数
|
||||
holdMiss: number // hold miss 数
|
||||
slideCriticalPerfect: number // slide critical perfect 数
|
||||
slidePerfect: number // slide perfect 数
|
||||
slideGreat: number // slide great 数
|
||||
slideGood: number // slide good 数
|
||||
slideMiss: number // slide miss 数
|
||||
touchCriticalPerfect: number // touch critical perfect 数
|
||||
touchPerfect: number // touch perfect 数
|
||||
touchGreat: number // touch great 数
|
||||
touchGood: number // touch good 数
|
||||
touchMiss: number // touch miss 数
|
||||
breakCriticalPerfect: number // 绝赞 critical perfect 数
|
||||
breakPerfect: number // 绝赞 perfect 数
|
||||
breakGreat: number // 绝赞 great 数
|
||||
breakGood: number // 绝赞 good 数
|
||||
breakMiss: number // 绝赞 miss 数
|
||||
isTap: boolean // 铺面有无 tap
|
||||
isHold: boolean // 铺面有无 hold
|
||||
isSlide: boolean // 铺面有无 slide
|
||||
isTouch: boolean // 铺面有无 touch
|
||||
isBreak: boolean // 铺面有无绝赞
|
||||
isCriticalDisp: boolean // 有无 critical 显示
|
||||
isFastLateDisp: boolean // 有无 fast/late 显示 (一定为 true)
|
||||
fastCount: number // fast 数
|
||||
lateCount: number // late 数
|
||||
isAchieveNewRecord: boolean // 是否达成新记录
|
||||
isDeluxscoreNewRecord: boolean // 是否达成 dx 分新记录
|
||||
comboStatus: PlayComboFlagID // 连击状态
|
||||
syncStatus: PlaySyncFlagID // 同步状态
|
||||
isClear: boolean // 是否通关
|
||||
beforeRating: number // 通关前 rating
|
||||
afterRating: number // 通关后 rating
|
||||
beforeGrade: number // 通关前友人对战 pt
|
||||
afterGrade: number // 通关后友人对战 pt
|
||||
afterGradeRank: UdemaeID // 通关后友人对战等级(B5, B4, etc.)
|
||||
beforeDeluxRating: number // 通关前 dx rating 同 beforeRating
|
||||
afterDeluxRating: number // 通关后 dx rating 同 afterRating
|
||||
isPlayTutorial: boolean // 是否为教程
|
||||
isEventMode: boolean // 是否为大会模式
|
||||
isFreedomMode: boolean // 是否为自由模式
|
||||
playMode: number // 0 = 通常, 1 = 段位考核, 2 = 自由模式
|
||||
isNewFree: boolean // 是否为首局免费
|
||||
trialPlayAchievement: number // 教程达成率 没玩设置为 -1
|
||||
extNum1: number // 完美挑战曲开始生命 * 10000 - 当前生命
|
||||
extNum2: number // 段位考核 id 或 0 (如果没有使用段位考核模式)
|
||||
extNum4: number // category id
|
||||
extBool1: boolean // 是否为宴会场 buddies 谱
|
||||
extBool2: boolean // 是否随机选择
|
||||
}
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
userPlaylogList: Playlog[]
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
returnCode: number
|
||||
apiName: string
|
||||
}
|
||||
93
src/typings/api/UpsertUserAllApi.ts
Normal file
93
src/typings/api/UpsertUserAllApi.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Playlog } from './UploadUserPlaylogListApi'
|
||||
import {
|
||||
MusicDifficultyID,
|
||||
UserCharacter,
|
||||
UserCharge,
|
||||
UserCourse,
|
||||
UserDetail,
|
||||
UserExtend,
|
||||
UserFavorite,
|
||||
UserGamePlaylog,
|
||||
UserGhost,
|
||||
UserItem,
|
||||
UserLoginBonus,
|
||||
UserMap,
|
||||
UserMusicDetail,
|
||||
UserOption,
|
||||
UserRating,
|
||||
UserActivity,
|
||||
UserMissionData,
|
||||
UserWeeklyData,
|
||||
UserIntimate,
|
||||
UserShopStock,
|
||||
UserGetPoint,
|
||||
UserTradeItem,
|
||||
UserFavoriteItem,
|
||||
UserKaleidxScope
|
||||
} from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
playlogId: bigint
|
||||
isEventMode: boolean
|
||||
isFreePlay: boolean
|
||||
loginDateTime: number
|
||||
userPlaylogList: Playlog[]
|
||||
upsertUserAll: {
|
||||
userData: (UserDetail & { banState: number })[]
|
||||
userExtend: UserExtend[]
|
||||
userOption: UserOption[]
|
||||
userCharacterList: UserCharacter[]
|
||||
userGhost: UserGhost[]
|
||||
userMapList: UserMap[]
|
||||
userLoginBonusList: UserLoginBonus[]
|
||||
userRatingList: UserRating[]
|
||||
userItemList: UserItem[]
|
||||
userMusicDetailList: UserMusicDetail[]
|
||||
userCourseList: UserCourse[]
|
||||
userFriendSeasonRankingList: unknown[] // TODO: Define type NOTE: 友人对战在 SDGB 未使用,原因为政策问题
|
||||
userChargeList: UserCharge[]
|
||||
userFavoriteList: UserFavorite[]
|
||||
userActivityList: UserActivity[]
|
||||
userMissionDataList: UserMissionData[]
|
||||
userWeeklyData: UserWeeklyData
|
||||
userGamePlaylogList: UserGamePlaylog[]
|
||||
user2pPlaylog: {
|
||||
userId1: number
|
||||
userId2: number
|
||||
userName1: string
|
||||
userName2: string
|
||||
regionId: number
|
||||
placeId: number
|
||||
user2pPlaylogDetailList: {
|
||||
musicId: number
|
||||
level: MusicDifficultyID
|
||||
achievement: number
|
||||
deluxscore: number
|
||||
userPlayDate: string // "yyyy-MM-dd HH:mm:ss.0"
|
||||
}[]
|
||||
}
|
||||
userIntimateList: UserIntimate[]
|
||||
userShopItemStockList: UserShopStock[]
|
||||
userGetPointList: UserGetPoint[]
|
||||
userTradeItemList: UserTradeItem[]
|
||||
userFavoritemusicList: UserFavoriteItem[]
|
||||
userKaleidxScopeList: UserKaleidxScope[]
|
||||
isNewCharacterList: string
|
||||
isNewMapList: string
|
||||
isNewLoginBonusList: string
|
||||
isNewItemList: string
|
||||
isNewMusicDetailList: string
|
||||
isNewCourseList: string
|
||||
isNewFavoriteList: string
|
||||
isNewFriendSeasonRankingList: string // 未使用
|
||||
isNewUserIntimateList: string
|
||||
isNewFavoritemusicList: string // 一定为 "0" (奇异搞笑)
|
||||
isNewKaleidxScopeList: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
returnCode: number
|
||||
apiName: string
|
||||
}
|
||||
12
src/typings/api/UpsertUserChargelogApi.ts
Normal file
12
src/typings/api/UpsertUserChargelogApi.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { UserCharge, UserChargelog } from './base'
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
userChargelog: UserChargelog
|
||||
userCharge: UserCharge
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
returnCode: number
|
||||
apiName: string
|
||||
}
|
||||
22
src/typings/api/UserLoginApi.ts
Normal file
22
src/typings/api/UserLoginApi.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface Request {
|
||||
userId: number
|
||||
accessCode: string
|
||||
regionId: number
|
||||
placeId: number
|
||||
clientId: string
|
||||
dateTime: number
|
||||
loginDateTime: number
|
||||
isContinue: boolean
|
||||
genericFlag: number
|
||||
token: string
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
returnCode: number
|
||||
loginDateTime: number
|
||||
token: string
|
||||
lastLoginDate: string
|
||||
loginCount: number
|
||||
consecutiveLoginCount: number
|
||||
loginId: bigint
|
||||
}
|
||||
22
src/typings/api/UserLogoutApi.ts
Normal file
22
src/typings/api/UserLogoutApi.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export enum LogoutType {
|
||||
None = 0,
|
||||
Logout = 1,
|
||||
Cancel = 2,
|
||||
Error = 3,
|
||||
TestIn = 4,
|
||||
Quit = 5
|
||||
}
|
||||
|
||||
export interface Request {
|
||||
userId: number
|
||||
accessCode: string
|
||||
regionId: number
|
||||
placeId: number
|
||||
clientId: string
|
||||
loginDateTime: number
|
||||
type: LogoutType
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
returnCode: number
|
||||
}
|
||||
16
src/typings/api/base/MusicClearRankID.ts
Normal file
16
src/typings/api/base/MusicClearRankID.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export enum MusicClearRankID {
|
||||
Rank_D = 0,
|
||||
Rank_C = 1,
|
||||
Rank_B = 2,
|
||||
Rank_BB = 3,
|
||||
Rank_BBB = 4,
|
||||
Rank_A = 5,
|
||||
Rank_AA = 6,
|
||||
Rank_AAA = 7,
|
||||
Rank_S = 8,
|
||||
Rank_SP = 9, // S+
|
||||
Rank_SS = 10,
|
||||
Rank_SSP = 11, // SS+
|
||||
Rank_SSS = 12,
|
||||
Rank_SSSP = 13 // SSS+
|
||||
}
|
||||
9
src/typings/api/base/MusicDifficultyID.ts
Normal file
9
src/typings/api/base/MusicDifficultyID.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum MusicDifficultyID {
|
||||
Basic = 0,
|
||||
Advanced = 1,
|
||||
Expert = 2,
|
||||
Master = 3,
|
||||
ReMaster = 4,
|
||||
// Strong = 5, // Strong is not used in the game
|
||||
Utage = 10
|
||||
}
|
||||
7
src/typings/api/base/PlayComboFlagID.ts
Normal file
7
src/typings/api/base/PlayComboFlagID.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum PlayComboFlagID {
|
||||
None = 0,
|
||||
FullCombo = 1, // Full Combo (Silver)
|
||||
FullComboPlus = 2, // Full Combo+ (Gold)
|
||||
AllPerfect = 3, // All Perfect
|
||||
AllPerfectPlus = 4 // All Perfect+
|
||||
}
|
||||
8
src/typings/api/base/PlaySyncFlagID.ts
Normal file
8
src/typings/api/base/PlaySyncFlagID.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export enum PlaySyncFlagID {
|
||||
None = 0,
|
||||
FullSync = 1, // ChainLow
|
||||
FullSyncPlus = 2, // ChainHi
|
||||
FullSyncDX = 3, // SyncLow
|
||||
FullSyncDXPlus = 4, // SyncHi
|
||||
SyncPlay = 5
|
||||
}
|
||||
28
src/typings/api/base/UdemaeID.ts
Normal file
28
src/typings/api/base/UdemaeID.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export enum UdemaeID {
|
||||
Class_B5 = 0,
|
||||
Class_B4 = 1,
|
||||
Class_B3 = 2,
|
||||
Class_B2 = 3,
|
||||
Class_B1 = 4,
|
||||
Class_A5 = 5,
|
||||
Class_A4 = 6,
|
||||
Class_A3 = 7,
|
||||
Class_A2 = 8,
|
||||
Class_A1 = 9,
|
||||
Class_S5 = 10,
|
||||
Class_S4 = 11,
|
||||
Class_S3 = 12,
|
||||
Class_S2 = 13,
|
||||
Class_S1 = 14,
|
||||
Class_SS5 = 15,
|
||||
Class_SS4 = 16,
|
||||
Class_SS3 = 17,
|
||||
Class_SS2 = 18,
|
||||
Class_SS1 = 19,
|
||||
Class_SSS5 = 20,
|
||||
Class_SSS4 = 21,
|
||||
Class_SSS3 = 22,
|
||||
Class_SSS2 = 23,
|
||||
Class_SSS1 = 24,
|
||||
Class_LEGEND = 25
|
||||
}
|
||||
42
src/typings/api/base/UserActivity.ts
Normal file
42
src/typings/api/base/UserActivity.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export interface UserAct {
|
||||
kind: 1 | 2 // 1: PlayResult, 2: PlayMusic
|
||||
id: ActivityCode
|
||||
sortNumber: number
|
||||
param1: number
|
||||
param2: number
|
||||
param3: number
|
||||
param4: number
|
||||
}
|
||||
|
||||
export enum ActivityCode {
|
||||
PlayDX = 10, // 0, 0, 0, 0
|
||||
RankS = 20, //
|
||||
RankSP = 21,
|
||||
RankSS = 22,
|
||||
RankSSP = 23,
|
||||
RankSSS = 24,
|
||||
RankSSSP = 25,
|
||||
FullCombo = 30,
|
||||
FullComboP = 31,
|
||||
AllPerfect = 32,
|
||||
AllPerfectP = 33,
|
||||
FullSync = 40,
|
||||
FullSyncP = 41,
|
||||
FullSyncDx = 42,
|
||||
FullSyncDxP = 43,
|
||||
ClassUp_old = 50,
|
||||
DxRate = 60,
|
||||
AwakeMax = 70,
|
||||
AwakePreMax = 71,
|
||||
MapComplete = 80,
|
||||
TransmissionMusic = 100,
|
||||
TaskMusicClear = 110,
|
||||
ChallengeMusicClear = 120,
|
||||
RankUp = 130,
|
||||
ClassUp = 140
|
||||
}
|
||||
|
||||
export interface UserActivity {
|
||||
playList: UserAct[] // 最大长度: 15
|
||||
musicList: UserAct[] // 最大长度: 10
|
||||
}
|
||||
7
src/typings/api/base/UserCharacter.ts
Normal file
7
src/typings/api/base/UserCharacter.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface UserCharacter {
|
||||
characterId: number
|
||||
point: number // 累计点数
|
||||
level: number
|
||||
awakening: number
|
||||
useCount: number // 使用其游玩的 pc 数
|
||||
}
|
||||
19
src/typings/api/base/UserCharge.ts
Normal file
19
src/typings/api/base/UserCharge.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface UserChargelog {
|
||||
chargeId: number
|
||||
price: number
|
||||
purchaseDate: string
|
||||
// Provided by the actual client but unnecessary for the API. It is possible to provide it for bypassing manual anticheat.
|
||||
playCount?: number
|
||||
// Provided by the actual client but unnecessary for the API. It is possible to provide it for bypassing manual anticheat.
|
||||
playerRating?: number
|
||||
placeId: number
|
||||
regionId: number
|
||||
clientId: string
|
||||
}
|
||||
|
||||
export interface UserCharge {
|
||||
chargeId: number
|
||||
stock: number
|
||||
purchaseDate: string
|
||||
validDate: string
|
||||
}
|
||||
15
src/typings/api/base/UserCourse.ts
Normal file
15
src/typings/api/base/UserCourse.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface UserCourse {
|
||||
courseId: number
|
||||
isLastClear: boolean
|
||||
totalRestlife: number
|
||||
totalAchievement: number
|
||||
totalDeluxscore: number
|
||||
bestAchievement: number
|
||||
bestDeluxscore: number
|
||||
bestAchievementDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
bestDeluxscoreDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
playCount: number
|
||||
clearDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
lastPlayDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
extNum1: number // TODO: 分析
|
||||
}
|
||||
86
src/typings/api/base/UserDetail.ts
Normal file
86
src/typings/api/base/UserDetail.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { UdemaeID } from './UdemaeID'
|
||||
|
||||
export interface UserDetail {
|
||||
accessCode: string | null
|
||||
userName: string
|
||||
isNetMember: 1 | 0
|
||||
point: number
|
||||
totalPoint: number
|
||||
playerRating: number
|
||||
playerOldRating: number
|
||||
playerNewRating: number
|
||||
highestRating: number
|
||||
gradeRating: number
|
||||
musicRating: number
|
||||
gradeRank: UdemaeID
|
||||
courseRank: number
|
||||
classRank: number
|
||||
frameId: number
|
||||
iconId: number
|
||||
// trophyId: number
|
||||
plateId: number
|
||||
titleId: number
|
||||
partnerId: number
|
||||
charaSlot: number[]
|
||||
charaLockSlot: number[]
|
||||
contentBit: number
|
||||
selectMapId: number
|
||||
playCount: number
|
||||
currentPlayCount: number
|
||||
playVsCount: number
|
||||
playSyncCount: number
|
||||
winCount: number
|
||||
helpCount: number
|
||||
comboCount: number
|
||||
totalDeluxscore: number
|
||||
totalBasicDeluxscore: number
|
||||
totalAdvancedDeluxscore: number
|
||||
totalExpertDeluxscore: number
|
||||
totalMasterDeluxscore: number
|
||||
totalReMasterDeluxscore: number
|
||||
totalSync: number
|
||||
totalBasicSync: number
|
||||
totalAdvancedSync: number
|
||||
totalExpertSync: number
|
||||
totalMasterSync: number
|
||||
totalReMasterSync: number
|
||||
totalAchievement: number
|
||||
totalBasicAchievement: number
|
||||
totalAdvancedAchievement: number
|
||||
totalExpertAchievement: number
|
||||
totalMasterAchievement: number
|
||||
totalReMasterAchievement: number
|
||||
eventWatchedDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
lastGameId: string | null
|
||||
lastRomVersion: string
|
||||
lastDataVersion: string
|
||||
lastLoginDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
lastPlayDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
lastPairLoginDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
lastTrialPlayDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
lastPlayCredit: number
|
||||
lastPlayMode: number
|
||||
lastPlaceId: number
|
||||
lastPlaceName: string | null
|
||||
lastAllNetId: number
|
||||
lastRegionId: number
|
||||
lastRegionName: string
|
||||
lastClientId: string | null
|
||||
lastCountryCode: string
|
||||
lastSelectEMoney: number
|
||||
lastSelectTicket: number
|
||||
lastSelectCourse: number
|
||||
lastCountCourse: number
|
||||
firstGameId: string
|
||||
firstRomVersion: string
|
||||
firstDataVersion: string
|
||||
firstPlayDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
compatibleCmVersion: string
|
||||
totalAwake: number
|
||||
dailyBonusDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
dailyCourseBonusDate: string // "YYYY-MM-DD HH:mm:ss"
|
||||
mapStock: number
|
||||
renameCredit: number
|
||||
friendRegistSkip: number
|
||||
dateTime: number | null
|
||||
}
|
||||
16
src/typings/api/base/UserExtend.ts
Normal file
16
src/typings/api/base/UserExtend.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface UserExtend {
|
||||
selectMusicId: number
|
||||
selectDifficultyId: number
|
||||
categoryIndex: number
|
||||
musicIndex: number
|
||||
extraFlag: number
|
||||
selectScoreType: number
|
||||
selectResultDetails: boolean
|
||||
selectResultScoreViewType: number
|
||||
sortCategorySetting: number
|
||||
sortMusicSetting: number
|
||||
selectedCardList: number[]
|
||||
encountMapNpcList: { npcId: number; musicId: number }[]
|
||||
extendContentBit: number
|
||||
playStatusSetting: number
|
||||
}
|
||||
5
src/typings/api/base/UserFavorite.ts
Normal file
5
src/typings/api/base/UserFavorite.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface UserFavorite {
|
||||
userId?: number // 0 TODO: 分析
|
||||
itemKind: number
|
||||
itemIdList: number[]
|
||||
}
|
||||
4
src/typings/api/base/UserFavoriteItem.ts
Normal file
4
src/typings/api/base/UserFavoriteItem.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface UserFavoriteItem {
|
||||
orderId: number // 下标
|
||||
id: number // 乐曲 id
|
||||
}
|
||||
16
src/typings/api/base/UserGamePlaylog.ts
Normal file
16
src/typings/api/base/UserGamePlaylog.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface UserGamePlaylog {
|
||||
playlogId: bigint
|
||||
version: string
|
||||
playDate: string // "yyyy-MM-dd HH:mm:ss.0"
|
||||
playMode: number
|
||||
useTicketId: number
|
||||
playCredit: number
|
||||
playTrack: number
|
||||
clientId: string
|
||||
isPlayTutorial: boolean
|
||||
isEventMode: boolean
|
||||
isNewFree: boolean
|
||||
playCount: number
|
||||
playSpecial: number
|
||||
playOtherUserId: number
|
||||
}
|
||||
10
src/typings/api/base/UserGetPoint.ts
Normal file
10
src/typings/api/base/UserGetPoint.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export enum MaiMileGetKind {
|
||||
Mission = 1,
|
||||
FriendBonus = 2,
|
||||
Present = 3
|
||||
}
|
||||
|
||||
export interface UserGetPoint {
|
||||
getKind: MaiMileGetKind // 获得方式
|
||||
point: number // 获得的点数
|
||||
}
|
||||
24
src/typings/api/base/UserGhost.ts
Normal file
24
src/typings/api/base/UserGhost.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { MusicDifficultyID } from './MusicDifficultyID'
|
||||
import { UdemaeID } from './UdemaeID'
|
||||
|
||||
export interface UserGhost {
|
||||
name: string
|
||||
iconId: number
|
||||
plateId: number
|
||||
titleId: number
|
||||
rate: number
|
||||
udemaeRate: number
|
||||
courseRank: number
|
||||
classRank: UdemaeID
|
||||
classValue: number
|
||||
playDatetime: string
|
||||
shopId: number
|
||||
regionCode: number
|
||||
typeId: MusicDifficultyID
|
||||
musicId: number
|
||||
difficulty: number // TODO: お願いだから誰か分析してくれ
|
||||
version: number
|
||||
resultBitList: number[]
|
||||
resultNum: number
|
||||
achievement: number
|
||||
}
|
||||
5
src/typings/api/base/UserIntimate.ts
Normal file
5
src/typings/api/base/UserIntimate.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface UserIntimate {
|
||||
partnerId: number // 伙伴 ID
|
||||
intimateLevel: number // 亲密度等级
|
||||
intimateCountRewarded: number // 下一个会得到奖励的亲密度等级
|
||||
}
|
||||
21
src/typings/api/base/UserItem.ts
Normal file
21
src/typings/api/base/UserItem.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export enum UserItemKind {
|
||||
Plate = 1, // Name plate
|
||||
Title = 2, // Title
|
||||
Icon = 3, // User icon
|
||||
Present = 4, // Present
|
||||
Music = 5, // Music (DX Charts)
|
||||
MusicMas = 6, // Music (Master Difficulty)
|
||||
MusicRem = 7, // Music (Re:Master Difficulty)
|
||||
MusicSrg = 8, // (Unused) Music (Strong Difficulty)
|
||||
Character = 9, // Character
|
||||
Partner = 10, // Partner
|
||||
Frame = 11, // Frame
|
||||
Ticket = 12 // Ticket
|
||||
}
|
||||
|
||||
export interface UserItem {
|
||||
itemKind: UserItemKind
|
||||
itemId: number
|
||||
stock: number
|
||||
isValid: boolean
|
||||
}
|
||||
20
src/typings/api/base/UserKaleidxScope.ts
Normal file
20
src/typings/api/base/UserKaleidxScope.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface UserKaleidxScope {
|
||||
gateId: number // 门 ID
|
||||
isGateFound: boolean // 是否已发现门
|
||||
isKeyFound: boolean // 是否已发现钥匙
|
||||
isClear: boolean // 是否已通关
|
||||
|
||||
// 以下没打设置为 0
|
||||
totalRestLife: number // 剩余生命
|
||||
totalAchievement: number // 总达成率
|
||||
totalDeluxscore: number // 总 dx 分数
|
||||
bestAchievement: number // 最佳达成率
|
||||
bestDeluxscore: number // 最佳 dx 分数
|
||||
bestAchievementDate: string // "YYYY-MM-DD HH:mm:ss.0" 没玩设置为空
|
||||
bestDeluxscoreDate: string // "YYYY-MM-DD HH:mm:ss.0" 没玩设置为空
|
||||
playCount: number // 游戏次数
|
||||
clearDate: string // "YYYY-MM-DD HH:mm:ss.0" 没玩设置为空
|
||||
lastPlayDate: string // "YYYY-MM-DD HH:mm:ss.0" 没玩设置为空
|
||||
|
||||
isInfoWatched: boolean // 是否已观看信息
|
||||
}
|
||||
6
src/typings/api/base/UserLoginBonus.ts
Normal file
6
src/typings/api/base/UserLoginBonus.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface UserLoginBonus {
|
||||
bonusId: number
|
||||
point: number
|
||||
isCurrent: boolean
|
||||
isComplete: boolean
|
||||
}
|
||||
8
src/typings/api/base/UserMap.ts
Normal file
8
src/typings/api/base/UserMap.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface UserMap {
|
||||
mapId: number
|
||||
distance: number
|
||||
isLock: boolean
|
||||
isClear: boolean
|
||||
isComplete: boolean
|
||||
unlockFlag: 0 | 1 // 0: Not unlocked, 1: Unlocked
|
||||
}
|
||||
22
src/typings/api/base/UserMissionData.ts
Normal file
22
src/typings/api/base/UserMissionData.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { MusicClearRankID } from './MusicClearRankID'
|
||||
|
||||
export enum MissionLevelID {
|
||||
Level1 = 0, // 任务难度 1
|
||||
Level2 = 1 // 任务难度 2
|
||||
}
|
||||
|
||||
export enum MissionTypeID {
|
||||
Login = 0, // 登入指定天数
|
||||
MusicPlay = 1, // 游玩曲目 (游玩指定曲目?)
|
||||
Buddy = 2 // 拼机 (游玩指定曲目?)
|
||||
}
|
||||
|
||||
export interface UserMissionData {
|
||||
type: MissionTypeID
|
||||
difficulty: MissionLevelID
|
||||
targetGenreId: number // 1 = 在 type 为 MusicPlay 的情况下,任务指定了流派
|
||||
targetGenreTableId: number // 在 type 不是 Login 的情况下,可指定乐曲 ID, 否则设置为 0
|
||||
conditionGenreId: number // 1 = 在 type 为 MusicPlay 的情况下,任务指定了流派
|
||||
conditionGenreTableId: MusicClearRankID // 在游玩乐曲条件下需要至少达到的评级 (sega 程序员在这里整了两个完全相同的 enum,奇异搞笑,因此使用 ClearRank 替代)
|
||||
clearFlag: boolean // 是否已完成任务
|
||||
}
|
||||
17
src/typings/api/base/UserMusicDetail.ts
Normal file
17
src/typings/api/base/UserMusicDetail.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MusicClearRankID } from './MusicClearRankID'
|
||||
import { MusicDifficultyID } from './MusicDifficultyID'
|
||||
import { PlayComboFlagID } from './PlayComboFlagID'
|
||||
import { PlaySyncFlagID } from './PlaySyncFlagID'
|
||||
|
||||
export interface UserMusicDetail {
|
||||
musicId: number
|
||||
level: MusicDifficultyID
|
||||
playCount: number
|
||||
achievement: number
|
||||
comboStatus: PlayComboFlagID
|
||||
syncStatus: PlaySyncFlagID
|
||||
deluxscoreMax: number
|
||||
scoreRank: MusicClearRankID
|
||||
extNum1: number // 理论值次数
|
||||
// extNum2: number // 0, 未使用
|
||||
}
|
||||
243
src/typings/api/base/UserOption.ts
Normal file
243
src/typings/api/base/UserOption.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
export enum OptionKindID {
|
||||
Basic = 0, // 适合新手的设置
|
||||
Advance = 1, // 适合中级玩家的设置
|
||||
Expert = 2, // 适合高级玩家的设置
|
||||
Custom = 3 // 自定义设置
|
||||
}
|
||||
|
||||
export enum OptionGameTapID {
|
||||
Default = 0, // 默认 (dx)
|
||||
Legacy = 1, // 经典
|
||||
Bear = 2, // 滴蜡熊 note
|
||||
Bar = 3, // 条形
|
||||
Any = 4 // tapくん?
|
||||
}
|
||||
|
||||
export enum OptionGameHoldID {
|
||||
Default = 0, // 默认 (dx)
|
||||
Legacy = 1 // 经典
|
||||
}
|
||||
|
||||
export enum OptionGameSlideID {
|
||||
Default = 0, // 默认 (dx)
|
||||
Legacy = 1 // 经典
|
||||
}
|
||||
|
||||
export enum OptionStarTypeID {
|
||||
Blue = 0, // 蓝色星星头
|
||||
Red = 1 // 红色星星头
|
||||
}
|
||||
|
||||
export enum OptionGameOutlineID {
|
||||
Hide = 0, // 隐藏
|
||||
Dot = 1, // 散点
|
||||
Simple = 2, // 简洁
|
||||
Sensor = 3, // 传感器
|
||||
Maimai = 4, // maimai
|
||||
GreeN = 5, // maimai GreeN
|
||||
ORANGE = 6, // maimai ORANGE
|
||||
PiNK = 7, // maimai PiNK
|
||||
MURASAKi = 8, // maimai MURASAKi
|
||||
MiLK = 9, // maimai MiLK
|
||||
FiNALE = 10, // maimai FiNALE
|
||||
DX = 11, // maimai DX (无印)
|
||||
Splash = 12, // maimai DX Splash
|
||||
UNiVERSE = 13, // maimai DX UNiVERSE
|
||||
FESTiVAL = 14, // maimai DX FESTiVAL
|
||||
BUDDiES = 15, // maimai DX BUDDiES
|
||||
PRiSM = 16 // maimai DX PRiSM
|
||||
}
|
||||
|
||||
export enum OptionNoteSizeID {
|
||||
Small = 0,
|
||||
Middle = 1,
|
||||
Big = 2
|
||||
}
|
||||
|
||||
export type OptionSlideSizeID = OptionNoteSizeID
|
||||
|
||||
export enum OptionTouchSizeID {
|
||||
Small = 0, // 小
|
||||
Middle = 1 // 中
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionMirrorID {
|
||||
Normal = 0, // 正常
|
||||
LR = 1, // 左右镜像
|
||||
UD = 2, // 上下镜像
|
||||
UDLR = 3 // 上下左右镜像
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionMovieBrightnessID {
|
||||
Dark = 0, // 暗
|
||||
Darker = 1, // 较暗
|
||||
Brighter = 2,
|
||||
Bright = 3 // 亮
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionDispRateID {
|
||||
AllDisp = 0, // 全部显示
|
||||
DispRateDan = 1, // 显示 rating 和段位
|
||||
DispRateClass = 2, // 显示 rating 和友人对战等级
|
||||
DispDanClass = 3, // 显示段位和友人对战等级
|
||||
DispRate = 4, // 显示 rating
|
||||
DispDan = 5, // 显示段位
|
||||
DispClass = 6, // 显示友人对战等级
|
||||
Hide = 7 // 隐藏
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionCenterDisplayID {
|
||||
Off = 0, // 关闭
|
||||
Combo = 1, // 显示连击
|
||||
AchievementPlus = 2, // 显示达成率 (0%+)
|
||||
AchievementMinus1 = 3, // 显示达成率 (100%-)
|
||||
AchievementMinus2 = 4, // 显示达成率 (101%-)
|
||||
SBorder = 5, // 显示距离 S 评级最低达成率的容差
|
||||
SSBorder = 6, // 显示距离 SS 评级最低达成率的容差
|
||||
SSSBorder = 7, // 显示距离 SSS 评级最低达成率的容差
|
||||
BestBorder = 8, // 显示距离最佳达成率的容差
|
||||
DeluxScore = 9, // 显示 DX 分数 (+)
|
||||
DeluxScoreMinus = 10, // 显示 DX 分数 (-)
|
||||
DeluxScoreStar = 11 // 显示 DX 分数 (-, 带星级显示)
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionDispChainID {
|
||||
Off = 0, // 关闭
|
||||
Achievement = 1, // VS 达成率
|
||||
Sync = 2 // Sync 连击数
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionTrackSkipID {
|
||||
Off = 0, // 关闭
|
||||
Push = 1, // 按钮跳过
|
||||
AutoS = 2, // 自动 (S)
|
||||
AutoSS = 3, // 自动 (SS)
|
||||
AutoSSS = 4, // 自动 (SSS)
|
||||
AutoBest = 5, // 自动 (个人最佳成绩)
|
||||
AutoLife300 = 6, // 自动 (完美挑战 300 生命值)
|
||||
AutoLife100 = 7, // 自动 (完美挑战 100 生命值)
|
||||
AutoLife50 = 8, // 自动 (完美挑战 50 生命值)
|
||||
AutoLife10 = 9, // 自动 (完美挑战 10 生命值)
|
||||
AutoLife1 = 10 // 自动 (完美挑战 1 生命值)
|
||||
}
|
||||
|
||||
export enum OptionTouchEffectID {
|
||||
Off = 0, // 关闭
|
||||
Outline = 1, // 仅在外圈显示
|
||||
On = 2 // 开启
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionSubMonitorID {
|
||||
AnimationType1 = 0, // 动画类型1
|
||||
CharacterOnly = 1,
|
||||
AchievementOnly = 2
|
||||
// 无效
|
||||
}
|
||||
export enum OptionSubMonitorAchievementID {
|
||||
AchievementPlus = 0, // 达成率 (0%+)
|
||||
AchievementMinus = 1 // 达成率 (101%-)
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionAppealID {
|
||||
Off = 0, // 关闭
|
||||
Together = 1, // 不一起来玩吗!
|
||||
Tiho = 2, // 一起前进吗?
|
||||
GoldPass = 3, // 想玩 4 曲小分队!
|
||||
FullSync = 4, // 以完全同步为目标吧!
|
||||
AllPlay = 5 // 全制霸者募集!
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum OptionOutFrameDisplayID {
|
||||
Off = 0, // 关闭
|
||||
AchievementPlus = 1, // 显示达成率 (0%+)
|
||||
AchievementMinus1 = 2, // 显示达成率 (100%-)
|
||||
AchievementMinus2 = 3, // 显示达成率 (101%-)
|
||||
DxScorePlus = 4, // 显示 DX 分数 (+)
|
||||
DxScoreMinus = 5, // 显示 DX 分数 (-)
|
||||
FastLate = 6, // 显示 Fast/Late
|
||||
Judge = 7 // 显示详细判定
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum SortTabID {
|
||||
Genre = 0, // 流派
|
||||
All = 1, // 全曲
|
||||
Version = 2, // 版本
|
||||
Level = 3, // 等级
|
||||
Name = 4, // 名称
|
||||
Rank = 5 // 评级
|
||||
// 无效
|
||||
}
|
||||
|
||||
export enum SortMusicID {
|
||||
ID = 0, // 按追加顺序?
|
||||
Level = 1, // 按等级
|
||||
Rank = 2, // 按评级
|
||||
ApFc = 3, // 按完成状态 (AP/FC)
|
||||
Sync = 4, // 按同步状态
|
||||
Name = 5, // 按名称
|
||||
DxScore = 6, // 按 DX 分数
|
||||
BPM = 7 // 按 BPM
|
||||
// 无效
|
||||
}
|
||||
|
||||
export interface UserOption {
|
||||
optionKind: OptionKindID
|
||||
noteSpeed: number // 0 = 1.0, 1 = 1.25, 2 = 1.5, 3 = 1.75, 4 = 2.0 ..., 36 = 10.0, 37 = Sonic 速, -1 = Invalid
|
||||
slideSpeed: number // 0 = -1.0, 1 = -0.9, ..., 20 = +1.0, -1 = Invalid
|
||||
touchSpeed: number // touch 速度,同 noteSpeed
|
||||
noteSize: OptionNoteSizeID
|
||||
slideSize: OptionSlideSizeID
|
||||
touchSize: OptionTouchSizeID
|
||||
tapDesign: OptionGameTapID
|
||||
holdDesign: OptionGameHoldID
|
||||
slideDesign: OptionGameSlideID
|
||||
starType: OptionStarTypeID
|
||||
starRotate: number // 0 = 关闭, 1 = 开启
|
||||
adjustTiming: number // A判 0 = -2.0, 1 = -1.9, ..., 40 = +2.0, -1 = Invalid
|
||||
judgeTiming: number // B判 0 = -2.0, 1 = -1.9, ..., 40 = +2.0, -1 = Invalid
|
||||
mirrorMode: OptionMirrorID
|
||||
ansVolume: number // 正解音 0 = 静音, 最大 5, -1 = Invalid
|
||||
// tempoVolume: number // 节拍 0 = 静音, 最大 5, -1 = Invalid
|
||||
tapHoldVolume: number // tap hold 0 = 静音, 最大 5, -1 = Invalid
|
||||
touchHoldVolume: number // touch hold 0 = 静音, 最大 5, -1 = Invalid
|
||||
breakVolume: number // 绝赞 0 = 静音, 最大 5, -1 = Invalid
|
||||
exVolume: number // extap 0 = 静音, 最大 5, -1 = Invalid
|
||||
slideVolume: number // slide 0 = 静音, 最大 5, -1 = Invalid
|
||||
breakSe: number // 音效种类 不分析
|
||||
slideSe: number // 音效种类 不分析
|
||||
exSe: number // 音效种类 不分析
|
||||
criticalSe: number // 音效种类 不分析
|
||||
tapSe: number // 音效种类 不分析
|
||||
headPhoneVolume: number // 0 = 音量1, 最大 19, -1 = Invalid
|
||||
matching: number // 允许友人对战 0 = 关闭, 1 = 开启, -1 = Invalid
|
||||
brightness: OptionMovieBrightnessID
|
||||
dispRate: OptionDispRateID // profile 显示
|
||||
dispCenter: OptionCenterDisplayID
|
||||
dispJudge: number // 判定类型0 = 1A, 1 = 1B, 2 = IC, 3 = ID, 4 = IE, 5 = 2A, ..., 12 = 3D, -1 = Invalid
|
||||
dispJudgePos: number // 通常判定显示位置 0 = 关闭, 1 = 最内侧, 2 = 内侧, 3 = 中间, 4 = 外侧, 5 = 最外侧, -1 = Invalid
|
||||
dispJudgeTouchPos: number // touch 判定显示位置 0 = 关闭, 1 = 内侧, 2 = 外侧, -1 = Invalid
|
||||
dispChain: OptionDispChainID
|
||||
dispBar: number // 是否显示上方条 0 = 关闭, 1 = 开启, -1 = Invalid
|
||||
trackSkip: OptionTrackSkipID // track skip 设定
|
||||
touchEffect: OptionTouchEffectID // 触摸到屏幕时的效果
|
||||
outlineDesign: OptionGameOutlineID
|
||||
submonitorAnimation: OptionSubMonitorID // 上屏显示动画类型 (TODO: 待分析)
|
||||
submonitorAppeal: OptionAppealID
|
||||
submonitorAchive: OptionSubMonitorAchievementID // 上屏显示达成率
|
||||
sortTab: SortTabID // 曲目分页
|
||||
sortMusic: SortMusicID // 曲目排序方式
|
||||
damageSeVolume: number // 完美挑战伤害音 0 = 静音, 最大 5, -1 = Invalid
|
||||
touchVolume: number // touch 0 = 静音, 最大 5, -1 = Invalid
|
||||
outFrameType: OptionOutFrameDisplayID
|
||||
breakSlideVolume: number // 绝赞 slide 0 = 静音, 最大 5, -1 = Invalid
|
||||
}
|
||||
41
src/typings/api/base/UserRating.ts
Normal file
41
src/typings/api/base/UserRating.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { MusicDifficultyID } from './MusicDifficultyID'
|
||||
|
||||
export interface UserRate {
|
||||
musicId: number
|
||||
level: MusicDifficultyID
|
||||
romVersion: number
|
||||
achievement: number
|
||||
}
|
||||
|
||||
export interface UserRating {
|
||||
rating: number // 0?
|
||||
ratingList: UserRate[]
|
||||
newRatingList: UserRate[]
|
||||
nextRatingList: UserRate[]
|
||||
nextNewRatingList: UserRate[]
|
||||
udemae: {
|
||||
maxLoseNum: number
|
||||
npcTotalWinNum: number
|
||||
npcTotalLoseNum: number
|
||||
npcMaxWinNum: number
|
||||
npcMaxLoseNum: number
|
||||
npcWinNum: number
|
||||
npcLoseNum: number
|
||||
rate: number
|
||||
classValue: number
|
||||
maxRate: number
|
||||
maxClassValue: number
|
||||
totalWinNum: number
|
||||
totalLoseNum: number
|
||||
maxWinNum: number
|
||||
// MaxLoseNum: number
|
||||
winNum: number
|
||||
loseNum: number
|
||||
// NpcTotalWinNum: number
|
||||
// NpcTotalLoseNum: number
|
||||
// NpcMaxWinNum: number
|
||||
// NpcMaxLoseNum: number
|
||||
// NpcWinNum: number
|
||||
// NpcLoseNum: number
|
||||
}
|
||||
}
|
||||
4
src/typings/api/base/UserShopStock.ts
Normal file
4
src/typings/api/base/UserShopStock.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface UserShopStock {
|
||||
shopItemId: number // 商店物品 ID
|
||||
tradeCount: number // 交易次数
|
||||
}
|
||||
5
src/typings/api/base/UserTradeItem.ts
Normal file
5
src/typings/api/base/UserTradeItem.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface UserTradeItem {
|
||||
shopItemId: number // 商店物品 ID
|
||||
point: number // 交易所需点数
|
||||
tradeCount: number // 交易次数
|
||||
}
|
||||
5
src/typings/api/base/UserWeeklyData.ts
Normal file
5
src/typings/api/base/UserWeeklyData.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface UserWeeklyData {
|
||||
lastLoginWeek: string // "yyyy/mm/dd",从 GetGameWeeklyDataApi 下载
|
||||
beforeLoginWeek: string // "yyyy/mm/dd",从 GetGameWeeklyDataApi 下载
|
||||
friendBonusFlag: boolean // 是否有好友奖励
|
||||
}
|
||||
28
src/typings/api/base/index.ts
Normal file
28
src/typings/api/base/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export * from './MusicClearRankID'
|
||||
export * from './MusicDifficultyID'
|
||||
export * from './PlayComboFlagID'
|
||||
export * from './PlaySyncFlagID'
|
||||
export * from './UdemaeID'
|
||||
export * from './UserCharacter'
|
||||
export * from './UserCharge'
|
||||
export * from './UserCourse'
|
||||
export * from './UserDetail'
|
||||
export * from './UserExtend'
|
||||
export * from './UserFavorite'
|
||||
export * from './UserGamePlaylog'
|
||||
export * from './UserGhost'
|
||||
export * from './UserItem'
|
||||
export * from './UserLoginBonus'
|
||||
export * from './UserMap'
|
||||
export * from './UserMusicDetail'
|
||||
export * from './UserOption'
|
||||
export * from './UserRating'
|
||||
export * from './UserActivity'
|
||||
export * from './UserMissionData'
|
||||
export * from './UserWeeklyData'
|
||||
export * from './UserIntimate'
|
||||
export * from './UserShopStock'
|
||||
export * from './UserGetPoint'
|
||||
export * from './UserFavoriteItem'
|
||||
export * from './UserTradeItem'
|
||||
export * from './UserKaleidxScope'
|
||||
81
src/typings/api/index.ts
Normal file
81
src/typings/api/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as GetUserPreviewApi from './GetUserPreviewApi'
|
||||
import * as UserLoginApi from './UserLoginApi'
|
||||
import * as GetUserDataApi from './GetUserDataApi'
|
||||
import * as GetUserCardApi from './GetUserCardApi'
|
||||
import * as GetUserCharacterApi from './GetUserCharacterApi'
|
||||
import * as GetUserItemApi from './GetUserItemApi'
|
||||
import * as GetUserCourseApi from './GetUserCourseApi'
|
||||
import * as GetUserChargeApi from './GetUserChargeApi'
|
||||
import * as GetUserRatingApi from './GetUserRatingApi'
|
||||
import * as GetUserMusicApi from './GetUserMusicApi'
|
||||
import * as GetUserActivityApi from './GetUserActivityApi'
|
||||
import * as GetUserExtendApi from './GetUserExtendApi'
|
||||
import * as GetUserOptionApi from './GetUserOptionApi'
|
||||
import * as GetUserRecommendSelectMusicApi from './GetUserRecommendSelectMusicApi'
|
||||
import * as GetUserRecommendRateMusicApi from './GetUserRecommendRateMusicApi'
|
||||
import * as GetUserRegionApi from './GetUserRegionApi'
|
||||
import * as GetUserFavoriteApi from './GetUserFavoriteApi'
|
||||
import * as GetUserFavoriteItemApi from './GetUserFavoriteItemApi'
|
||||
import * as GetUserGhostApi from './GetUserGhostApi'
|
||||
import * as GetUserMapApi from './GetUserMapApi'
|
||||
import * as GetUserLoginBonusApi from './GetUserLoginBonusApi'
|
||||
import * as UserLogoutApi from './UserLogoutApi'
|
||||
import * as UpsertUserChargelogApi from './UpsertUserChargelogApi'
|
||||
import * as UploadUserPlaylogListApi from './UploadUserPlaylogListApi'
|
||||
import * as UpsertUserAllApi from './UpsertUserAllApi'
|
||||
import * as GetUserMissionDataApi from './GetUserMissionDataApi'
|
||||
|
||||
export * from './base'
|
||||
|
||||
export interface Api {
|
||||
GetUserPreviewApi: [GetUserPreviewApi.Request, GetUserPreviewApi.Response]
|
||||
UserLoginApi: [UserLoginApi.Request, UserLoginApi.Response]
|
||||
GetUserDataApi: [GetUserDataApi.Request, GetUserDataApi.Response]
|
||||
GetUserCardApi: [GetUserCardApi.Request, GetUserCardApi.Response]
|
||||
GetUserCharacterApi: [
|
||||
GetUserCharacterApi.Request,
|
||||
GetUserCharacterApi.Response
|
||||
]
|
||||
GetUserItemApi: [GetUserItemApi.Request, GetUserItemApi.Response]
|
||||
GetUserCourseApi: [GetUserCourseApi.Request, GetUserCourseApi.Response]
|
||||
GetUserChargeApi: [GetUserChargeApi.Request, GetUserChargeApi.Response]
|
||||
GetUserRatingApi: [GetUserRatingApi.Request, GetUserRatingApi.Response]
|
||||
GetUserMusicApi: [GetUserMusicApi.Request, GetUserMusicApi.Response]
|
||||
GetUserActivityApi: [GetUserActivityApi.Request, GetUserActivityApi.Response]
|
||||
GetUserExtendApi: [GetUserExtendApi.Request, GetUserExtendApi.Response]
|
||||
GetUserOptionApi: [GetUserOptionApi.Request, GetUserOptionApi.Response]
|
||||
GetUserRecommendSelectMusicApi: [
|
||||
GetUserRecommendSelectMusicApi.Request,
|
||||
GetUserRecommendSelectMusicApi.Response
|
||||
]
|
||||
GetUserRecommendRateMusicApi: [
|
||||
GetUserRecommendRateMusicApi.Request,
|
||||
GetUserRecommendRateMusicApi.Response
|
||||
]
|
||||
GetUserRegionApi: [GetUserRegionApi.Request, GetUserRegionApi.Response]
|
||||
GetUserFavoriteApi: [GetUserFavoriteApi.Request, GetUserFavoriteApi.Response]
|
||||
GetUserFavoriteItemApi: [
|
||||
GetUserFavoriteItemApi.Request,
|
||||
GetUserFavoriteItemApi.Response
|
||||
]
|
||||
GetUserGhostApi: [GetUserGhostApi.Request, GetUserGhostApi.Response]
|
||||
GetUserMapApi: [GetUserMapApi.Request, GetUserMapApi.Response]
|
||||
GetUserLoginBonusApi: [
|
||||
GetUserLoginBonusApi.Request,
|
||||
GetUserLoginBonusApi.Response
|
||||
]
|
||||
UserLogoutApi: [UserLogoutApi.Request, UserLogoutApi.Response]
|
||||
UpsertUserChargelogApi: [
|
||||
UpsertUserChargelogApi.Request,
|
||||
UpsertUserChargelogApi.Response
|
||||
]
|
||||
UploadUserPlaylogListApi: [
|
||||
UploadUserPlaylogListApi.Request,
|
||||
UploadUserPlaylogListApi.Response
|
||||
]
|
||||
UpsertUserAllApi: [UpsertUserAllApi.Request, UpsertUserAllApi.Response]
|
||||
GetUserMissionDataApi: [
|
||||
GetUserMissionDataApi.Request,
|
||||
GetUserMissionDataApi.Response
|
||||
]
|
||||
}
|
||||
1648
src/user.ts
Normal file
1648
src/user.ts
Normal file
File diff suppressed because it is too large
Load Diff
38
src/utils/date.ts
Normal file
38
src/utils/date.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export function toLocalDateTimeString(
|
||||
date: Date,
|
||||
includeMilliseconds = false
|
||||
): string {
|
||||
return toUTCDateTimeString(
|
||||
new Date(date.getTime() - date.getTimezoneOffset() * 60000),
|
||||
includeMilliseconds
|
||||
)
|
||||
}
|
||||
|
||||
export function toUTCDateTimeString(
|
||||
date: Date,
|
||||
includeMilliseconds = false
|
||||
): string {
|
||||
return (
|
||||
date.toISOString().split('.')[0].replace('T', ' ') +
|
||||
(includeMilliseconds ? '.0' : '')
|
||||
)
|
||||
}
|
||||
|
||||
export function toUTCDateString(date: Date): string {
|
||||
return date.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
export function toLocalDateString(date: Date): string {
|
||||
return toLocalDateTimeString(date).split(' ')[0]
|
||||
}
|
||||
|
||||
export function fromLocalDateTimeString(dateString: string): Date {
|
||||
return new Date(
|
||||
fromUTCDateTimeString(dateString).getTime() +
|
||||
new Date().getTimezoneOffset() * 60000
|
||||
)
|
||||
}
|
||||
|
||||
export function fromUTCDateTimeString(dateString: string): Date {
|
||||
return new Date(dateString.replace(' ', 'T') + 'Z')
|
||||
}
|
||||
86
src/utils/event.ts
Normal file
86
src/utils/event.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
export class DoneEvent<T> extends Event {
|
||||
public readonly result: T
|
||||
constructor(result: T) {
|
||||
super('done')
|
||||
this.result = result
|
||||
}
|
||||
}
|
||||
|
||||
export interface AwaitableEventMap<T> extends Record<string, any[]> {
|
||||
done: [DoneEvent<T>]
|
||||
error: [CustomEvent]
|
||||
}
|
||||
|
||||
export class AwaitableEventEmitter<
|
||||
ResultT,
|
||||
T extends AwaitableEventMap<ResultT>
|
||||
> implements PromiseLike<ResultT>
|
||||
{
|
||||
private listeners: {
|
||||
[K in keyof T]?: ((...args: T[K]) => void)[]
|
||||
} = {}
|
||||
private result?: Promise<ResultT>
|
||||
|
||||
on<K extends keyof T>(event: K, listener: (...args: T[K]) => void) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = []
|
||||
}
|
||||
this.listeners[event]!.push(listener)
|
||||
}
|
||||
|
||||
off<K extends keyof T>(event: K, listener: (...args: T[K]) => void) {
|
||||
if (!this.listeners[event]) return
|
||||
this.listeners[event] = this.listeners[event]!.filter(l => l !== listener)
|
||||
}
|
||||
|
||||
once<K extends keyof T>(event: K, listener: (...args: T[K]) => void) {
|
||||
const onceListener = (...args: T[K]) => {
|
||||
listener(...args)
|
||||
this.off(event, onceListener)
|
||||
}
|
||||
this.on(event, onceListener)
|
||||
}
|
||||
|
||||
emit<K extends keyof T>(event: K, ...args: T[K]) {
|
||||
if (!this.listeners[event]) return
|
||||
for (const listener of this.listeners[event]!) {
|
||||
listener(...args)
|
||||
}
|
||||
}
|
||||
|
||||
then<TResult1 = ResultT, TResult2 = never>(
|
||||
onfulfilled?:
|
||||
| ((value: ResultT) => TResult1 | PromiseLike<TResult1>)
|
||||
| null
|
||||
| undefined,
|
||||
onrejected?:
|
||||
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
|
||||
| null
|
||||
| undefined
|
||||
): PromiseLike<TResult1 | TResult2> {
|
||||
if (!this.result) {
|
||||
this.result = new Promise<ResultT>((resolve, reject) => {
|
||||
this.once('done', event => resolve(event.result))
|
||||
this.once('error', event => reject(event.detail))
|
||||
})
|
||||
}
|
||||
return this.result.then(onfulfilled, onrejected)
|
||||
}
|
||||
|
||||
static fromAsync<
|
||||
T,
|
||||
EventMap extends AwaitableEventMap<T> = AwaitableEventMap<T>
|
||||
>(promise: PromiseLike<T> | (() => PromiseLike<T>)) {
|
||||
if (typeof promise === 'function') promise = promise()
|
||||
const emitter = new AwaitableEventEmitter<T, EventMap>()
|
||||
promise.then(
|
||||
result => {
|
||||
emitter.emit('done', new DoneEvent(result))
|
||||
},
|
||||
error => {
|
||||
emitter.emit('error', new CustomEvent('error', { detail: error }))
|
||||
}
|
||||
)
|
||||
return emitter
|
||||
}
|
||||
}
|
||||
179
src/utils/http.ts
Normal file
179
src/utils/http.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import https from 'node:https'
|
||||
import http from 'node:http'
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
|
||||
export class HTTPResponse implements Response {
|
||||
public readonly status: number
|
||||
public readonly headers: Headers
|
||||
public readonly statusText: string
|
||||
get ok(): boolean {
|
||||
return this.status >= 200 && this.status < 300
|
||||
}
|
||||
get type(): ResponseType {
|
||||
return 'default'
|
||||
}
|
||||
|
||||
get redirected() {
|
||||
return false
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _incomingMessage: http.IncomingMessage,
|
||||
public readonly url: string
|
||||
) {
|
||||
this.status = _incomingMessage.statusCode || 200
|
||||
this.statusText = _incomingMessage.statusMessage || 'OK'
|
||||
this.headers = new Headers(
|
||||
Object.entries(_incomingMessage.headers).filter(v => !!v[1]) as [
|
||||
string,
|
||||
string
|
||||
][]
|
||||
)
|
||||
this.body = new ReadableStream<Uint8Array<ArrayBuffer>>({
|
||||
start: controller => {
|
||||
this._incomingMessage.on('data', chunk => {
|
||||
controller.enqueue(
|
||||
new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.length)
|
||||
)
|
||||
})
|
||||
this._incomingMessage.on('end', () => {
|
||||
controller.close()
|
||||
})
|
||||
this._incomingMessage.on('error', err => {
|
||||
controller.error(err)
|
||||
})
|
||||
},
|
||||
pull: () => {
|
||||
// No-op, as the stream is pulled automatically by the consumer
|
||||
},
|
||||
cancel: () => {
|
||||
this._incomingMessage.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
clone(): HTTPResponse {
|
||||
throw new Error('HTTPResponse does not support cloning')
|
||||
}
|
||||
public readonly body: ReadableStream<Uint8Array<ArrayBuffer>>
|
||||
private _bodyUsed: boolean = false
|
||||
public get bodyUsed(): boolean {
|
||||
return this._bodyUsed
|
||||
}
|
||||
private async _consumeBody(): Promise<Uint8Array<ArrayBuffer>> {
|
||||
if (this._bodyUsed) {
|
||||
return Promise.reject(new TypeError('Body already used'))
|
||||
}
|
||||
this._bodyUsed = true
|
||||
const reader = this.body.getReader()
|
||||
const chunks: Uint8Array[] = []
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (value) chunks.push(value)
|
||||
}
|
||||
const length = chunks.reduce((acc, chunk) => acc + chunk.length, 0)
|
||||
const result = new Uint8Array(length)
|
||||
let offset = 0
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset)
|
||||
offset += chunk.length
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
const bytes = await this._consumeBody()
|
||||
return bytes.buffer
|
||||
}
|
||||
|
||||
async blob(): Promise<Blob> {
|
||||
const bytes = await this._consumeBody()
|
||||
return new Blob([bytes])
|
||||
}
|
||||
|
||||
async bytes(): Promise<Uint8Array<ArrayBuffer>> {
|
||||
return this._consumeBody()
|
||||
}
|
||||
|
||||
async formData(): Promise<FormData> {
|
||||
const textData = await this.text()
|
||||
const formData = new FormData()
|
||||
const params = new URLSearchParams(textData)
|
||||
for (const [key, value] of params) {
|
||||
formData.append(key, value)
|
||||
}
|
||||
return formData
|
||||
}
|
||||
|
||||
async json() {
|
||||
const textData = await this.text()
|
||||
return JSON.parse(textData)
|
||||
}
|
||||
|
||||
async text(): Promise<string> {
|
||||
const bytes = await this._consumeBody()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
return decoder.decode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
export function makeRequest(opt?: {
|
||||
proxy?: string | URL
|
||||
hosts?: Record<string, string>
|
||||
rejectUnauthorized?: boolean
|
||||
}) {
|
||||
if (typeof opt?.proxy === 'string') {
|
||||
opt.proxy = new URL(opt.proxy)
|
||||
}
|
||||
const agent =
|
||||
opt?.proxy?.protocol === 'http:' || opt?.proxy?.protocol === 'https:'
|
||||
? new HttpProxyAgent(opt.proxy)
|
||||
: opt?.proxy?.protocol === 'socks:' || opt?.proxy?.protocol === 'socks5:'
|
||||
? new SocksProxyAgent(opt.proxy)
|
||||
: undefined
|
||||
return function (
|
||||
url: string | URL,
|
||||
options?: {
|
||||
method?: 'GET' | 'POST'
|
||||
headers?: Record<string, string>
|
||||
body?: string | ArrayBuffer | Uint8Array<ArrayBuffer>
|
||||
}
|
||||
): Promise<HTTPResponse> {
|
||||
if (typeof url === 'string') {
|
||||
url = new URL(url)
|
||||
}
|
||||
const hostname = opt?.hosts?.[url.hostname] ?? url.hostname
|
||||
const client = (url.protocol === 'https:' ? https.request : http.request)({
|
||||
method: options?.method || 'GET',
|
||||
agent,
|
||||
rejectUnauthorized: opt?.rejectUnauthorized,
|
||||
host: `${hostname}`,
|
||||
port: url.port
|
||||
? parseInt(url.port)
|
||||
: url.protocol === 'https:'
|
||||
? 443
|
||||
: 80,
|
||||
path: url.pathname,
|
||||
search: url.search,
|
||||
headers: Object.assign(
|
||||
{
|
||||
host: url.host
|
||||
},
|
||||
options?.headers || {}
|
||||
)
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
client.on('response', res => {
|
||||
resolve(new HTTPResponse(res, url.toString()))
|
||||
})
|
||||
client.on('error', err => {
|
||||
reject(err)
|
||||
})
|
||||
if (options?.body) {
|
||||
client.write(options.body)
|
||||
}
|
||||
client.end()
|
||||
})
|
||||
}
|
||||
}
|
||||
53
src/utils/iterator.ts
Normal file
53
src/utils/iterator.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export interface IteratorCompatible<KeyT, ValueT> {
|
||||
keys(): IterableIterator<KeyT>
|
||||
get(key: KeyT): ValueT | undefined
|
||||
}
|
||||
|
||||
export function values<
|
||||
KeyT,
|
||||
ValueT,
|
||||
T extends IteratorCompatible<KeyT, ValueT>
|
||||
>(target: T): Iterable<ValueT> {
|
||||
return {
|
||||
[Symbol.iterator]: (): Iterator<ValueT> => {
|
||||
const keys = Array.from(target.keys())
|
||||
let index = 0
|
||||
return {
|
||||
next: () => {
|
||||
if (index < keys.length) {
|
||||
const key = keys[index]
|
||||
index++
|
||||
return { value: target.get(key) as ValueT, done: false }
|
||||
}
|
||||
return { value: undefined, done: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function entries<
|
||||
KeyT,
|
||||
ValueT,
|
||||
T extends IteratorCompatible<KeyT, ValueT>
|
||||
>(target: T): Iterable<[KeyT, ValueT]> {
|
||||
return {
|
||||
[Symbol.iterator]: (): Iterator<[KeyT, ValueT]> => {
|
||||
const keys = Array.from(target.keys())
|
||||
let index = 0
|
||||
return {
|
||||
next: () => {
|
||||
if (index < keys.length) {
|
||||
const key = keys[index]
|
||||
const value = target.get(key)
|
||||
if (value === undefined)
|
||||
throw new Error(`Unexpected undefined value for key: ${key}`)
|
||||
index++
|
||||
return { value: [key, value], done: false }
|
||||
}
|
||||
return { value: undefined, done: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext", // Use ESNext as the output
|
||||
"module": "nodenext", // Keep commonjs for Node; change to "esnext" if you want native ESM output
|
||||
"lib": ["esnext", "dom"], // Update lib to ESNext features
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
12
tsup.config.ts
Normal file
12
tsup.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'], // Entry point for your library/application
|
||||
format: ['cjs', 'esm'], // Bundle in CommonJS and ES Module formats
|
||||
outDir: 'dist',
|
||||
dts: true, // Generate TypeScript declaration files
|
||||
name: 'MaimaiAPI',
|
||||
target: 'esnext',
|
||||
clean: true, // Clean output directory before each build
|
||||
minify: false // Set to true if you need minification
|
||||
})
|
||||
Reference in New Issue
Block a user