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