replace zxq.co/ripple/hanayo
This commit is contained in:
620
vendor/zxq.co/ripple/agplwarning/LICENSE
vendored
Normal file
620
vendor/zxq.co/ripple/agplwarning/LICENSE
vendored
Normal file
@@ -0,0 +1,620 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU 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
|
||||
|
5
vendor/zxq.co/ripple/agplwarning/README.md
vendored
Normal file
5
vendor/zxq.co/ripple/agplwarning/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# agplwarning
|
||||
|
||||
Show a warning in your Go programs regarding the use of AGPL software.
|
||||
|
||||
[](https://asciinema.org/a/rUolzFDQebbWMPcaSgRQRTBEC)
|
82
vendor/zxq.co/ripple/agplwarning/agplwarning.go
vendored
Normal file
82
vendor/zxq.co/ripple/agplwarning/agplwarning.go
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
package agplwarning
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var pages = [...]string{
|
||||
`This is a friendly reminder that the GNU AGPL adds an additional clause to
|
||||
the standard GNU GPL, which is that you MUST distribute the source code for the
|
||||
software once you publish it on the web.
|
||||
This is not to be considered professional legal advice. For further
|
||||
information, refer to the LICENSE file which contains the whole license, or ask
|
||||
your lawyer. If you did not receive a copy of the LICENSE file with this
|
||||
software, you can refer to the online version:
|
||||
https://www.gnu.org/licenses/agpl-3.0.html`,
|
||||
`In order to comply with the license, should you have made any modification
|
||||
to the original copy of the software, which should contain a link to the
|
||||
source code, however minor it is, you are under the legal obligation to provide
|
||||
the source code once you publish the software on the Web.
|
||||
Another obligation is that of stating your changes. This is usually done by
|
||||
cloning the original git repository of the project and stating your changes
|
||||
through the creation of commits, which allow us to determine when a specific
|
||||
change was done.`,
|
||||
`Furthermore, all the original clauses of the GNU General Public License
|
||||
are kept intact, which means you have the obligation to
|
||||
* Keep the AGPL License, without possibility of sublicensing the software
|
||||
or making it available under any other more liberal license.
|
||||
* Keep the copyright notice of the original authors
|
||||
Failure to do so will result in a request to follow the License, and
|
||||
repeated violation of the license could result in a legal fight.`,
|
||||
`For more information on the FSF and software freedom, refer to:
|
||||
* What is free software? https://www.gnu.org/philosophy/free-sw.html
|
||||
* Free Software Is Even More Important Now
|
||||
https://www.gnu.org/philosophy/free-software-even-more-important.html
|
||||
* The GNU operating system https://www.gnu.org
|
||||
* The Free Software Foundation https://www.fsf.org
|
||||
Thank you for reading this and following our license terms.`,
|
||||
}
|
||||
|
||||
// Warn shows a warning about the GNU Affero General Public License the first
|
||||
// time the software is run. The state is saved in
|
||||
// ~/.config/[namespace]_license_agreed.
|
||||
func Warn(namespace, projectName string) error {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialization of agplwarning failed: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(usr.HomeDir+"/.config", 0755); err != nil {
|
||||
return fmt.Errorf("can't create config dir: %v", err)
|
||||
}
|
||||
agreedFilename := usr.HomeDir + "/.config/" + namespace + "_license_agreed"
|
||||
if _, err := os.Stat(agreedFilename); !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
// file does not exist. Show warning.
|
||||
fmt.Printf(" %s, and most/all software related to %s,\n"+
|
||||
"is licensed under the GNU Affero General Public License.\n\n", projectName, namespace)
|
||||
for _, page := range pages {
|
||||
fmt.Println(" " + page)
|
||||
fmt.Println("\nPress Enter to continue")
|
||||
_, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Println("Please write 'I agree' to accept the terms of the license.")
|
||||
res, err := reader.ReadString('\n')
|
||||
if err != nil || !strings.Contains(strings.ToLower(res), "i agree") {
|
||||
fmt.Println("License not agreed. Quitting.")
|
||||
os.Exit(1)
|
||||
}
|
||||
f, err := os.Create(agreedFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't save read status: %v", err)
|
||||
}
|
||||
return f.Close()
|
||||
}
|
15
vendor/zxq.co/ripple/agplwarning/example/main.go
vendored
Normal file
15
vendor/zxq.co/ripple/agplwarning/example/main.go
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"zxq.co/ripple/agplwarning"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := agplwarning.Warn("agplwarning", "AGPLWarning")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fmt.Println("License agreed")
|
||||
}
|
10
vendor/zxq.co/ripple/go-discord-oauth/discord.go
vendored
Normal file
10
vendor/zxq.co/ripple/go-discord-oauth/discord.go
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// Package discordoauth provides constant for using OAuth2 to access Discord.
|
||||
package discordoauth
|
||||
|
||||
import "golang.org/x/oauth2"
|
||||
|
||||
// Endpoint is Discord's OAuth 2.0 endpoint.
|
||||
var Endpoint = oauth2.Endpoint{
|
||||
AuthURL: "https://discordapp.com/api/oauth2/authorize",
|
||||
TokenURL: "https://discordapp.com/api/oauth2/token",
|
||||
}
|
18
vendor/zxq.co/ripple/playstyle/LICENSE
vendored
Normal file
18
vendor/zxq.co/ripple/playstyle/LICENSE
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
Copyright (c) 2016 Morgan Bazalgette
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
48
vendor/zxq.co/ripple/playstyle/playstyle.go
vendored
Normal file
48
vendor/zxq.co/ripple/playstyle/playstyle.go
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
// Package playstyle provides an enum for Ripple's playstyles.
|
||||
package playstyle
|
||||
|
||||
import "strings"
|
||||
|
||||
// PlayStyle is a bitwise enum containing the instruments a Ripple user likes
|
||||
// to play with.
|
||||
type PlayStyle int
|
||||
|
||||
// various playstyles on ripple.
|
||||
const (
|
||||
Mouse int = 1 << iota
|
||||
Tablet
|
||||
Keyboard
|
||||
Touchscreen
|
||||
Spoon
|
||||
LeapMotion
|
||||
OculusRift
|
||||
Dick
|
||||
Eggplant
|
||||
)
|
||||
|
||||
// Styles are string representations of the various playstyles someone can have.
|
||||
var Styles = [...]string{
|
||||
"Mouse",
|
||||
"Tablet",
|
||||
"Keyboard",
|
||||
"Touchscreen",
|
||||
"Spoon",
|
||||
"Leap motion",
|
||||
"Oculus rift",
|
||||
"Dick",
|
||||
"Eggplant",
|
||||
}
|
||||
|
||||
// String is the string representation of a playstyle.
|
||||
func (p PlayStyle) String() string {
|
||||
var parts []string
|
||||
|
||||
i := int(p)
|
||||
for k, v := range Styles {
|
||||
if i&(1<<uint(k)) > 0 {
|
||||
parts = append(parts, v)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
13
vendor/zxq.co/ripple/playstyle/playstyle_test.go
vendored
Normal file
13
vendor/zxq.co/ripple/playstyle/playstyle_test.go
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package playstyle
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPlayStyle(t *testing.T) {
|
||||
ps := PlayStyle((1 << 10) - 1)
|
||||
t.Log(ps.String())
|
||||
}
|
||||
func BenchmarkString(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
PlayStyle(i).String()
|
||||
}
|
||||
}
|
6
vendor/zxq.co/ripple/rippleapi/.gitignore
vendored
Normal file
6
vendor/zxq.co/ripple/rippleapi/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
rippleapi
|
||||
rippleapi.exe
|
||||
api
|
||||
api.conf
|
||||
debug
|
||||
launch.json
|
619
vendor/zxq.co/ripple/rippleapi/LICENSE
vendored
Normal file
619
vendor/zxq.co/ripple/rippleapi/LICENSE
vendored
Normal file
@@ -0,0 +1,619 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU 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
|
6
vendor/zxq.co/ripple/rippleapi/README.md
vendored
Normal file
6
vendor/zxq.co/ripple/rippleapi/README.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# rippleapi
|
||||
|
||||
This is the source code for Ripple's API.
|
||||
|
||||
- Origin: https://git.zxq.co/ripple/rippleapi
|
||||
- Mirror: https://github.com/osuripple/api
|
11
vendor/zxq.co/ripple/rippleapi/app/internals/status.go
vendored
Normal file
11
vendor/zxq.co/ripple/rippleapi/app/internals/status.go
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Package internals has methods that suit none of the API packages.
|
||||
package internals
|
||||
|
||||
import "github.com/valyala/fasthttp"
|
||||
|
||||
var statusResp = []byte(`{ "status": 1 }`)
|
||||
|
||||
// Status is used for checking the API is up by the ripple website, on the status page.
|
||||
func Status(c *fasthttp.RequestCtx) {
|
||||
c.Write(statusResp)
|
||||
}
|
144
vendor/zxq.co/ripple/rippleapi/app/method.go
vendored
Normal file
144
vendor/zxq.co/ripple/rippleapi/app/method.go
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// Method wraps an API method to a HandlerFunc.
|
||||
func Method(f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) fasthttp.RequestHandler {
|
||||
return func(c *fasthttp.RequestCtx) {
|
||||
initialCaretaker(c, f, privilegesNeeded...)
|
||||
}
|
||||
}
|
||||
|
||||
func initialCaretaker(c *fasthttp.RequestCtx, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
|
||||
var doggoTags []string
|
||||
|
||||
qa := c.Request.URI().QueryArgs()
|
||||
var token string
|
||||
var bearerToken bool
|
||||
switch {
|
||||
case len(c.Request.Header.Peek("X-Ripple-Token")) > 0:
|
||||
token = string(c.Request.Header.Peek("X-Ripple-Token"))
|
||||
case strings.HasPrefix(string(c.Request.Header.Peek("Authorization")), "Bearer "):
|
||||
token = strings.TrimPrefix(string(c.Request.Header.Peek("Authorization")), "Bearer ")
|
||||
bearerToken = true
|
||||
case len(qa.Peek("token")) > 0:
|
||||
token = string(qa.Peek("token"))
|
||||
case len(qa.Peek("k")) > 0:
|
||||
token = string(qa.Peek("k"))
|
||||
default:
|
||||
token = string(c.Request.Header.Cookie("rt"))
|
||||
}
|
||||
|
||||
md := common.MethodData{
|
||||
DB: db,
|
||||
Ctx: c,
|
||||
Doggo: doggo,
|
||||
R: red,
|
||||
}
|
||||
if token != "" {
|
||||
var (
|
||||
tokenReal common.Token
|
||||
exists bool
|
||||
)
|
||||
if bearerToken {
|
||||
tokenReal, exists = BearerToken(token, db)
|
||||
} else {
|
||||
tokenReal, exists = GetTokenFull(token, db)
|
||||
}
|
||||
if exists {
|
||||
md.User = tokenReal
|
||||
doggoTags = append(doggoTags, "authorised")
|
||||
}
|
||||
}
|
||||
|
||||
// log into datadog that this is an hanayo request
|
||||
if b2s(c.Request.Header.Peek("H-Key")) == cf.HanayoKey && b2s(c.UserAgent()) == "hanayo" {
|
||||
doggoTags = append(doggoTags, "hanayo")
|
||||
}
|
||||
|
||||
doggo.Incr("requests.v1", doggoTags, 1)
|
||||
|
||||
missingPrivileges := 0
|
||||
for _, privilege := range privilegesNeeded {
|
||||
if uint64(md.User.TokenPrivileges)&uint64(privilege) == 0 {
|
||||
missingPrivileges |= privilege
|
||||
}
|
||||
}
|
||||
if missingPrivileges != 0 {
|
||||
c.SetStatusCode(401)
|
||||
mkjson(c, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+"."))
|
||||
return
|
||||
}
|
||||
|
||||
resp := f(md)
|
||||
if md.HasQuery("pls200") {
|
||||
c.SetStatusCode(200)
|
||||
} else {
|
||||
c.SetStatusCode(resp.GetCode())
|
||||
}
|
||||
|
||||
if md.HasQuery("callback") {
|
||||
c.Response.Header.Add("Content-Type", "application/javascript; charset=utf-8")
|
||||
} else {
|
||||
c.Response.Header.Add("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
mkjson(c, resp)
|
||||
}
|
||||
|
||||
// Very restrictive, but this way it shouldn't completely fuck up.
|
||||
var callbackJSONP = regexp.MustCompile(`^[a-zA-Z_\$][a-zA-Z0-9_\$]*$`)
|
||||
|
||||
// mkjson auto indents json, and wraps json into a jsonp callback if specified by the request.
|
||||
// then writes to the RequestCtx the data.
|
||||
func mkjson(c *fasthttp.RequestCtx, data interface{}) {
|
||||
exported, err := json.MarshalIndent(data, "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
exported = []byte(`{ "code": 500, "message": "something has gone really really really really really really wrong." }`)
|
||||
}
|
||||
cb := string(c.URI().QueryArgs().Peek("callback"))
|
||||
willcb := cb != "" &&
|
||||
len(cb) < 100 &&
|
||||
callbackJSONP.MatchString(cb)
|
||||
if willcb {
|
||||
c.Write([]byte("/**/ typeof " + cb + " === 'function' && " + cb + "("))
|
||||
}
|
||||
c.Write(exported)
|
||||
if willcb {
|
||||
c.Write([]byte(");"))
|
||||
}
|
||||
}
|
||||
|
||||
// b2s converts byte slice to a string without memory allocation.
|
||||
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
// s2b converts string to a byte slice without memory allocation.
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func s2b(s string) []byte {
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh := reflect.SliceHeader{
|
||||
Data: sh.Data,
|
||||
Len: sh.Len,
|
||||
Cap: sh.Len,
|
||||
}
|
||||
return *(*[]byte)(unsafe.Pointer(&bh))
|
||||
}
|
133
vendor/zxq.co/ripple/rippleapi/app/peppy/beatmap.go
vendored
Normal file
133
vendor/zxq.co/ripple/rippleapi/app/peppy/beatmap.go
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/thehowl/go-osuapi"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// GetBeatmap retrieves general beatmap information.
|
||||
func GetBeatmap(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
var whereClauses []string
|
||||
var params []interface{}
|
||||
limit := strconv.Itoa(common.InString(1, query(c, "limit"), 500, 500))
|
||||
|
||||
// since value is not stored, silently ignore
|
||||
if query(c, "s") != "" {
|
||||
whereClauses = append(whereClauses, "beatmaps.beatmapset_id = ?")
|
||||
params = append(params, query(c, "s"))
|
||||
}
|
||||
if query(c, "b") != "" {
|
||||
whereClauses = append(whereClauses, "beatmaps.beatmap_id = ?")
|
||||
params = append(params, query(c, "b"))
|
||||
// b is unique, so change limit to 1
|
||||
limit = "1"
|
||||
}
|
||||
// creator is not stored, silently ignore u and type
|
||||
if query(c, "m") != "" {
|
||||
m := genmode(query(c, "m"))
|
||||
if m == "std" {
|
||||
// Since STD beatmaps are converted, all of the diffs must be != 0
|
||||
for _, i := range modes {
|
||||
whereClauses = append(whereClauses, "beatmaps.difficulty_"+i+" != 0")
|
||||
}
|
||||
} else {
|
||||
whereClauses = append(whereClauses, "beatmaps.difficulty_"+m+" != 0")
|
||||
if query(c, "a") == "1" {
|
||||
whereClauses = append(whereClauses, "beatmaps.difficulty_std = 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
if query(c, "h") != "" {
|
||||
whereClauses = append(whereClauses, "beatmaps.beatmap_md5 = ?")
|
||||
params = append(params, query(c, "h"))
|
||||
}
|
||||
|
||||
where := strings.Join(whereClauses, " AND ")
|
||||
if where != "" {
|
||||
where = "WHERE " + where
|
||||
}
|
||||
|
||||
rows, err := db.Query(`SELECT
|
||||
beatmapset_id, beatmap_id, ranked, hit_length,
|
||||
song_name, beatmap_md5, ar, od, bpm, playcount,
|
||||
passcount, max_combo, difficulty_std, difficulty_taiko, difficulty_ctb, difficulty_mania,
|
||||
latest_update
|
||||
|
||||
FROM beatmaps `+where+" ORDER BY id DESC LIMIT "+limit,
|
||||
params...)
|
||||
if err != nil {
|
||||
common.Err(c, err)
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
|
||||
var bms []osuapi.Beatmap
|
||||
for rows.Next() {
|
||||
var (
|
||||
bm osuapi.Beatmap
|
||||
rawRankedStatus int
|
||||
rawName string
|
||||
rawLastUpdate common.UnixTimestamp
|
||||
diffs [4]float64
|
||||
)
|
||||
err := rows.Scan(
|
||||
&bm.BeatmapSetID, &bm.BeatmapID, &rawRankedStatus, &bm.HitLength,
|
||||
&rawName, &bm.FileMD5, &bm.ApproachRate, &bm.OverallDifficulty, &bm.BPM, &bm.Playcount,
|
||||
&bm.Passcount, &bm.MaxCombo, &diffs[0], &diffs[1], &diffs[2], &diffs[3],
|
||||
&rawLastUpdate,
|
||||
)
|
||||
if err != nil {
|
||||
common.Err(c, err)
|
||||
continue
|
||||
}
|
||||
bm.TotalLength = bm.HitLength
|
||||
bm.LastUpdate = osuapi.MySQLDate(rawLastUpdate)
|
||||
if rawRankedStatus >= 2 {
|
||||
bm.ApprovedDate = osuapi.MySQLDate(rawLastUpdate)
|
||||
}
|
||||
// zero value of ApprovedStatus == osuapi.StatusPending, so /shrug
|
||||
bm.Approved = rippleToOsuRankedStatus[rawRankedStatus]
|
||||
bm.Artist, bm.Title, bm.DiffName = parseDiffName(rawName)
|
||||
for i, diffVal := range diffs {
|
||||
if diffVal != 0 {
|
||||
bm.Mode = osuapi.Mode(i)
|
||||
bm.DifficultyRating = diffVal
|
||||
break
|
||||
}
|
||||
}
|
||||
bms = append(bms, bm)
|
||||
}
|
||||
|
||||
json(c, 200, bms)
|
||||
}
|
||||
|
||||
var rippleToOsuRankedStatus = map[int]osuapi.ApprovedStatus{
|
||||
0: osuapi.StatusPending,
|
||||
1: osuapi.StatusWIP, // it means "needs updating", as the one in the db needs to be updated, but whatever
|
||||
2: osuapi.StatusRanked,
|
||||
3: osuapi.StatusApproved,
|
||||
4: osuapi.StatusQualified,
|
||||
5: osuapi.StatusLoved,
|
||||
}
|
||||
|
||||
// buggy diffname parser
|
||||
func parseDiffName(name string) (author string, title string, diffName string) {
|
||||
parts := strings.SplitN(name, " - ", 2)
|
||||
author = parts[0]
|
||||
if len(parts) > 1 {
|
||||
title = parts[1]
|
||||
if s := strings.Index(title, " ["); s != -1 {
|
||||
diffName = title[s+2:]
|
||||
if len(diffName) != 0 && diffName[len(diffName)-1] == ']' {
|
||||
diffName = diffName[:len(diffName)-1]
|
||||
}
|
||||
title = title[:s]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
75
vendor/zxq.co/ripple/rippleapi/app/peppy/common.go
vendored
Normal file
75
vendor/zxq.co/ripple/rippleapi/app/peppy/common.go
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_json "encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
var modes = []string{"std", "taiko", "ctb", "mania"}
|
||||
|
||||
var defaultResponse = []struct{}{}
|
||||
|
||||
func genmode(m string) string {
|
||||
i := genmodei(m)
|
||||
return modes[i]
|
||||
}
|
||||
func genmodei(m string) int {
|
||||
v := common.Int(m)
|
||||
if v > 3 || v < 0 {
|
||||
v = 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
func rankable(m string) bool {
|
||||
x := genmodei(m)
|
||||
return x != 2
|
||||
}
|
||||
|
||||
func genUser(c *fasthttp.RequestCtx, db *sqlx.DB) (string, string) {
|
||||
var whereClause string
|
||||
var p string
|
||||
|
||||
// used in second case of switch
|
||||
s, err := strconv.Atoi(query(c, "u"))
|
||||
|
||||
switch {
|
||||
// We know for sure that it's an username.
|
||||
case query(c, "type") == "string":
|
||||
whereClause = "users.username_safe = ?"
|
||||
p = common.SafeUsername(query(c, "u"))
|
||||
// It could be an user ID, so we look for an user with that username first.
|
||||
case err == nil:
|
||||
err = db.QueryRow("SELECT id FROM users WHERE id = ? LIMIT 1", s).Scan(&p)
|
||||
if err == sql.ErrNoRows {
|
||||
// If no user with that userID were found, assume username.
|
||||
whereClause = "users.username_safe = ?"
|
||||
p = common.SafeUsername(query(c, "u"))
|
||||
} else {
|
||||
// An user with that userID was found. Thus it's an userID.
|
||||
whereClause = "users.id = ?"
|
||||
}
|
||||
// u contains letters, so it's an username.
|
||||
default:
|
||||
whereClause = "users.username_safe = ?"
|
||||
p = common.SafeUsername(query(c, "u"))
|
||||
}
|
||||
return whereClause, p
|
||||
}
|
||||
|
||||
func query(c *fasthttp.RequestCtx, s string) string {
|
||||
return string(c.QueryArgs().Peek(s))
|
||||
}
|
||||
|
||||
func json(c *fasthttp.RequestCtx, code int, data interface{}) {
|
||||
c.SetStatusCode(code)
|
||||
d, err := _json.Marshal(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Write(d)
|
||||
}
|
12
vendor/zxq.co/ripple/rippleapi/app/peppy/match.go
vendored
Normal file
12
vendor/zxq.co/ripple/rippleapi/app/peppy/match.go
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// Package peppy implements the osu! API as defined on the osu-api repository wiki (https://github.com/ppy/osu-api/wiki).
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// GetMatch retrieves general match information.
|
||||
func GetMatch(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
json(c, 200, defaultResponse)
|
||||
}
|
98
vendor/zxq.co/ripple/rippleapi/app/peppy/score.go
vendored
Normal file
98
vendor/zxq.co/ripple/rippleapi/app/peppy/score.go
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
// GetScores retrieve information about the top 100 scores of a specified beatmap.
|
||||
func GetScores(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
if query(c, "b") == "" {
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
var beatmapMD5 string
|
||||
err := db.Get(&beatmapMD5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", query(c, "b"))
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
case err != nil:
|
||||
common.Err(c, err)
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
var sb = "scores.score"
|
||||
if rankable(query(c, "m")) {
|
||||
sb = "scores.pp"
|
||||
}
|
||||
var (
|
||||
extraWhere string
|
||||
extraParams []interface{}
|
||||
)
|
||||
if query(c, "u") != "" {
|
||||
w, p := genUser(c, db)
|
||||
extraWhere = "AND " + w
|
||||
extraParams = append(extraParams, p)
|
||||
}
|
||||
mods := common.Int(query(c, "mods"))
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
scores.id, scores.score, users.username, scores.300_count, scores.100_count,
|
||||
scores.50_count, scores.misses_count, scores.gekis_count, scores.katus_count,
|
||||
scores.max_combo, scores.full_combo, scores.mods, users.id, scores.time, scores.pp,
|
||||
scores.accuracy
|
||||
FROM scores
|
||||
INNER JOIN users ON users.id = scores.userid
|
||||
WHERE scores.completed = '3'
|
||||
AND users.privileges & 1 > 0
|
||||
AND scores.beatmap_md5 = ?
|
||||
AND scores.play_mode = ?
|
||||
AND scores.mods & ? = ?
|
||||
`+extraWhere+`
|
||||
ORDER BY `+sb+` DESC LIMIT `+strconv.Itoa(common.InString(1, query(c, "limit"), 100, 50)),
|
||||
append([]interface{}{beatmapMD5, genmodei(query(c, "m")), mods, mods}, extraParams...)...)
|
||||
if err != nil {
|
||||
common.Err(c, err)
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
var results []osuapi.GSScore
|
||||
for rows.Next() {
|
||||
var (
|
||||
s osuapi.GSScore
|
||||
fullcombo bool
|
||||
mods int
|
||||
date common.UnixTimestamp
|
||||
accuracy float64
|
||||
)
|
||||
err := rows.Scan(
|
||||
&s.ScoreID, &s.Score.Score, &s.Username, &s.Count300, &s.Count100,
|
||||
&s.Count50, &s.CountMiss, &s.CountGeki, &s.CountKatu,
|
||||
&s.MaxCombo, &fullcombo, &mods, &s.UserID, &date, &s.PP,
|
||||
&accuracy,
|
||||
)
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
common.Err(c, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.FullCombo = osuapi.OsuBool(fullcombo)
|
||||
s.Mods = osuapi.Mods(mods)
|
||||
s.Date = osuapi.MySQLDate(date)
|
||||
s.Rank = strings.ToUpper(getrank.GetRank(osuapi.Mode(genmodei(query(c, "m"))), s.Mods,
|
||||
accuracy, s.Count300, s.Count100, s.Count50, s.CountMiss))
|
||||
results = append(results, s)
|
||||
}
|
||||
json(c, 200, results)
|
||||
return
|
||||
}
|
64
vendor/zxq.co/ripple/rippleapi/app/peppy/user.go
vendored
Normal file
64
vendor/zxq.co/ripple/rippleapi/app/peppy/user.go
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// Package peppy implements the osu! API as defined on the osu-api repository wiki (https://github.com/ppy/osu-api/wiki).
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/thehowl/go-osuapi"
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/redis.v5"
|
||||
"zxq.co/ripple/ocl"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// R is a redis client.
|
||||
var R *redis.Client
|
||||
|
||||
// GetUser retrieves general user information.
|
||||
func GetUser(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
if query(c, "u") == "" {
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
var user osuapi.User
|
||||
whereClause, p := genUser(c, db)
|
||||
whereClause = "WHERE " + whereClause
|
||||
|
||||
mode := genmode(query(c, "m"))
|
||||
|
||||
err := db.QueryRow(fmt.Sprintf(
|
||||
`SELECT
|
||||
users.id, users.username,
|
||||
users_stats.playcount_%s, users_stats.ranked_score_%s, users_stats.total_score_%s,
|
||||
users_stats.pp_%s, users_stats.avg_accuracy_%s,
|
||||
users_stats.country
|
||||
FROM users
|
||||
LEFT JOIN users_stats ON users_stats.id = users.id
|
||||
%s
|
||||
LIMIT 1`,
|
||||
mode, mode, mode, mode, mode, whereClause,
|
||||
), p).Scan(
|
||||
&user.UserID, &user.Username,
|
||||
&user.Playcount, &user.RankedScore, &user.TotalScore,
|
||||
&user.PP, &user.Accuracy,
|
||||
&user.Country,
|
||||
)
|
||||
if err != nil {
|
||||
json(c, 200, defaultResponse)
|
||||
if err != sql.ErrNoRows {
|
||||
common.Err(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user.Rank = int(R.ZRevRank("ripple:leaderboard:"+mode, strconv.Itoa(user.UserID)).Val()) + 1
|
||||
user.CountryRank = int(R.ZRevRank("ripple:leaderboard:"+mode+":"+strings.ToLower(user.Country), strconv.Itoa(user.UserID)).Val()) + 1
|
||||
user.Level = ocl.GetLevelPrecise(user.TotalScore)
|
||||
|
||||
json(c, 200, []osuapi.User{user})
|
||||
}
|
95
vendor/zxq.co/ripple/rippleapi/app/peppy/user_x.go
vendored
Normal file
95
vendor/zxq.co/ripple/rippleapi/app/peppy/user_x.go
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
// GetUserRecent retrieves an user's recent scores.
|
||||
func GetUserRecent(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
getUserX(c, db, "ORDER BY scores.time DESC", common.InString(1, query(c, "limit"), 50, 10))
|
||||
}
|
||||
|
||||
// GetUserBest retrieves an user's best scores.
|
||||
func GetUserBest(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
var sb string
|
||||
if rankable(query(c, "m")) {
|
||||
sb = "scores.pp"
|
||||
} else {
|
||||
sb = "scores.score"
|
||||
}
|
||||
getUserX(c, db, "AND completed = '3' ORDER BY "+sb+" DESC", common.InString(1, query(c, "limit"), 100, 10))
|
||||
}
|
||||
|
||||
func getUserX(c *fasthttp.RequestCtx, db *sqlx.DB, orderBy string, limit int) {
|
||||
whereClause, p := genUser(c, db)
|
||||
sqlQuery := fmt.Sprintf(
|
||||
`SELECT
|
||||
beatmaps.beatmap_id, scores.score, scores.max_combo,
|
||||
scores.300_count, scores.100_count, scores.50_count,
|
||||
scores.gekis_count, scores.katus_count, scores.misses_count,
|
||||
scores.full_combo, scores.mods, users.id, scores.time,
|
||||
scores.pp, scores.accuracy
|
||||
FROM scores
|
||||
LEFT JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5
|
||||
LEFT JOIN users ON scores.userid = users.id
|
||||
WHERE %s AND scores.play_mode = ? AND users.privileges & 1 > 0
|
||||
%s
|
||||
LIMIT %d`, whereClause, orderBy, limit,
|
||||
)
|
||||
scores := make([]osuapi.GUSScore, 0, limit)
|
||||
m := genmodei(query(c, "m"))
|
||||
rows, err := db.Query(sqlQuery, p, m)
|
||||
if err != nil {
|
||||
json(c, 200, defaultResponse)
|
||||
common.Err(c, err)
|
||||
return
|
||||
}
|
||||
for rows.Next() {
|
||||
var (
|
||||
curscore osuapi.GUSScore
|
||||
rawTime common.UnixTimestamp
|
||||
acc float64
|
||||
fc bool
|
||||
mods int
|
||||
bid *int
|
||||
)
|
||||
err := rows.Scan(
|
||||
&bid, &curscore.Score.Score, &curscore.MaxCombo,
|
||||
&curscore.Count300, &curscore.Count100, &curscore.Count50,
|
||||
&curscore.CountGeki, &curscore.CountKatu, &curscore.CountMiss,
|
||||
&fc, &mods, &curscore.UserID, &rawTime,
|
||||
&curscore.PP, &acc,
|
||||
)
|
||||
if err != nil {
|
||||
json(c, 200, defaultResponse)
|
||||
common.Err(c, err)
|
||||
return
|
||||
}
|
||||
if bid == nil {
|
||||
curscore.BeatmapID = 0
|
||||
} else {
|
||||
curscore.BeatmapID = *bid
|
||||
}
|
||||
curscore.FullCombo = osuapi.OsuBool(fc)
|
||||
curscore.Mods = osuapi.Mods(mods)
|
||||
curscore.Date = osuapi.MySQLDate(rawTime)
|
||||
curscore.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(m),
|
||||
curscore.Mods,
|
||||
acc,
|
||||
curscore.Count300,
|
||||
curscore.Count100,
|
||||
curscore.Count50,
|
||||
curscore.CountMiss,
|
||||
))
|
||||
scores = append(scores, curscore)
|
||||
}
|
||||
json(c, 200, scores)
|
||||
}
|
19
vendor/zxq.co/ripple/rippleapi/app/peppy_method.go
vendored
Normal file
19
vendor/zxq.co/ripple/rippleapi/app/peppy_method.go
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// PeppyMethod generates a method for the peppyapi
|
||||
func PeppyMethod(a func(c *fasthttp.RequestCtx, db *sqlx.DB)) fasthttp.RequestHandler {
|
||||
return func(c *fasthttp.RequestCtx) {
|
||||
doggo.Incr("requests.peppy", nil, 1)
|
||||
|
||||
c.Response.Header.Add("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// I have no idea how, but I manged to accidentally string the first 4
|
||||
// letters of the alphabet into a single function call.
|
||||
a(c, db)
|
||||
}
|
||||
}
|
106
vendor/zxq.co/ripple/rippleapi/app/router.go
vendored
Normal file
106
vendor/zxq.co/ripple/rippleapi/app/router.go
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/buaazp/fasthttprouter"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type router struct {
|
||||
r *fasthttprouter.Router
|
||||
}
|
||||
|
||||
func (r router) Method(path string, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
|
||||
r.r.GET(path, wrap(Method(f, privilegesNeeded...)))
|
||||
}
|
||||
func (r router) POSTMethod(path string, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
|
||||
r.r.POST(path, wrap(Method(f, privilegesNeeded...)))
|
||||
}
|
||||
func (r router) Peppy(path string, a func(c *fasthttp.RequestCtx, db *sqlx.DB)) {
|
||||
r.r.GET(path, wrap(PeppyMethod(a)))
|
||||
}
|
||||
func (r router) GET(path string, handle fasthttp.RequestHandler) {
|
||||
r.r.GET(path, wrap(handle))
|
||||
}
|
||||
func (r router) PlainGET(path string, handle fasthttp.RequestHandler) {
|
||||
r.r.GET(path, handle)
|
||||
}
|
||||
|
||||
const (
|
||||
// \x1b is escape code for ESC
|
||||
// <ESC>[<n>m is escape sequence for a certain colour
|
||||
// no IP is written out because of the hundreds of possible ways to pass IPs
|
||||
// to a request when using a reverse proxy
|
||||
// this is partly inspired from gin, though made even more simplistic.
|
||||
fmtString = "%s | %15s |\x1b[%sm %3d \x1b[0m %-7s %s\n"
|
||||
// a kind of human readable RFC3339
|
||||
timeFormat = "2006-01-02 15:04:05"
|
||||
// color reference
|
||||
// http://misc.flogisoft.com/bash/tip_colors_and_formatting
|
||||
colorOk = "42" // green
|
||||
colorError = "41" // red
|
||||
)
|
||||
|
||||
// wrap returns a function that wraps around handle, providing middleware
|
||||
// functionality to apply to all API calls, which is to say:
|
||||
// - logging
|
||||
// - panic recovery (reporting to sentry)
|
||||
// - gzipping
|
||||
func wrap(handle fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||
return func(c *fasthttp.RequestCtx) {
|
||||
start := time.Now()
|
||||
|
||||
doggo.Incr("requests", nil, 1)
|
||||
|
||||
defer func() {
|
||||
if rval := recover(); rval != nil {
|
||||
var err error
|
||||
switch rval := rval.(type) {
|
||||
case string:
|
||||
err = errors.New(rval)
|
||||
case error:
|
||||
err = rval
|
||||
default:
|
||||
err = fmt.Errorf("%v - %#v", rval, rval)
|
||||
}
|
||||
common.Err(c, err)
|
||||
c.SetStatusCode(500)
|
||||
c.SetBodyString(`{ "code": 500, "message": "something really bad happened" }`)
|
||||
}
|
||||
|
||||
// switch color to colorError if statusCode is in [500;600)
|
||||
color := colorOk
|
||||
statusCode := c.Response.StatusCode()
|
||||
if statusCode >= 500 && statusCode < 600 {
|
||||
color = colorError
|
||||
}
|
||||
|
||||
if bytes.Contains(c.Request.Header.Peek("Accept-Encoding"), s2b("gzip")) {
|
||||
c.Response.Header.Add("Content-Encoding", "gzip")
|
||||
c.Response.Header.Add("Vary", "Accept-Encoding")
|
||||
b := c.Response.Body()
|
||||
c.Response.ResetBody()
|
||||
fasthttp.WriteGzip(c.Response.BodyWriter(), b)
|
||||
}
|
||||
|
||||
// print stuff
|
||||
fmt.Printf(
|
||||
fmtString,
|
||||
time.Now().Format(timeFormat),
|
||||
time.Since(start).String(),
|
||||
color,
|
||||
statusCode,
|
||||
c.Method(),
|
||||
c.Path(),
|
||||
)
|
||||
}()
|
||||
|
||||
handle(c)
|
||||
}
|
||||
}
|
168
vendor/zxq.co/ripple/rippleapi/app/start.go
vendored
Normal file
168
vendor/zxq.co/ripple/rippleapi/app/start.go
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/DataDog/datadog-go/statsd"
|
||||
fhr "github.com/buaazp/fasthttprouter"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/redis.v5"
|
||||
"zxq.co/ripple/rippleapi/app/internals"
|
||||
"zxq.co/ripple/rippleapi/app/peppy"
|
||||
"zxq.co/ripple/rippleapi/app/v1"
|
||||
"zxq.co/ripple/rippleapi/app/websockets"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
var (
|
||||
db *sqlx.DB
|
||||
cf common.Conf
|
||||
doggo *statsd.Client
|
||||
red *redis.Client
|
||||
)
|
||||
|
||||
// Start begins taking HTTP connections.
|
||||
func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
|
||||
db = dbO
|
||||
cf = conf
|
||||
|
||||
rawRouter := fhr.New()
|
||||
r := router{rawRouter}
|
||||
// TODO: add back gzip
|
||||
// TODO: add logging
|
||||
// TODO: add sentry panic recovering
|
||||
|
||||
// sentry
|
||||
if conf.SentryDSN != "" {
|
||||
ravenClient, err := raven.New(conf.SentryDSN)
|
||||
ravenClient.SetRelease(common.Version)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
// r.Use(Recovery(ravenClient, false))
|
||||
common.RavenClient = ravenClient
|
||||
}
|
||||
}
|
||||
|
||||
// datadog
|
||||
var err error
|
||||
doggo, err = statsd.New("127.0.0.1:8125")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
doggo.Namespace = "api."
|
||||
|
||||
// redis
|
||||
red = redis.NewClient(&redis.Options{
|
||||
Addr: conf.RedisAddr,
|
||||
Password: conf.RedisPassword,
|
||||
DB: conf.RedisDB,
|
||||
})
|
||||
peppy.R = red
|
||||
|
||||
// token updater
|
||||
go tokenUpdater(db)
|
||||
|
||||
// start websocket
|
||||
websockets.Start(red, db)
|
||||
|
||||
// start load achievements
|
||||
go v1.LoadAchievementsEvery(db, time.Minute*10)
|
||||
|
||||
// peppyapi
|
||||
{
|
||||
r.Peppy("/api/get_user", peppy.GetUser)
|
||||
r.Peppy("/api/get_match", peppy.GetMatch)
|
||||
r.Peppy("/api/get_user_recent", peppy.GetUserRecent)
|
||||
r.Peppy("/api/get_user_best", peppy.GetUserBest)
|
||||
r.Peppy("/api/get_scores", peppy.GetScores)
|
||||
r.Peppy("/api/get_beatmaps", peppy.GetBeatmap)
|
||||
}
|
||||
|
||||
// v1 API
|
||||
{
|
||||
r.POSTMethod("/api/v1/tokens/self/delete", v1.TokenSelfDeletePOST)
|
||||
|
||||
// Auth-free API endpoints (public data)
|
||||
r.Method("/api/v1/ping", v1.PingGET)
|
||||
r.Method("/api/v1/surprise_me", v1.SurpriseMeGET)
|
||||
r.Method("/api/v1/users", v1.UsersGET)
|
||||
r.Method("/api/v1/users/whatid", v1.UserWhatsTheIDGET)
|
||||
r.Method("/api/v1/users/full", v1.UserFullGET)
|
||||
r.Method("/api/v1/users/achievements", v1.UserAchievementsGET)
|
||||
r.Method("/api/v1/users/userpage", v1.UserUserpageGET)
|
||||
r.Method("/api/v1/users/lookup", v1.UserLookupGET)
|
||||
r.Method("/api/v1/users/scores/best", v1.UserScoresBestGET)
|
||||
r.Method("/api/v1/users/scores/recent", v1.UserScoresRecentGET)
|
||||
r.Method("/api/v1/badges", v1.BadgesGET)
|
||||
r.Method("/api/v1/badges/members", v1.BadgeMembersGET)
|
||||
r.Method("/api/v1/beatmaps", v1.BeatmapGET)
|
||||
r.Method("/api/v1/leaderboard", v1.LeaderboardGET)
|
||||
r.Method("/api/v1/tokens", v1.TokenGET)
|
||||
r.Method("/api/v1/users/self", v1.UserSelfGET)
|
||||
r.Method("/api/v1/tokens/self", v1.TokenSelfGET)
|
||||
r.Method("/api/v1/blog/posts", v1.BlogPostsGET)
|
||||
r.Method("/api/v1/scores", v1.ScoresGET)
|
||||
r.Method("/api/v1/beatmaps/rank_requests/status", v1.BeatmapRankRequestsStatusGET)
|
||||
|
||||
// ReadConfidential privilege required
|
||||
r.Method("/api/v1/friends", v1.FriendsGET, common.PrivilegeReadConfidential)
|
||||
r.Method("/api/v1/friends/with", v1.FriendsWithGET, common.PrivilegeReadConfidential)
|
||||
r.Method("/api/v1/users/self/donor_info", v1.UsersSelfDonorInfoGET, common.PrivilegeReadConfidential)
|
||||
r.Method("/api/v1/users/self/favourite_mode", v1.UsersSelfFavouriteModeGET, common.PrivilegeReadConfidential)
|
||||
r.Method("/api/v1/users/self/settings", v1.UsersSelfSettingsGET, common.PrivilegeReadConfidential)
|
||||
|
||||
// Write privilege required
|
||||
r.POSTMethod("/api/v1/friends/add", v1.FriendsAddPOST, common.PrivilegeWrite)
|
||||
r.POSTMethod("/api/v1/friends/del", v1.FriendsDelPOST, common.PrivilegeWrite)
|
||||
r.POSTMethod("/api/v1/users/self/settings", v1.UsersSelfSettingsPOST, common.PrivilegeWrite)
|
||||
r.POSTMethod("/api/v1/users/self/userpage", v1.UserSelfUserpagePOST, common.PrivilegeWrite)
|
||||
r.POSTMethod("/api/v1/beatmaps/rank_requests", v1.BeatmapRankRequestsSubmitPOST, common.PrivilegeWrite)
|
||||
|
||||
// Admin: RAP
|
||||
r.POSTMethod("/api/v1/rap/log", v1.RAPLogPOST)
|
||||
|
||||
// Admin: beatmap
|
||||
r.POSTMethod("/api/v1/beatmaps/set_status", v1.BeatmapSetStatusPOST, common.PrivilegeBeatmap)
|
||||
r.Method("/api/v1/beatmaps/ranked_frozen_full", v1.BeatmapRankedFrozenFullGET, common.PrivilegeBeatmap)
|
||||
|
||||
// Admin: user managing
|
||||
r.POSTMethod("/api/v1/users/manage/set_allowed", v1.UserManageSetAllowedPOST, common.PrivilegeManageUser)
|
||||
r.POSTMethod("/api/v1/users/edit", v1.UserEditPOST, common.PrivilegeManageUser)
|
||||
|
||||
// M E T A
|
||||
// E T "wow thats so meta"
|
||||
// T E -- the one who said "wow thats so meta"
|
||||
// A T E M
|
||||
r.Method("/api/v1/meta/restart", v1.MetaRestartGET, common.PrivilegeAPIMeta)
|
||||
r.Method("/api/v1/meta/up_since", v1.MetaUpSinceGET, common.PrivilegeAPIMeta)
|
||||
r.Method("/api/v1/meta/update", v1.MetaUpdateGET, common.PrivilegeAPIMeta)
|
||||
|
||||
// User Managing + meta
|
||||
r.POSTMethod("/api/v1/tokens/fix_privileges", v1.TokenFixPrivilegesPOST,
|
||||
common.PrivilegeManageUser, common.PrivilegeAPIMeta)
|
||||
}
|
||||
|
||||
// Websocket API
|
||||
{
|
||||
r.PlainGET("/api/v1/ws", websockets.WebsocketV1Entry)
|
||||
}
|
||||
|
||||
// in the new osu-web, the old endpoints are also in /v1 it seems. So /shrug
|
||||
{
|
||||
r.Peppy("/api/v1/get_user", peppy.GetUser)
|
||||
r.Peppy("/api/v1/get_match", peppy.GetMatch)
|
||||
r.Peppy("/api/v1/get_user_recent", peppy.GetUserRecent)
|
||||
r.Peppy("/api/v1/get_user_best", peppy.GetUserBest)
|
||||
r.Peppy("/api/v1/get_scores", peppy.GetScores)
|
||||
r.Peppy("/api/v1/get_beatmaps", peppy.GetBeatmap)
|
||||
}
|
||||
|
||||
r.GET("/api/status", internals.Status)
|
||||
|
||||
rawRouter.NotFound = v1.Handle404
|
||||
|
||||
return rawRouter
|
||||
}
|
112
vendor/zxq.co/ripple/rippleapi/app/tokens.go
vendored
Normal file
112
vendor/zxq.co/ripple/rippleapi/app/tokens.go
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// GetTokenFull retrieves an user ID and their token privileges knowing their API token.
|
||||
func GetTokenFull(token string, db *sqlx.DB) (common.Token, bool) {
|
||||
var (
|
||||
t common.Token
|
||||
tokenPrivsRaw uint64
|
||||
userPrivsRaw uint64
|
||||
priv8 bool
|
||||
)
|
||||
err := db.QueryRow(`SELECT
|
||||
t.id, t.user, t.privileges, t.private, u.privileges
|
||||
FROM tokens t
|
||||
LEFT JOIN users u ON u.id = t.user
|
||||
WHERE token = ? LIMIT 1`,
|
||||
fmt.Sprintf("%x", md5.Sum([]byte(token)))).
|
||||
Scan(
|
||||
&t.ID, &t.UserID, &tokenPrivsRaw, &priv8, &userPrivsRaw,
|
||||
)
|
||||
updateTokens <- t.ID
|
||||
if priv8 {
|
||||
// all privileges, they'll get removed by canOnly anyway.
|
||||
tokenPrivsRaw = (common.PrivilegeBeatmap << 1) - 1
|
||||
}
|
||||
t.UserPrivileges = common.UserPrivileges(userPrivsRaw)
|
||||
t.TokenPrivileges = common.Privileges(tokenPrivsRaw).CanOnly(t.UserPrivileges)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.Token{}, false
|
||||
case err != nil:
|
||||
panic(err)
|
||||
default:
|
||||
t.Value = token
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
|
||||
var updateTokens = make(chan int, 100)
|
||||
|
||||
func tokenUpdater(db *sqlx.DB) {
|
||||
for {
|
||||
// prepare a queue of tokens to update.
|
||||
tokensToUpdate := make([]int, 0, 50)
|
||||
AwaitLoop:
|
||||
for {
|
||||
// if we got ten, move on and update
|
||||
if len(tokensToUpdate) >= 50 {
|
||||
break
|
||||
}
|
||||
// if we ain't got any, add what we get straight from updateTokens
|
||||
if len(tokensToUpdate) == 0 {
|
||||
x := <-updateTokens
|
||||
tokensToUpdate = append(tokensToUpdate, x)
|
||||
continue
|
||||
}
|
||||
|
||||
// otherwise, wait from updateTokens with a timeout of 10 seconds
|
||||
select {
|
||||
case x := <-updateTokens:
|
||||
tokensToUpdate = append(tokensToUpdate, x)
|
||||
case <-time.After(10 * time.Second):
|
||||
// wondering what this means?
|
||||
// https://golang.org/ref/spec#Break_statements
|
||||
break AwaitLoop
|
||||
}
|
||||
}
|
||||
|
||||
q, a, _ := sqlx.In("UPDATE tokens SET last_updated = ? WHERE id IN (?)", time.Now().Unix(), tokensToUpdate)
|
||||
|
||||
q = db.Rebind(q)
|
||||
_, err := db.Exec(q, a...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BearerToken parses a Token given in the Authorization header, with the
|
||||
// Bearer prefix.
|
||||
func BearerToken(token string, db *sqlx.DB) (common.Token, bool) {
|
||||
var x struct {
|
||||
Scope string
|
||||
Extra int
|
||||
}
|
||||
db.Get(&x, "SELECT scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token))))
|
||||
if x.Extra == 0 {
|
||||
return common.Token{}, false
|
||||
}
|
||||
|
||||
var privs uint64
|
||||
db.Get(&privs, "SELECT privileges FROM users WHERE id = ? LIMIT 1", x.Extra)
|
||||
|
||||
var t common.Token
|
||||
t.ID = -1
|
||||
t.UserID = x.Extra
|
||||
t.Value = token
|
||||
t.UserPrivileges = common.UserPrivileges(privs)
|
||||
t.TokenPrivileges = common.OAuthPrivileges(x.Scope).CanOnly(t.UserPrivileges)
|
||||
|
||||
return t, true
|
||||
}
|
29
vendor/zxq.co/ripple/rippleapi/app/v1/404.go
vendored
Normal file
29
vendor/zxq.co/ripple/rippleapi/app/v1/404.go
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type response404 struct {
|
||||
common.ResponseBase
|
||||
Cats string `json:"cats"`
|
||||
}
|
||||
|
||||
// Handle404 handles requests with no implemented handlers.
|
||||
func Handle404(c *fasthttp.RequestCtx) {
|
||||
c.Response.Header.Add("X-Real-404", "yes")
|
||||
data, err := json.MarshalIndent(response404{
|
||||
ResponseBase: common.ResponseBase{
|
||||
Code: 404,
|
||||
},
|
||||
Cats: surpriseMe(),
|
||||
}, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.SetStatusCode(404)
|
||||
c.Write(data)
|
||||
}
|
82
vendor/zxq.co/ripple/rippleapi/app/v1/badge.go
vendored
Normal file
82
vendor/zxq.co/ripple/rippleapi/app/v1/badge.go
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type singleBadge struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type multiBadgeData struct {
|
||||
common.ResponseBase
|
||||
Badges []singleBadge `json:"badges"`
|
||||
}
|
||||
|
||||
// BadgesGET retrieves all the badges on this ripple instance.
|
||||
func BadgesGET(md common.MethodData) common.CodeMessager {
|
||||
var (
|
||||
r multiBadgeData
|
||||
rows *sql.Rows
|
||||
err error
|
||||
)
|
||||
if md.Query("id") != "" {
|
||||
rows, err = md.DB.Query("SELECT id, name, icon FROM badges WHERE id = ? LIMIT 1", md.Query("id"))
|
||||
} else {
|
||||
rows, err = md.DB.Query("SELECT id, name, icon FROM badges " + common.Paginate(md.Query("p"), md.Query("l"), 50))
|
||||
}
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
nb := singleBadge{}
|
||||
err = rows.Scan(&nb.ID, &nb.Name, &nb.Icon)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
r.Badges = append(r.Badges, nb)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
r.ResponseBase.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type badgeMembersData struct {
|
||||
common.ResponseBase
|
||||
Members []userData `json:"members"`
|
||||
}
|
||||
|
||||
// BadgeMembersGET retrieves the people who have a certain badge.
|
||||
func BadgeMembersGET(md common.MethodData) common.CodeMessager {
|
||||
i := common.Int(md.Query("id"))
|
||||
if i == 0 {
|
||||
return ErrMissingField("id")
|
||||
}
|
||||
|
||||
var members badgeMembersData
|
||||
|
||||
err := md.DB.Select(&members.Members, `SELECT users.id, users.username, register_datetime, users.privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM user_badges ub
|
||||
INNER JOIN users ON users.id = ub.user
|
||||
INNER JOIN users_stats ON users_stats.id = ub.user
|
||||
WHERE badge = ?
|
||||
ORDER BY id ASC `+common.Paginate(md.Query("p"), md.Query("l"), 50), i)
|
||||
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
members.Code = 200
|
||||
return members
|
||||
}
|
228
vendor/zxq.co/ripple/rippleapi/app/v1/beatmap.go
vendored
Normal file
228
vendor/zxq.co/ripple/rippleapi/app/v1/beatmap.go
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type difficulty struct {
|
||||
STD float64 `json:"std"`
|
||||
Taiko float64 `json:"taiko"`
|
||||
CTB float64 `json:"ctb"`
|
||||
Mania float64 `json:"mania"`
|
||||
}
|
||||
|
||||
type beatmap struct {
|
||||
BeatmapID int `json:"beatmap_id"`
|
||||
BeatmapsetID int `json:"beatmapset_id"`
|
||||
BeatmapMD5 string `json:"beatmap_md5"`
|
||||
SongName string `json:"song_name"`
|
||||
AR float32 `json:"ar"`
|
||||
OD float32 `json:"od"`
|
||||
Difficulty float64 `json:"difficulty"`
|
||||
Diff2 difficulty `json:"difficulty2"` // fuck nyo
|
||||
MaxCombo int `json:"max_combo"`
|
||||
HitLength int `json:"hit_length"`
|
||||
Ranked int `json:"ranked"`
|
||||
RankedStatusFrozen int `json:"ranked_status_frozen"`
|
||||
LatestUpdate common.UnixTimestamp `json:"latest_update"`
|
||||
}
|
||||
|
||||
type beatmapResponse struct {
|
||||
common.ResponseBase
|
||||
beatmap
|
||||
}
|
||||
type beatmapSetResponse struct {
|
||||
common.ResponseBase
|
||||
Beatmaps []beatmap `json:"beatmaps"`
|
||||
}
|
||||
|
||||
type beatmapSetStatusData struct {
|
||||
BeatmapsetID int `json:"beatmapset_id"`
|
||||
BeatmapID int `json:"beatmap_id"`
|
||||
RankedStatus int `json:"ranked_status"`
|
||||
Frozen int `json:"frozen"`
|
||||
}
|
||||
|
||||
// BeatmapSetStatusPOST changes the ranked status of a beatmap, and whether
|
||||
// the beatmap ranked status is frozen. Or freezed. Freezed best meme 2k16
|
||||
func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
|
||||
var req beatmapSetStatusData
|
||||
md.Unmarshal(&req)
|
||||
|
||||
var miss []string
|
||||
if req.BeatmapsetID <= 0 && req.BeatmapID <= 0 {
|
||||
miss = append(miss, "beatmapset_id or beatmap_id")
|
||||
}
|
||||
if len(miss) != 0 {
|
||||
return ErrMissingField(miss...)
|
||||
}
|
||||
|
||||
if req.Frozen != 0 && req.Frozen != 1 {
|
||||
return common.SimpleResponse(400, "frozen status must be either 0 or 1")
|
||||
}
|
||||
if req.RankedStatus > 4 || -1 > req.RankedStatus {
|
||||
return common.SimpleResponse(400, "ranked status must be 5 < x < -2")
|
||||
}
|
||||
|
||||
param := req.BeatmapsetID
|
||||
if req.BeatmapID != 0 {
|
||||
err := md.DB.QueryRow("SELECT beatmapset_id FROM beatmaps WHERE beatmap_id = ? LIMIT 1", req.BeatmapID).Scan(¶m)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That beatmap could not be found!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
|
||||
md.DB.Exec(`UPDATE beatmaps
|
||||
SET ranked = ?, ranked_status_freezed = ?
|
||||
WHERE beatmapset_id = ?`, req.RankedStatus, req.Frozen, param)
|
||||
|
||||
if req.BeatmapID > 0 {
|
||||
md.Ctx.Request.URI().QueryArgs().SetUint("bb", req.BeatmapID)
|
||||
} else {
|
||||
md.Ctx.Request.URI().QueryArgs().SetUint("s", req.BeatmapsetID)
|
||||
}
|
||||
return getMultipleBeatmaps(md)
|
||||
}
|
||||
|
||||
// BeatmapGET retrieves a beatmap.
|
||||
func BeatmapGET(md common.MethodData) common.CodeMessager {
|
||||
beatmapID := common.Int(md.Query("b"))
|
||||
if beatmapID != 0 {
|
||||
return getBeatmapSingle(md, beatmapID)
|
||||
}
|
||||
return getMultipleBeatmaps(md)
|
||||
}
|
||||
|
||||
const baseBeatmapSelect = `
|
||||
SELECT
|
||||
beatmap_id, beatmapset_id, beatmap_md5,
|
||||
song_name, ar, od, difficulty_std, difficulty_taiko,
|
||||
difficulty_ctb, difficulty_mania, max_combo,
|
||||
hit_length, ranked, ranked_status_freezed,
|
||||
latest_update
|
||||
FROM beatmaps
|
||||
`
|
||||
|
||||
func getMultipleBeatmaps(md common.MethodData) common.CodeMessager {
|
||||
sort := common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"beatmapset_id",
|
||||
"beatmap_id",
|
||||
"id",
|
||||
"ar",
|
||||
"od",
|
||||
"difficulty_std",
|
||||
"difficulty_taiko",
|
||||
"difficulty_ctb",
|
||||
"difficulty_mania",
|
||||
"max_combo",
|
||||
"latest_update",
|
||||
"playcount",
|
||||
"passcount",
|
||||
},
|
||||
Default: "id DESC",
|
||||
Table: "beatmaps",
|
||||
})
|
||||
pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
|
||||
where := common.
|
||||
Where("song_name = ?", md.Query("song_name")).
|
||||
Where("ranked_status_freezed = ?", md.Query("ranked_status_frozen"), "0", "1").
|
||||
In("beatmap_id", pm("bb")...).
|
||||
In("beatmapset_id", pm("s")...).
|
||||
In("beatmap_md5", pm("md5")...)
|
||||
|
||||
rows, err := md.DB.Query(baseBeatmapSelect+
|
||||
where.Clause+" "+sort+" "+
|
||||
common.Paginate(md.Query("p"), md.Query("l"), 50), where.Params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r beatmapSetResponse
|
||||
for rows.Next() {
|
||||
var b beatmap
|
||||
err = rows.Scan(
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD, &b.Diff2.Taiko,
|
||||
&b.Diff2.CTB, &b.Diff2.Mania, &b.MaxCombo,
|
||||
&b.HitLength, &b.Ranked, &b.RankedStatusFrozen,
|
||||
&b.LatestUpdate,
|
||||
)
|
||||
b.Difficulty = b.Diff2.STD
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Beatmaps = append(r.Beatmaps, b)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
func getBeatmapSingle(md common.MethodData, beatmapID int) common.CodeMessager {
|
||||
var b beatmap
|
||||
err := md.DB.QueryRow(baseBeatmapSelect+"WHERE beatmap_id = ? LIMIT 1", beatmapID).Scan(
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD, &b.Diff2.Taiko,
|
||||
&b.Diff2.CTB, &b.Diff2.Mania, &b.MaxCombo,
|
||||
&b.HitLength, &b.Ranked, &b.RankedStatusFrozen,
|
||||
&b.LatestUpdate,
|
||||
)
|
||||
b.Difficulty = b.Diff2.STD
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That beatmap could not be found!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r beatmapResponse
|
||||
r.Code = 200
|
||||
r.beatmap = b
|
||||
return r
|
||||
}
|
||||
|
||||
type beatmapReduced struct {
|
||||
BeatmapID int `json:"beatmap_id"`
|
||||
BeatmapsetID int `json:"beatmapset_id"`
|
||||
BeatmapMD5 string `json:"beatmap_md5"`
|
||||
Ranked int `json:"ranked"`
|
||||
RankedStatusFrozen int `json:"ranked_status_frozen"`
|
||||
}
|
||||
|
||||
type beatmapRankedFrozenFullResponse struct {
|
||||
common.ResponseBase
|
||||
Beatmaps []beatmapReduced `json:"beatmaps"`
|
||||
}
|
||||
|
||||
// BeatmapRankedFrozenFullGET retrieves all beatmaps with a certain
|
||||
// ranked_status_freezed
|
||||
func BeatmapRankedFrozenFullGET(md common.MethodData) common.CodeMessager {
|
||||
rows, err := md.DB.Query(`
|
||||
SELECT beatmap_id, beatmapset_id, beatmap_md5, ranked, ranked_status_freezed
|
||||
FROM beatmaps
|
||||
WHERE ranked_status_freezed = '1'
|
||||
`)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r beatmapRankedFrozenFullResponse
|
||||
for rows.Next() {
|
||||
var b beatmapReduced
|
||||
err = rows.Scan(&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5, &b.Ranked, &b.RankedStatusFrozen)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Beatmaps = append(r.Beatmaps, b)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
159
vendor/zxq.co/ripple/rippleapi/app/v1/beatmap_requests.go
vendored
Normal file
159
vendor/zxq.co/ripple/rippleapi/app/v1/beatmap_requests.go
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/limit"
|
||||
)
|
||||
|
||||
type rankRequestsStatusResponse struct {
|
||||
common.ResponseBase
|
||||
QueueSize int `json:"queue_size"`
|
||||
MaxPerUser int `json:"max_per_user"`
|
||||
Submitted int `json:"submitted"`
|
||||
SubmittedByUser *int `json:"submitted_by_user,omitempty"`
|
||||
CanSubmit *bool `json:"can_submit,omitempty"`
|
||||
NextExpiration *time.Time `json:"next_expiration"`
|
||||
}
|
||||
|
||||
// BeatmapRankRequestsStatusGET gets the current status for beatmap ranking requests.
|
||||
func BeatmapRankRequestsStatusGET(md common.MethodData) common.CodeMessager {
|
||||
c := common.GetConf()
|
||||
rows, err := md.DB.Query("SELECT userid, time FROM rank_requests WHERE time > ? ORDER BY id ASC LIMIT "+strconv.Itoa(c.RankQueueSize), time.Now().Add(-time.Hour*24).Unix())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r rankRequestsStatusResponse
|
||||
// if it's not auth-free access and we have got ReadConfidential, we can
|
||||
// know if this user can submit beatmaps or not.
|
||||
hasConfid := md.ID() != 0 && md.User.TokenPrivileges&common.PrivilegeReadConfidential > 0
|
||||
if hasConfid {
|
||||
r.SubmittedByUser = new(int)
|
||||
}
|
||||
isFirst := true
|
||||
for rows.Next() {
|
||||
var (
|
||||
user int
|
||||
timestamp common.UnixTimestamp
|
||||
)
|
||||
err := rows.Scan(&user, ×tamp)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
// if the user submitted this rank request, increase the number of
|
||||
// rank requests submitted by this user
|
||||
if user == md.ID() && r.SubmittedByUser != nil {
|
||||
(*r.SubmittedByUser)++
|
||||
}
|
||||
// also, if this is the first result, it means it will be the next to
|
||||
// expire.
|
||||
if isFirst {
|
||||
x := time.Time(timestamp)
|
||||
r.NextExpiration = &x
|
||||
isFirst = false
|
||||
}
|
||||
r.Submitted++
|
||||
}
|
||||
r.QueueSize = c.RankQueueSize
|
||||
r.MaxPerUser = c.BeatmapRequestsPerUser
|
||||
if hasConfid {
|
||||
x := r.Submitted < r.QueueSize && *r.SubmittedByUser < r.MaxPerUser
|
||||
r.CanSubmit = &x
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type submitRequestData struct {
|
||||
ID int `json:"id"`
|
||||
SetID int `json:"set_id"`
|
||||
}
|
||||
|
||||
// BeatmapRankRequestsSubmitPOST submits a new beatmap for ranking approval.
|
||||
func BeatmapRankRequestsSubmitPOST(md common.MethodData) common.CodeMessager {
|
||||
var d submitRequestData
|
||||
err := md.Unmarshal(&d)
|
||||
if err != nil {
|
||||
return ErrBadJSON
|
||||
}
|
||||
// check json data is present
|
||||
if d.ID == 0 && d.SetID == 0 {
|
||||
return ErrMissingField("id|set_id")
|
||||
}
|
||||
|
||||
// you've been rate limited
|
||||
if !limit.NonBlockingRequest("rankrequest:u:"+strconv.Itoa(md.ID()), 5) {
|
||||
return common.SimpleResponse(429, "You may only try to request 5 beatmaps per minute.")
|
||||
}
|
||||
|
||||
// find out from BeatmapRankRequestsStatusGET if we can submit beatmaps.
|
||||
statusRaw := BeatmapRankRequestsStatusGET(md)
|
||||
status, ok := statusRaw.(rankRequestsStatusResponse)
|
||||
if !ok {
|
||||
// if it's not a rankRequestsStatusResponse, it means it's an error
|
||||
return statusRaw
|
||||
}
|
||||
if !*status.CanSubmit {
|
||||
return common.SimpleResponse(403, "It's not possible to do a rank request at this time.")
|
||||
}
|
||||
|
||||
w := common.
|
||||
Where("beatmap_id = ?", strconv.Itoa(d.ID)).Or().
|
||||
Where("beatmapset_id = ?", strconv.Itoa(d.SetID))
|
||||
|
||||
var ranked int
|
||||
err = md.DB.QueryRow("SELECT ranked FROM beatmaps "+w.Clause+" LIMIT 1", w.Params...).Scan(&ranked)
|
||||
if ranked >= 2 {
|
||||
return common.SimpleResponse(406, "That beatmap is already ranked.")
|
||||
}
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
// move on
|
||||
case sql.ErrNoRows:
|
||||
data, _ := json.Marshal(d)
|
||||
md.R.Publish("lets:beatmap_updates", string(data))
|
||||
default:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// type and value of beatmap rank request
|
||||
t := "b"
|
||||
v := d.ID
|
||||
if d.SetID != 0 {
|
||||
t = "s"
|
||||
v = d.SetID
|
||||
}
|
||||
err = md.DB.QueryRow("SELECT 1 FROM rank_requests WHERE bid = ? AND type = ? AND time > ?",
|
||||
v, t, time.Now().Add(-time.Hour*24).Unix()).Scan(new(int))
|
||||
|
||||
// error handling
|
||||
switch err {
|
||||
case sql.ErrNoRows:
|
||||
break
|
||||
case nil:
|
||||
// we're returning a success because if the request was already sent in the past 24
|
||||
// hours, it's as if the user submitted it.
|
||||
return BeatmapRankRequestsStatusGET(md)
|
||||
default:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
_, err = md.DB.Exec(
|
||||
"INSERT INTO rank_requests (userid, bid, type, time, blacklisted) VALUES (?, ?, ?, ?, 0)",
|
||||
md.ID(), v, t, time.Now().Unix())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
return BeatmapRankRequestsStatusGET(md)
|
||||
}
|
184
vendor/zxq.co/ripple/rippleapi/app/v1/blog.go
vendored
Normal file
184
vendor/zxq.co/ripple/rippleapi/app/v1/blog.go
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// This basically proxies requests from Medium's API and is used on Ripple's
|
||||
// home page to display the latest blog posts.
|
||||
|
||||
type mediumResp struct {
|
||||
Success bool `json:"success"`
|
||||
Payload struct {
|
||||
Posts []mediumPost `json:"posts"`
|
||||
References struct {
|
||||
User map[string]mediumUser
|
||||
} `json:"references"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
type mediumPost struct {
|
||||
ID string `json:"id"`
|
||||
CreatorID string `json:"creatorId"`
|
||||
Title string `json:"title"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
Virtuals mediumPostVirtuals `json:"virtuals"`
|
||||
ImportedURL string `json:"importedUrl"`
|
||||
UniqueSlug string `json:"uniqueSlug"`
|
||||
}
|
||||
|
||||
type mediumUser struct {
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type mediumPostVirtuals struct {
|
||||
Subtitle string `json:"subtitle"`
|
||||
WordCount int `json:"wordCount"`
|
||||
ReadingTime float64 `json:"readingTime"`
|
||||
}
|
||||
|
||||
// there's gotta be a better way
|
||||
|
||||
type blogPost struct {
|
||||
ID string `json:"id"`
|
||||
Creator blogUser `json:"creator"`
|
||||
Title string `json:"title"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ImportedURL string `json:"imported_url"`
|
||||
UniqueSlug string `json:"unique_slug"`
|
||||
|
||||
Snippet string `json:"snippet"`
|
||||
WordCount int `json:"word_count"`
|
||||
ReadingTime float64 `json:"reading_time"`
|
||||
}
|
||||
|
||||
type blogUser struct {
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type blogPostsResponse struct {
|
||||
common.ResponseBase
|
||||
Posts []blogPost `json:"posts"`
|
||||
}
|
||||
|
||||
// consts for the medium API
|
||||
const (
|
||||
mediumAPIResponsePrefix = `])}while(1);</x>`
|
||||
mediumAPIAllPosts = `https://blog.ripple.moe/latest?format=json`
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register([]blogPost{})
|
||||
}
|
||||
|
||||
// BlogPostsGET retrieves the latest blog posts on the Ripple blog.
|
||||
func BlogPostsGET(md common.MethodData) common.CodeMessager {
|
||||
// check if posts are cached in redis
|
||||
res := md.R.Get("api:blog_posts").Val()
|
||||
if res != "" {
|
||||
// decode values
|
||||
posts := make([]blogPost, 0, 20)
|
||||
err := gob.NewDecoder(strings.NewReader(res)).Decode(&posts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// create response and return
|
||||
var r blogPostsResponse
|
||||
r.Code = 200
|
||||
r.Posts = blogLimit(posts, md.Query("l"))
|
||||
return r
|
||||
}
|
||||
|
||||
// get data from medium api
|
||||
resp, err := http.Get(mediumAPIAllPosts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// read body and trim the prefix
|
||||
all, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
all = bytes.TrimPrefix(all, []byte(mediumAPIResponsePrefix))
|
||||
|
||||
// unmarshal into response struct
|
||||
var mResp mediumResp
|
||||
err = json.Unmarshal(all, &mResp)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
if !mResp.Success {
|
||||
md.Err(errors.New("medium api call is not successful"))
|
||||
return Err500
|
||||
}
|
||||
|
||||
// create posts slice and fill it up with converted posts from the medium
|
||||
// API
|
||||
posts := make([]blogPost, len(mResp.Payload.Posts))
|
||||
for idx, mp := range mResp.Payload.Posts {
|
||||
var p blogPost
|
||||
|
||||
// convert structs
|
||||
p.ID = mp.ID
|
||||
p.Title = mp.Title
|
||||
p.CreatedAt = time.Unix(0, mp.CreatedAt*1000000)
|
||||
p.UpdatedAt = time.Unix(0, mp.UpdatedAt*1000000)
|
||||
p.ImportedURL = mp.ImportedURL
|
||||
p.UniqueSlug = mp.UniqueSlug
|
||||
|
||||
cr := mResp.Payload.References.User[mp.CreatorID]
|
||||
p.Creator.UserID = cr.UserID
|
||||
p.Creator.Name = cr.Name
|
||||
p.Creator.Username = cr.Username
|
||||
|
||||
p.Snippet = mp.Virtuals.Subtitle
|
||||
p.WordCount = mp.Virtuals.WordCount
|
||||
p.ReadingTime = mp.Virtuals.ReadingTime
|
||||
|
||||
posts[idx] = p
|
||||
}
|
||||
|
||||
// save in redis
|
||||
bb := new(bytes.Buffer)
|
||||
err = gob.NewEncoder(bb).Encode(posts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
md.R.Set("api:blog_posts", bb.Bytes(), time.Minute*5)
|
||||
|
||||
var r blogPostsResponse
|
||||
r.Code = 200
|
||||
r.Posts = blogLimit(posts, md.Query("l"))
|
||||
return r
|
||||
}
|
||||
|
||||
func blogLimit(posts []blogPost, s string) []blogPost {
|
||||
i := common.Int(s)
|
||||
if i >= len(posts) || i < 1 {
|
||||
return posts
|
||||
}
|
||||
return posts[:i]
|
||||
}
|
21
vendor/zxq.co/ripple/rippleapi/app/v1/errors.go
vendored
Normal file
21
vendor/zxq.co/ripple/rippleapi/app/v1/errors.go
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// Boilerplate errors
|
||||
var (
|
||||
Err500 = common.SimpleResponse(500, "An error occurred. Trying again may work. If it doesn't, yell at this Ripple instance admin and tell them to fix the API.")
|
||||
ErrBadJSON = common.SimpleResponse(400, "Your JSON for this request is invalid.")
|
||||
)
|
||||
|
||||
// ErrMissingField generates a response to a request when some fields in the JSON are missing.
|
||||
func ErrMissingField(missingFields ...string) common.CodeMessager {
|
||||
return common.ResponseBase{
|
||||
Code: 422, // http://stackoverflow.com/a/10323055/5328069
|
||||
Message: "Missing parameters: " + strings.Join(missingFields, ", ") + ".",
|
||||
}
|
||||
}
|
202
vendor/zxq.co/ripple/rippleapi/app/v1/friend.go
vendored
Normal file
202
vendor/zxq.co/ripple/rippleapi/app/v1/friend.go
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type friendData struct {
|
||||
userData
|
||||
IsMutual bool `json:"is_mutual"`
|
||||
}
|
||||
|
||||
type friendsGETResponse struct {
|
||||
common.ResponseBase
|
||||
Friends []friendData `json:"friends"`
|
||||
}
|
||||
|
||||
// FriendsGET is the API request handler for GET /friends.
|
||||
// It retrieves an user's friends, and whether the friendship is mutual or not.
|
||||
func FriendsGET(md common.MethodData) common.CodeMessager {
|
||||
var myFrienders []int
|
||||
myFriendersRaw, err := md.DB.Query("SELECT user1 FROM users_relationships WHERE user2 = ?", md.ID())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
defer myFriendersRaw.Close()
|
||||
for myFriendersRaw.Next() {
|
||||
var i int
|
||||
err := myFriendersRaw.Scan(&i)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
myFrienders = append(myFrienders, i)
|
||||
}
|
||||
if err := myFriendersRaw.Err(); err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
|
||||
myFriendsQuery := `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM users_relationships
|
||||
LEFT JOIN users
|
||||
ON users_relationships.user2 = users.id
|
||||
LEFT JOIN users_stats
|
||||
ON users_relationships.user2=users_stats.id
|
||||
WHERE users_relationships.user1=?
|
||||
`
|
||||
|
||||
myFriendsQuery += common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"id",
|
||||
"username",
|
||||
"latest_activity",
|
||||
},
|
||||
Default: "users.id asc",
|
||||
Table: "users",
|
||||
}) + "\n"
|
||||
|
||||
results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.Query("p"), md.Query("l"), 100), md.ID())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var myFriends []friendData
|
||||
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
newFriend := friendPuts(md, results)
|
||||
for _, uid := range myFrienders {
|
||||
if uid == newFriend.ID {
|
||||
newFriend.IsMutual = true
|
||||
break
|
||||
}
|
||||
}
|
||||
myFriends = append(myFriends, newFriend)
|
||||
}
|
||||
if err := results.Err(); err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
|
||||
r := friendsGETResponse{}
|
||||
r.Code = 200
|
||||
r.Friends = myFriends
|
||||
return r
|
||||
}
|
||||
|
||||
func friendPuts(md common.MethodData, row *sql.Rows) (user friendData) {
|
||||
var err error
|
||||
|
||||
err = row.Scan(&user.ID, &user.Username, &user.RegisteredOn, &user.Privileges, &user.LatestActivity, &user.UsernameAKA, &user.Country)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type friendsWithResponse struct {
|
||||
common.ResponseBase
|
||||
Friends bool `json:"friend"`
|
||||
Mutual bool `json:"mutual"`
|
||||
}
|
||||
|
||||
// FriendsWithGET checks the current user is friends with the one passed in the request path.
|
||||
func FriendsWithGET(md common.MethodData) common.CodeMessager {
|
||||
var r friendsWithResponse
|
||||
r.Code = 200
|
||||
uid := common.Int(md.Query("id"))
|
||||
if uid == 0 {
|
||||
return r
|
||||
}
|
||||
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users_relationships WHERE user1 = ? AND user2 = ? LIMIT 1), EXISTS(SELECT 1 FROM users_relationships WHERE user2 = ? AND user1 = ? LIMIT 1)", md.ID(), uid, md.ID(), uid).Scan(&r.Friends, &r.Mutual)
|
||||
if err != sql.ErrNoRows && err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if !r.Friends {
|
||||
r.Mutual = false
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// FriendsAddPOST adds an user to the friends.
|
||||
func FriendsAddPOST(md common.MethodData) common.CodeMessager {
|
||||
var u struct {
|
||||
User int `json:"user"`
|
||||
}
|
||||
md.Unmarshal(&u)
|
||||
return addFriend(md, u.User)
|
||||
}
|
||||
|
||||
func addFriend(md common.MethodData, u int) common.CodeMessager {
|
||||
if md.ID() == u {
|
||||
return common.SimpleResponse(406, "Just so you know: you can't add yourself to your friends.")
|
||||
}
|
||||
if !userExists(md, u) {
|
||||
return common.SimpleResponse(404, "I'd also like to be friends with someone who doesn't even exist (???), however that's NOT POSSIBLE.")
|
||||
}
|
||||
var (
|
||||
relExists bool
|
||||
isMutual bool
|
||||
)
|
||||
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users_relationships WHERE user1 = ? AND user2 = ?), EXISTS(SELECT 1 FROM users_relationships WHERE user2 = ? AND user1 = ?)", md.ID(), u, md.ID(), u).Scan(&relExists, &isMutual)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if !relExists {
|
||||
_, err := md.DB.Exec("INSERT INTO users_relationships(user1, user2) VALUES (?, ?)", md.User.UserID, u)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
var r friendsWithResponse
|
||||
r.Code = 200
|
||||
r.Friends = true
|
||||
r.Mutual = isMutual
|
||||
return r
|
||||
}
|
||||
|
||||
// userExists makes sure an user exists.
|
||||
func userExists(md common.MethodData, u int) (r bool) {
|
||||
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ? AND "+
|
||||
md.User.OnlyUserPublic(true)+")", u).Scan(&r)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
md.Err(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FriendsDelPOST deletes an user's friend.
|
||||
func FriendsDelPOST(md common.MethodData) common.CodeMessager {
|
||||
var u struct {
|
||||
User int `json:"user"`
|
||||
}
|
||||
md.Unmarshal(&u)
|
||||
return delFriend(md, u.User)
|
||||
}
|
||||
|
||||
func delFriend(md common.MethodData, u int) common.CodeMessager {
|
||||
_, err := md.DB.Exec("DELETE FROM users_relationships WHERE user1 = ? AND user2 = ?", md.ID(), u)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
r := friendsWithResponse{
|
||||
Friends: false,
|
||||
Mutual: false,
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
121
vendor/zxq.co/ripple/rippleapi/app/v1/leaderboard.go
vendored
Normal file
121
vendor/zxq.co/ripple/rippleapi/app/v1/leaderboard.go
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
redis "gopkg.in/redis.v5"
|
||||
|
||||
"zxq.co/ripple/ocl"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type leaderboardUser struct {
|
||||
userData
|
||||
ChosenMode modeData `json:"chosen_mode"`
|
||||
PlayStyle int `json:"play_style"`
|
||||
FavouriteMode int `json:"favourite_mode"`
|
||||
}
|
||||
|
||||
type leaderboardResponse struct {
|
||||
common.ResponseBase
|
||||
Users []leaderboardUser `json:"users"`
|
||||
}
|
||||
|
||||
const lbUserQuery = `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka, users_stats.country,
|
||||
users_stats.play_style, users_stats.favourite_mode,
|
||||
|
||||
users_stats.ranked_score_%[1]s, users_stats.total_score_%[1]s, users_stats.playcount_%[1]s,
|
||||
users_stats.replays_watched_%[1]s, users_stats.total_hits_%[1]s,
|
||||
users_stats.avg_accuracy_%[1]s, users_stats.pp_%[1]s
|
||||
FROM users
|
||||
INNER JOIN users_stats ON users_stats.id = users.id
|
||||
WHERE users.id IN (?)
|
||||
`
|
||||
|
||||
// LeaderboardGET gets the leaderboard.
|
||||
func LeaderboardGET(md common.MethodData) common.CodeMessager {
|
||||
m := getMode(md.Query("mode"))
|
||||
|
||||
// md.Query.Country
|
||||
p := common.Int(md.Query("p")) - 1
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
l := common.InString(1, md.Query("l"), 500, 50)
|
||||
|
||||
key := "ripple:leaderboard:" + m
|
||||
if md.Query("country") != "" {
|
||||
key += ":" + md.Query("country")
|
||||
}
|
||||
|
||||
results, err := md.R.ZRevRange(key, int64(p*l), int64(p*l+l-1)).Result()
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var resp leaderboardResponse
|
||||
resp.Code = 200
|
||||
|
||||
if len(results) == 0 {
|
||||
return resp
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(lbUserQuery+` ORDER BY users_stats.pp_%[1]s DESC, users_stats.ranked_score_%[1]s DESC`, m)
|
||||
query, params, _ := sqlx.In(query, results)
|
||||
rows, err := md.DB.Query(query, params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
for rows.Next() {
|
||||
var u leaderboardUser
|
||||
err := rows.Scan(
|
||||
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges, &u.LatestActivity,
|
||||
|
||||
&u.UsernameAKA, &u.Country, &u.PlayStyle, &u.FavouriteMode,
|
||||
|
||||
&u.ChosenMode.RankedScore, &u.ChosenMode.TotalScore, &u.ChosenMode.PlayCount,
|
||||
&u.ChosenMode.ReplaysWatched, &u.ChosenMode.TotalHits,
|
||||
&u.ChosenMode.Accuracy, &u.ChosenMode.PP,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
u.ChosenMode.Level = ocl.GetLevelPrecise(int64(u.ChosenMode.TotalScore))
|
||||
if i := leaderboardPosition(md.R, m, u.ID); i != nil {
|
||||
u.ChosenMode.GlobalLeaderboardRank = i
|
||||
}
|
||||
if i := countryPosition(md.R, m, u.ID, u.Country); i != nil {
|
||||
u.ChosenMode.CountryLeaderboardRank = i
|
||||
}
|
||||
resp.Users = append(resp.Users, u)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func leaderboardPosition(r *redis.Client, mode string, user int) *int {
|
||||
return _position(r, "ripple:leaderboard:"+mode, user)
|
||||
}
|
||||
|
||||
func countryPosition(r *redis.Client, mode string, user int, country string) *int {
|
||||
return _position(r, "ripple:leaderboard:"+mode+":"+strings.ToLower(country), user)
|
||||
}
|
||||
|
||||
func _position(r *redis.Client, key string, user int) *int {
|
||||
res := r.ZRevRank(key, strconv.Itoa(user))
|
||||
if res.Err() == redis.Nil {
|
||||
return nil
|
||||
}
|
||||
x := int(res.Val()) + 1
|
||||
return &x
|
||||
}
|
192
vendor/zxq.co/ripple/rippleapi/app/v1/manage_user.go
vendored
Normal file
192
vendor/zxq.co/ripple/rippleapi/app/v1/manage_user.go
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type setAllowedData struct {
|
||||
UserID int `json:"user_id"`
|
||||
Allowed int `json:"allowed"`
|
||||
}
|
||||
|
||||
// UserManageSetAllowedPOST allows to set the allowed status of an user.
|
||||
func UserManageSetAllowedPOST(md common.MethodData) common.CodeMessager {
|
||||
var data setAllowedData
|
||||
if err := md.Unmarshal(&data); err != nil {
|
||||
return ErrBadJSON
|
||||
}
|
||||
if data.Allowed < 0 || data.Allowed > 2 {
|
||||
return common.SimpleResponse(400, "Allowed status must be between 0 and 2")
|
||||
}
|
||||
var banDatetime int64
|
||||
var privsSet string
|
||||
if data.Allowed == 0 {
|
||||
banDatetime = time.Now().Unix()
|
||||
privsSet = "privileges = (privileges & ~3)"
|
||||
} else {
|
||||
banDatetime = 0
|
||||
privsSet = "privileges = (privileges | 3)"
|
||||
}
|
||||
_, err := md.DB.Exec("UPDATE users SET "+privsSet+", ban_datetime = ? WHERE id = ?", banDatetime, data.UserID)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
rapLog(md, fmt.Sprintf("changed UserID:%d's allowed to %d. This was done using the API's terrible ManageSetAllowed.", data.UserID, data.Allowed))
|
||||
go fixPrivileges(data.UserID, md.DB)
|
||||
query := `
|
||||
SELECT users.id, users.username, register_datetime, privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM users
|
||||
LEFT JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
WHERE users.id=?
|
||||
LIMIT 1`
|
||||
return userPutsSingle(md, md.DB.QueryRowx(query, data.UserID))
|
||||
}
|
||||
|
||||
type userEditData struct {
|
||||
ID int `json:"id"`
|
||||
Username *string `json:"username"`
|
||||
UsernameAKA *string `json:"username_aka"`
|
||||
//Privileges *uint64 `json:"privileges"`
|
||||
Country *string `json:"country"`
|
||||
SilenceInfo *silenceInfo `json:"silence_info"`
|
||||
ResetUserpage bool `json:"reset_userpage"`
|
||||
//ResetAvatar bool `json:"reset_avatar"`
|
||||
}
|
||||
|
||||
// UserEditPOST allows to edit an user's information.
|
||||
func UserEditPOST(md common.MethodData) common.CodeMessager {
|
||||
var data userEditData
|
||||
if err := md.Unmarshal(&data); err != nil {
|
||||
fmt.Println(err)
|
||||
return ErrBadJSON
|
||||
}
|
||||
|
||||
if data.ID == 0 {
|
||||
return common.SimpleResponse(404, "That user could not be found")
|
||||
}
|
||||
|
||||
var prevUser struct {
|
||||
Username string
|
||||
Privileges uint64
|
||||
}
|
||||
err := md.DB.Get(&prevUser, "SELECT username, privileges FROM users WHERE id = ? LIMIT 1", data.ID)
|
||||
|
||||
switch err {
|
||||
case nil: // carry on
|
||||
case sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That user could not be found")
|
||||
default:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
const initQuery = "UPDATE users SET\n"
|
||||
q := initQuery
|
||||
var args []interface{}
|
||||
|
||||
// totally did not realise I had to update some fields in users_stats as well
|
||||
// and just copy pasting the above code by prefixing "stats" to every
|
||||
// variable
|
||||
const statsInitQuery = "UPDATE users_stats SET\n"
|
||||
statsQ := statsInitQuery
|
||||
var statsArgs []interface{}
|
||||
|
||||
if common.UserPrivileges(prevUser.Privileges)&common.AdminPrivilegeManageUsers != 0 &&
|
||||
data.ID != md.User.UserID {
|
||||
return common.SimpleResponse(403, "Can't edit that user")
|
||||
}
|
||||
|
||||
if data.Username != nil {
|
||||
if strings.Contains(*data.Username, " ") && strings.Contains(*data.Username, "_") {
|
||||
return common.SimpleResponse(400, "Mixed spaces and underscores")
|
||||
}
|
||||
if usernameAvailable(md, *data.Username, data.ID) {
|
||||
return common.SimpleResponse(409, "User with that username exists")
|
||||
}
|
||||
jsonData, _ := json.Marshal(struct {
|
||||
UserID int `json:"userID"`
|
||||
NewUsername string `json:"newUsername"`
|
||||
}{data.ID, *data.Username})
|
||||
md.R.Publish("peppy:change_username", string(jsonData))
|
||||
appendToUserNotes(md, "Username change: "+prevUser.Username+" -> "+*data.Username, data.ID)
|
||||
}
|
||||
if data.UsernameAKA != nil {
|
||||
statsQ += "username_aka = ?,\n"
|
||||
statsArgs = append(statsArgs, *data.UsernameAKA)
|
||||
}
|
||||
/*if data.Privileges != nil {
|
||||
q += "privileges = ?,\n"
|
||||
args = append(args, *data.Privileges)
|
||||
// UserNormal or UserPublic changed
|
||||
if *data.Privileges & 3 != 3 && *data.Privileges & 3 != prevUser.Privileges & 3 {
|
||||
q += "ban_datetime = ?"
|
||||
args = append(args, meme)
|
||||
}
|
||||
// https://zxq.co/ripple/old-frontend/src/master/inc/Do.php#L355 ?
|
||||
// should also check for AdminManagePrivileges
|
||||
// should also check out the code for CM restring/banning
|
||||
}*/
|
||||
if data.Country != nil {
|
||||
statsQ += "country = ?,\n"
|
||||
statsArgs = append(statsArgs, *data.Country)
|
||||
rapLog(md, fmt.Sprintf("has changed %s country to %s", prevUser.Username, *data.Country))
|
||||
appendToUserNotes(md, "country changed to "+*data.Country, data.ID)
|
||||
}
|
||||
if data.SilenceInfo != nil && md.User.UserPrivileges&common.AdminPrivilegeSilenceUsers != 0 {
|
||||
q += "silence_end = ?, silence_reason = ?,\n"
|
||||
args = append(args, time.Time(data.SilenceInfo.End).Unix(), data.SilenceInfo.Reason)
|
||||
}
|
||||
if data.ResetUserpage {
|
||||
statsQ += "userpage_content = '',\n"
|
||||
}
|
||||
|
||||
if q != initQuery {
|
||||
q = q[:len(q)-2] + " WHERE id = ? LIMIT 1"
|
||||
args = append(args, data.ID)
|
||||
_, err = md.DB.Exec(q, args...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
if statsQ != statsInitQuery {
|
||||
statsQ = statsQ[:len(statsQ)-2] + " WHERE id = ? LIMIT 1"
|
||||
statsArgs = append(statsArgs, data.ID)
|
||||
_, err = md.DB.Exec(statsQ, statsArgs...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
|
||||
rapLog(md, fmt.Sprintf("has updated user %s", prevUser.Username))
|
||||
|
||||
return userPutsSingle(md, md.DB.QueryRowx(userFields+" WHERE users.id = ? LIMIT 1", data.ID))
|
||||
}
|
||||
|
||||
func appendToUserNotes(md common.MethodData, message string, user int) {
|
||||
message = "\n[" + time.Now().Format("2006-01-02 15:04:05") + "] API: " + message
|
||||
_, err := md.DB.Exec("UPDATE users SET notes = CONCAT(COALESCE(notes, ''), ?) WHERE id = ?",
|
||||
message, user)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
func usernameAvailable(md common.MethodData, u string, userID int) (r bool) {
|
||||
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE username_safe = ? AND id != ?)", common.SafeUsername(u), userID).Scan(&r)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
md.Err(err)
|
||||
}
|
||||
return
|
||||
}
|
108
vendor/zxq.co/ripple/rippleapi/app/v1/meta_linux.go
vendored
Normal file
108
vendor/zxq.co/ripple/rippleapi/app/v1/meta_linux.go
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
// +build !windows
|
||||
|
||||
// TODO: Make all these methods POST
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// MetaRestartGET restarts the API with Zero Downtime™.
|
||||
func MetaRestartGET(md common.MethodData) common.CodeMessager {
|
||||
proc, err := os.FindProcess(syscall.Getpid())
|
||||
if err != nil {
|
||||
return common.SimpleResponse(500, "couldn't find process. what the fuck?")
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
proc.Signal(syscall.SIGUSR2)
|
||||
}()
|
||||
return common.SimpleResponse(200, "brb")
|
||||
}
|
||||
|
||||
var upSince = time.Now()
|
||||
|
||||
type metaUpSinceResponse struct {
|
||||
common.ResponseBase
|
||||
Code int `json:"code"`
|
||||
Since int64 `json:"since"`
|
||||
}
|
||||
|
||||
// MetaUpSinceGET retrieves the moment the API application was started.
|
||||
// Mainly used to get if the API was restarted.
|
||||
func MetaUpSinceGET(md common.MethodData) common.CodeMessager {
|
||||
return metaUpSinceResponse{
|
||||
Code: 200,
|
||||
Since: int64(upSince.UnixNano()),
|
||||
}
|
||||
}
|
||||
|
||||
// MetaUpdateGET updates the API to the latest version, and restarts it.
|
||||
func MetaUpdateGET(md common.MethodData) common.CodeMessager {
|
||||
if f, err := os.Stat(".git"); err == os.ErrNotExist || !f.IsDir() {
|
||||
return common.SimpleResponse(500, "instance is not using git")
|
||||
}
|
||||
go func() {
|
||||
if !execCommand("git", "pull", "origin", "master") {
|
||||
return
|
||||
}
|
||||
// go get
|
||||
// -u: update all dependencies
|
||||
// -d: stop after downloading deps
|
||||
if !execCommand("go", "get", "-v", "-u", "-d") {
|
||||
return
|
||||
}
|
||||
if !execCommand("bash", "-c", "go build -v -ldflags \"-X main.Version=`git rev-parse HEAD`\"") {
|
||||
return
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(syscall.Getpid())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
proc.Signal(syscall.SIGUSR2)
|
||||
}()
|
||||
return common.SimpleResponse(200, "Started updating! "+surpriseMe())
|
||||
}
|
||||
|
||||
func execCommand(command string, args ...string) bool {
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Env = os.Environ()
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
data, err := ioutil.ReadAll(stderr)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
// Bob. We got a problem.
|
||||
if len(data) != 0 {
|
||||
log.Println(string(data))
|
||||
}
|
||||
io.Copy(os.Stdout, stdout)
|
||||
cmd.Wait()
|
||||
stdout.Close()
|
||||
return true
|
||||
}
|
42
vendor/zxq.co/ripple/rippleapi/app/v1/meta_windows.go
vendored
Normal file
42
vendor/zxq.co/ripple/rippleapi/app/v1/meta_windows.go
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
// +build windows
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// MetaRestartGET restarts the API with Zero Downtime™.
|
||||
func MetaRestartGET(md common.MethodData) common.CodeMessager {
|
||||
return common.SimpleResponse(200, "brb in your dreams")
|
||||
}
|
||||
|
||||
// MetaKillGET kills the API process. NOTE TO EVERYONE: NEVER. EVER. USE IN PROD.
|
||||
// Mainly created because I couldn't bother to fire up a terminal, do htop and kill the API each time.
|
||||
func MetaKillGET(md common.MethodData) common.CodeMessager {
|
||||
return common.SimpleResponse(200, "haha")
|
||||
}
|
||||
|
||||
var upSince = time.Now()
|
||||
|
||||
type metaUpSinceResponse struct {
|
||||
common.ResponseBase
|
||||
Code int `json:"code"`
|
||||
Since int64 `json:"since"`
|
||||
}
|
||||
|
||||
// MetaUpSinceGET retrieves the moment the API application was started.
|
||||
// Mainly used to get if the API was restarted.
|
||||
func MetaUpSinceGET(md common.MethodData) common.CodeMessager {
|
||||
return metaUpSinceResponse{
|
||||
Code: 200,
|
||||
Since: int64(upSince.UnixNano()),
|
||||
}
|
||||
}
|
||||
|
||||
// MetaUpdateGET updates the API to the latest version, and restarts it.
|
||||
func MetaUpdateGET(md common.MethodData) common.CodeMessager {
|
||||
return common.SimpleResponse(200, "lol u wish")
|
||||
}
|
139
vendor/zxq.co/ripple/rippleapi/app/v1/ping.go
vendored
Normal file
139
vendor/zxq.co/ripple/rippleapi/app/v1/ping.go
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
var rn = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
var kaomojis = [...]string{
|
||||
"Σ(ノ°▽°)ノ",
|
||||
"( ƅ°ਉ°)ƅ",
|
||||
"ヽ( ・∀・)ノ",
|
||||
"˭̡̞(◞⁎˃ᆺ˂)◞*✰",
|
||||
"(p^-^)p",
|
||||
"(ノ^∇^)ノ゚",
|
||||
"ヽ(〃・ω・)ノ",
|
||||
"(۶* ‘ꆚ’)۶”",
|
||||
"(。>ω<)。",
|
||||
"(ノ。≧◇≦)ノ",
|
||||
"ヾ(。・ω・)シ",
|
||||
"(ノ・д・)ノ",
|
||||
".+:。(ノ・ω・)ノ゙",
|
||||
"Σ(*ノ´>ω<。`)ノ",
|
||||
"ヾ(〃^∇^)ノ♪",
|
||||
"\(@ ̄∇ ̄@)/",
|
||||
"\(^▽^@)ノ",
|
||||
"ヾ(@^▽^@)ノ",
|
||||
"(((\(@v@)/)))",
|
||||
"\(*T▽T*)/",
|
||||
"\(^▽^)/",
|
||||
"\(T∇T)/",
|
||||
"ヽ( ★ω★)ノ",
|
||||
"ヽ(;▽;)ノ",
|
||||
"ヾ(。◕ฺ∀◕ฺ)ノ",
|
||||
"ヾ(@† ▽ †@)ノ",
|
||||
"ヾ(@^∇^@)ノ",
|
||||
"ヾ(@^▽^@)ノ",
|
||||
"ヾ(@^▽^@)ノ",
|
||||
"ヾ(@゜▽゜@)ノ",
|
||||
"(.=^・ェ・^=)",
|
||||
"((≡^⚲͜^≡))",
|
||||
"(^・o・^)ノ”",
|
||||
"(^._.^)ノ",
|
||||
"(^人^)",
|
||||
"(=;ェ;=)",
|
||||
"(=`ω´=)",
|
||||
"(=`ェ´=)",
|
||||
"(=´∇`=)",
|
||||
"(=^・^=)",
|
||||
"(=^・ェ・^=)",
|
||||
"(=^‥^=)",
|
||||
"(=TェT=)",
|
||||
"(=xェx=)",
|
||||
"\(=^‥^)/’`",
|
||||
"~(=^‥^)/",
|
||||
"└(=^‥^=)┐",
|
||||
"ヾ(=゚・゚=)ノ",
|
||||
"ヽ(=^・ω・^=)丿",
|
||||
"d(=^・ω・^=)b",
|
||||
"o(^・x・^)o",
|
||||
"V(=^・ω・^=)v",
|
||||
"(⁎˃ᆺ˂)",
|
||||
"(,,^・⋏・^,,)",
|
||||
}
|
||||
|
||||
var randomSentences = [...]string{
|
||||
"Proudly sponsored by Kirotuso!",
|
||||
"The brace is on fire!",
|
||||
"deverupa ga daisuki!",
|
||||
"It works!!!!",
|
||||
"Feelin' groovy!",
|
||||
"sudo rm -rf /",
|
||||
"Hi! I'm Flowey! Flowey the flower!",
|
||||
"Ripple devs are actually cats",
|
||||
"Support Howl's fund for buying a power supply for his SSD!",
|
||||
"Superman dies",
|
||||
"PP when?",
|
||||
"RWC hype",
|
||||
"I'd just like to interject for a moment.",
|
||||
"Running on an apple pie!",
|
||||
":thinking:",
|
||||
"The total entropy of an isolated system can only increase over time",
|
||||
"Where are my testicles, Summer?",
|
||||
"Why don't you ask the smartest people in the universe? Oh yeah, you can't. They blew up.",
|
||||
}
|
||||
|
||||
func surpriseMe() string {
|
||||
n := int(time.Now().UnixNano())
|
||||
return randomSentences[n%len(randomSentences)] + " " + kaomojis[n%len(kaomojis)]
|
||||
}
|
||||
|
||||
type pingResponse struct {
|
||||
common.ResponseBase
|
||||
ID int `json:"user_id"`
|
||||
Privileges common.Privileges `json:"privileges"`
|
||||
UserPrivileges common.UserPrivileges `json:"user_privileges"`
|
||||
PrivilegesS string `json:"privileges_string"`
|
||||
UserPrivilegesS string `json:"user_privileges_string"`
|
||||
}
|
||||
|
||||
// PingGET is a message to check with the API that we are logged in, and know what are our privileges.
|
||||
func PingGET(md common.MethodData) common.CodeMessager {
|
||||
var r pingResponse
|
||||
r.Code = 200
|
||||
|
||||
if md.ID() == 0 {
|
||||
r.Message = "You have not given us a token, so we don't know who you are! But you can still login with POST /tokens " + kaomojis[rn.Intn(len(kaomojis))]
|
||||
} else {
|
||||
r.Message = surpriseMe()
|
||||
}
|
||||
|
||||
r.ID = md.ID()
|
||||
r.Privileges = md.User.TokenPrivileges
|
||||
r.UserPrivileges = md.User.UserPrivileges
|
||||
r.PrivilegesS = md.User.TokenPrivileges.String()
|
||||
r.UserPrivilegesS = md.User.UserPrivileges.String()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type surpriseMeResponse struct {
|
||||
common.ResponseBase
|
||||
Cats [100]string `json:"cats"`
|
||||
}
|
||||
|
||||
// SurpriseMeGET generates cute cats.
|
||||
//
|
||||
// ... Yes.
|
||||
func SurpriseMeGET(md common.MethodData) common.CodeMessager {
|
||||
var r surpriseMeResponse
|
||||
r.Code = 200
|
||||
for i := 0; i < 100; i++ {
|
||||
r.Cats[i] = surpriseMe()
|
||||
}
|
||||
return r
|
||||
}
|
87
vendor/zxq.co/ripple/rippleapi/app/v1/rap.go
vendored
Normal file
87
vendor/zxq.co/ripple/rippleapi/app/v1/rap.go
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type rapLogData struct {
|
||||
Through string `json:"through"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type rapLogMessage struct {
|
||||
rapLogData
|
||||
Author int `json:"author"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type rapLogResponse struct {
|
||||
common.ResponseBase
|
||||
rapLogMessage
|
||||
}
|
||||
|
||||
// RAPLogPOST creates a new entry in the RAP logs
|
||||
func RAPLogPOST(md common.MethodData) common.CodeMessager {
|
||||
if md.User.UserPrivileges&common.AdminPrivilegeAccessRAP == 0 {
|
||||
return common.SimpleResponse(403, "Got lost, kiddo?")
|
||||
}
|
||||
|
||||
var d rapLogData
|
||||
if err := md.Unmarshal(&d); err != nil {
|
||||
fmt.Println(err)
|
||||
return ErrBadJSON
|
||||
}
|
||||
|
||||
if d.Text == "" {
|
||||
return ErrMissingField("text")
|
||||
}
|
||||
if d.Through == "" {
|
||||
ua := string(md.Ctx.UserAgent())
|
||||
if len(ua) > 20 {
|
||||
ua = ua[:20] + "…"
|
||||
}
|
||||
d.Through = "API"
|
||||
if ua != "" {
|
||||
d.Through += " (" + ua + ")"
|
||||
}
|
||||
}
|
||||
if len(d.Through) > 30 {
|
||||
d.Through = d.Through[:30]
|
||||
}
|
||||
|
||||
created := time.Now()
|
||||
_, err := md.DB.Exec("INSERT INTO rap_logs(userid, text, datetime, through) VALUES (?, ?, ?, ?)",
|
||||
md.User.UserID, d.Text, created.Unix(), d.Through)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var resp rapLogResponse
|
||||
resp.rapLogData = d
|
||||
resp.Author = md.User.UserID
|
||||
resp.CreatedAt = created.Truncate(time.Second)
|
||||
resp.Code = 200
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func rapLog(md common.MethodData, message string) {
|
||||
ua := string(md.Ctx.UserAgent())
|
||||
if len(ua) > 20 {
|
||||
ua = ua[:20] + "…"
|
||||
}
|
||||
through := "API"
|
||||
if ua != "" {
|
||||
through += " (" + ua + ")"
|
||||
}
|
||||
|
||||
_, err := md.DB.Exec("INSERT INTO rap_logs(userid, text, datetime, through) VALUES (?, ?, ?, ?)",
|
||||
md.User.UserID, message, time.Now().Unix(), through)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
}
|
156
vendor/zxq.co/ripple/rippleapi/app/v1/score.go
vendored
Normal file
156
vendor/zxq.co/ripple/rippleapi/app/v1/score.go
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
// Score is a score done on Ripple.
|
||||
type Score struct {
|
||||
ID int `json:"id"`
|
||||
BeatmapMD5 string `json:"beatmap_md5"`
|
||||
Score int64 `json:"score"`
|
||||
MaxCombo int `json:"max_combo"`
|
||||
FullCombo bool `json:"full_combo"`
|
||||
Mods int `json:"mods"`
|
||||
Count300 int `json:"count_300"`
|
||||
Count100 int `json:"count_100"`
|
||||
Count50 int `json:"count_50"`
|
||||
CountGeki int `json:"count_geki"`
|
||||
CountKatu int `json:"count_katu"`
|
||||
CountMiss int `json:"count_miss"`
|
||||
Time common.UnixTimestamp `json:"time"`
|
||||
PlayMode int `json:"play_mode"`
|
||||
Accuracy float64 `json:"accuracy"`
|
||||
PP float32 `json:"pp"`
|
||||
Rank string `json:"rank"`
|
||||
Completed int `json:"completed"`
|
||||
}
|
||||
|
||||
// beatmapScore is to differentiate from userScore, as beatmapScore contains
|
||||
// also an user, while userScore contains the beatmap.
|
||||
type beatmapScore struct {
|
||||
Score
|
||||
User userData `json:"user"`
|
||||
}
|
||||
|
||||
type scoresResponse struct {
|
||||
common.ResponseBase
|
||||
Scores []beatmapScore `json:"scores"`
|
||||
}
|
||||
|
||||
// ScoresGET retrieves the top scores for a certain beatmap.
|
||||
func ScoresGET(md common.MethodData) common.CodeMessager {
|
||||
var (
|
||||
beatmapMD5 string
|
||||
r scoresResponse
|
||||
)
|
||||
switch {
|
||||
case md.Query("md5") != "":
|
||||
beatmapMD5 = md.Query("md5")
|
||||
case md.Query("b") != "":
|
||||
err := md.DB.Get(&beatmapMD5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", md.Query("b"))
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
r.Code = 200
|
||||
return r
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
default:
|
||||
return ErrMissingField("md5|b")
|
||||
}
|
||||
|
||||
sort := common.Sort(md, common.SortConfiguration{
|
||||
Default: "scores.pp DESC, scores.score DESC",
|
||||
Table: "scores",
|
||||
Allowed: []string{"pp", "score", "accuracy", "id"},
|
||||
})
|
||||
|
||||
rows, err := md.DB.Query(`
|
||||
SELECT
|
||||
scores.id, scores.beatmap_md5, scores.score,
|
||||
scores.max_combo, scores.full_combo, scores.mods,
|
||||
scores.300_count, scores.100_count, scores.50_count,
|
||||
scores.gekis_count, scores.katus_count, scores.misses_count,
|
||||
scores.time, scores.play_mode, scores.accuracy, scores.pp,
|
||||
scores.completed,
|
||||
|
||||
users.id, users.username, users.register_datetime, users.privileges,
|
||||
users.latest_activity, users_stats.username_aka, users_stats.country
|
||||
FROM scores
|
||||
INNER JOIN users ON users.id = scores.userid
|
||||
INNER JOIN users_stats ON users_stats.id = scores.userid
|
||||
WHERE scores.beatmap_md5 = ? AND scores.completed = '3' AND `+md.User.OnlyUserPublic(false)+
|
||||
` `+genModeClause(md)+`
|
||||
`+sort+common.Paginate(md.Query("p"), md.Query("l"), 100), beatmapMD5)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
for rows.Next() {
|
||||
var (
|
||||
s beatmapScore
|
||||
u userData
|
||||
)
|
||||
err := rows.Scan(
|
||||
&s.ID, &s.BeatmapMD5, &s.Score.Score,
|
||||
&s.MaxCombo, &s.FullCombo, &s.Mods,
|
||||
&s.Count300, &s.Count100, &s.Count50,
|
||||
&s.CountGeki, &s.CountKatu, &s.CountMiss,
|
||||
&s.Time, &s.PlayMode, &s.Accuracy, &s.PP,
|
||||
&s.Completed,
|
||||
|
||||
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges,
|
||||
&u.LatestActivity, &u.UsernameAKA, &u.Country,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
s.User = u
|
||||
s.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(s.PlayMode),
|
||||
osuapi.Mods(s.Mods),
|
||||
s.Accuracy,
|
||||
s.Count300,
|
||||
s.Count100,
|
||||
s.Count50,
|
||||
s.CountMiss,
|
||||
))
|
||||
r.Scores = append(r.Scores, s)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
func getMode(m string) string {
|
||||
switch m {
|
||||
case "1":
|
||||
return "taiko"
|
||||
case "2":
|
||||
return "ctb"
|
||||
case "3":
|
||||
return "mania"
|
||||
default:
|
||||
return "std"
|
||||
}
|
||||
}
|
||||
|
||||
func genModeClause(md common.MethodData) string {
|
||||
var modeClause string
|
||||
if md.Query("mode") != "" {
|
||||
m, err := strconv.Atoi(md.Query("mode"))
|
||||
if err == nil && m >= 0 && m <= 3 {
|
||||
modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m)
|
||||
}
|
||||
}
|
||||
return modeClause
|
||||
}
|
167
vendor/zxq.co/ripple/rippleapi/app/v1/self.go
vendored
Normal file
167
vendor/zxq.co/ripple/rippleapi/app/v1/self.go
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/semantic-icons-ugc"
|
||||
)
|
||||
|
||||
type donorInfoResponse struct {
|
||||
common.ResponseBase
|
||||
HasDonor bool `json:"has_donor"`
|
||||
Expiration common.UnixTimestamp `json:"expiration"`
|
||||
}
|
||||
|
||||
// UsersSelfDonorInfoGET returns information about the users' donor status
|
||||
func UsersSelfDonorInfoGET(md common.MethodData) common.CodeMessager {
|
||||
var r donorInfoResponse
|
||||
var privileges uint64
|
||||
err := md.DB.QueryRow("SELECT privileges, donor_expire FROM users WHERE id = ?", md.ID()).
|
||||
Scan(&privileges, &r.Expiration)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
r.HasDonor = common.UserPrivileges(privileges)&common.UserPrivilegeDonor > 0
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type favouriteModeResponse struct {
|
||||
common.ResponseBase
|
||||
FavouriteMode int `json:"favourite_mode"`
|
||||
}
|
||||
|
||||
// UsersSelfFavouriteModeGET gets the current user's favourite mode
|
||||
func UsersSelfFavouriteModeGET(md common.MethodData) common.CodeMessager {
|
||||
var f favouriteModeResponse
|
||||
f.Code = 200
|
||||
if md.ID() == 0 {
|
||||
return f
|
||||
}
|
||||
err := md.DB.QueryRow("SELECT users_stats.favourite_mode FROM users_stats WHERE id = ?", md.ID()).
|
||||
Scan(&f.FavouriteMode)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type userSettingsData struct {
|
||||
UsernameAKA *string `json:"username_aka"`
|
||||
FavouriteMode *int `json:"favourite_mode"`
|
||||
CustomBadge struct {
|
||||
singleBadge
|
||||
Show *bool `json:"show"`
|
||||
} `json:"custom_badge"`
|
||||
PlayStyle *int `json:"play_style"`
|
||||
}
|
||||
|
||||
// UsersSelfSettingsPOST allows to modify information about the current user.
|
||||
func UsersSelfSettingsPOST(md common.MethodData) common.CodeMessager {
|
||||
var d userSettingsData
|
||||
md.Unmarshal(&d)
|
||||
|
||||
// input sanitisation
|
||||
*d.UsernameAKA = common.SanitiseString(*d.UsernameAKA)
|
||||
if md.User.UserPrivileges&common.UserPrivilegeDonor > 0 {
|
||||
d.CustomBadge.Name = common.SanitiseString(d.CustomBadge.Name)
|
||||
d.CustomBadge.Icon = sanitiseIconName(d.CustomBadge.Icon)
|
||||
} else {
|
||||
d.CustomBadge.singleBadge = singleBadge{}
|
||||
d.CustomBadge.Show = nil
|
||||
}
|
||||
d.FavouriteMode = intPtrIn(0, d.FavouriteMode, 3)
|
||||
|
||||
q := new(common.UpdateQuery).
|
||||
Add("s.username_aka", d.UsernameAKA).
|
||||
Add("s.favourite_mode", d.FavouriteMode).
|
||||
Add("s.custom_badge_name", d.CustomBadge.Name).
|
||||
Add("s.custom_badge_icon", d.CustomBadge.Icon).
|
||||
Add("s.show_custom_badge", d.CustomBadge.Show).
|
||||
Add("s.play_style", d.PlayStyle)
|
||||
_, err := md.DB.Exec("UPDATE users u, users_stats s SET "+q.Fields()+" WHERE s.id = u.id AND u.id = ?", append(q.Parameters, md.ID())...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
return UsersSelfSettingsGET(md)
|
||||
}
|
||||
|
||||
func sanitiseIconName(s string) string {
|
||||
classes := strings.Split(s, " ")
|
||||
n := make([]string, 0, len(classes))
|
||||
for _, c := range classes {
|
||||
if !in(c, n) && in(c, semanticiconsugc.SaneIcons) {
|
||||
n = append(n, c)
|
||||
}
|
||||
}
|
||||
return strings.Join(n, " ")
|
||||
}
|
||||
|
||||
func in(a string, b []string) bool {
|
||||
for _, x := range b {
|
||||
if x == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type userSettingsResponse struct {
|
||||
common.ResponseBase
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Flags uint `json:"flags"`
|
||||
userSettingsData
|
||||
}
|
||||
|
||||
// UsersSelfSettingsGET allows to get "sensitive" information about the current user.
|
||||
func UsersSelfSettingsGET(md common.MethodData) common.CodeMessager {
|
||||
var r userSettingsResponse
|
||||
var ccb bool
|
||||
r.Code = 200
|
||||
err := md.DB.QueryRow(`
|
||||
SELECT
|
||||
u.id, u.username,
|
||||
u.email, s.username_aka, s.favourite_mode,
|
||||
s.show_custom_badge, s.custom_badge_icon,
|
||||
s.custom_badge_name, s.can_custom_badge,
|
||||
s.play_style, u.flags
|
||||
FROM users u
|
||||
LEFT JOIN users_stats s ON u.id = s.id
|
||||
WHERE u.id = ?`, md.ID()).Scan(
|
||||
&r.ID, &r.Username,
|
||||
&r.Email, &r.UsernameAKA, &r.FavouriteMode,
|
||||
&r.CustomBadge.Show, &r.CustomBadge.Icon,
|
||||
&r.CustomBadge.Name, &ccb,
|
||||
&r.PlayStyle, &r.Flags,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if !ccb {
|
||||
r.CustomBadge = struct {
|
||||
singleBadge
|
||||
Show *bool `json:"show"`
|
||||
}{}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func intPtrIn(x int, y *int, z int) *int {
|
||||
if y == nil {
|
||||
return nil
|
||||
}
|
||||
if *y > z {
|
||||
return nil
|
||||
}
|
||||
if *y < x {
|
||||
return nil
|
||||
}
|
||||
return y
|
||||
}
|
219
vendor/zxq.co/ripple/rippleapi/app/v1/token.go
vendored
Normal file
219
vendor/zxq.co/ripple/rippleapi/app/v1/token.go
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/schiavolib"
|
||||
)
|
||||
|
||||
// TokenSelfDeletePOST deletes the token the user is connecting with.
|
||||
func TokenSelfDeletePOST(md common.MethodData) common.CodeMessager {
|
||||
if md.ID() == 0 {
|
||||
return common.SimpleResponse(400, "How should we delete your token if you haven't even given us one?!")
|
||||
}
|
||||
var err error
|
||||
if md.IsBearer() {
|
||||
_, err = md.DB.Exec("DELETE FROM osin_access WHERE access_token = ? LIMIT 1",
|
||||
fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value))))
|
||||
} else {
|
||||
_, err = md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1",
|
||||
fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value))))
|
||||
}
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
return common.SimpleResponse(200, "Bye!")
|
||||
}
|
||||
|
||||
type token struct {
|
||||
ID int `json:"id"`
|
||||
Privileges uint64 `json:"privileges"`
|
||||
Description string `json:"description"`
|
||||
LastUpdated common.UnixTimestamp `json:"last_updated"`
|
||||
}
|
||||
type tokenResponse struct {
|
||||
common.ResponseBase
|
||||
Tokens []token `json:"tokens"`
|
||||
}
|
||||
|
||||
// TokenGET retrieves a list listing all the user's public tokens.
|
||||
func TokenGET(md common.MethodData) common.CodeMessager {
|
||||
wc := common.Where("user = ? AND private = 0", strconv.Itoa(md.ID()))
|
||||
if md.Query("id") != "" {
|
||||
wc.Where("id = ?", md.Query("id"))
|
||||
}
|
||||
rows, err := md.DB.Query("SELECT id, privileges, description, last_updated FROM tokens "+
|
||||
wc.Clause+common.Paginate(md.Query("p"), md.Query("l"), 50), wc.Params...)
|
||||
|
||||
if err != nil {
|
||||
return Err500
|
||||
}
|
||||
var r tokenResponse
|
||||
for rows.Next() {
|
||||
var t token
|
||||
err = rows.Scan(&t.ID, &t.Privileges, &t.Description, &t.LastUpdated)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Tokens = append(r.Tokens, t)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type oauthClient struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID int `json:"owner_id"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// Scan scans the extra in the mysql table into Name, OwnerID and Avatar.
|
||||
func (o *oauthClient) Scan(src interface{}) error {
|
||||
var s []byte
|
||||
switch x := src.(type) {
|
||||
case string:
|
||||
s = []byte(x)
|
||||
case []byte:
|
||||
s = x
|
||||
default:
|
||||
return errors.New("Can't scan non-string")
|
||||
}
|
||||
|
||||
var vals [3]string
|
||||
err := json.Unmarshal(s, &vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Name = vals[0]
|
||||
o.OwnerID, _ = strconv.Atoi(vals[1])
|
||||
o.Avatar = vals[2]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bearerToken struct {
|
||||
Client oauthClient `json:"client"`
|
||||
Scope string `json:"scope"`
|
||||
Privileges common.Privileges `json:"privileges"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
type tokenSingleResponse struct {
|
||||
common.ResponseBase
|
||||
token
|
||||
}
|
||||
|
||||
type bearerTokenSingleResponse struct {
|
||||
common.ResponseBase
|
||||
bearerToken
|
||||
}
|
||||
|
||||
// TokenSelfGET retrieves information about the token the user is connecting with.
|
||||
func TokenSelfGET(md common.MethodData) common.CodeMessager {
|
||||
if md.ID() == 0 {
|
||||
return common.SimpleResponse(404, "How are we supposed to find the token you're using if you ain't even using one?!")
|
||||
}
|
||||
if md.IsBearer() {
|
||||
return getBearerToken(md)
|
||||
}
|
||||
var r tokenSingleResponse
|
||||
// md.User.ID = token id, userid would have been md.User.UserID. what a clusterfuck
|
||||
err := md.DB.QueryRow("SELECT id, privileges, description, last_updated FROM tokens WHERE id = ?", md.User.ID).Scan(
|
||||
&r.ID, &r.Privileges, &r.Description, &r.LastUpdated,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
func getBearerToken(md common.MethodData) common.CodeMessager {
|
||||
var b bearerTokenSingleResponse
|
||||
err := md.DB.
|
||||
QueryRow(`
|
||||
SELECT t.scope, t.created_at, c.id, c.extra
|
||||
FROM osin_access t INNER JOIN osin_client c
|
||||
WHERE t.access_token = ?
|
||||
`, fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value)))).Scan(
|
||||
&b.Scope, &b.Created, &b.Client.ID, &b.Client,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
b.Code = 200
|
||||
b.Privileges = md.User.TokenPrivileges
|
||||
return b
|
||||
}
|
||||
|
||||
// TokenFixPrivilegesPOST fixes the privileges on the token of the given user,
|
||||
// or of all the users if no user is given.
|
||||
func TokenFixPrivilegesPOST(md common.MethodData) common.CodeMessager {
|
||||
id := common.Int(md.Query("id"))
|
||||
if md.Query("id") == "self" {
|
||||
id = md.ID()
|
||||
}
|
||||
go fixPrivileges(id, md.DB)
|
||||
return common.SimpleResponse(200, "Privilege fixing started!")
|
||||
}
|
||||
|
||||
func fixPrivileges(user int, db *sqlx.DB) {
|
||||
var wc string
|
||||
var params = make([]interface{}, 0, 1)
|
||||
if user != 0 {
|
||||
// dirty, but who gives a shit
|
||||
wc = "WHERE user = ?"
|
||||
params = append(params, user)
|
||||
}
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
tokens.id, tokens.privileges, users.privileges
|
||||
FROM tokens
|
||||
LEFT JOIN users ON users.id = tokens.user
|
||||
`+wc, params...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
return
|
||||
}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int
|
||||
privsRaw uint64
|
||||
privs common.Privileges
|
||||
newPrivs common.Privileges
|
||||
privilegesRaw uint64
|
||||
)
|
||||
err := rows.Scan(&id, &privsRaw, &privilegesRaw)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
privileges := common.UserPrivileges(privilegesRaw)
|
||||
privs = common.Privileges(privsRaw)
|
||||
newPrivs = privs.CanOnly(privileges)
|
||||
if newPrivs != privs {
|
||||
_, err := db.Exec("UPDATE tokens SET privileges = ? WHERE id = ? LIMIT 1", uint64(newPrivs), id)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
447
vendor/zxq.co/ripple/rippleapi/app/v1/user.go
vendored
Normal file
447
vendor/zxq.co/ripple/rippleapi/app/v1/user.go
vendored
Normal file
@@ -0,0 +1,447 @@
|
||||
// Package v1 implements the first version of the Ripple API.
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"zxq.co/ripple/ocl"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type userData struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
UsernameAKA string `json:"username_aka"`
|
||||
RegisteredOn common.UnixTimestamp `json:"registered_on"`
|
||||
Privileges uint64 `json:"privileges"`
|
||||
LatestActivity common.UnixTimestamp `json:"latest_activity"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
const userFields = `SELECT users.id, users.username, register_datetime, users.privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM users
|
||||
INNER JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
`
|
||||
|
||||
// UsersGET is the API handler for GET /users
|
||||
func UsersGET(md common.MethodData) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users")
|
||||
if shouldRet != nil {
|
||||
return userPutsMulti(md)
|
||||
}
|
||||
|
||||
query := userFields + `
|
||||
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
|
||||
LIMIT 1`
|
||||
return userPutsSingle(md, md.DB.QueryRowx(query, param))
|
||||
}
|
||||
|
||||
type userPutsSingleUserData struct {
|
||||
common.ResponseBase
|
||||
userData
|
||||
}
|
||||
|
||||
func userPutsSingle(md common.MethodData, row *sqlx.Row) common.CodeMessager {
|
||||
var err error
|
||||
var user userPutsSingleUserData
|
||||
|
||||
err = row.StructScan(&user.userData)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "No such user was found!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
user.Code = 200
|
||||
return user
|
||||
}
|
||||
|
||||
type userPutsMultiUserData struct {
|
||||
common.ResponseBase
|
||||
Users []userData `json:"users"`
|
||||
}
|
||||
|
||||
func userPutsMulti(md common.MethodData) common.CodeMessager {
|
||||
pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
|
||||
// query composition
|
||||
wh := common.
|
||||
Where("users.username_safe = ?", common.SafeUsername(md.Query("nname"))).
|
||||
Where("users.id = ?", md.Query("iid")).
|
||||
Where("users.privileges = ?", md.Query("privileges")).
|
||||
Where("users.privileges & ? > 0", md.Query("has_privileges")).
|
||||
Where("users.privileges & ? = 0", md.Query("has_not_privileges")).
|
||||
Where("users_stats.country = ?", md.Query("country")).
|
||||
Where("users_stats.username_aka = ?", md.Query("name_aka")).
|
||||
Where("privileges_groups.name = ?", md.Query("privilege_group")).
|
||||
In("users.id", pm("ids")...).
|
||||
In("users.username_safe", safeUsernameBulk(pm("names"))...).
|
||||
In("users_stats.username_aka", pm("names_aka")...).
|
||||
In("users_stats.country", pm("countries")...)
|
||||
|
||||
var extraJoin string
|
||||
if md.Query("privilege_group") != "" {
|
||||
extraJoin = " LEFT JOIN privileges_groups ON users.privileges & privileges_groups.privileges = privileges_groups.privileges "
|
||||
}
|
||||
|
||||
query := userFields + extraJoin + wh.ClauseSafe() + " AND " + md.User.OnlyUserPublic(true) +
|
||||
" " + common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"id",
|
||||
"username",
|
||||
"privileges",
|
||||
"donor_expire",
|
||||
"latest_activity",
|
||||
"silence_end",
|
||||
},
|
||||
Default: "id ASC",
|
||||
Table: "users",
|
||||
}) +
|
||||
" " + common.Paginate(md.Query("p"), md.Query("l"), 100)
|
||||
|
||||
// query execution
|
||||
rows, err := md.DB.Queryx(query, wh.Params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r userPutsMultiUserData
|
||||
for rows.Next() {
|
||||
var u userData
|
||||
err := rows.StructScan(&u)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Users = append(r.Users, u)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
// UserSelfGET is a shortcut for /users/id/self. (/users/self)
|
||||
func UserSelfGET(md common.MethodData) common.CodeMessager {
|
||||
md.Ctx.Request.URI().SetQueryString("id=self")
|
||||
return UsersGET(md)
|
||||
}
|
||||
|
||||
func safeUsernameBulk(us [][]byte) [][]byte {
|
||||
for _, u := range us {
|
||||
for idx, v := range u {
|
||||
if v == ' ' {
|
||||
u[idx] = '_'
|
||||
continue
|
||||
}
|
||||
u[idx] = byte(unicode.ToLower(rune(v)))
|
||||
}
|
||||
}
|
||||
return us
|
||||
}
|
||||
|
||||
type whatIDResponse struct {
|
||||
common.ResponseBase
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
// UserWhatsTheIDGET is an API request that only returns an user's ID.
|
||||
func UserWhatsTheIDGET(md common.MethodData) common.CodeMessager {
|
||||
var (
|
||||
r whatIDResponse
|
||||
privileges uint64
|
||||
)
|
||||
err := md.DB.QueryRow("SELECT id, privileges FROM users WHERE username_safe = ? LIMIT 1", common.SafeUsername(md.Query("name"))).Scan(&r.ID, &privileges)
|
||||
if err != nil || ((privileges&uint64(common.UserPrivilegePublic)) == 0 &&
|
||||
(md.User.UserPrivileges&common.AdminPrivilegeManageUsers == 0)) {
|
||||
return common.SimpleResponse(404, "That user could not be found!")
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
var modesToReadable = [...]string{
|
||||
"std",
|
||||
"taiko",
|
||||
"ctb",
|
||||
"mania",
|
||||
}
|
||||
|
||||
type modeData struct {
|
||||
RankedScore uint64 `json:"ranked_score"`
|
||||
TotalScore uint64 `json:"total_score"`
|
||||
PlayCount int `json:"playcount"`
|
||||
ReplaysWatched int `json:"replays_watched"`
|
||||
TotalHits int `json:"total_hits"`
|
||||
Level float64 `json:"level"`
|
||||
Accuracy float64 `json:"accuracy"`
|
||||
PP int `json:"pp"`
|
||||
GlobalLeaderboardRank *int `json:"global_leaderboard_rank"`
|
||||
CountryLeaderboardRank *int `json:"country_leaderboard_rank"`
|
||||
}
|
||||
type userFullResponse struct {
|
||||
common.ResponseBase
|
||||
userData
|
||||
STD modeData `json:"std"`
|
||||
Taiko modeData `json:"taiko"`
|
||||
CTB modeData `json:"ctb"`
|
||||
Mania modeData `json:"mania"`
|
||||
PlayStyle int `json:"play_style"`
|
||||
FavouriteMode int `json:"favourite_mode"`
|
||||
Badges []singleBadge `json:"badges"`
|
||||
CustomBadge *singleBadge `json:"custom_badge"`
|
||||
SilenceInfo silenceInfo `json:"silence_info"`
|
||||
CMNotes *string `json:"cm_notes,omitempty"`
|
||||
BanDate *common.UnixTimestamp `json:"ban_date,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
type silenceInfo struct {
|
||||
Reason string `json:"reason"`
|
||||
End common.UnixTimestamp `json:"end"`
|
||||
}
|
||||
|
||||
// UserFullGET gets all of an user's information, with one exception: their userpage.
|
||||
func UserFullGET(md common.MethodData) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users")
|
||||
if shouldRet != nil {
|
||||
return *shouldRet
|
||||
}
|
||||
|
||||
// Hellest query I've ever done.
|
||||
query := `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka, users_stats.country, users_stats.play_style, users_stats.favourite_mode,
|
||||
|
||||
users_stats.custom_badge_icon, users_stats.custom_badge_name, users_stats.can_custom_badge,
|
||||
users_stats.show_custom_badge,
|
||||
|
||||
users_stats.ranked_score_std, users_stats.total_score_std, users_stats.playcount_std,
|
||||
users_stats.replays_watched_std, users_stats.total_hits_std,
|
||||
users_stats.avg_accuracy_std, users_stats.pp_std,
|
||||
|
||||
users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.playcount_taiko,
|
||||
users_stats.replays_watched_taiko, users_stats.total_hits_taiko,
|
||||
users_stats.avg_accuracy_taiko, users_stats.pp_taiko,
|
||||
|
||||
users_stats.ranked_score_ctb, users_stats.total_score_ctb, users_stats.playcount_ctb,
|
||||
users_stats.replays_watched_ctb, users_stats.total_hits_ctb,
|
||||
users_stats.avg_accuracy_ctb, users_stats.pp_ctb,
|
||||
|
||||
users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.playcount_mania,
|
||||
users_stats.replays_watched_mania, users_stats.total_hits_mania,
|
||||
users_stats.avg_accuracy_mania, users_stats.pp_mania,
|
||||
|
||||
users.silence_reason, users.silence_end,
|
||||
users.notes, users.ban_datetime, users.email
|
||||
|
||||
FROM users
|
||||
LEFT JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
|
||||
LIMIT 1
|
||||
`
|
||||
// Fuck.
|
||||
r := userFullResponse{}
|
||||
var (
|
||||
b singleBadge
|
||||
can bool
|
||||
show bool
|
||||
)
|
||||
err := md.DB.QueryRow(query, param).Scan(
|
||||
&r.ID, &r.Username, &r.RegisteredOn, &r.Privileges, &r.LatestActivity,
|
||||
|
||||
&r.UsernameAKA, &r.Country,
|
||||
&r.PlayStyle, &r.FavouriteMode,
|
||||
|
||||
&b.Icon, &b.Name, &can, &show,
|
||||
|
||||
&r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount,
|
||||
&r.STD.ReplaysWatched, &r.STD.TotalHits,
|
||||
&r.STD.Accuracy, &r.STD.PP,
|
||||
|
||||
&r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount,
|
||||
&r.Taiko.ReplaysWatched, &r.Taiko.TotalHits,
|
||||
&r.Taiko.Accuracy, &r.Taiko.PP,
|
||||
|
||||
&r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount,
|
||||
&r.CTB.ReplaysWatched, &r.CTB.TotalHits,
|
||||
&r.CTB.Accuracy, &r.CTB.PP,
|
||||
|
||||
&r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount,
|
||||
&r.Mania.ReplaysWatched, &r.Mania.TotalHits,
|
||||
&r.Mania.Accuracy, &r.Mania.PP,
|
||||
|
||||
&r.SilenceInfo.Reason, &r.SilenceInfo.End,
|
||||
&r.CMNotes, &r.BanDate, &r.Email,
|
||||
)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That user could not be found!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
can = can && show && common.UserPrivileges(r.Privileges)&common.UserPrivilegeDonor > 0
|
||||
if can && (b.Name != "" || b.Icon != "") {
|
||||
r.CustomBadge = &b
|
||||
}
|
||||
|
||||
for modeID, m := range [...]*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
|
||||
m.Level = ocl.GetLevelPrecise(int64(m.TotalScore))
|
||||
|
||||
if i := leaderboardPosition(md.R, modesToReadable[modeID], r.ID); i != nil {
|
||||
m.GlobalLeaderboardRank = i
|
||||
}
|
||||
if i := countryPosition(md.R, modesToReadable[modeID], r.ID, r.Country); i != nil {
|
||||
m.CountryLeaderboardRank = i
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := md.DB.Query("SELECT b.id, b.name, b.icon FROM user_badges ub "+
|
||||
"LEFT JOIN badges b ON ub.badge = b.id WHERE user = ?", r.ID)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var badge singleBadge
|
||||
err := rows.Scan(&badge.ID, &badge.Name, &badge.Icon)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Badges = append(r.Badges, badge)
|
||||
}
|
||||
|
||||
if md.User.TokenPrivileges&common.PrivilegeManageUser == 0 {
|
||||
r.CMNotes = nil
|
||||
r.BanDate = nil
|
||||
r.Email = ""
|
||||
}
|
||||
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type userpageResponse struct {
|
||||
common.ResponseBase
|
||||
Userpage *string `json:"userpage"`
|
||||
}
|
||||
|
||||
// UserUserpageGET gets an user's userpage, as in the customisable thing.
|
||||
func UserUserpageGET(md common.MethodData) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users_stats")
|
||||
if shouldRet != nil {
|
||||
return *shouldRet
|
||||
}
|
||||
var r userpageResponse
|
||||
err := md.DB.QueryRow("SELECT userpage_content FROM users_stats WHERE "+whereClause+" LIMIT 1", param).Scan(&r.Userpage)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "No such user!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if r.Userpage == nil {
|
||||
r.Userpage = new(string)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
// UserSelfUserpagePOST allows to change the current user's userpage.
|
||||
func UserSelfUserpagePOST(md common.MethodData) common.CodeMessager {
|
||||
var d struct {
|
||||
Data *string `json:"data"`
|
||||
}
|
||||
md.Unmarshal(&d)
|
||||
if d.Data == nil {
|
||||
return ErrMissingField("data")
|
||||
}
|
||||
cont := common.SanitiseString(*d.Data)
|
||||
_, err := md.DB.Exec("UPDATE users_stats SET userpage_content = ? WHERE id = ? LIMIT 1", cont, md.ID())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
md.Ctx.URI().SetQueryString("id=self")
|
||||
return UserUserpageGET(md)
|
||||
}
|
||||
|
||||
func whereClauseUser(md common.MethodData, tableName string) (*common.CodeMessager, string, interface{}) {
|
||||
switch {
|
||||
case md.Query("id") == "self":
|
||||
return nil, tableName + ".id = ?", md.ID()
|
||||
case md.Query("id") != "":
|
||||
id, err := strconv.Atoi(md.Query("id"))
|
||||
if err != nil {
|
||||
a := common.SimpleResponse(400, "please pass a valid user ID")
|
||||
return &a, "", nil
|
||||
}
|
||||
return nil, tableName + ".id = ?", id
|
||||
case md.Query("name") != "":
|
||||
return nil, tableName + ".username_safe = ?", common.SafeUsername(md.Query("name"))
|
||||
}
|
||||
a := common.SimpleResponse(400, "you need to pass either querystring parameters name or id")
|
||||
return &a, "", nil
|
||||
}
|
||||
|
||||
type userLookupResponse struct {
|
||||
common.ResponseBase
|
||||
Users []lookupUser `json:"users"`
|
||||
}
|
||||
type lookupUser struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// UserLookupGET does a quick lookup of users beginning with the passed
|
||||
// querystring value name.
|
||||
func UserLookupGET(md common.MethodData) common.CodeMessager {
|
||||
name := common.SafeUsername(md.Query("name"))
|
||||
name = strings.NewReplacer(
|
||||
"%", "\\%",
|
||||
"_", "\\_",
|
||||
"\\", "\\\\",
|
||||
).Replace(name)
|
||||
if name == "" {
|
||||
return common.SimpleResponse(400, "please provide an username to start searching")
|
||||
}
|
||||
name = "%" + name + "%"
|
||||
|
||||
var email string
|
||||
if md.User.TokenPrivileges&common.PrivilegeManageUser != 0 &&
|
||||
strings.Contains(md.Query("name"), "@") {
|
||||
email = md.Query("name")
|
||||
}
|
||||
|
||||
rows, err := md.DB.Query("SELECT users.id, users.username FROM users WHERE "+
|
||||
"(username_safe LIKE ? OR email = ?) AND "+
|
||||
md.User.OnlyUserPublic(true)+" LIMIT 25", name, email)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var r userLookupResponse
|
||||
for rows.Next() {
|
||||
var l lookupUser
|
||||
err := rows.Scan(&l.ID, &l.Username)
|
||||
if err != nil {
|
||||
continue // can't be bothered to handle properly
|
||||
}
|
||||
r.Users = append(r.Users, l)
|
||||
}
|
||||
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
84
vendor/zxq.co/ripple/rippleapi/app/v1/user_achievements.go
vendored
Normal file
84
vendor/zxq.co/ripple/rippleapi/app/v1/user_achievements.go
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// Achievement represents an achievement in the database.
|
||||
type Achievement struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// LoadAchievementsEvery reloads the achievements in the database every given
|
||||
// amount of time.
|
||||
func LoadAchievementsEvery(db *sqlx.DB, d time.Duration) {
|
||||
for {
|
||||
achievs = nil
|
||||
err := db.Select(&achievs,
|
||||
"SELECT id, name, description, icon FROM achievements ORDER BY id ASC")
|
||||
if err != nil {
|
||||
fmt.Println("LoadAchievements error", err)
|
||||
common.GenericError(err)
|
||||
}
|
||||
time.Sleep(d)
|
||||
}
|
||||
}
|
||||
|
||||
var achievs []Achievement
|
||||
|
||||
type userAchievement struct {
|
||||
Achievement
|
||||
Achieved bool `json:"achieved"`
|
||||
}
|
||||
|
||||
type userAchievementsResponse struct {
|
||||
common.ResponseBase
|
||||
Achievements []userAchievement `json:"achievements"`
|
||||
}
|
||||
|
||||
// UserAchievementsGET handles requests for retrieving the achievements of a
|
||||
// given user.
|
||||
func UserAchievementsGET(md common.MethodData) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users")
|
||||
if shouldRet != nil {
|
||||
return *shouldRet
|
||||
}
|
||||
var ids []int
|
||||
err := md.DB.Select(&ids, `SELECT ua.achievement_id FROM users_achievements ua
|
||||
INNER JOIN users ON users.id = ua.user_id
|
||||
WHERE `+whereClause+` ORDER BY ua.achievement_id ASC`, param)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "No such user!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
all := md.HasQuery("all")
|
||||
resp := userAchievementsResponse{Achievements: make([]userAchievement, 0, len(achievs))}
|
||||
for _, ach := range achievs {
|
||||
achieved := inInt(ach.ID, ids)
|
||||
if all || achieved {
|
||||
resp.Achievements = append(resp.Achievements, userAchievement{ach, achieved})
|
||||
}
|
||||
}
|
||||
resp.Code = 200
|
||||
return resp
|
||||
}
|
||||
|
||||
func inInt(i int, js []int) bool {
|
||||
for _, j := range js {
|
||||
if i == j {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
127
vendor/zxq.co/ripple/rippleapi/app/v1/user_scores.go
vendored
Normal file
127
vendor/zxq.co/ripple/rippleapi/app/v1/user_scores.go
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
type userScore struct {
|
||||
Score
|
||||
Beatmap beatmap `json:"beatmap"`
|
||||
}
|
||||
|
||||
type userScoresResponse struct {
|
||||
common.ResponseBase
|
||||
Scores []userScore `json:"scores"`
|
||||
}
|
||||
|
||||
const userScoreSelectBase = `
|
||||
SELECT
|
||||
scores.id, scores.beatmap_md5, scores.score,
|
||||
scores.max_combo, scores.full_combo, scores.mods,
|
||||
scores.300_count, scores.100_count, scores.50_count,
|
||||
scores.gekis_count, scores.katus_count, scores.misses_count,
|
||||
scores.time, scores.play_mode, scores.accuracy, scores.pp,
|
||||
scores.completed,
|
||||
|
||||
beatmaps.beatmap_id, beatmaps.beatmapset_id, beatmaps.beatmap_md5,
|
||||
beatmaps.song_name, beatmaps.ar, beatmaps.od, beatmaps.difficulty_std,
|
||||
beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania,
|
||||
beatmaps.max_combo, beatmaps.hit_length, beatmaps.ranked,
|
||||
beatmaps.ranked_status_freezed, beatmaps.latest_update
|
||||
FROM scores
|
||||
INNER JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5
|
||||
INNER JOIN users ON users.id = scores.userid
|
||||
`
|
||||
|
||||
// UserScoresBestGET retrieves the best scores of an user, sorted by PP if
|
||||
// mode is standard and sorted by ranked score otherwise.
|
||||
func UserScoresBestGET(md common.MethodData) common.CodeMessager {
|
||||
cm, wc, param := whereClauseUser(md, "users")
|
||||
if cm != nil {
|
||||
return *cm
|
||||
}
|
||||
mc := genModeClause(md)
|
||||
// For all modes that have PP, we leave out 0 PP scores.
|
||||
if getMode(md.Query("mode")) != "ctb" {
|
||||
mc += " AND scores.pp > 0"
|
||||
}
|
||||
return scoresPuts(md, fmt.Sprintf(
|
||||
`WHERE
|
||||
scores.completed = '3'
|
||||
AND %s
|
||||
%s
|
||||
AND `+md.User.OnlyUserPublic(true)+`
|
||||
ORDER BY scores.pp DESC, scores.score DESC %s`,
|
||||
wc, mc, common.Paginate(md.Query("p"), md.Query("l"), 100),
|
||||
), param)
|
||||
}
|
||||
|
||||
// UserScoresRecentGET retrieves an user's latest scores.
|
||||
func UserScoresRecentGET(md common.MethodData) common.CodeMessager {
|
||||
cm, wc, param := whereClauseUser(md, "users")
|
||||
if cm != nil {
|
||||
return *cm
|
||||
}
|
||||
return scoresPuts(md, fmt.Sprintf(
|
||||
`WHERE
|
||||
%s
|
||||
%s
|
||||
AND `+md.User.OnlyUserPublic(true)+`
|
||||
ORDER BY scores.id DESC %s`,
|
||||
wc, genModeClause(md), common.Paginate(md.Query("p"), md.Query("l"), 100),
|
||||
), param)
|
||||
}
|
||||
|
||||
func scoresPuts(md common.MethodData, whereClause string, params ...interface{}) common.CodeMessager {
|
||||
rows, err := md.DB.Query(userScoreSelectBase+whereClause, params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var scores []userScore
|
||||
for rows.Next() {
|
||||
var (
|
||||
us userScore
|
||||
b beatmap
|
||||
)
|
||||
err = rows.Scan(
|
||||
&us.ID, &us.BeatmapMD5, &us.Score.Score,
|
||||
&us.MaxCombo, &us.FullCombo, &us.Mods,
|
||||
&us.Count300, &us.Count100, &us.Count50,
|
||||
&us.CountGeki, &us.CountKatu, &us.CountMiss,
|
||||
&us.Time, &us.PlayMode, &us.Accuracy, &us.PP,
|
||||
&us.Completed,
|
||||
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD,
|
||||
&b.Diff2.Taiko, &b.Diff2.CTB, &b.Diff2.Mania,
|
||||
&b.MaxCombo, &b.HitLength, &b.Ranked,
|
||||
&b.RankedStatusFrozen, &b.LatestUpdate,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
b.Difficulty = b.Diff2.STD
|
||||
us.Beatmap = b
|
||||
us.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(us.PlayMode),
|
||||
osuapi.Mods(us.Mods),
|
||||
us.Accuracy,
|
||||
us.Count300,
|
||||
us.Count100,
|
||||
us.Count50,
|
||||
us.CountMiss,
|
||||
))
|
||||
scores = append(scores, us)
|
||||
}
|
||||
r := userScoresResponse{}
|
||||
r.Code = 200
|
||||
r.Scores = scores
|
||||
return r
|
||||
}
|
18
vendor/zxq.co/ripple/rippleapi/app/websockets/entry.go
vendored
Normal file
18
vendor/zxq.co/ripple/rippleapi/app/websockets/entry.go
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"github.com/leavengood/websocket"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var upgrader = websocket.FastHTTPUpgrader{
|
||||
Handler: handler,
|
||||
CheckOrigin: func(ctx *fasthttp.RequestCtx) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// WebsocketV1Entry upgrades a connection to a websocket.
|
||||
func WebsocketV1Entry(ctx *fasthttp.RequestCtx) {
|
||||
upgrader.UpgradeHandler(ctx)
|
||||
}
|
101
vendor/zxq.co/ripple/rippleapi/app/websockets/identify.go
vendored
Normal file
101
vendor/zxq.co/ripple/rippleapi/app/websockets/identify.go
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"database/sql"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type websocketUser struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
UserPrivileges uint64 `json:"user_privileges"`
|
||||
TokenPrivileges uint64 `json:"token_privileges"`
|
||||
ApplicationID *string `json:"application_id"`
|
||||
}
|
||||
|
||||
type identifyMessage struct {
|
||||
Token string `json:"token"`
|
||||
IsBearer bool `json:"is_bearer"`
|
||||
}
|
||||
|
||||
// Identify sets the identity of the user.
|
||||
func Identify(c *conn, message incomingMessage) {
|
||||
var idMsg identifyMessage
|
||||
err := json.Unmarshal(message.Data, &idMsg)
|
||||
if err != nil {
|
||||
c.WriteJSON(TypeInvalidMessage, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var wsu websocketUser
|
||||
if idMsg.IsBearer {
|
||||
err = getBearerToken(idMsg.Token, &wsu)
|
||||
} else {
|
||||
err = db.Get(&wsu, `
|
||||
SELECT
|
||||
t.user as id, t.privileges as token_privileges,
|
||||
u.username, u.privileges as user_privileges
|
||||
FROM tokens t
|
||||
INNER JOIN users u ON t.user = u.id
|
||||
WHERE t.token = ?`, fmt.Sprintf("%x", md5.Sum([]byte(idMsg.Token))))
|
||||
}
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
break
|
||||
case sql.ErrNoRows:
|
||||
c.WriteJSON(TypeNotFound, nil)
|
||||
return
|
||||
default:
|
||||
common.WSErr(err)
|
||||
c.WriteJSON(TypeUnexpectedError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
wsu.TokenPrivileges = uint64(
|
||||
common.Privileges(wsu.TokenPrivileges).CanOnly(
|
||||
common.UserPrivileges(wsu.UserPrivileges),
|
||||
),
|
||||
)
|
||||
|
||||
c.Mtx.Lock()
|
||||
c.User = &wsu
|
||||
c.Mtx.Unlock()
|
||||
|
||||
c.WriteJSON(TypeIdentified, wsu)
|
||||
}
|
||||
|
||||
func getBearerToken(token string, wsu *websocketUser) error {
|
||||
var x struct {
|
||||
Client string
|
||||
Scope string
|
||||
Extra int
|
||||
}
|
||||
err := db.Get(&x, "SELECT client, scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token))))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var userInfo struct {
|
||||
Username string
|
||||
Privileges uint64
|
||||
}
|
||||
err = db.Get(&userInfo, "SELECT username, privileges FROM users WHERE id = ? LIMIT 1", x.Extra)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wsu.ApplicationID = &x.Client
|
||||
wsu.ID = x.Extra
|
||||
wsu.Username = userInfo.Username
|
||||
wsu.UserPrivileges = userInfo.Privileges
|
||||
wsu.TokenPrivileges = uint64(common.OAuthPrivileges(x.Scope))
|
||||
|
||||
return nil
|
||||
}
|
125
vendor/zxq.co/ripple/rippleapi/app/websockets/main_handler.go
vendored
Normal file
125
vendor/zxq.co/ripple/rippleapi/app/websockets/main_handler.go
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/leavengood/websocket"
|
||||
)
|
||||
|
||||
var stepNumber uint64
|
||||
|
||||
func handler(rawConn *websocket.Conn) {
|
||||
defer catchPanic()
|
||||
defer rawConn.Close()
|
||||
|
||||
step := atomic.AddUint64(&stepNumber, 1)
|
||||
|
||||
// 5 is a security margin in case
|
||||
if step == (1<<10 - 5) {
|
||||
atomic.StoreUint64(&stepNumber, 0)
|
||||
}
|
||||
|
||||
c := &conn{
|
||||
rawConn,
|
||||
sync.Mutex{},
|
||||
step | uint64(time.Now().UnixNano()<<10),
|
||||
false,
|
||||
nil,
|
||||
}
|
||||
|
||||
c.WriteJSON(TypeConnected, nil)
|
||||
|
||||
defer cleanup(c.ID)
|
||||
|
||||
for {
|
||||
var i incomingMessage
|
||||
err := c.Conn.ReadJSON(&i)
|
||||
if _, ok := err.(*websocket.CloseError); ok {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.WriteJSON(TypeInvalidMessage, err.Error())
|
||||
continue
|
||||
}
|
||||
f, ok := messageHandler[i.Type]
|
||||
if !ok {
|
||||
c.WriteJSON(TypeInvalidMessage, "invalid message type")
|
||||
continue
|
||||
}
|
||||
if f != nil {
|
||||
f(c, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
Conn *websocket.Conn
|
||||
Mtx sync.Mutex
|
||||
ID uint64
|
||||
RestrictedVisible bool
|
||||
User *websocketUser
|
||||
}
|
||||
|
||||
func (c *conn) WriteJSON(t string, data interface{}) error {
|
||||
c.Mtx.Lock()
|
||||
err := c.Conn.WriteJSON(newMessage(t, data))
|
||||
c.Mtx.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
var messageHandler = map[string]func(c *conn, message incomingMessage){
|
||||
TypeSubscribeScores: SubscribeScores,
|
||||
TypeSubscribeMultiMatches: SubscribeMultiMatches,
|
||||
TypeSetRestrictedVisibility: SetRestrictedVisibility,
|
||||
TypeIdentify: Identify,
|
||||
TypePing: pingHandler,
|
||||
}
|
||||
|
||||
// Server Message Types
|
||||
const (
|
||||
TypeConnected = "connected"
|
||||
TypeInvalidMessage = "invalid_message_type"
|
||||
TypeUnexpectedError = "unexpected_error"
|
||||
TypeNotFound = "not_found"
|
||||
TypeSubscribedToScores = "subscribed_to_scores"
|
||||
TypeNewScore = "new_score"
|
||||
TypeSubscribedToMultiMatches = "subscribed_mp_complete_match"
|
||||
TypeNewMatch = "new_completed_match"
|
||||
TypeIdentified = "identified"
|
||||
TypeRestrictedVisibilitySet = "restricted_visibility_set"
|
||||
TypePong = "pong"
|
||||
)
|
||||
|
||||
// Client Message Types
|
||||
const (
|
||||
TypeSubscribeScores = "subscribe_scores"
|
||||
TypeSubscribeMultiMatches = "subscribe_mp_complete_match"
|
||||
TypeIdentify = "identify"
|
||||
TypeSetRestrictedVisibility = "set_restricted_visibility"
|
||||
TypePing = "ping"
|
||||
)
|
||||
|
||||
func pingHandler(c *conn, message incomingMessage) {
|
||||
c.WriteJSON(TypePong, nil)
|
||||
}
|
||||
|
||||
// Message is the wrapped information for a message sent to the client.
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func newMessage(t string, data interface{}) Message {
|
||||
return Message{
|
||||
Type: t,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
type incomingMessage struct {
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
57
vendor/zxq.co/ripple/rippleapi/app/websockets/multi.go
vendored
Normal file
57
vendor/zxq.co/ripple/rippleapi/app/websockets/multi.go
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SubscribeMultiMatches subscribes to receiving information from completed
|
||||
// games in multiplayer matches.
|
||||
func SubscribeMultiMatches(c *conn, message incomingMessage) {
|
||||
multiSubscriptionsMtx.Lock()
|
||||
var found bool
|
||||
for _, el := range multiSubscriptions {
|
||||
if el.ID == c.ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// if it was not found, we need to add it
|
||||
if !found {
|
||||
multiSubscriptions = append(multiSubscriptions, c)
|
||||
}
|
||||
multiSubscriptionsMtx.Unlock()
|
||||
|
||||
c.WriteJSON(TypeSubscribedToMultiMatches, nil)
|
||||
}
|
||||
|
||||
var multiSubscriptions []*conn
|
||||
var multiSubscriptionsMtx = new(sync.RWMutex)
|
||||
|
||||
func matchRetriever() {
|
||||
ps, err := red.Subscribe("api:mp_complete_match")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
for {
|
||||
msg, err := ps.ReceiveMessage()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
go handleNewMultiGame(msg.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
func handleNewMultiGame(payload string) {
|
||||
defer catchPanic()
|
||||
multiSubscriptionsMtx.RLock()
|
||||
cp := make([]*conn, len(multiSubscriptions))
|
||||
copy(cp, multiSubscriptions)
|
||||
multiSubscriptionsMtx.RUnlock()
|
||||
|
||||
for _, el := range cp {
|
||||
el.WriteJSON(TypeNewMatch, json.RawMessage(payload))
|
||||
}
|
||||
}
|
31
vendor/zxq.co/ripple/rippleapi/app/websockets/restricted_visibility.go
vendored
Normal file
31
vendor/zxq.co/ripple/rippleapi/app/websockets/restricted_visibility.go
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// SetRestrictedVisibility sets whether the information of restricted users
|
||||
// can be seen.
|
||||
func SetRestrictedVisibility(c *conn, message incomingMessage) {
|
||||
var visibility bool
|
||||
|
||||
err := json.Unmarshal(message.Data, &visibility)
|
||||
if err != nil {
|
||||
c.WriteJSON(TypeInvalidMessage, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var userIsManager bool
|
||||
if c.User != nil && (c.User.UserPrivileges&uint64(common.AdminPrivilegeManageUsers) > 0) {
|
||||
userIsManager = true
|
||||
}
|
||||
|
||||
c.Mtx.Lock()
|
||||
visibility = visibility && userIsManager
|
||||
c.RestrictedVisible = visibility
|
||||
c.Mtx.Unlock()
|
||||
|
||||
c.WriteJSON(TypeRestrictedVisibilitySet, visibility)
|
||||
}
|
173
vendor/zxq.co/ripple/rippleapi/app/websockets/scores.go
vendored
Normal file
173
vendor/zxq.co/ripple/rippleapi/app/websockets/scores.go
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/app/v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
type subscribeScoresUser struct {
|
||||
User int `json:"user"`
|
||||
Modes []int `json:"modes"`
|
||||
}
|
||||
|
||||
// SubscribeScores subscribes a connection to score updates.
|
||||
func SubscribeScores(c *conn, message incomingMessage) {
|
||||
var ssu []subscribeScoresUser
|
||||
err := json.Unmarshal(message.Data, &ssu)
|
||||
if err != nil {
|
||||
c.WriteJSON(TypeInvalidMessage, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
scoreSubscriptionsMtx.Lock()
|
||||
|
||||
var found bool
|
||||
for idx, el := range scoreSubscriptions {
|
||||
// already exists, change the users
|
||||
if el.Conn.ID == c.ID {
|
||||
found = true
|
||||
scoreSubscriptions[idx].Users = ssu
|
||||
}
|
||||
}
|
||||
|
||||
// if it was not found, we need to add it
|
||||
if !found {
|
||||
scoreSubscriptions = append(scoreSubscriptions, scoreSubscription{c, ssu})
|
||||
}
|
||||
|
||||
scoreSubscriptionsMtx.Unlock()
|
||||
|
||||
c.WriteJSON(TypeSubscribedToScores, ssu)
|
||||
}
|
||||
|
||||
type scoreSubscription struct {
|
||||
Conn *conn
|
||||
Users []subscribeScoresUser
|
||||
}
|
||||
|
||||
var scoreSubscriptions []scoreSubscription
|
||||
var scoreSubscriptionsMtx = new(sync.RWMutex)
|
||||
|
||||
func scoreRetriever() {
|
||||
ps, err := red.Subscribe("api:score_submission")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
for {
|
||||
msg, err := ps.ReceiveMessage()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
go handleNewScore(msg.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
type scoreUser struct {
|
||||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Privileges uint64 `json:"privileges"`
|
||||
}
|
||||
|
||||
type score struct {
|
||||
v1.Score
|
||||
scoreUser
|
||||
}
|
||||
|
||||
type scoreJSON struct {
|
||||
v1.Score
|
||||
UserID int `json:"user_id"`
|
||||
User scoreUser `json:"user"`
|
||||
}
|
||||
|
||||
func handleNewScore(id string) {
|
||||
defer catchPanic()
|
||||
var s score
|
||||
err := db.Get(&s, `
|
||||
SELECT
|
||||
s.id, s.beatmap_md5, s.score, s.max_combo, s.full_combo, s.mods,
|
||||
s.300_count, s.100_count, s.50_count, s.gekis_count, s.katus_count, s.misses_count,
|
||||
s.time, s.play_mode, s.accuracy, s.pp, s.completed, s.userid AS user_id,
|
||||
u.username, u.privileges
|
||||
FROM scores s
|
||||
INNER JOIN users u ON s.userid = u.id
|
||||
WHERE s.id = ?`, id)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
s.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(s.PlayMode),
|
||||
osuapi.Mods(s.Mods),
|
||||
s.Accuracy,
|
||||
s.Count300,
|
||||
s.Count100,
|
||||
s.Count50,
|
||||
s.CountMiss,
|
||||
))
|
||||
|
||||
sj := scoreJSON{
|
||||
Score: s.Score,
|
||||
UserID: s.UserID,
|
||||
User: s.scoreUser,
|
||||
}
|
||||
|
||||
scoreSubscriptionsMtx.RLock()
|
||||
cp := make([]scoreSubscription, len(scoreSubscriptions))
|
||||
copy(cp, scoreSubscriptions)
|
||||
scoreSubscriptionsMtx.RUnlock()
|
||||
|
||||
for _, el := range cp {
|
||||
if len(el.Users) > 0 && !scoreUserValid(el.Users, sj) {
|
||||
continue
|
||||
}
|
||||
|
||||
if sj.User.Privileges&3 != 3 && !el.Conn.RestrictedVisible {
|
||||
continue
|
||||
}
|
||||
|
||||
el.Conn.WriteJSON(TypeNewScore, sj)
|
||||
}
|
||||
}
|
||||
|
||||
func scoreUserValid(users []subscribeScoresUser, s scoreJSON) bool {
|
||||
for _, u := range users {
|
||||
if u.User == s.UserID {
|
||||
if len(u.Modes) > 0 {
|
||||
if !inModes(u.Modes, s.PlayMode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inModes(modes []int, i int) bool {
|
||||
for _, m := range modes {
|
||||
if m == i {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func catchPanic() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
switch r := r.(type) {
|
||||
case error:
|
||||
common.WSErr(r)
|
||||
default:
|
||||
fmt.Println("PANIC", r)
|
||||
}
|
||||
}
|
||||
}
|
42
vendor/zxq.co/ripple/rippleapi/app/websockets/websockets.go
vendored
Normal file
42
vendor/zxq.co/ripple/rippleapi/app/websockets/websockets.go
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
// Package websockets implements functionality related to the API websockets.
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/redis.v5"
|
||||
)
|
||||
|
||||
var (
|
||||
red *redis.Client
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
// Start begins websocket functionality
|
||||
func Start(r *redis.Client, _db *sqlx.DB) error {
|
||||
red = r
|
||||
db = _db
|
||||
go scoreRetriever()
|
||||
go matchRetriever()
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup(connID uint64) {
|
||||
scoreSubscriptionsMtx.Lock()
|
||||
for idx, el := range scoreSubscriptions {
|
||||
if el.Conn.ID == connID {
|
||||
scoreSubscriptions[idx] = scoreSubscriptions[len(scoreSubscriptions)-1]
|
||||
scoreSubscriptions = scoreSubscriptions[:len(scoreSubscriptions)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
scoreSubscriptionsMtx.Unlock()
|
||||
multiSubscriptionsMtx.Lock()
|
||||
for idx, el := range multiSubscriptions {
|
||||
if el.ID == connID {
|
||||
multiSubscriptions[idx] = multiSubscriptions[len(multiSubscriptions)-1]
|
||||
multiSubscriptions = multiSubscriptions[:len(multiSubscriptions)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
multiSubscriptionsMtx.Unlock()
|
||||
}
|
185
vendor/zxq.co/ripple/rippleapi/beatmapget/beatmapget.go
vendored
Normal file
185
vendor/zxq.co/ripple/rippleapi/beatmapget/beatmapget.go
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
// Package beatmapget is an helper package to retrieve beatmap information from
|
||||
// the osu! API, if the beatmap in the database is too old.
|
||||
package beatmapget
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
)
|
||||
|
||||
// Expire is the duration after which a beatmap expires.
|
||||
const Expire = time.Hour * 12
|
||||
|
||||
// DB is the database.
|
||||
var DB *sqlx.DB
|
||||
|
||||
// Client is the osu! api client to use
|
||||
var Client *osuapi.Client
|
||||
|
||||
// BeatmapDefiningQuality is the defining quality of the beatmap to be updated,
|
||||
// which is to say either the ID, the set ID or the md5 hash.
|
||||
type BeatmapDefiningQuality struct {
|
||||
ID int
|
||||
MD5 string
|
||||
frozen bool
|
||||
ranked int
|
||||
}
|
||||
|
||||
func (b BeatmapDefiningQuality) String() string {
|
||||
if b.MD5 != "" {
|
||||
return "#/" + b.MD5
|
||||
}
|
||||
if b.ID != 0 {
|
||||
return "b/" + strconv.Itoa(b.ID)
|
||||
}
|
||||
return "n/a"
|
||||
}
|
||||
|
||||
func (b BeatmapDefiningQuality) isSomethingSet() error {
|
||||
if b.ID == 0 && b.MD5 == "" {
|
||||
return errors.New("beatmapget: at least one field in BeatmapDefiningQuality must be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b BeatmapDefiningQuality) whereAndParams() (where string, params []interface{}) {
|
||||
var wheres []string
|
||||
if b.ID != 0 {
|
||||
wheres = append(wheres, "beatmap_id = ?")
|
||||
params = append(params, b.ID)
|
||||
}
|
||||
if b.MD5 != "" {
|
||||
wheres = append(wheres, "beatmap_md5 = ?")
|
||||
params = append(params, b.MD5)
|
||||
}
|
||||
where = strings.Join(wheres, " AND ")
|
||||
if where == "" {
|
||||
where = "1"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateIfRequired updates the beatmap in the database if an update is required.
|
||||
func UpdateIfRequired(b BeatmapDefiningQuality) error {
|
||||
required, err := UpdateRequired(&b)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
if !required {
|
||||
return nil
|
||||
}
|
||||
return Update(b, err != sql.ErrNoRows)
|
||||
}
|
||||
|
||||
// UpdateRequired checks an update is required. If error is sql.ErrNoRows,
|
||||
// it means that the beatmap should be updated, and that there was not the
|
||||
// beatmap in the database previously.
|
||||
func UpdateRequired(b *BeatmapDefiningQuality) (bool, error) {
|
||||
if err := b.isSomethingSet(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
where, params := b.whereAndParams()
|
||||
var data struct {
|
||||
Difficulties [3]float64
|
||||
Ranked int
|
||||
Frozen bool
|
||||
LatestUpdate common.UnixTimestamp
|
||||
}
|
||||
err := DB.QueryRow("SELECT difficulty_taiko, difficulty_ctb, difficulty_mania, ranked,"+
|
||||
"ranked_status_freezed, latest_update FROM beatmaps WHERE "+
|
||||
where+" LIMIT 1", params...).
|
||||
Scan(
|
||||
&data.Difficulties[0], &data.Difficulties[1], &data.Difficulties[2],
|
||||
&data.Ranked, &data.Frozen, &data.LatestUpdate,
|
||||
)
|
||||
b.frozen = data.Frozen
|
||||
if b.frozen {
|
||||
b.ranked = data.Ranked
|
||||
}
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return true, err
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if data.Difficulties[0] == 0 && data.Difficulties[1] == 0 && data.Difficulties[2] == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
expire := Expire
|
||||
if data.Ranked == 2 {
|
||||
expire *= 6
|
||||
}
|
||||
|
||||
if expire != 0 && time.Now().After(time.Time(data.LatestUpdate).Add(expire)) && !data.Frozen {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Update updates a beatmap.
|
||||
func Update(b BeatmapDefiningQuality, beatmapInDB bool) error {
|
||||
var data [4]osuapi.Beatmap
|
||||
for i := 0; i <= 3; i++ {
|
||||
mode := osuapi.Mode(i)
|
||||
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
|
||||
BeatmapID: b.ID,
|
||||
BeatmapHash: b.MD5,
|
||||
Mode: &mode,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(beatmaps) == 0 {
|
||||
continue
|
||||
}
|
||||
data[i] = beatmaps[0]
|
||||
}
|
||||
var main *osuapi.Beatmap
|
||||
for _, el := range data {
|
||||
if el.FileMD5 != "" {
|
||||
main = &el
|
||||
break
|
||||
}
|
||||
}
|
||||
if main == nil {
|
||||
return fmt.Errorf("beatmapget: beatmap %s not found", b.String())
|
||||
}
|
||||
if beatmapInDB {
|
||||
w, p := b.whereAndParams()
|
||||
DB.MustExec("DELETE FROM beatmaps WHERE "+w, p...)
|
||||
}
|
||||
if b.frozen {
|
||||
main.Approved = osuapi.ApprovedStatus(b.ranked)
|
||||
}
|
||||
songName := fmt.Sprintf("%s - %s [%s]", main.Artist, main.Title, main.DiffName)
|
||||
_, err := DB.Exec(`INSERT INTO
|
||||
beatmaps (
|
||||
beatmap_id, beatmapset_id, beatmap_md5,
|
||||
song_name, ar, od, difficulty_std, difficulty_taiko,
|
||||
difficulty_ctb, difficulty_mania, max_combo, hit_length,
|
||||
bpm, ranked, latest_update, ranked_status_freezed
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
|
||||
main.BeatmapID, main.BeatmapSetID, main.FileMD5,
|
||||
songName, main.ApproachRate, main.OverallDifficulty, data[0].DifficultyRating, data[1].DifficultyRating,
|
||||
data[2].DifficultyRating, data[3].DifficultyRating, main.MaxCombo, main.HitLength,
|
||||
int(main.BPM), main.Approved, time.Now().Unix(), b.frozen,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
osuapi.RateLimit(200)
|
||||
}
|
81
vendor/zxq.co/ripple/rippleapi/beatmapget/fullset.go
vendored
Normal file
81
vendor/zxq.co/ripple/rippleapi/beatmapget/fullset.go
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
package beatmapget
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
)
|
||||
|
||||
// Set checks if an update is required for all beatmaps in a set.
|
||||
func Set(s int) error {
|
||||
var (
|
||||
lastUpdated common.UnixTimestamp
|
||||
ranked int
|
||||
)
|
||||
err := DB.QueryRow("SELECT latest_update, ranked FROM beatmaps WHERE beatmapset_id = ? LIMIT 1", s).
|
||||
Scan(&lastUpdated, &ranked)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
return set(s, lastUpdated, ranked)
|
||||
}
|
||||
|
||||
// ErrBeatmapNotFound is returned by Beatmap if a beatmap could not be found.
|
||||
var ErrBeatmapNotFound = errors.New("beatmapget: beatmap not found")
|
||||
|
||||
// Beatmap check if an update is required for all beatmaps in the set
|
||||
// containing this beatmap.
|
||||
func Beatmap(b int) (int, error) {
|
||||
var (
|
||||
setID int
|
||||
lastUpdated common.UnixTimestamp
|
||||
ranked int
|
||||
)
|
||||
err := DB.QueryRow("SELECT beatmapset_id, latest_update, ranked FROM beatmaps WHERE beatmap_id = ? LIMIT 1", b).
|
||||
Scan(&setID, &lastUpdated, &ranked)
|
||||
switch err {
|
||||
case nil:
|
||||
return setID, set(setID, lastUpdated, ranked)
|
||||
case sql.ErrNoRows:
|
||||
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
|
||||
BeatmapID: b,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(beatmaps) == 0 {
|
||||
return 0, ErrBeatmapNotFound
|
||||
}
|
||||
return beatmaps[0].BeatmapSetID, set(beatmaps[0].BeatmapSetID, common.UnixTimestamp(time.Time{}), 0)
|
||||
default:
|
||||
return setID, err
|
||||
}
|
||||
}
|
||||
|
||||
func set(s int, updated common.UnixTimestamp, ranked int) error {
|
||||
expire := Expire
|
||||
if ranked == 2 {
|
||||
expire *= 6
|
||||
}
|
||||
if time.Now().Before(time.Time(updated).Add(expire)) {
|
||||
return nil
|
||||
}
|
||||
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
|
||||
BeatmapSetID: s,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, beatmap := range beatmaps {
|
||||
err := UpdateIfRequired(BeatmapDefiningQuality{
|
||||
ID: beatmap.BeatmapID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
65
vendor/zxq.co/ripple/rippleapi/common/conf.go
vendored
Normal file
65
vendor/zxq.co/ripple/rippleapi/common/conf.go
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/thehowl/conf"
|
||||
)
|
||||
|
||||
// Version is the git hash of the application. Do not edit. This is
|
||||
// automatically set using -ldflags during build time.
|
||||
var Version string
|
||||
|
||||
// Conf is the configuration file data for the ripple API.
|
||||
// Conf uses https://github.com/thehowl/conf
|
||||
type Conf struct {
|
||||
DatabaseType string `description:"At the moment, 'mysql' is the only supported database type."`
|
||||
DSN string `description:"The Data Source Name for the database. More: https://github.com/go-sql-driver/mysql#dsn-data-source-name"`
|
||||
ListenTo string `description:"The IP/Port combination from which to take connections, e.g. :8080"`
|
||||
Unix bool `description:"Bool indicating whether ListenTo is a UNIX socket or an address."`
|
||||
SentryDSN string `description:"thing for sentry whatever"`
|
||||
HanayoKey string
|
||||
BeatmapRequestsPerUser int
|
||||
RankQueueSize int
|
||||
OsuAPIKey string
|
||||
RedisAddr string
|
||||
RedisPassword string
|
||||
RedisDB int
|
||||
}
|
||||
|
||||
var cachedConf *Conf
|
||||
|
||||
// Load creates a new Conf, using the data in the file "api.conf".
|
||||
func Load() (c Conf, halt bool) {
|
||||
if cachedConf != nil {
|
||||
c = *cachedConf
|
||||
return
|
||||
}
|
||||
err := conf.Load(&c, "api.conf")
|
||||
halt = err == conf.ErrNoFile
|
||||
if halt {
|
||||
conf.MustExport(Conf{
|
||||
DatabaseType: "mysql",
|
||||
DSN: "root@/ripple",
|
||||
ListenTo: ":40001",
|
||||
Unix: false,
|
||||
HanayoKey: "Potato",
|
||||
BeatmapRequestsPerUser: 2,
|
||||
RankQueueSize: 25,
|
||||
RedisAddr: "localhost:6379",
|
||||
}, "api.conf")
|
||||
fmt.Println("Please compile the configuration file (api.conf).")
|
||||
}
|
||||
cachedConf = &c
|
||||
return
|
||||
}
|
||||
|
||||
// GetConf returns the cachedConf.
|
||||
func GetConf() *Conf {
|
||||
if cachedConf == nil {
|
||||
return nil
|
||||
}
|
||||
// so that the cachedConf cannot actually get modified
|
||||
c := *cachedConf
|
||||
return &c
|
||||
}
|
29
vendor/zxq.co/ripple/rippleapi/common/conversions.go
vendored
Normal file
29
vendor/zxq.co/ripple/rippleapi/common/conversions.go
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// b2s converts byte slice to a string without memory allocation.
|
||||
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
// s2b converts string to a byte slice without memory allocation.
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func s2b(s string) []byte {
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh := reflect.SliceHeader{
|
||||
Data: sh.Data,
|
||||
Len: sh.Len,
|
||||
Cap: sh.Len,
|
||||
}
|
||||
return *(*[]byte)(unsafe.Pointer(&bh))
|
||||
}
|
8
vendor/zxq.co/ripple/rippleapi/common/flags.go
vendored
Normal file
8
vendor/zxq.co/ripple/rippleapi/common/flags.go
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
package common
|
||||
|
||||
// These are the flags an user can have. Mostly settings or things like whether
|
||||
// the user has verified their email address.
|
||||
const (
|
||||
FlagEmailVerified = 1 << iota
|
||||
FlagCountry2FA
|
||||
)
|
25
vendor/zxq.co/ripple/rippleapi/common/in.go
vendored
Normal file
25
vendor/zxq.co/ripple/rippleapi/common/in.go
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
package common
|
||||
|
||||
import "strconv"
|
||||
|
||||
// In picks x if y < x, picks z if y > z, or if none of the previous
|
||||
// conditions is satisfies, it simply picks y.
|
||||
func In(x, y, z int) int {
|
||||
switch {
|
||||
case y < x:
|
||||
return x
|
||||
case y > z:
|
||||
return z
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
// InString takes y as a string, also allows for a default value should y be
|
||||
// invalid as a number.
|
||||
func InString(x int, y string, z, def int) int {
|
||||
num, err := strconv.Atoi(y)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return In(x, num, z)
|
||||
}
|
9
vendor/zxq.co/ripple/rippleapi/common/int.go
vendored
Normal file
9
vendor/zxq.co/ripple/rippleapi/common/int.go
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
package common
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Int converts s to an int. If s in an invalid int, it defaults to 0.
|
||||
func Int(s string) int {
|
||||
r, _ := strconv.Atoi(s)
|
||||
return r
|
||||
}
|
166
vendor/zxq.co/ripple/rippleapi/common/method_data.go
vendored
Normal file
166
vendor/zxq.co/ripple/rippleapi/common/method_data.go
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/DataDog/datadog-go/statsd"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/redis.v5"
|
||||
)
|
||||
|
||||
// RavenClient is the raven client to which report errors happening.
|
||||
// If nil, errors will just be fmt.Println'd
|
||||
var RavenClient *raven.Client
|
||||
|
||||
// MethodData is a struct containing the data passed over to an API method.
|
||||
type MethodData struct {
|
||||
User Token
|
||||
DB *sqlx.DB
|
||||
Doggo *statsd.Client
|
||||
R *redis.Client
|
||||
Ctx *fasthttp.RequestCtx
|
||||
}
|
||||
|
||||
// ClientIP implements a best effort algorithm to return the real client IP, it parses
|
||||
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
|
||||
func (md MethodData) ClientIP() string {
|
||||
clientIP := strings.TrimSpace(string(md.Ctx.Request.Header.Peek("X-Real-Ip")))
|
||||
if len(clientIP) > 0 {
|
||||
return clientIP
|
||||
}
|
||||
clientIP = string(md.Ctx.Request.Header.Peek("X-Forwarded-For"))
|
||||
if index := strings.IndexByte(clientIP, ','); index >= 0 {
|
||||
clientIP = clientIP[0:index]
|
||||
}
|
||||
clientIP = strings.TrimSpace(clientIP)
|
||||
if len(clientIP) > 0 {
|
||||
return clientIP
|
||||
}
|
||||
return md.Ctx.RemoteIP().String()
|
||||
}
|
||||
|
||||
// Err logs an error. If RavenClient is set, it will use the client to report
|
||||
// the error to sentry, otherwise it will just write the error to stdout.
|
||||
func (md MethodData) Err(err error) {
|
||||
user := &raven.User{
|
||||
ID: strconv.Itoa(md.User.UserID),
|
||||
Username: md.User.Value,
|
||||
IP: md.Ctx.RemoteAddr().String(),
|
||||
}
|
||||
// Generate tags for error
|
||||
tags := map[string]string{
|
||||
"endpoint": string(md.Ctx.RequestURI()),
|
||||
"token": md.User.Value,
|
||||
}
|
||||
_err(err, tags, user, md.Ctx)
|
||||
}
|
||||
|
||||
// Err for peppy API calls
|
||||
func Err(c *fasthttp.RequestCtx, err error) {
|
||||
// Generate tags for error
|
||||
tags := map[string]string{
|
||||
"endpoint": string(c.RequestURI()),
|
||||
}
|
||||
|
||||
_err(err, tags, nil, c)
|
||||
}
|
||||
|
||||
// WSErr is the error function for errors happening in the websockets.
|
||||
func WSErr(err error) {
|
||||
_err(err, map[string]string{
|
||||
"endpoint": "/api/v1/ws",
|
||||
}, nil, nil)
|
||||
}
|
||||
|
||||
// GenericError is just an error. Can't make a good description.
|
||||
func GenericError(err error) {
|
||||
_err(err, nil, nil, nil)
|
||||
}
|
||||
|
||||
func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.RequestCtx) {
|
||||
if RavenClient == nil {
|
||||
fmt.Println("ERROR!!!!")
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create stacktrace
|
||||
st := raven.NewStacktrace(0, 3, []string{"zxq.co/ripple", "git.zxq.co/ripple"})
|
||||
|
||||
ifaces := []raven.Interface{st, generateRavenHTTP(c)}
|
||||
if user != nil {
|
||||
ifaces = append(ifaces, user)
|
||||
}
|
||||
|
||||
RavenClient.CaptureError(
|
||||
err,
|
||||
tags,
|
||||
ifaces...,
|
||||
)
|
||||
}
|
||||
|
||||
func generateRavenHTTP(ctx *fasthttp.RequestCtx) *raven.Http {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// build uri
|
||||
uri := ctx.URI()
|
||||
// safe to use b2s because a new string gets allocated eventually for
|
||||
// concatenation
|
||||
sURI := b2s(uri.Scheme()) + "://" + b2s(uri.Host()) + b2s(uri.Path())
|
||||
|
||||
// build header map
|
||||
// using ctx.Request.Header.Len would mean calling .VisitAll two times
|
||||
// which can be quite expensive since it means iterating over all the
|
||||
// headers, so we give a rough estimate of the number of headers we expect
|
||||
// to have
|
||||
m := make(map[string]string, 16)
|
||||
ctx.Request.Header.VisitAll(func(k, v []byte) {
|
||||
// not using b2s because we mustn't keep references to the underlying
|
||||
// k and v
|
||||
m[string(k)] = string(v)
|
||||
})
|
||||
|
||||
return &raven.Http{
|
||||
URL: sURI,
|
||||
// Not using b2s because raven sending is concurrent and may happen
|
||||
// AFTER the request, meaning that values could potentially be replaced
|
||||
// by new ones.
|
||||
Method: string(ctx.Method()),
|
||||
Query: string(uri.QueryString()),
|
||||
Cookies: string(ctx.Request.Header.Peek("Cookie")),
|
||||
Headers: m,
|
||||
}
|
||||
}
|
||||
|
||||
// ID retrieves the Token's owner user ID.
|
||||
func (md MethodData) ID() int {
|
||||
return md.User.UserID
|
||||
}
|
||||
|
||||
// Query is shorthand for md.C.Query.
|
||||
func (md MethodData) Query(q string) string {
|
||||
return b2s(md.Ctx.QueryArgs().Peek(q))
|
||||
}
|
||||
|
||||
// HasQuery returns true if the parameter is encountered in the querystring.
|
||||
// It returns true even if the parameter is "" (the case of ?param&etc=etc)
|
||||
func (md MethodData) HasQuery(q string) bool {
|
||||
return md.Ctx.QueryArgs().Has(q)
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals a request's JSON body into an interface.
|
||||
func (md MethodData) Unmarshal(into interface{}) error {
|
||||
return json.Unmarshal(md.Ctx.PostBody(), into)
|
||||
}
|
||||
|
||||
// IsBearer tells whether the current token is a Bearer (oauth) token.
|
||||
func (md MethodData) IsBearer() bool {
|
||||
return md.User.ID == -1
|
||||
}
|
22
vendor/zxq.co/ripple/rippleapi/common/paginate.go
vendored
Normal file
22
vendor/zxq.co/ripple/rippleapi/common/paginate.go
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
package common
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Paginate creates an additional SQL LIMIT clause for paginating.
|
||||
func Paginate(page, limit string, maxLimit int) string {
|
||||
var (
|
||||
p = Int(page)
|
||||
l = Int(limit)
|
||||
)
|
||||
if p < 1 {
|
||||
p = 1
|
||||
}
|
||||
if l < 1 {
|
||||
l = 50
|
||||
}
|
||||
if l > maxLimit {
|
||||
l = maxLimit
|
||||
}
|
||||
start := uint(p-1) * uint(l)
|
||||
return fmt.Sprintf(" LIMIT %d,%d ", start, l)
|
||||
}
|
49
vendor/zxq.co/ripple/rippleapi/common/paginate_test.go
vendored
Normal file
49
vendor/zxq.co/ripple/rippleapi/common/paginate_test.go
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPaginate(t *testing.T) {
|
||||
type args struct {
|
||||
page string
|
||||
limit string
|
||||
maxLimit int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"1",
|
||||
args{
|
||||
"10",
|
||||
"",
|
||||
100,
|
||||
},
|
||||
" LIMIT 450,50 ",
|
||||
},
|
||||
{
|
||||
"2",
|
||||
args{
|
||||
"-5",
|
||||
"-15",
|
||||
100,
|
||||
},
|
||||
" LIMIT 0,50 ",
|
||||
},
|
||||
{
|
||||
"3",
|
||||
args{
|
||||
"2",
|
||||
"150",
|
||||
100,
|
||||
},
|
||||
" LIMIT 100,100 ",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := Paginate(tt.args.page, tt.args.limit, tt.args.maxLimit); got != tt.want {
|
||||
t.Errorf("%q. Paginate() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
94
vendor/zxq.co/ripple/rippleapi/common/privileges.go
vendored
Normal file
94
vendor/zxq.co/ripple/rippleapi/common/privileges.go
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
package common
|
||||
|
||||
import "strings"
|
||||
|
||||
// These are the various privileges a token can have.
|
||||
const (
|
||||
PrivilegeRead = 1 << iota // used to be to fetch public data, such as user information etc. this is deprecated.
|
||||
PrivilegeReadConfidential // (eventual) private messages, reports... of self
|
||||
PrivilegeWrite // change user information, write into confidential stuff...
|
||||
PrivilegeManageBadges // can change various users' badges.
|
||||
PrivilegeBetaKeys // can add, remove, upgrade/downgrade, make public beta keys.
|
||||
PrivilegeManageSettings // maintainance, set registrations, global alerts, bancho settings
|
||||
PrivilegeViewUserAdvanced // can see user email, and perhaps warnings in the future, basically.
|
||||
PrivilegeManageUser // can change user email, allowed status, userpage, rank, username...
|
||||
PrivilegeManageRoles // translates as admin, as they can basically assign roles to anyone, even themselves
|
||||
PrivilegeManageAPIKeys // admin permission to manage user permission, not only self permissions. Only ever do this if you completely trust the application, because this essentially means to put the entire ripple database in the hands of a (potentially evil?) application.
|
||||
PrivilegeBlog // can do pretty much anything to the blog, and the documentation.
|
||||
PrivilegeAPIMeta // can do /meta API calls. basically means they can restart the API server.
|
||||
PrivilegeBeatmap // rank/unrank beatmaps. also BAT when implemented
|
||||
)
|
||||
|
||||
// Privileges is a bitwise enum of the privileges of an user's API key.
|
||||
type Privileges uint64
|
||||
|
||||
var privilegeString = [...]string{
|
||||
"Read",
|
||||
"ReadConfidential",
|
||||
"Write",
|
||||
"ManageBadges",
|
||||
"BetaKeys",
|
||||
"ManageSettings",
|
||||
"ViewUserAdvanced",
|
||||
"ManageUser",
|
||||
"ManageRoles",
|
||||
"ManageAPIKeys",
|
||||
"Blog",
|
||||
"APIMeta",
|
||||
"Beatmap",
|
||||
}
|
||||
|
||||
func (p Privileges) String() string {
|
||||
var pvs []string
|
||||
for i, v := range privilegeString {
|
||||
if uint64(p)&uint64(1<<uint(i)) != 0 {
|
||||
pvs = append(pvs, v)
|
||||
}
|
||||
}
|
||||
return strings.Join(pvs, ", ")
|
||||
}
|
||||
|
||||
var privilegeMustBe = [...]UserPrivileges{
|
||||
1 << 30, // read is deprecated, and should be given out to no-one.
|
||||
UserPrivilegeNormal,
|
||||
UserPrivilegeNormal,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageBadges,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageBetaKey,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageSetting,
|
||||
AdminPrivilegeAccessRAP,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeBanUsers,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeManagePrivilege,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeManageServer,
|
||||
AdminPrivilegeChatMod, // temporary?
|
||||
AdminPrivilegeManageServer,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageBeatmap,
|
||||
}
|
||||
|
||||
// CanOnly removes any privilege that the user has requested to have, but cannot have due to their rank.
|
||||
func (p Privileges) CanOnly(userPrivs UserPrivileges) Privileges {
|
||||
newPrivilege := 0
|
||||
for i, v := range privilegeMustBe {
|
||||
wants := p&1 == 1
|
||||
can := userPrivs&v == v
|
||||
if wants && can {
|
||||
newPrivilege |= 1 << uint(i)
|
||||
}
|
||||
p >>= 1
|
||||
}
|
||||
return Privileges(newPrivilege)
|
||||
}
|
||||
|
||||
var privilegeMap = map[string]Privileges{
|
||||
"read_confidential": PrivilegeReadConfidential,
|
||||
"write": PrivilegeWrite,
|
||||
}
|
||||
|
||||
// OAuthPrivileges returns the equivalent in Privileges of a space-separated
|
||||
// list of scopes.
|
||||
func OAuthPrivileges(scopes string) Privileges {
|
||||
var p Privileges
|
||||
for _, x := range strings.Split(scopes, " ") {
|
||||
p |= privilegeMap[x]
|
||||
}
|
||||
return p
|
||||
}
|
33
vendor/zxq.co/ripple/rippleapi/common/random_string.go
vendored
Normal file
33
vendor/zxq.co/ripple/rippleapi/common/random_string.go
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const letterBytes = "0123456789abcdef"
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var randSrc = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
// RandomString generates a random string.
|
||||
func RandomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
// A randSrc.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, randSrc.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = randSrc.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
b[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
return string(b)
|
||||
}
|
36
vendor/zxq.co/ripple/rippleapi/common/response.go
vendored
Normal file
36
vendor/zxq.co/ripple/rippleapi/common/response.go
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
// ResponseBase is the data that is always returned with an API request.
|
||||
type ResponseBase struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// GetCode retrieves the response code.
|
||||
func (r ResponseBase) GetCode() int {
|
||||
return r.Code
|
||||
}
|
||||
|
||||
// SetCode changes the response code.
|
||||
func (r *ResponseBase) SetCode(i int) {
|
||||
r.Code = i
|
||||
}
|
||||
|
||||
// GetMessage retrieves the response message.
|
||||
func (r ResponseBase) GetMessage() string {
|
||||
return r.Message
|
||||
}
|
||||
|
||||
// CodeMessager is something that has the Code() and Message() methods.
|
||||
type CodeMessager interface {
|
||||
GetMessage() string
|
||||
GetCode() int
|
||||
}
|
||||
|
||||
// SimpleResponse returns the most basic response.
|
||||
func SimpleResponse(code int, message string) CodeMessager {
|
||||
return ResponseBase{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
16
vendor/zxq.co/ripple/rippleapi/common/sanitisation.go
vendored
Normal file
16
vendor/zxq.co/ripple/rippleapi/common/sanitisation.go
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// SanitiseString removes all control codes from a string.
|
||||
func SanitiseString(s string) string {
|
||||
n := make([]rune, 0, len(s))
|
||||
for _, c := range s {
|
||||
if c == '\n' || !unicode.Is(unicode.Other, c) {
|
||||
n = append(n, c)
|
||||
}
|
||||
}
|
||||
return string(n)
|
||||
}
|
41
vendor/zxq.co/ripple/rippleapi/common/sanitisation_test.go
vendored
Normal file
41
vendor/zxq.co/ripple/rippleapi/common/sanitisation_test.go
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
const pen = "I trattori di palmizio 나는 펜이있다. 私はリンゴを持っています。" +
|
||||
"啊! 苹果笔。 у меня есть ручка, Tôi có dứa. අන්නාසි පෑන"
|
||||
|
||||
func TestSanitiseString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"Normal",
|
||||
pen,
|
||||
pen,
|
||||
},
|
||||
{
|
||||
"Arabic (rtl)",
|
||||
"أناناس",
|
||||
"أناناس",
|
||||
},
|
||||
{
|
||||
"Null",
|
||||
"A\x00B",
|
||||
"AB",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := SanitiseString(tt.arg); got != tt.want {
|
||||
t.Errorf("%q. SanitiseString() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSanitiseString(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SanitiseString(pen)
|
||||
}
|
||||
}
|
52
vendor/zxq.co/ripple/rippleapi/common/sort.go
vendored
Normal file
52
vendor/zxq.co/ripple/rippleapi/common/sort.go
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
package common
|
||||
|
||||
import "strings"
|
||||
|
||||
// SortConfiguration is the configuration of Sort.
|
||||
type SortConfiguration struct {
|
||||
Allowed []string // Allowed parameters
|
||||
Default string
|
||||
DefaultSorting string // if empty, DESC
|
||||
Table string
|
||||
}
|
||||
|
||||
// Sort allows the request to modify how the query is sorted.
|
||||
func Sort(md MethodData, config SortConfiguration) string {
|
||||
if config.DefaultSorting == "" {
|
||||
config.DefaultSorting = "DESC"
|
||||
}
|
||||
if config.Table != "" {
|
||||
config.Table += "."
|
||||
}
|
||||
var sortBy string
|
||||
for _, s := range md.Ctx.Request.URI().QueryArgs().PeekMulti("sort") {
|
||||
sortParts := strings.Split(strings.ToLower(b2s(s)), ",")
|
||||
if contains(config.Allowed, sortParts[0]) {
|
||||
if sortBy != "" {
|
||||
sortBy += ", "
|
||||
}
|
||||
sortBy += config.Table + sortParts[0] + " "
|
||||
if len(sortParts) > 1 && contains([]string{"asc", "desc"}, sortParts[1]) {
|
||||
sortBy += sortParts[1]
|
||||
} else {
|
||||
sortBy += config.DefaultSorting
|
||||
}
|
||||
}
|
||||
}
|
||||
if sortBy == "" {
|
||||
sortBy = config.Default
|
||||
}
|
||||
if sortBy == "" {
|
||||
return ""
|
||||
}
|
||||
return "ORDER BY " + sortBy
|
||||
}
|
||||
|
||||
func contains(a []string, s string) bool {
|
||||
for _, el := range a {
|
||||
if s == el {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
23
vendor/zxq.co/ripple/rippleapi/common/token.go
vendored
Normal file
23
vendor/zxq.co/ripple/rippleapi/common/token.go
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Token is an API token.
|
||||
type Token struct {
|
||||
ID int
|
||||
Value string
|
||||
UserID int
|
||||
TokenPrivileges Privileges
|
||||
UserPrivileges UserPrivileges
|
||||
}
|
||||
|
||||
// OnlyUserPublic returns a string containing "(user.privileges & 1 = 1 OR users.id = <userID>)"
|
||||
// if the user does not have the UserPrivilege AdminManageUsers, and returns "1" otherwise.
|
||||
func (t Token) OnlyUserPublic(userManagerSeesEverything bool) string {
|
||||
if userManagerSeesEverything &&
|
||||
t.UserPrivileges&AdminPrivilegeManageUsers == AdminPrivilegeManageUsers {
|
||||
return "1"
|
||||
}
|
||||
// It's safe to use sprintf directly even if it's a query, because UserID is an int.
|
||||
return fmt.Sprintf("(users.privileges & 1 = 1 OR users.id = '%d')", t.UserID)
|
||||
}
|
55
vendor/zxq.co/ripple/rippleapi/common/unix_timestamp.go
vendored
Normal file
55
vendor/zxq.co/ripple/rippleapi/common/unix_timestamp.go
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UnixTimestamp is simply a time.Time, but can be used to convert an
|
||||
// unix timestamp in the database into a native time.Time.
|
||||
type UnixTimestamp time.Time
|
||||
|
||||
// Scan decodes src into an unix timestamp.
|
||||
func (u *UnixTimestamp) Scan(src interface{}) error {
|
||||
if u == nil {
|
||||
return errors.New("rippleapi/common: UnixTimestamp is nil")
|
||||
}
|
||||
switch src := src.(type) {
|
||||
case int64:
|
||||
*u = UnixTimestamp(time.Unix(src, 0))
|
||||
case float64:
|
||||
*u = UnixTimestamp(time.Unix(int64(src), 0))
|
||||
case string:
|
||||
return u._string(src)
|
||||
case []byte:
|
||||
return u._string(string(src))
|
||||
case nil:
|
||||
// Nothing, leave zero value on timestamp
|
||||
default:
|
||||
return errors.New("rippleapi/common: unhandleable type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UnixTimestamp) _string(s string) error {
|
||||
ts, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*u = UnixTimestamp(time.Unix(int64(ts), 0))
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON -> time.Time.MarshalJSON
|
||||
func (u UnixTimestamp) MarshalJSON() ([]byte, error) {
|
||||
return time.Time(u).MarshalJSON()
|
||||
}
|
||||
|
||||
// UnmarshalJSON -> time.Time.UnmarshalJSON
|
||||
func (u *UnixTimestamp) UnmarshalJSON(x []byte) error {
|
||||
t := new(time.Time)
|
||||
err := t.UnmarshalJSON(x)
|
||||
*u = UnixTimestamp(*t)
|
||||
return err
|
||||
}
|
32
vendor/zxq.co/ripple/rippleapi/common/update.go
vendored
Normal file
32
vendor/zxq.co/ripple/rippleapi/common/update.go
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UpdateQuery is simply an SQL update query,
|
||||
// that can be built upon passed parameters.
|
||||
type UpdateQuery struct {
|
||||
fields []string
|
||||
Parameters []interface{}
|
||||
}
|
||||
|
||||
// Add adds a new field with correspective value to UpdateQuery
|
||||
func (u *UpdateQuery) Add(field string, value interface{}) *UpdateQuery {
|
||||
val := reflect.ValueOf(value)
|
||||
if val.Kind() == reflect.Ptr && val.IsNil() {
|
||||
return u
|
||||
}
|
||||
if s, ok := value.(string); ok && s == "" {
|
||||
return u
|
||||
}
|
||||
u.fields = append(u.fields, field+" = ?")
|
||||
u.Parameters = append(u.Parameters, value)
|
||||
return u
|
||||
}
|
||||
|
||||
// Fields retrieves the fields joined by a comma.
|
||||
func (u *UpdateQuery) Fields() string {
|
||||
return strings.Join(u.fields, ", ")
|
||||
}
|
68
vendor/zxq.co/ripple/rippleapi/common/user_privileges.go
vendored
Normal file
68
vendor/zxq.co/ripple/rippleapi/common/user_privileges.go
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
package common
|
||||
|
||||
import "strings"
|
||||
|
||||
// user/admin privileges
|
||||
const (
|
||||
UserPrivilegePublic UserPrivileges = 1 << iota
|
||||
UserPrivilegeNormal
|
||||
UserPrivilegeDonor
|
||||
AdminPrivilegeAccessRAP
|
||||
AdminPrivilegeManageUsers
|
||||
AdminPrivilegeBanUsers
|
||||
AdminPrivilegeSilenceUsers
|
||||
AdminPrivilegeWipeUsers
|
||||
AdminPrivilegeManageBeatmap
|
||||
AdminPrivilegeManageServer
|
||||
AdminPrivilegeManageSetting
|
||||
AdminPrivilegeManageBetaKey
|
||||
AdminPrivilegeManageReport
|
||||
AdminPrivilegeManageDocs
|
||||
AdminPrivilegeManageBadges
|
||||
AdminPrivilegeViewRAPLogs
|
||||
AdminPrivilegeManagePrivilege
|
||||
AdminPrivilegeSendAlerts
|
||||
AdminPrivilegeChatMod
|
||||
AdminPrivilegeKickUsers
|
||||
UserPrivilegePendingVerification
|
||||
UserPrivilegeTournamentStaff
|
||||
AdminPrivilegeCaker
|
||||
)
|
||||
|
||||
// UserPrivileges represents a bitwise enum of the privileges of an user.
|
||||
type UserPrivileges uint64
|
||||
|
||||
var userPrivilegeString = [...]string{
|
||||
"UserPublic",
|
||||
"UserNormal",
|
||||
"UserDonor",
|
||||
"AdminAccessRAP",
|
||||
"AdminManageUsers",
|
||||
"AdminBanUsers",
|
||||
"AdminSilenceUsers",
|
||||
"AdminWipeUsers",
|
||||
"AdminManageBeatmap",
|
||||
"AdminManageServer",
|
||||
"AdminManageSetting",
|
||||
"AdminManageBetaKey",
|
||||
"AdminManageReport",
|
||||
"AdminManageDocs",
|
||||
"AdminManageBadges",
|
||||
"AdminViewRAPLogs",
|
||||
"AdminManagePrivilege",
|
||||
"AdminSendAlerts",
|
||||
"AdminChatMod",
|
||||
"AdminKickUsers",
|
||||
"UserPendingVerification",
|
||||
"UserTournamentStaff",
|
||||
}
|
||||
|
||||
func (p UserPrivileges) String() string {
|
||||
var pvs []string
|
||||
for i, v := range userPrivilegeString {
|
||||
if uint64(p)&uint64(1<<uint(i)) != 0 {
|
||||
pvs = append(pvs, v)
|
||||
}
|
||||
}
|
||||
return strings.Join(pvs, ", ")
|
||||
}
|
11
vendor/zxq.co/ripple/rippleapi/common/utils.go
vendored
Normal file
11
vendor/zxq.co/ripple/rippleapi/common/utils.go
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SafeUsername makes a string lowercase and replaces all spaces with
|
||||
// underscores.
|
||||
func SafeUsername(s string) string {
|
||||
return strings.Replace(strings.ToLower(s), " ", "_", -1)
|
||||
}
|
20
vendor/zxq.co/ripple/rippleapi/common/utils_test.go
vendored
Normal file
20
vendor/zxq.co/ripple/rippleapi/common/utils_test.go
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSafeUsername(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
want string
|
||||
}{
|
||||
{"noChange", "no_change", "no_change"},
|
||||
{"toLower", "Change_Me", "change_me"},
|
||||
{"complete", "La_M a m m a_putt na", "la_m_a_m_m_a_putt_na"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := SafeUsername(tt.arg); got != tt.want {
|
||||
t.Errorf("%q. SafeUsername() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
91
vendor/zxq.co/ripple/rippleapi/common/where.go
vendored
Normal file
91
vendor/zxq.co/ripple/rippleapi/common/where.go
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
package common
|
||||
|
||||
// WhereClause is a struct representing a where clause.
|
||||
// This is made to easily create WHERE clauses from parameters passed from a request.
|
||||
type WhereClause struct {
|
||||
Clause string
|
||||
Params []interface{}
|
||||
useOr bool
|
||||
}
|
||||
|
||||
// Where adds a new WHERE clause to the WhereClause.
|
||||
func (w *WhereClause) Where(clause, passedParam string, allowedValues ...string) *WhereClause {
|
||||
if passedParam == "" {
|
||||
return w
|
||||
}
|
||||
if len(allowedValues) != 0 && !contains(allowedValues, passedParam) {
|
||||
return w
|
||||
}
|
||||
w.addWhere()
|
||||
w.Clause += clause
|
||||
w.Params = append(w.Params, passedParam)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *WhereClause) addWhere() {
|
||||
// if string is empty add "WHERE", else add AND
|
||||
if w.Clause == "" {
|
||||
w.Clause += "WHERE "
|
||||
} else {
|
||||
if w.useOr {
|
||||
w.Clause += " OR "
|
||||
return
|
||||
}
|
||||
w.Clause += " AND "
|
||||
}
|
||||
}
|
||||
|
||||
// Or enables using OR instead of AND
|
||||
func (w *WhereClause) Or() *WhereClause {
|
||||
w.useOr = true
|
||||
return w
|
||||
}
|
||||
|
||||
// And enables using AND instead of OR
|
||||
func (w *WhereClause) And() *WhereClause {
|
||||
w.useOr = false
|
||||
return w
|
||||
}
|
||||
|
||||
// In generates an IN clause.
|
||||
// initial is the initial part, e.g. "users.id".
|
||||
// Fields are the possible values.
|
||||
// Sample output: users.id IN ('1', '2', '3')
|
||||
func (w *WhereClause) In(initial string, fields ...[]byte) *WhereClause {
|
||||
if len(fields) == 0 {
|
||||
return w
|
||||
}
|
||||
w.addWhere()
|
||||
w.Clause += initial + " IN (" + generateQuestionMarks(len(fields)) + ")"
|
||||
fieldsInterfaced := make([]interface{}, len(fields))
|
||||
for k, f := range fields {
|
||||
fieldsInterfaced[k] = string(f)
|
||||
}
|
||||
w.Params = append(w.Params, fieldsInterfaced...)
|
||||
return w
|
||||
}
|
||||
|
||||
func generateQuestionMarks(x int) (qm string) {
|
||||
for i := 0; i < x-1; i++ {
|
||||
qm += "?, "
|
||||
}
|
||||
if x > 0 {
|
||||
qm += "?"
|
||||
}
|
||||
return qm
|
||||
}
|
||||
|
||||
// ClauseSafe returns the clause, always containing something. If w.Clause is
|
||||
// empty, it returns "WHERE 1".
|
||||
func (w *WhereClause) ClauseSafe() string {
|
||||
if w.Clause == "" {
|
||||
return "WHERE 1"
|
||||
}
|
||||
return w.Clause
|
||||
}
|
||||
|
||||
// Where is the same as WhereClause.Where, but creates a new WhereClause.
|
||||
func Where(clause, passedParam string, allowedValues ...string) *WhereClause {
|
||||
w := new(WhereClause)
|
||||
return w.Where(clause, passedParam, allowedValues...)
|
||||
}
|
97
vendor/zxq.co/ripple/rippleapi/common/where_test.go
vendored
Normal file
97
vendor/zxq.co/ripple/rippleapi/common/where_test.go
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_generateQuestionMarks(t *testing.T) {
|
||||
type args struct {
|
||||
x int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantQm string
|
||||
}{
|
||||
{"-1", args{-1}, ""},
|
||||
{"0", args{0}, ""},
|
||||
{"1", args{1}, "?"},
|
||||
{"2", args{2}, "?, ?"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if gotQm := generateQuestionMarks(tt.args.x); gotQm != tt.wantQm {
|
||||
t.Errorf("%q. generateQuestionMarks() = %v, want %v", tt.name, gotQm, tt.wantQm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhereClause_In(t *testing.T) {
|
||||
type args struct {
|
||||
initial string
|
||||
fields []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields *WhereClause
|
||||
args args
|
||||
want *WhereClause
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
&WhereClause{},
|
||||
args{"users.id", []string{"1", "2", "3"}},
|
||||
&WhereClause{"WHERE users.id IN (?, ?, ?)", []interface{}{"1", "2", "3"}, false},
|
||||
},
|
||||
{
|
||||
"withExisting",
|
||||
Where("users.username = ?", "Howl").Where("users.xd > ?", "6"),
|
||||
args{"users.id", []string{"1"}},
|
||||
&WhereClause{
|
||||
"WHERE users.username = ? AND users.xd > ? AND users.id IN (?)",
|
||||
[]interface{}{"Howl", "6", "1"},
|
||||
false,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
w := tt.fields
|
||||
if got := w.In(tt.args.initial, tt.args.fields...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. WhereClause.In() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhere(t *testing.T) {
|
||||
type args struct {
|
||||
clause string
|
||||
passedParam string
|
||||
allowedValues []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *WhereClause
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{"users.id = ?", "5", nil},
|
||||
&WhereClause{"WHERE users.id = ?", []interface{}{"5"}, false},
|
||||
},
|
||||
{
|
||||
"allowed",
|
||||
args{"users.id = ?", "5", []string{"1", "3", "5"}},
|
||||
&WhereClause{"WHERE users.id = ?", []interface{}{"5"}, false},
|
||||
},
|
||||
{
|
||||
"notAllowed",
|
||||
args{"users.id = ?", "5", []string{"0"}},
|
||||
&WhereClause{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := Where(tt.args.clause, tt.args.passedParam, tt.args.allowedValues...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. Where() = %#v, want %#v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
122
vendor/zxq.co/ripple/rippleapi/limit/limit.go
vendored
Normal file
122
vendor/zxq.co/ripple/rippleapi/limit/limit.go
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
package limit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Request is a Request with DefaultLimiter.
|
||||
func Request(u string, perMinute int) { DefaultLimiter.Request(u, perMinute) }
|
||||
|
||||
// NonBlockingRequest is a NonBlockingRequest with DefaultLimiter.
|
||||
func NonBlockingRequest(u string, perMinute int) bool {
|
||||
return DefaultLimiter.NonBlockingRequest(u, perMinute)
|
||||
}
|
||||
|
||||
// DefaultLimiter is the RateLimiter used by the package-level
|
||||
// Request and NonBlockingRequest.
|
||||
var DefaultLimiter = &RateLimiter{
|
||||
Map: make(map[string]chan struct{}),
|
||||
Mutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// RateLimiter is a simple rate limiter.
|
||||
type RateLimiter struct {
|
||||
Map map[string]chan struct{}
|
||||
Mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// Request is a simple request. Blocks until it can make the request.
|
||||
func (s *RateLimiter) Request(u string, perMinute int) {
|
||||
s.request(u, perMinute, true)
|
||||
}
|
||||
|
||||
// NonBlockingRequest checks if it can do a request. If it can't, it returns
|
||||
// false, else it returns true if the request succeded.
|
||||
func (s *RateLimiter) NonBlockingRequest(u string, perMinute int) bool {
|
||||
return s.request(u, perMinute, false)
|
||||
}
|
||||
|
||||
func (s *RateLimiter) request(u string, perMinute int, blocking bool) bool {
|
||||
s.check()
|
||||
s.Mutex.RLock()
|
||||
c, exists := s.Map[u]
|
||||
s.Mutex.RUnlock()
|
||||
if !exists {
|
||||
c = makePrefilledChan(perMinute)
|
||||
s.Mutex.Lock()
|
||||
// Now that we have exclusive read and write-access, we want to
|
||||
// make sure we don't overwrite an existing channel. Otherwise,
|
||||
// race conditions and panic happen.
|
||||
if cNew, exists := s.Map[u]; exists {
|
||||
c = cNew
|
||||
s.Mutex.Unlock()
|
||||
} else {
|
||||
s.Map[u] = c
|
||||
s.Mutex.Unlock()
|
||||
<-c
|
||||
go s.filler(u, perMinute)
|
||||
}
|
||||
}
|
||||
return rcv(c, blocking)
|
||||
}
|
||||
|
||||
// rcv receives from a channel, but if blocking is true it waits til something
|
||||
// is received and always returns true, otherwise if it can't receive it
|
||||
// returns false.
|
||||
func rcv(c chan struct{}, blocking bool) bool {
|
||||
if blocking {
|
||||
<-c
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case <-c:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RateLimiter) filler(el string, perMinute int) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
fmt.Println(r)
|
||||
}
|
||||
}()
|
||||
|
||||
s.Mutex.RLock()
|
||||
c := s.Map[el]
|
||||
s.Mutex.RUnlock()
|
||||
for {
|
||||
select {
|
||||
case c <- struct{}{}:
|
||||
time.Sleep(time.Minute / time.Duration(perMinute))
|
||||
default: // c is full
|
||||
s.Mutex.Lock()
|
||||
close(c)
|
||||
delete(s.Map, el)
|
||||
s.Mutex.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check makes sure the map and the mutex are properly initialised.
|
||||
func (s *RateLimiter) check() {
|
||||
if s.Map == nil {
|
||||
s.Map = make(map[string]chan struct{})
|
||||
}
|
||||
if s.Mutex == nil {
|
||||
s.Mutex = new(sync.RWMutex)
|
||||
}
|
||||
}
|
||||
|
||||
func makePrefilledChan(l int) chan struct{} {
|
||||
c := make(chan struct{}, l)
|
||||
for i := 0; i < l; i++ {
|
||||
c <- struct{}{}
|
||||
}
|
||||
return c
|
||||
}
|
87
vendor/zxq.co/ripple/rippleapi/main.go
vendored
Normal file
87
vendor/zxq.co/ripple/rippleapi/main.go
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"zxq.co/ripple/rippleapi/app"
|
||||
"zxq.co/ripple/rippleapi/beatmapget"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/schiavolib"
|
||||
// Golint pls dont break balls
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/serenize/snaker"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
)
|
||||
|
||||
// Version is the git hash of the application. Do not edit. This is
|
||||
// automatically set using -ldflags during build time.
|
||||
var Version string
|
||||
|
||||
func init() {
|
||||
log.SetFlags(log.Ltime)
|
||||
log.SetPrefix(fmt.Sprintf("%d|", syscall.Getpid()))
|
||||
common.Version = Version
|
||||
}
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
func main() {
|
||||
fmt.Print("Ripple API")
|
||||
if Version != "" {
|
||||
fmt.Print("; git commit hash: ", Version)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
conf, halt := common.Load()
|
||||
if halt {
|
||||
return
|
||||
}
|
||||
|
||||
schiavo.Prefix = "Ripple API"
|
||||
|
||||
if !strings.Contains(conf.DSN, "parseTime=true") {
|
||||
c := "?"
|
||||
if strings.Contains(conf.DSN, "?") {
|
||||
c = "&"
|
||||
}
|
||||
conf.DSN += c + "parseTime=true&charset=utf8mb4,utf8&collation=utf8mb4_general_ci"
|
||||
}
|
||||
|
||||
var err error
|
||||
db, err = sqlx.Open(conf.DatabaseType, conf.DSN)
|
||||
if err != nil {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
db.MapperFunc(func(s string) string {
|
||||
if x, ok := commonClusterfucks[s]; ok {
|
||||
return x
|
||||
}
|
||||
return snaker.CamelToSnake(s)
|
||||
})
|
||||
|
||||
beatmapget.Client = osuapi.NewClient(conf.OsuAPIKey)
|
||||
beatmapget.DB = db
|
||||
|
||||
engine := app.Start(conf, db)
|
||||
|
||||
startuato(engine.Handler)
|
||||
}
|
||||
|
||||
var commonClusterfucks = map[string]string{
|
||||
"RegisteredOn": "register_datetime",
|
||||
"UsernameAKA": "username_aka",
|
||||
"BeatmapMD5": "beatmap_md5",
|
||||
"Count300": "300_count",
|
||||
"Count100": "100_count",
|
||||
"Count50": "50_count",
|
||||
"CountGeki": "gekis_count",
|
||||
"CountKatu": "katus_count",
|
||||
"CountMiss": "misses_count",
|
||||
"PP": "pp",
|
||||
}
|
71
vendor/zxq.co/ripple/rippleapi/startuato_linux.go
vendored
Normal file
71
vendor/zxq.co/ripple/rippleapi/startuato_linux.go
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rcrowley/goagain"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/schiavolib"
|
||||
)
|
||||
|
||||
func startuato(hn fasthttp.RequestHandler) {
|
||||
conf, _ := common.Load()
|
||||
// Inherit a net.Listener from our parent process or listen anew.
|
||||
l, err := goagain.Listener()
|
||||
if nil != err {
|
||||
|
||||
// Listen on a TCP or a UNIX domain socket (TCP here).
|
||||
if conf.Unix {
|
||||
l, err = net.Listen("unix", conf.ListenTo)
|
||||
} else {
|
||||
l, err = net.Listen("tcp", conf.ListenTo)
|
||||
}
|
||||
if nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU STARTUATO ON ", l.Addr()))
|
||||
|
||||
// Accept connections in a new goroutine.
|
||||
go fasthttp.Serve(l, hn)
|
||||
} else {
|
||||
|
||||
// Resume accepting connections in a new goroutine.
|
||||
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU RESUMINGU ON ", l.Addr()))
|
||||
go fasthttp.Serve(l, hn)
|
||||
|
||||
// Kill the parent, now that the child has started successfully.
|
||||
if err := goagain.Kill(); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Block the main goroutine awaiting signals.
|
||||
if _, err := goagain.Wait(l); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Do whatever's necessary to ensure a graceful exit like waiting for
|
||||
// goroutines to terminate or a channel to become closed.
|
||||
//
|
||||
// In this case, we'll simply stop listening and wait one second.
|
||||
if err := l.Close(); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
34
vendor/zxq.co/ripple/rippleapi/startuato_windows.go
vendored
Normal file
34
vendor/zxq.co/ripple/rippleapi/startuato_windows.go
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// +build windows
|
||||
|
||||
// The Ripple API on Windows is not officially supported and you're probably
|
||||
// gonna swear a lot if you intend to use it on Windows. Caveat emptor.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
func startuato(hn fasthttp.RequestHandler) {
|
||||
conf, _ := common.Load()
|
||||
var (
|
||||
l net.Listener
|
||||
err error
|
||||
)
|
||||
|
||||
// Listen on a TCP or a UNIX domain socket.
|
||||
if conf.Unix {
|
||||
l, err = net.Listen("unix", conf.ListenTo)
|
||||
} else {
|
||||
l, err = net.Listen("tcp", conf.ListenTo)
|
||||
}
|
||||
if nil != err {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
fasthttp.Serve(l, hn)
|
||||
}
|
18
vendor/zxq.co/ripple/schiavolib/LICENSE
vendored
Normal file
18
vendor/zxq.co/ripple/schiavolib/LICENSE
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
Copyright (c) 2016 Morgan Bazalgette
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
3
vendor/zxq.co/ripple/schiavolib/README.md
vendored
Normal file
3
vendor/zxq.co/ripple/schiavolib/README.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# schiavolib
|
||||
|
||||
A lightweight library for Schiavo.
|
14
vendor/zxq.co/ripple/schiavolib/schiavo_test.go
vendored
Normal file
14
vendor/zxq.co/ripple/schiavolib/schiavo_test.go
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
package schiavo
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSend(t *testing.T) {
|
||||
err := Bunker.Send("onii-chan be gentle pls >///< '); DROP TABLE users;-- **hello markdown!** Just testing schiavolib ~")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Example() {
|
||||
Bunker.Send("Hello world!")
|
||||
}
|
61
vendor/zxq.co/ripple/schiavolib/schiavolib.go
vendored
Normal file
61
vendor/zxq.co/ripple/schiavolib/schiavolib.go
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
package schiavo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Channels to which a message can be sent.
|
||||
const (
|
||||
General Channel = "general"
|
||||
Bunker Channel = "bunk"
|
||||
ChatLog Channel = "chatlog"
|
||||
Staff Channel = "staff"
|
||||
CMs Channel = "cm"
|
||||
)
|
||||
|
||||
// Channel is just a channel on the discord to which you can send messages.
|
||||
type Channel string
|
||||
|
||||
// SchiavoURL is the base URL for schiavo. Change to var when not hardcoded
|
||||
var SchiavoURL = ""
|
||||
|
||||
// Prefix is a prefix that will be appended to all Schiavo messages if set.
|
||||
var Prefix = ""
|
||||
|
||||
// ForceDo is a meme
|
||||
var ForceDo bool
|
||||
|
||||
var shouldDo = os.Getenv("GIN_MODE") == "release" || os.Getenv("SCHIAVO_LOG") != ""
|
||||
|
||||
// Send sends a message to a channel.
|
||||
func (c Channel) Send(m string) error {
|
||||
if !shouldDo && !ForceDo {
|
||||
return nil
|
||||
}
|
||||
if SchiavoURL == "" {
|
||||
return nil
|
||||
}
|
||||
if Prefix != "" {
|
||||
m = fmt.Sprintf("**%s** %s", Prefix, m)
|
||||
}
|
||||
urgay := SchiavoURL + "/" + string(c) + "?message=" + url.QueryEscape(m)
|
||||
resp, err := http.Get(urgay)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(body) != "ok" {
|
||||
return errors.New("Schiavo response not ok: " + string(body) + "; status code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
return nil
|
||||
}
|
46
vendor/zxq.co/ripple/schiavolib/schiavosay/schiavosay.go
vendored
Normal file
46
vendor/zxq.co/ripple/schiavolib/schiavosay/schiavosay.go
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.zxq.co/ripple/schiavolib"
|
||||
)
|
||||
|
||||
var (
|
||||
url = "general"
|
||||
messages = make(chan string, 20)
|
||||
)
|
||||
|
||||
func main() {
|
||||
schiavo.ForceDo = true
|
||||
for i := 0; i < 10; i++ {
|
||||
go sender()
|
||||
}
|
||||
fmt.Println("schiavosay")
|
||||
fmt.Print("> ")
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
for sc.Scan() {
|
||||
if strings.Index(sc.Text(), "/switch ") == 0 {
|
||||
url = sc.Text()[len("/switch "):]
|
||||
fmt.Println("=> Switched to", url)
|
||||
fmt.Print("> ")
|
||||
continue
|
||||
}
|
||||
messages <- sc.Text()
|
||||
fmt.Print("> ")
|
||||
}
|
||||
}
|
||||
|
||||
func sender() {
|
||||
for m := range messages {
|
||||
err := schiavo.Channel(url).Send(m)
|
||||
if err != nil {
|
||||
fmt.Println()
|
||||
fmt.Println("=>", err)
|
||||
fmt.Print("> ")
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user