new sonorancad

This commit is contained in:
KingMcDonalds
2025-02-28 22:21:38 -08:00
parent 0927da1996
commit 38efb1b17a
803 changed files with 122517 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+8
View File
@@ -0,0 +1,8 @@
.vscode
*.zip
wk_wars2x/config.lua
sonorancad/stream/*.ydr
sonorancad/stream/data/*.ytyp
tablet/config.lua
sonoran_updatehelper/run.lock
filestore/**/*
+6
View File
@@ -0,0 +1,6 @@
[submodule "sonoran_idcard"]
path = sonoran_idcard
url = https://github.com/Sonoran-Software/id_card_ui.git
[submodule "wk_wars2x"]
path = wk_wars2x
url = https://github.com/Sonoran-Software/wk_wars2x.git
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+20
View File
@@ -0,0 +1,20 @@
# SonoranCAD FiveM Integration
The Sonoran CAD FiveM integration core includes many drag-and-drop integration scripts for your gaming server.
## Installation
Please view the [installation guide](https://info.sonorancad.com/integration-plugins/in-game-integration/fivem-installation) for more details.
## Updating from v2.x.x?
See our documentation [here](https://info.sonorancad.com/integration-plugins/in-game-integration/fivem-installation#update-from-v2.x.x-or-earlier).
## API Resources
Sonoran CAD's API offers direct access to your CAD data, making it very useful to use for integration with various scripts. Detailed API and push event information can be found [here](https://info.sonorancad.com/sonoran-cad/api-integration/api-endpoints/).
## Development Bounties
Get PAID to contribute to the open source project!!!
Sonoran Software offers 💵CASH💵 for contributing to the open source integration framework plugin library. Check out more information [here](https://info.sonorancad.com/sonoran-cad/developer-bounties).
@@ -0,0 +1,17 @@
description "Simple Notification Script using https://notifyjs.com/"
ui_page "html/index.html"
client_script "cl_notify.lua"
export "SetQueueMax"
export "SendNotification"
files {
"html/index.html",
"html/pNotify.js",
"html/noty.js",
"html/noty.css",
"html/themes.css",
"html/sound-example.wav"
}
@@ -0,0 +1,166 @@
--[[
Complete List of Options:
type
layout
theme
text
timeout
progressBar
closeWith
animation = {
open
close
}
sounds = {
volume
conditions
sources
}
docTitle = {
conditions
}
modal
id
force
queue
killer
container
buttons
More details below or visit the creators website http://ned.im/noty/options.html
Layouts:
top
topLeft
topCenter
topRight
center
centerLeft
centerRight
bottom
bottomLeft
bottomCenter
bottomRight
Types:
alert
success
error
warning
info
Themes: -- You can create more themes inside html/themes.css, use the gta theme as a template.
gta
mint
relax
metroui
Animations:
open:
noty_effects_open
gta_effects_open
gta_effects_open_left
gta_effects_fade_in
close:
noty_effects_close
gta_effects_close
gta_effects_close_left
gta_effects_fade_out
closeWith: -- array, You will probably never use this.
click
button
sounds:
volume: 0.0 - 1.0
conditions: -- array
docVisible
docHidden
sources: -- array of sound files
modal:
true
false
force:
true
false
queue: -- default is global, you can make it what ever you want though.
global
killer: -- will close all visible notifications and show only this one
true
false
visit the creators website http://ned.im/noty/options.html for more information
--]]
function SetQueueMax(queue, max)
local tmp = {
queue = tostring(queue),
max = tonumber(max)
}
SendNUIMessage({maxNotifications = tmp})
end
function SendNotification(options)
options.animation = options.animation or {}
options.sounds = options.sounds or {}
options.docTitle = options.docTitle or {}
local options = {
type = options.type or "success",
layout = options.layout or "topRight",
theme = options.theme or "gta",
text = options.text or "Empty Notification",
timeout = options.timeout or 5000,
progressBar = options.progressBar ~= false and true or false,
closeWith = options.closeWith or {},
animation = {
open = options.animation.open or "gta_effects_open",
close = options.animation.close or "gta_effects_close"
},
sounds = {
volume = options.sounds.volume or 1,
conditions = options.sounds.conditions or {},
sources = options.sounds.sources or {}
},
docTitle = {
conditions = options.docTitle.conditions or {}
},
modal = options.modal or false,
id = options.id or false,
force = options.force or false,
queue = options.queue or "global",
killer = options.killer or false,
container = options.container or false,
buttons = options.button or false
}
SendNUIMessage({options = options})
end
RegisterNetEvent("pNotify:SendNotification")
AddEventHandler("pNotify:SendNotification", function(options)
SendNotification(options)
end)
RegisterNetEvent("pNotify:SetQueueMax")
AddEventHandler("pNotify:SetQueueMax", function(queue, max)
SetQueueMax(queue, max)
end)
--[[RegisterNetEvent("chatMessage")
AddEventHandler("chatMessage", function(author, color, text)
TriggerEvent("pNotify:SendNotification", {text = "<span style='font-weight: 900'>" .. text .. "</span>",
layout = "centerLeft",
timeout = 2000,
progressBar = false,
type = "error",
animation = {
open = "gta_effects_fade_in",
close = "gta_effects_fade_out"
}})
end)]]
@@ -0,0 +1,13 @@
<html>
<head>
<meta charset="utf-8" />
<title>pNotify</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script src="pNotify.js" type="text/javascript"></script>
<link href="noty.css" rel="stylesheet"></script>
<link href="themes.css" rel="stylesheet"></script>
<script src="noty.js" type="text/javascript"></script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,650 @@
.noty_layout_mixin, #noty_layout__top, #noty_layout__topLeft, #noty_layout__topCenter, #noty_layout__topRight, #noty_layout__bottom, #noty_layout__bottomLeft, #noty_layout__bottomCenter, #noty_layout__bottomRight, #noty_layout__center, #noty_layout__centerLeft, #noty_layout__centerRight {
position: fixed;
margin: 0;
padding: 0;
z-index: 9999999;
-webkit-transform: translateZ(0) scale(1, 1);
transform: translateZ(0) scale(1, 1);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-font-smoothing: subpixel-antialiased;
filter: blur(0);
-webkit-filter: blur(0);
max-width: 90%; }
#noty_layout__top {
top: 0;
left: 5%;
width: 90%; }
#noty_layout__topLeft {
top: 20px;
left: 20px;
width: 325px; }
#noty_layout__topCenter {
top: 5%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__topRight {
top: 20px;
right: 20px;
width: 325px; }
#noty_layout__bottom {
bottom: 0;
left: 5%;
width: 90%; }
#noty_layout__bottomLeft {
bottom: 20px;
left: 20px;
width: 325px; }
#noty_layout__bottomCenter {
bottom: 5%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__bottomRight {
bottom: 20px;
right: 20px;
width: 325px; }
#noty_layout__center {
top: 50%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px), -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px), calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__centerLeft {
top: 50%;
left: 20px;
width: 325px;
-webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__centerRight {
top: 50%;
right: 20px;
width: 325px;
-webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
.noty_progressbar {
display: none; }
.noty_has_timeout .noty_progressbar {
display: block;
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 100%;
background-color: #646464;
opacity: 0.2;
filter: alpha(opacity=10); }
.noty_bar {
-webkit-backface-visibility: hidden;
-webkit-transform: translate(0, 0) translateZ(0) scale(1, 1);
-ms-transform: translate(0, 0) scale(1, 1);
transform: translate(0, 0) scale(1, 1);
-webkit-font-smoothing: subpixel-antialiased;
overflow: hidden; }
.noty_effects_open {
opacity: 0;
-webkit-transform: translate(50%);
-ms-transform: translate(50%);
transform: translate(50%);
-webkit-animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
.noty_effects_close {
-webkit-animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
.noty_fix_effects_height {
-webkit-animation: noty_anim_height 75ms ease-out;
animation: noty_anim_height 75ms ease-out; }
.noty_close_with_click {
cursor: pointer; }
.noty_close_button {
position: absolute;
top: 2px;
right: 2px;
font-weight: bold;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 2px;
cursor: pointer;
-webkit-transition: all .2s ease-out;
transition: all .2s ease-out; }
.noty_close_button:hover {
background-color: rgba(0, 0, 0, 0.1); }
.noty_modal {
position: fixed;
width: 100%;
height: 100%;
background-color: #000;
z-index: 10000;
opacity: .3;
left: 0;
top: 0; }
.noty_modal.noty_modal_open {
opacity: 0;
-webkit-animation: noty_modal_in .3s ease-out;
animation: noty_modal_in .3s ease-out; }
.noty_modal.noty_modal_close {
-webkit-animation: noty_modal_out .3s ease-out;
animation: noty_modal_out .3s ease-out;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
@-webkit-keyframes noty_modal_in {
100% {
opacity: .3; } }
@keyframes noty_modal_in {
100% {
opacity: .3; } }
@-webkit-keyframes noty_modal_out {
100% {
opacity: 0; } }
@keyframes noty_modal_out {
100% {
opacity: 0; } }
@keyframes noty_modal_out {
100% {
opacity: 0; } }
@-webkit-keyframes noty_anim_in {
100% {
-webkit-transform: translate(0);
transform: translate(0);
opacity: 1; } }
@keyframes noty_anim_in {
100% {
-webkit-transform: translate(0);
transform: translate(0);
opacity: 1; } }
@-webkit-keyframes noty_anim_out {
100% {
-webkit-transform: translate(50%);
transform: translate(50%);
opacity: 0; } }
@keyframes noty_anim_out {
100% {
-webkit-transform: translate(50%);
transform: translate(50%);
opacity: 0; } }
@-webkit-keyframes noty_anim_height {
100% {
height: 0; } }
@keyframes noty_anim_height {
100% {
height: 0; } }
.noty_theme__relax.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative; }
.noty_theme__relax.noty_bar .noty_body {
padding: 10px; }
.noty_theme__relax.noty_bar .noty_buttons {
border-top: 1px solid #e7e7e7;
padding: 5px 10px; }
.noty_theme__relax.noty_type__alert,
.noty_theme__relax.noty_type__notification {
background-color: #fff;
border: 1px solid #dedede;
color: #444; }
.noty_theme__relax.noty_type__warning {
background-color: #FFEAA8;
border: 1px solid #FFC237;
color: #826200; }
.noty_theme__relax.noty_type__warning .noty_buttons {
border-color: #dfaa30; }
.noty_theme__relax.noty_type__error {
background-color: #FF8181;
border: 1px solid #e25353;
color: #FFF; }
.noty_theme__relax.noty_type__error .noty_buttons {
border-color: darkred; }
.noty_theme__relax.noty_type__info,
.noty_theme__relax.noty_type__information {
background-color: #78C5E7;
border: 1px solid #3badd6;
color: #FFF; }
.noty_theme__relax.noty_type__info .noty_buttons,
.noty_theme__relax.noty_type__information .noty_buttons {
border-color: #0B90C4; }
.noty_theme__relax.noty_type__success {
background-color: #BCF5BC;
border: 1px solid #7cdd77;
color: darkgreen; }
.noty_theme__relax.noty_type__success .noty_buttons {
border-color: #50C24E; }
.noty_theme__metroui.noty_bar {
margin: 4px 0;
overflow: hidden;
position: relative;
box-shadow: rgba(0, 0, 0, 0.298039) 0 0 5px 0; }
.noty_theme__metroui.noty_bar .noty_progressbar {
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 100%;
background-color: #000;
opacity: 0.2;
filter: alpha(opacity=20); }
.noty_theme__metroui.noty_bar .noty_body {
padding: 1.25em;
font-size: 14px; }
.noty_theme__metroui.noty_bar .noty_buttons {
padding: 0 10px .5em 10px; }
.noty_theme__metroui.noty_type__alert,
.noty_theme__metroui.noty_type__notification {
background-color: #fff;
color: #1d1d1d; }
.noty_theme__metroui.noty_type__warning {
background-color: #FA6800;
color: #fff; }
.noty_theme__metroui.noty_type__error {
background-color: #CE352C;
color: #FFF; }
.noty_theme__metroui.noty_type__info,
.noty_theme__metroui.noty_type__information {
background-color: #1BA1E2;
color: #FFF; }
.noty_theme__metroui.noty_type__success {
background-color: #60A917;
color: #fff; }
.noty_theme__mint.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative; }
.noty_theme__mint.noty_bar .noty_body {
padding: 10px;
font-size: 14px; }
.noty_theme__mint.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__mint.noty_type__alert,
.noty_theme__mint.noty_type__notification {
background-color: #fff;
border-bottom: 1px solid #D1D1D1;
color: #2F2F2F; }
.noty_theme__mint.noty_type__warning {
background-color: #FFAE42;
border-bottom: 1px solid #E89F3C;
color: #fff; }
.noty_theme__mint.noty_type__error {
background-color: #DE636F;
border-bottom: 1px solid #CA5A65;
color: #fff; }
.noty_theme__mint.noty_type__info,
.noty_theme__mint.noty_type__information {
background-color: #7F7EFF;
border-bottom: 1px solid #7473E8;
color: #fff; }
.noty_theme__mint.noty_type__success {
background-color: #AFC765;
border-bottom: 1px solid #A0B55C;
color: #fff; }
.noty_theme__sunset.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative; }
.noty_theme__sunset.noty_bar .noty_body {
padding: 10px;
font-size: 14px;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); }
.noty_theme__sunset.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__sunset.noty_type__alert,
.noty_theme__sunset.noty_type__notification {
background-color: #073B4C;
color: #fff; }
.noty_theme__sunset.noty_type__alert .noty_progressbar,
.noty_theme__sunset.noty_type__notification .noty_progressbar {
background-color: #fff; }
.noty_theme__sunset.noty_type__warning {
background-color: #FFD166;
color: #fff; }
.noty_theme__sunset.noty_type__error {
background-color: #EF476F;
color: #fff; }
.noty_theme__sunset.noty_type__error .noty_progressbar {
opacity: .4; }
.noty_theme__sunset.noty_type__info,
.noty_theme__sunset.noty_type__information {
background-color: #118AB2;
color: #fff; }
.noty_theme__sunset.noty_type__info .noty_progressbar,
.noty_theme__sunset.noty_type__information .noty_progressbar {
opacity: .6; }
.noty_theme__sunset.noty_type__success {
background-color: #06D6A0;
color: #fff; }
.noty_theme__bootstrap-v3.noty_bar {
margin: 4px 0;
overflow: hidden;
position: relative;
border: 1px solid transparent;
border-radius: 4px; }
.noty_theme__bootstrap-v3.noty_bar .noty_body {
padding: 15px; }
.noty_theme__bootstrap-v3.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__bootstrap-v3.noty_bar .noty_close_button {
font-size: 21px;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
filter: alpha(opacity=20);
opacity: .2;
background: transparent; }
.noty_theme__bootstrap-v3.noty_bar .noty_close_button:hover {
background: transparent;
text-decoration: none;
cursor: pointer;
filter: alpha(opacity=50);
opacity: .5; }
.noty_theme__bootstrap-v3.noty_type__alert,
.noty_theme__bootstrap-v3.noty_type__notification {
background-color: #fff;
color: inherit; }
.noty_theme__bootstrap-v3.noty_type__warning {
background-color: #fcf8e3;
color: #8a6d3b;
border-color: #faebcc; }
.noty_theme__bootstrap-v3.noty_type__error {
background-color: #f2dede;
color: #a94442;
border-color: #ebccd1; }
.noty_theme__bootstrap-v3.noty_type__info,
.noty_theme__bootstrap-v3.noty_type__information {
background-color: #d9edf7;
color: #31708f;
border-color: #bce8f1; }
.noty_theme__bootstrap-v3.noty_type__success {
background-color: #dff0d8;
color: #3c763d;
border-color: #d6e9c6; }
.noty_theme__bootstrap-v4.noty_bar {
margin: 4px 0;
overflow: hidden;
position: relative;
border: 1px solid transparent;
border-radius: .25rem; }
.noty_theme__bootstrap-v4.noty_bar .noty_body {
padding: .75rem 1.25rem; }
.noty_theme__bootstrap-v4.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__bootstrap-v4.noty_bar .noty_close_button {
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
filter: alpha(opacity=20);
opacity: .5;
background: transparent; }
.noty_theme__bootstrap-v4.noty_bar .noty_close_button:hover {
background: transparent;
text-decoration: none;
cursor: pointer;
filter: alpha(opacity=50);
opacity: .75; }
.noty_theme__bootstrap-v4.noty_type__alert,
.noty_theme__bootstrap-v4.noty_type__notification {
background-color: #fff;
color: inherit; }
.noty_theme__bootstrap-v4.noty_type__warning {
background-color: #fcf8e3;
color: #8a6d3b;
border-color: #faebcc; }
.noty_theme__bootstrap-v4.noty_type__error {
background-color: #f2dede;
color: #a94442;
border-color: #ebccd1; }
.noty_theme__bootstrap-v4.noty_type__info,
.noty_theme__bootstrap-v4.noty_type__information {
background-color: #d9edf7;
color: #31708f;
border-color: #bce8f1; }
.noty_theme__bootstrap-v4.noty_type__success {
background-color: #dff0d8;
color: #3c763d;
border-color: #d6e9c6; }
.noty_theme__semanticui.noty_bar {
margin: 4px 0;
overflow: hidden;
position: relative;
border: 1px solid transparent;
font-size: 1em;
border-radius: .28571429rem;
box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 0 0 0 transparent; }
.noty_theme__semanticui.noty_bar .noty_body {
padding: 1em 1.5em;
line-height: 1.4285em; }
.noty_theme__semanticui.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__semanticui.noty_type__alert,
.noty_theme__semanticui.noty_type__notification {
background-color: #f8f8f9;
color: rgba(0, 0, 0, 0.87); }
.noty_theme__semanticui.noty_type__warning {
background-color: #fffaf3;
color: #573a08;
box-shadow: 0 0 0 1px #c9ba9b inset, 0 0 0 0 transparent; }
.noty_theme__semanticui.noty_type__error {
background-color: #fff6f6;
color: #9f3a38;
box-shadow: 0 0 0 1px #e0b4b4 inset, 0 0 0 0 transparent; }
.noty_theme__semanticui.noty_type__info,
.noty_theme__semanticui.noty_type__information {
background-color: #f8ffff;
color: #276f86;
box-shadow: 0 0 0 1px #a9d5de inset, 0 0 0 0 transparent; }
.noty_theme__semanticui.noty_type__success {
background-color: #fcfff5;
color: #2c662d;
box-shadow: 0 0 0 1px #a3c293 inset, 0 0 0 0 transparent; }
.noty_theme__nest.noty_bar {
margin: 0 0 15px 0;
overflow: hidden;
border-radius: 2px;
position: relative;
box-shadow: rgba(0, 0, 0, 0.098039) 5px 4px 10px 0; }
.noty_theme__nest.noty_bar .noty_body {
padding: 10px;
font-size: 14px;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); }
.noty_theme__nest.noty_bar .noty_buttons {
padding: 10px; }
.noty_layout .noty_theme__nest.noty_bar {
z-index: 5; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(2) {
position: absolute;
top: 0;
margin-top: 4px;
margin-right: -4px;
margin-left: 4px;
z-index: 4;
width: 100%; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(3) {
position: absolute;
top: 0;
margin-top: 8px;
margin-right: -8px;
margin-left: 8px;
z-index: 3;
width: 100%; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(4) {
position: absolute;
top: 0;
margin-top: 12px;
margin-right: -12px;
margin-left: 12px;
z-index: 2;
width: 100%; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(5) {
position: absolute;
top: 0;
margin-top: 16px;
margin-right: -16px;
margin-left: 16px;
z-index: 1;
width: 100%; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(n+6) {
position: absolute;
top: 0;
margin-top: 20px;
margin-right: -20px;
margin-left: 20px;
z-index: -1;
width: 100%; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(2),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(2) {
margin-top: 4px;
margin-left: -4px;
margin-right: 4px; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(3),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(3) {
margin-top: 8px;
margin-left: -8px;
margin-right: 8px; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(4),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(4) {
margin-top: 12px;
margin-left: -12px;
margin-right: 12px; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(5),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(5) {
margin-top: 16px;
margin-left: -16px;
margin-right: 16px; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(n+6),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(n+6) {
margin-top: 20px;
margin-left: -20px;
margin-right: 20px; }
.noty_theme__nest.noty_type__alert,
.noty_theme__nest.noty_type__notification {
background-color: #073B4C;
color: #fff; }
.noty_theme__nest.noty_type__alert .noty_progressbar,
.noty_theme__nest.noty_type__notification .noty_progressbar {
background-color: #fff; }
.noty_theme__nest.noty_type__warning {
background-color: #FFD166;
color: #fff; }
.noty_theme__nest.noty_type__error {
background-color: #EF476F;
color: #fff; }
.noty_theme__nest.noty_type__error .noty_progressbar {
opacity: .4; }
.noty_theme__nest.noty_type__info,
.noty_theme__nest.noty_type__information {
background-color: #118AB2;
color: #fff; }
.noty_theme__nest.noty_type__info .noty_progressbar,
.noty_theme__nest.noty_type__information .noty_progressbar {
opacity: .6; }
.noty_theme__nest.noty_type__success {
background-color: #06D6A0;
color: #fff; }
/*# sourceMappingURL=noty.css.map*/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
Copyright (c) 2012 Nedim Arabacı
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.
@@ -0,0 +1,11 @@
$(function(){
window.addEventListener("message", function(event){
if(event.data.options){
var options = event.data.options;
new Noty(options).show();
}else{
var maxNotifications = event.data.maxNotifications;
Noty.setMaxVisible(maxNotifications.max, maxNotifications.queue);
};
});
});
@@ -0,0 +1,131 @@
.noty_theme__gta.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 1px;
font-family: sans-serif;
position: relative;
height: auto;
word-wrap: break-word;
}
.noty_theme__gta.noty_bar .noty_body {
padding: 8px;
font-size: 14px;
}
.noty_theme__gta.noty_bar .noty_buttons {
padding: 0px;
}
.noty_theme__gta.noty_bar .noty_progressbar {
position: absolute;
left: 0;
bottom: 0;
height: 1px;
width: 100%;
background-color: white;
opacity: 0.8;
filter: alpha(opacity=80);
}
.noty_theme__gta.noty_type__alert,
.noty_theme__gta.noty_type__notification {
background-color: rgb(40, 40, 40);
border-top: 2px solid #D1D1D1;
color: white;
}
.noty_theme__gta.noty_type__warning {
background-color: rgb(40, 40, 40);
border-top: 2px solid #E89F3C;
color: white;
}
.noty_theme__gta.noty_type__error {
background-color: rgb(40, 40, 40);
border-top: 2px solid #CA5A65;
color: #fff;
}
.noty_theme__gta.noty_type__info,
.noty_theme__gta.noty_type__information {
background-color: rgb(40, 40, 40);
border-top: 2px solid #7473E8;
color: #fff;
}
.noty_theme__gta.noty_type__success {
background-color: rgb(40, 40, 40);
border-top: 2px solid #A0B55C;
color: #fff;
}
.gta_effects_open {
opacity: 0;
-webkit-transform: translate(50%);
-ms-transform: translate(50%);
transform: translate(50%);
-webkit-animation: noty_anim_in 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
animation: noty_anim_in 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
.gta_effects_close {
-webkit-animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
@-webkit-keyframes noty_anim_out_left {
100% {
-webkit-transform: translate(-50%);
transform: translate(-50%);
opacity: 0; } }
@keyframes noty_anim_out_left {
100% {
-webkit-transform: translate(-50%);
transform: translate(-50%);
opacity: 0; } }
.gta_effects_open_left {
opacity: 0;
-webkit-transform: translate(-50%);
-ms-transform: translate(-50%);
transform: translate(-50%);
-webkit-animation: noty_anim_in 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
animation: noty_anim_in 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
.gta_effects_close_left {
-webkit-animation: noty_anim_out_left 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: noty_anim_out_left 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-webkit-animation-fill-mode: backwards;
animation-fill-mode: backwards;
}
@-webkit-keyframes noty_anim_fade_in {
100% { opacity: 1; } }
@keyframes noty_anim_fade_in {
100% { opacity: 1; } }
@-webkit-keyframes noty_anim_fade_out {
100% { opacity: 0; } }
@keyframes noty_anim_fade_out {
100% { opacity: 0; } }
.gta_effects_fade_in {
opacity: 0;
animation: noty_anim_fade_in 0.5s;
}
.gta_effects_fade_out {
opacity: 1;
animation: noty_anim_fade_out 0.5s;
}
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
@@ -0,0 +1,13 @@
local time_before_close = 1000000
RegisterNetEvent("SonoranCAD::civint:DisplayID")
AddEventHandler("SonoranCAD::civint:DisplayID", function(imgUrl, target, fullName, dob)
SendNUIMessage({
type = "SonoranCAD::civint:id",
show = true,
img = imgUrl,
fullName = fullName,
dob = dob,
playerID = target
})
end)
@@ -0,0 +1,17 @@
fx_version 'cerulean'
games { 'gta5' }
description 'UI Resource for SonoranCAD Civilian Integration'
version '1.0.0'
client_script 'cl_main.lua'
files {
'ui/index.html',
'ui/img/*.png',
'ui/img/*.jpeg',
'ui/styles.css',
'ui/script.js'
}
ui_page 'ui/index.html'
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Roboto&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Pinyon+Script&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="styles.css" />
<title>Sonoran ID Card</title>
</head>
<body>
<script src="script.js"></script>
</body>
</html>
@@ -0,0 +1,55 @@
window.addEventListener("message", (e) => {
if (e.data.type === "SonoranCAD::civint:id") {
if (e.data.show) {
const fullName = e.data.fullName;
const nameArray = fullName.split(" ");
const firstName = nameArray[0];
const lastName = nameArray[1];
const playerID = e.data.playerID;
const dob = e.data.dob;
const html = `
<div id="id-card">
<div id="top-info">
<h2 id="state-name">SAN ANDREAS</h2>
<h2 id="top-id">IDENTIFICATION CARD</h2>
</div>
<div id="info-wrapper">
<div id="id-photo">
<img
src="${e.data.img}"
alt="PHOTO"
id="id-pic"
/>
<h2 id="signature">${fullName}</h2>
</div>
<div id="personal-info">
<h2>
<span class="info-text">LN</span>
<span id="last-name">${lastName}</span>
</h2>
<h2>
<span class="info-text">FN</span>
<span id="first-name">${firstName}</span>
</h2>
<h2>
<span class="info-text">DOB</span
><span class="number" id="dob"> ${dob}</span>
</h2>
<h2>
<span class="info-text">PLAYER ID</span>
<span class="number" id="player-id"> ${playerID}</span>
</h2>
</div>
</div>
</div>
`;
document.querySelector("body").innerHTML = html;
setTimeout(() => {
document.querySelector("body").innerHTML = "";
}, 6000);
}
}
});
@@ -0,0 +1,81 @@
* {
font-family: "Roboto", sans-serif;
}
html,
body {
margin: 0;
padding: 0;
height: 100vh;
}
body {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end;
}
#id-card {
background-image: url("img/id_background.jpeg");
border-radius: 23px;
margin: 5vh;
width: 50vh;
height: 30vh;
}
#top-info {
display: flex;
justify-content: space-evenly;
color: #223663;
border-bottom: solid 1px #1c2c52;
margin: 10px 0;
}
#state-name,
#top-id {
margin: 0;
padding: 0;
}
#info-wrapper {
display: flex;
padding-left: 20px;
}
#id-photo {
height: 200px;
width: 200px;
margin-right: 10px;
}
#id-photo img {
min-width: 200px;
min-height: 200px;
max-height: 100%;
max-width: 100%;
}
#signature {
text-align: center;
font-family: "Pinyon Script", cursive;
}
#personal-info {
display: flex;
flex-direction: column;
justify-content: space;
}
#personal-info h2 {
padding: 0;
margin: 7px 5px;
}
#personal-info .info-text {
color: #234eb1;
}
#personal-info .number {
color: #df1313;
}
@@ -0,0 +1,4 @@
fx_version 'bodacious'
games {'gta5'}
server_only 'yes'
server_script 's.lua'
@@ -0,0 +1,35 @@
ManagedResources = { "wk_wars2x", "tablet", "sonorancad"}
CreateThread(function()
file = io.open(GetResourcePath(GetCurrentResourceName()).."/run.lock", "a+")
io.input(file)
line = io.read()
file:close()
if line == "core" or line == "plugin" then
ExecuteCommand("refresh")
Wait(1000)
if line == "core" then
for k, v in pairs(ManagedResources) do
if GetResourceState(v) ~= "started" then
print(("Not restarting resource %s as it is not started. This may be fine. State: %s"):format(v, GetResourceState(v)))
else
ExecuteCommand("restart "..v)
Wait(1000)
end
end
elseif line == "plugin" then
print("Restarting sonorancad resource for plugin updates...")
if GetResourceState("sonorancad") ~= "started" then
print(("Not restarting resource %s as it is not in the started state to avoid server crashing. State: %s"):format("sonorancad", GetResourceState("sonorancad")))
print("If you are seeing this message, you have started sonoran_updatehelper in your configuration which is incorrect. Please do not start sonoran_updatehelper manually.")
return
else
ExecuteCommand("restart sonorancad")
end
end
else
os.remove(GetResourcePath(GetCurrentResourceName()).."/run.lock")
print("sonoran_updatehelper is for internal use and should not be started as a resource.")
end
os.remove(GetResourcePath(GetCurrentResourceName()).."/run.lock")
end)
@@ -0,0 +1,8 @@
config.json
update.zip
unzip/
.vscode
.git
stream/*
!stream/.gitkeep
npm-shrinkwrap.json
@@ -0,0 +1,39 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
pluginName = "callcommands", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
configVersion = "2.1",
-- put your configuration options below
callTypes = {
{
command = "911",
isEmergency = true,
suggestionText = "Sends a emergency call to your SonoranCAD",
descriptionPrefix = ""
}, {
command = "311",
isEmergency = true,
suggestionText = "Sends a non-emergency call to your SonoranCAD",
descriptionPrefix = "(311)"
}, {
command = "511",
isEmergency = true,
suggestionText = "Sends a call for a towing service.",
descriptionPrefix = "(511)"
}
},
enablePanic = true,
-- adds an emergency call when panic button is pressed
addPanicCall = true,
usePositionForMetadata = false
}
if config.enabled then Config.RegisterPluginConfig(config.pluginName, config) end
@@ -0,0 +1,32 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
configVersion = "1.1",
pluginName = "civintegration", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
-- time to cache characters in seconds
cacheTime = 3600 -- one hour
-- allow civilians to use /setid and set a custom ID (for characters not registered in the CAD)
,allowCustomIds = true
-- allow players to use /refreshid which causes the next /showid to re-fetch from the CAD. Useful if the player swaps characters.
,allowPurge = true
-- if false, disables the built-in commands of this plugin so it can be used in custom code instead.
,enableCommands = true
-- if true, you must have the sonoran_idcard resource started in your server in order for it to work
,enableIDCardUI = true
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,177 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
configVersion = "3.0",
pluginName = "dispatchnotify", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
requiresPlugins = {
{name = "locations", critical = true},
{name = "callcommands", critical = true},
{name = "postals", critical = true}
}, -- required plugins for this plugin to work, separated by commas
--[[
Enable incoming 911 call notifications
]]
enableUnitNotify = true,
--[[
Specifies what emergency calls are displayed as. Some countries use different numbers (like 999)
]]
emergencyCallType = "911",
--[[
Specifies non-emergency call types. If unused, set to blank ("")
]]
civilCallType = "311",
--[[
Some communities use 511 for tow calls. Specify below, or set blank ("") to disable
]]
dotCallType = "511",
--[[
Command to respond to calls with
]]
respondCommandName = "rcall",
--[[
Enable call responding (self-dispatching)
If disabled, running commandName will return an error to the unit
]]
enableUnitResponse = true,
--[[
If a dispatcher is detected to be online, automatically disable the response command.
]]
dispatchDisablesSelfResponse = false,
--[[
Enable "units are on the way" notifications
]]
enableCallerNotify = true,
--[[
notifyMethod: how should the caller be notified?
none: disable notification
chat: Sends a message in chat
pnotify: Uses pNotify to show a notification
custom: Use the custom event instead (see docs)
]]
callerNotifyMethod = "chat",
--[[
notifyMessage: Message template to use when sending to the player
You can use the following replacements:
{officer} - officer name
]]
notifyMessage = "Officer {officer} is responding to your call!",
--[[
unitNotifyMethod: how should units be notified?
none: disable notification
chat: Sends a message in chat
pnotify: Uses pNotify to show a notification
custom: Use the custom event instead (see docs)
]]
unitNotifyMethod = "chat",
--[[
incomingCallMessage: how should officers be notified of a new 911 call?
Parameters:
{location} - location of call (street + postal)
{description} - description as given by civilian
{caller} - caller's name
{callId} - ID of the call so LEO can respond with /r911 <id>
{command} - The command to use
Note: pNotify uses HTML (commented below), chat uses special codes.
]]
-- incomingCallMessage = "<b>Incoming Call!</b><br/>Location: {location}<br/>Description: {description}<br/>Use command /r911 <b>{callId}</b> to respond!",
incomingCallMessage = "Incoming call from ^*{caller}^r! Location: ^3{location}^0 Description: ^3{description}^0 - Use /{command} ^*{callId}^r to respond!",
--[[
unitDutyMethod: How to detect if units are online?
incad: units must be logged into the CAD
permissions: units must have the "sonorancad.dispatchnotify" ACE permission (see docs)
esxjob: requires esxsupport plugin, use jobs instead for on duty detection
custom: Use custom function (defined below as unitDutyCustom)
]]
unitDutyMethod = "incad",
--[[
esxJobsAllowed: What jobs should count as being on duty?
]]
esxJobsAllowed = {["police"] = true, ["ambulance"] = true, ["fire"] = true},
--[[
waypointType: Type of waypoint to use when officer is attached
postal: set gps to caller's postal (less accurate, more realistic) - REQUIRES CONFIGURED POSTAL PLUGIN
exact: set gps to caller's position (less realistic)
none: disable waypointing
]]
waypointType = "postal",
--[[
waypointFallbackEnabled: Fall back to postal if exact coordinates cannot be found (for self-generated calls)
]]
waypointFallbackEnabled = true,
--[[
callTitle: Customize the title of a call made
]]
callTitle = "OFFICER RESPONSE",
--[[
sendNotesToUnits: Whether the script will fire events related to call notes.
]]
sendNotesToUnits = true,
--[[
noteNotifyMethod:
chat: send new notes via chat
pnotify: send new notes via a pNotify popup (requires pNotify resource)
custom: fire a client-side event that your script will consume (each active unit gets SonoranCAD::dispatchnotify:NewCallNote with an object containing callId and note)
]]
noteNotifyMethod = "chat",
--[[
noteMessage: Message to send to officers when a note is added, using the placeholders:
{callid} - the call ID
{note} - the note added
]]
noteMessage = "New note added for call ^*{callid}^r: {note}",
--[[
enableAddNote: Whether or not to enable the addnote command, allowing units attached to calls to add notes to their call.
]]
enableAddNote = true,
--[[
addNoteCommand: The command to create for adding notes.
]]
addNoteCommand = "addnote",
--[[
enableAddPlate: Enable the addplate command, allowing units to send locked plate data as a note to their current call. Will require the wraithv2 plugin to work.
]]
enableAddPlate = true,
--[[
addPlateCommand: The command to create for sending plate data
]]
addPlateCommand = "addplate",
--[[
onSceneHandler: Enables automatically disabling waypointing when marked on scene
]]
onSceneHandler = true,
--[[
onSceneIndex: Usually don't have to touch this. Controls which button is "on scene"
]]
onSceneIndex = 4
}
if config.enabled then
function unitDutyCustom(player) return false end
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,19 @@
--[[
SonoranCAD FivePD Plugin
Plugin Configuration
]]
local config = {
enabled = false,
configVersion = "1.0",
pluginName = "fivepd", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
-- put your configuration options below
origin = 1, -- Call Origin: 0=CALLER/1=RADIO DISPATCH/2=OBSERVED/3=WALK_UP
status = 1, -- Call Status: 0=PENDING/1=ACTIVE/2=CLOSED
code = "" -- Not Used Yet: TODO: Map Callout Id to Code
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,52 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
configVersion = "1.1",
pluginName = "forcereg", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
requiresPlugins = {}, -- required plugins for this plugin to work, separated by commas
--[[
Below defines the "captive" option to use:
Nag: Simply nags the user with a big notification across the top of their screen.
Freeze: Freezes the player at their spawn point with a big notification.
Whitelist: Prevents connection to the server entirely via deferrals (WARNING: NOT COMPATIBLE WITH ADAPTIVE CARD RESOURCES)
]]
captiveOption = "Nag",
-- If using Nag, should the text be centered in the users screen or at the top? ('Center' or 'Top')
nagDrawTextLocation = "Top",
-- What message to show with the above options? Nag, Freeze, and Captive can use colors.
captiveMessage = "You must ~r~register~s~ with our CAD before playing! Visit ~r~http://yourwebsite.here~s~ to do so.",
-- What message to show the /verifycad command? This displays under the notice.
verifyMessage = "Type ~r~/verifycad~s~ in chat when finished.",
-- What does the user do once they log in?
instructionalMessage = "Head over to settings once logged in, and enter the ~g~API ID~w~ given below in the API ID field.",
-- Would you like to only show this message to players who are whitelisted?
whitelist = {
enabled = false,
mode = "qb-core", -- qb-core, esx, ace
aces = { -- ace permissions will see the message
"forcereg.whitelist"
},
jobs = { -- QB or ESX jobs will see the message
"police"
}
}
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,46 @@
--[[
Sonoran Plugins
frameworksupport Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = false,
configVersion = "1.2",
pluginName = "frameworksupport", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
requiresPlugins = {}, -- required plugins for this plugin to work, separated by commas
-- Newer ESX versions use license instead of steam for identity, specify the other below if different
identityType = "license",
-- Some ESX versions don't use the prefix (such as license:abcdef), set to false to disable the prefix
usePrefix = true,
-- If you are using QBCore set this to true
usingQBCore = true,
-- If using qb-management for LEO set this to true
usingQBManagement = false,
-- Setup the qb-management account names dependent on department issuing fine
qbManagementAccountNames = {
['LSPD'] = 'police',
['SAHP'] = 'sahp'
-- ['DEPARTMENT ABBREVIATION IN CAD ADMIN>CUSTOMIZATION>DEPARTMENTS'] = 'qb-management_account_name'
},
qbNotifyFinedPlayer = true,
-- Placeholders $AMOUNT and $OFFICER_NAME where $AMOUNT is the fine total and $OFFICER_NAME is the Unit Name of the officer issuing the fine
qbFineMessage = "You have been fined $$AMOUNT by $OFFICER_NAME",
-- Fine payment system
issueFines = true, -- Use the fine system
fineNotify = false, -- Send a message in chat when someone is fined.
fineableForms = {"Arrest Report", "General Citation"}, -- List of form names that should issue fines (Don't Include Warrants or Bolos)
-- ESX Legacy Support (Created for and tested using ESX v1.1.0 esx_identity v1.0.2)
legacyESX = false -- Set to true if default settings do not get character name properly (older esx_identity/ESX legacy versions)
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,21 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
pluginName = "kick", -- name your plugin here
pluginAuthor = "TaylorMade#4860", -- author
configVersion = "1.0", -- version of the plugin
requiresPlugins = {}, -- required plugins for this plugin to work, separated by commas
-- put your configuration options below
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,21 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
pluginName = "locations", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
configVersion = "1.1", -- version of your plugin
requiresPlugins = {},
-- put your configuration options below
checkTime = 5000, -- how frequently to send locations to the server
prefixPostal = true -- prefix postal code on locations sent, requires postal plugin
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,21 @@
--[[
Sonoran Plugins
Plugin Configuration
]]
local config = {
enabled = true,
pluginName = "lookups", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
configVersion = "1.0",
requiresPlugins = {}, -- required plugins for this plugin to work, separated by commas
-- put your configuration options below
maxCacheTime = 120, -- max time to cache a plate hit, in seconds
stalePurgeTimer = 600, -- delay between garbage collection, default 10 minutes
autoLookupEnabled = true
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,27 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
pluginName = "postals", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
configVersion = "1.3.0",
requiresPlugins = {{name = "locations", critical = true}},
-- put your configuration options below
sendTimer = 950, -- how often to send postal to client
shouldSendPostalData = true, -- toggles this plugin on/off
nearestPostalResourceName = "nearest-postal", -- if using nearestpostal, specify the name of the resource here if you changed it
-- optionally use an event fired by another resource, set mode to "event" and add the name of the event below, set mode to "file" if you are using a custom postal file
mode = "resource",
nearestPostalEvent = "",
-- if not using nearest-postal, place a json file containing the postals in the plugin's folder and specify a name below
customPostalCodesFile = ""
}
if config.enabled then Config.RegisterPluginConfig(config.pluginName, config) end
@@ -0,0 +1,23 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
configVersion = "1.0",
pluginName = "sonrad", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
requiresPlugins = {},
-- put your configuration options below
-- Should radio panics generate CAD calls?
addPanicCall = true
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,29 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
pluginName = "trafficstop", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
requiresPlugins = {
{name = "locations", critical = true},
{name = "callcommands", critical = true},
{name = "postals", critical = false}
}, -- required plugins for this plugin to work, separated by commas
configVersion = "1.2.0",
-- put your configuration options below
origin = 2, -- 0 = CALLER / 1 = RADIO DISPATCH / 2 = OBSERVED / 3 = WALK_UP
status = 1, -- 0 = PENDING / 1 = ACTIVE / 2 = CLOSED
priority = 1, -- 1, 2, or 3
title = "Traffic Stop", -- This is the title of the call by default it is sent as "Traffic Stop"
code = "10-11 - Traffic Stop", -- Change this to reflect your communities 10 Code for a Traffic Stop
trafficCommand = "ts", -- command to trigger the traffic stop
usePermissions = true -- if true, user will need the permission "command.ts" to run the command.
}
if config.enabled then Config.RegisterPluginConfig(config.pluginName, config) end
@@ -0,0 +1,13 @@
{
"note": "If you do not wish to set the ts3server variable here you may leave these blank and utilize convars in your server.cfg instead",
"note2": "Convars are as follows: sonorants3_server_host, sonorants3_server_port, sonorants3_server_qport, sonorants3_server_user, sonorants3_server_pass",
"ts3server_host": "127.0.0.1",
"ts3server_port": 9987,
"ts3server_qport": 10011,
"ts3server_user": "",
"ts3server_pass": "",
"onduty_servergroup": "On Duty",
"enforced_channels": ["test", "example"],
"logoutGraceTime": 5000,
"loginGraceTime": 5000
}
@@ -0,0 +1,19 @@
--[[
Sonoran Plugins
Plugin Configuration
This plugin has no configuration. It only exists to add the plugin to the loaded list.
]]
local config = {
enabled = false,
pluginName = "ts3integration", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
configVersion = "1.0",
requiresPlugins = {}, -- required plugins for this plugin to work, separated by commas
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,28 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
pluginName = "unitstatus", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
configVersion = "1.0",
requiresPlugins = {}, -- required plugins for this plugin to work, separated by commas
setStatusCommand = "setstatus", -- user command for setting their own status, leave blank to not use
-- put your configuration options below
statusCodes = {
["UNAVAILABLE"] = 0,
["BUSY"] = 1,
["AVAILABLE"] = 2,
["ENROUTE"] = 3,
["ON_SCENE"] = 4
},
enableAceCheck = true -- restrict command via ace permission
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,45 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
pluginName = "vehreg", -- name your plugin here
pluginAuthor = "Jordan.#2139", -- author
configVersion = "1.2",
reigsterCommand = "reg", -- Command to register car
defaultRegExpire = '01/02/2030', -- The default date that all registrations will expire
defaultRegStatus = 'VALID', -- The default status that all registrations will have | MUST BE IN CAPS
language = {
notInVeh = "Player Not In Vehicle... Please Ensure You're In A Vehicle And Try Again!",
noApiId = "API ID NOT LINKED TO AN ACCOUNT IN THIS COMMUNITY",
plateAlrRegisted = "This plate has already been registered to another person",
helpMsg = 'Register your current vehicle in CAD',
noCharFound = "No character found. Please ensure you are logged in to a character.",
incompleteCharData = "Character data is incomplete. Please ensure you have all required data filled out in CAD. Unable to register vehicle.",
--[[
Placeholders:
{{PLATE}} = The plate of the vehicle
{{FIRST}} = The first name of the charactes currently active in CAD
{{LAST}} = The first name of the charactes currently active in CAD
]]
successReg = "Vehicle ({{PLATE}}) successfully registered to ^2{{FIRST}} {{LAST}}"
},
-- If you have changed your field UID's in CAD please update the corresponding values here. (Field UID goes in the quotes on the right)
recordData = {
colorUid = "color",
plateUid = "plate",
typeUid = "type",
modelUid = "model",
statusUid = "status",
expiresUid = "_imtoih149",
}
}
if config.enabled then
Config.RegisterPluginConfig(config.pluginName, config)
end
@@ -0,0 +1,35 @@
--[[
Sonoran Plugins
Plugin Configuration
Put all needed configuration in this file.
]]
local config = {
enabled = true,
pluginName = "wraithv2", -- name your plugin here
pluginAuthor = "SonoranCAD", -- author
configVersion = "1.6",
requiresPlugins = {{name = "lookups", critical = true}}, -- required plugins for this plugin to work, separated by commas
-- use vehicle registration expirations, or not
useExpires = true,
-- use middle initials?
useMiddleInitial = true,
-- alert if no registration was found on scan?
alertNoRegistration = true,
-- if your custom vehicle record is different, change the below
statusUid = "status",
expiresUid = "expiration",
-- statuses to flag on when scanned
flagOnStatuses = {"STOLEN", "EXPIRED", "PENDING", "SUSPENDED"}
-- Vehicle classes that will NOT get ran through CAD | Classes: https://docs.fivem.net/natives/?_0x29439776AAA00A62
,vehTypeFilter = { 13, 14, 15, 16, 21, 22 }
,notificationTimers = {
validReg = 30000, -- 30 seconds for valid registration
warrant = 20000, -- 20 seconds for warrant
bolo = 20000, -- 20 seconds for bolo
noReg = 5000, -- 5 seconds for no registration
}
}
if config.enabled then Config.RegisterPluginConfig(config.pluginName, config) end
@@ -0,0 +1,108 @@
registerApiType("CHECK_APIID", "general")
function cadApiIdExists(apiId, callback)
if apiId == "" or apiId == nil then
debugLog("cadApiIdExists: No API ID specified, assuming false.")
callback(false)
else
performApiRequest({{["apiId"] = apiId}}, "CHECK_APIID", function(res, exists)
callback(exists)
end)
end
end
RegisterCommand("forcecheck", function(source, args, rawCommand)
performApiRequest({{["apiId"] = args[1]}}, "CHECK_APIID", function(res, exists)
print("exists: "..tostring(exists))
end)
end)
RegisterServerEvent("SonoranCAD::apicheck:CheckPlayerLinked")
AddEventHandler("SonoranCAD::apicheck:CheckPlayerLinked", function(player)
local identifier = GetIdentifiers(player)[Config.primaryIdentifier]
cadApiIdExists(identifier, function(exists)
TriggerEvent("SonoranCAD::apicheck:CheckPlayerLinkedResponse", player, identifier, exists)
end)
end)
exports('CadIsPlayerLinked', cadApiIdExists)
RegisterCommand("apiid", function(source, args, rawCommand)
local identifiers = GetIdentifiers(source)
local pid = nil
if isPluginLoaded("esxsupport") then
local type = Config.plugins["esxsupport"].identityType
if identifiers[type] ~= nil then
if Config.plugins["esxsupport"].usePrefix then
pid = ("%s:%s"):format(type, identifiers[type])
else
pid = identifiers[type]
end
end
elseif isPluginLoaded("frameworksupport") then
local type = Config.plugins["frameworksupport"].identityType
if identifiers[type] ~= nil then
if Config.plugins["frameworksupport"].usePrefix then
pid = ("%s:%s"):format(type, identifiers[type])
else
pid = identifiers[type]
end
end
else
if identifiers[Config.primaryIdentifier] ~= nil then
pid = identifiers[Config.primaryIdentifier]
end
end
if pid ~= nil then
print("Your API ID: "..tostring(pid))
else
print("API ID not found")
end
end)
if Config.forceSetApiId == nil then Config.forceSetApiId = false end
if Config.forceSetApiId then
debugLog("forceSetApiId enabled")
RegisterNetEvent("sonoran:tablet:forceCheckApiId")
AddEventHandler("sonoran:tablet:forceCheckApiId", function()
local identifier=GetIdentifiers(source)[Config.primaryIdentifier]
local plid=source
cadApiIdExists(identifier, function(exists)
if not exists then
TriggerClientEvent("sonoran:tablet:apiIdNotFound", plid)
else
TriggerClientEvent("sonoran:tablet:apiIdFound", plid)
end
end)
end)
RegisterNetEvent("sonoran:tablet:setApiId")
AddEventHandler("sonoran:tablet:setApiId", function(session,username)
local identifier=GetIdentifiers(source)[Config.primaryIdentifier]
local source = source
cadApiIdExists(identifier, function(exists)
if not exists then
registerApiType("SET_API_ID", "general")
local data = {{
["apiIds"] = { identifier },
["sessionId"] = session,
["username"] = username
}}
performApiRequest(data, "SET_API_ID", function(res, flag)
if (not flag) then
TriggerClientEvent("sonoran:tablet:failed", source, res)
end
end)
end
end)
end)
end
@@ -0,0 +1,439 @@
Config = {plugins = {}}
Plugins = {}
bodyCamOn = false;
bodyCamFrequency = 2000;
local bodyCamConfigReady = false;
Config.RegisterPluginConfig = function(pluginName, configs)
Config.plugins[pluginName] = {}
for k, v in pairs(configs) do Config.plugins[pluginName][k] = v end
table.insert(Plugins, pluginName)
end
--[[
@function getApiMode
@description Returns the API mode for the current server. 0 = Development, 1 = Production
@returns int
]]
function getApiMode()
if Config.mode == nil then
return 1
elseif Config.mode == 'development' then
return 0
else
return 1
end
end
exports('getApiMode', getApiMode)
Config.GetPluginConfig = function(pluginName)
local correctConfig = nil
if Config.plugins[pluginName] ~= nil then
if Config.critError then
Config.plugins[pluginName].enabled = false
Config.plugins[pluginName].disableReason = 'startup aborted'
elseif Config.plugins[pluginName].enabled == nil then
Config.plugins[pluginName].enabled = true
elseif Config.plugins[pluginName].enabled == false then
Config.plugins[pluginName].disableReason = 'Disabled'
end
return Config.plugins[pluginName]
else
if pluginName == 'apicheck' or pluginName == 'livemap' or pluginName ==
'smartsigns' then
return {enabled = false, disableReason = 'deprecated plugin'}
end
correctConfig = LoadResourceFile(GetCurrentResourceName(),
'/configuration/' .. pluginName ..
'_config.lua')
if not correctConfig then
warnLog(
('Plugin %s is missing critical configuration. Please check our plugin install guide at https://info.sonorancad.com/integration-submodules/integration-submodules/plugin-installation for steps to properly install.'):format(
pluginName))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Missing configuration file'
}
return {
enabled = false,
disableReason = 'Missing configuration file'
}
else
local configChunk = correctConfig:match("local config = {.-\n}") ..
"\nreturn config"
if not configChunk then
errorLog("No config table found in the string.")
end
local tempEnv = {}
setmetatable(tempEnv, {__index = _G}) -- Allow access to global functions if needed
local loadedPlugin, pluginError =
load(configChunk, 'config', 't', tempEnv)
if loadedPlugin then
-- Execute and capture the returned config table
local success, res = pcall(loadedPlugin)
if not success then
errorLog(
('Plugin %s failed to load due to error: %s'):format(
pluginName, res))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Failed to load'
}
return {enabled = false, disableReason = 'Failed to load'}
end
if res and type(res) == "table" then
-- Assign the extracted config to Config.plugins[pluginName]
Config.plugins[pluginName] = res
else
-- Handle case where config is not available
errorLog(
('Plugin %s did not define a valid config table.'):format(
pluginName))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Invalid or missing config'
}
return {
enabled = false,
disableReason = 'Invalid or missing config'
}
end
if Config.critError then
Config.plugins[pluginName].enabled = false
Config.plugins[pluginName].disableReason = 'startup aborted'
elseif Config.plugins[pluginName].enabled == nil then
Config.plugins[pluginName].enabled = true
elseif Config.plugins[pluginName].enabled == false then
Config.plugins[pluginName].disableReason = 'Disabled'
end
else
errorLog(('Plugin %s failed to load due to error: %s'):format(
pluginName, pluginError))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Failed to load'
}
return {enabled = false, disableReason = 'Failed to load'}
end
return Config.plugins[pluginName]
end
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Missing configuration file'
}
return {enabled = false, disableReason = 'Missing configuration file'}
end
end
Config.LoadPlugin = function(pluginName, cb)
local correctConfig = nil
while Config.apiVersion == -1 do Wait(1) end
if Config.plugins[pluginName] ~= nil then
if Config.critError then
Config.plugins[pluginName].enabled = false
Config.plugins[pluginName].disableReason = 'startup aborted'
elseif Config.plugins[pluginName].enabled == nil then
Config.plugins[pluginName].enabled = true
elseif Config.plugins[pluginName].enabled == false then
Config.plugins[pluginName].disableReason = 'Disabled'
end
return cb(Config.plugins[pluginName])
else
if pluginName == 'yourpluginname' then
return cb({enabled = false, disableReason = 'Template plugin'})
end
correctConfig = LoadResourceFile(GetCurrentResourceName(),
'/configuration/' .. pluginName ..
'_config.lua')
if not correctConfig then
warnLog(
('Submodule %s is missing critical configuration. Please check our submodule install guide at https://info.sonorancad.com/integration-plugins/in-game-integration/fivem-installation/submodule-configuration#activating-a-submodule for steps to properly install.'):format(
pluginName))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Missing configuration file'
}
return {
enabled = false,
disableReason = 'Missing configuration file'
}
else
local configChunk = correctConfig:match("local config = {.-\n}") ..
"\nreturn config"
if not configChunk then
errorLog("No config table found in the string.")
end
local tempEnv = {}
setmetatable(tempEnv, {__index = _G}) -- Allow access to global functions if needed
local loadedPlugin, pluginError =
load(configChunk, 'config', 't', tempEnv)
if loadedPlugin then
-- Execute and capture the returned config table
local success, res = pcall(loadedPlugin)
if not success then
errorLog(
('Submodule %s failed to load due to error: %s'):format(
pluginName, res))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Failed to load'
}
return {enabled = false, disableReason = 'Failed to load'}
end
if res and type(res) == "table" then
-- Assign the extracted config to Config.plugins[pluginName]
Config.plugins[pluginName] = res
else
-- Handle case where config is not available
errorLog(
('Submodule %s did not define a valid config table.'):format(
pluginName))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Invalid or missing config'
}
return {
enabled = false,
disableReason = 'Invalid or missing config'
}
end
if Config.critError then
Config.plugins[pluginName].enabled = false
Config.plugins[pluginName].disableReason = 'startup aborted'
elseif Config.plugins[pluginName].enabled == nil then
Config.plugins[pluginName].enabled = true
elseif Config.plugins[pluginName].enabled == false then
Config.plugins[pluginName].disableReason = 'Disabled'
end
else
errorLog(('Submodule %s failed to load due to error: %s'):format(
pluginName, pluginError))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Failed to load'
}
return {enabled = false, disableReason = 'Failed to load'}
end
return Config.plugins[pluginName]
end
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Missing configuration file'
}
return cb({
enabled = false,
disableReason = 'Missing configuration file'
})
end
end
CreateThread(function()
while not NetworkIsPlayerActive(PlayerId()) do Wait(1) end
TriggerServerEvent('SonoranCAD::core:sendClientConfig')
end)
RegisterNetEvent('SonoranCAD::core:recvClientConfig')
AddEventHandler('SonoranCAD::core:recvClientConfig', function(config)
for k, v in pairs(config) do Config[k] = v end
Config.inited = true
debugLog('Configuration received')
debugLog('Bodycam config ready')
end)
--[[
SonoranCAD Bodycam Callback if unit is not found in CAD
]]
RegisterNetEvent('SonoranCAD::core::ScreenshotOff', function()
if Config.bodycamEnabled then
bodyCamOn = false
if Config.bodycamOverlayEnabled then
SendNUIMessage({type = 'toggleGif'})
end
TriggerEvent('chat:addMessage', {
args = {
'Sonoran Bodycam',
'Bodycam disabled - You must be in CAD to enable bodycam'
}
})
end
end)
RegisterNetEvent('SonoranCAD::Core::InitBodycam', function(isReady, apiVersion)
if isReady == 0 then
CreateThread(function()
-- still waiting, request again in 10s
debugLog('Bodycam not ready, retrying in 10s')
Wait(10000)
TriggerServerEvent('SonoranCAD::Core::RequestBodycam')
end)
return
end
if apiVersion ~= -1 then
Config.apiVersion = apiVersion
end
if Config.bodycamEnabled then
print('Bodycam init')
-- Command to toggle bodycam on and off
RegisterCommand(Config.bodycamCommandToggle,
function(source, args, rawCommand)
if Config.apiVersion < 4 then
errorLog('Bodycam is only enabled with Sonoran CAD Pro.')
TriggerEvent('chat:addMessage', {
args = {
'Sonoran Bodycam',
'Bodycam is only enabled with Sonoran CAD Pro.'
}
})
return
end
if bodyCamOn then
bodyCamOn = false
TriggerServerEvent('SonoranCAD::core::bodyCamOff')
TriggerEvent('chat:addMessage',
{args = {'Sonoran Bodycam', 'Bodycam disabled.'}})
if Config.bodycamOverlayEnabled then
SendNUIMessage({
type = 'toggleGif',
location = Config.bodycamOverlayLocation
})
end
else
bodyCamOn = true
TriggerEvent('chat:addMessage',
{args = {'Sonoran Bodycam', 'Bodycam enabled.'}})
if Config.bodycamOverlayEnabled then
SendNUIMessage({
type = 'toggleGif',
location = Config.bodycamOverlayLocation
})
end
end
end, false)
-- Command to change the frequency of bodycam screenshots
RegisterCommand(Config.bodycamCommandChangeFrequncy,
function(source, args, rawCommand)
if Config.apiVersion < 4 then
errorLog('Bodycam is only enabled with Sonoran CAD Pro.')
TriggerEvent('chat:addMessage', {
args = {
'Sonoran Bodycam',
'Bodycam is only enabled with Sonoran CAD Pro.'
}
})
return
end
if args[1] then
args[1] = tonumber(args[1])
if not args[1] or args[1] <= 0 or args[1] > 10 then
errorLog(
'Frequency must a number greater than 0 and less than than 10 seconds.')
TriggerEvent('chat:addMessage', {
args = {
'Sonoran Bodycam',
'Frequency must a number greater than 0 and less than than 10 seconds.'
}
})
return
end
bodyCamFrequency = (tonumber(args[1]) * 1000)
TriggerEvent('chat:addMessage', {
args = {
'Sonoran Bodycam',
('Frequency set to %s.'):format(
(bodyCamFrequency / 1000))
}
})
else
TriggerEvent('chat:addMessage', {
args = {
'Sonoran Bodycam',
('Current bodycam frequency is %s.'):format(
(bodyCamFrequency / 1000))
}
})
end
end, false)
-- Add suggestions to the chat
TriggerEvent('chat:addSuggestion', '/' .. Config.bodycamCommandToggle,
'Enable or disable bodycam mode.')
TriggerEvent('chat:addSuggestion',
'/' .. Config.bodycamCommandChangeFrequncy,
'Change the frequency of bodycam screenshots.',
{{name = 'frequency', help = 'Frequency in seconds.'}})
end
end)
CreateThread(function()
while not Config.inited do Wait(10) end
if Config.devHiddenSwitch then
debugLog('Spawned discord thread')
SetDiscordAppId(867548404724531210)
SetDiscordRichPresenceAsset('icon')
SetDiscordRichPresenceAssetSmall('icon')
while true do
SetRichPresence('Developing SonoranCAD!')
Wait(5000)
SetRichPresence('sonorancad.com')
Wait(5000)
end
end
end)
local inited = false
AddEventHandler('playerSpawned', function()
TriggerServerEvent('SonoranCAD::core:PlayerReady')
inited = true
TriggerServerEvent('SonoranCAD::Core::RequestBodycam')
end)
AddEventHandler('onClientResourceStart', function(resourceName) --When resource starts, stop the GUI showing.
if(GetCurrentResourceName() ~= resourceName) then
return
end
Wait(10000)
if not inited then
TriggerServerEvent('SonoranCAD::core:PlayerReady')
inited = true
TriggerServerEvent('SonoranCAD::Core::RequestBodycam')
end
end)
RegisterNetEvent('SonoranCAD::core:debugModeToggle')
AddEventHandler('SonoranCAD::core:debugModeToggle',
function(toggle) Config.debugMode = toggle end)
RegisterNetEvent('SonoranCAD::core:AddPlayer')
RegisterNetEvent('SonoranCAD::core:RemovePlayer')
--[[
SonoranCAD Bodycam Plugin
]]
-- Main bodycam loops
CreateThread(function()
while true do
Wait(1)
if bodyCamOn then
TriggerServerEvent('SonoranCAD::core:TakeScreenshot')
Wait(bodyCamFrequency)
end
end
end)
CreateThread(function()
while true do
Wait(1)
if Config.bodycamPlayBeeps then
if bodyCamOn then
SendNUIMessage({
type = 'playSound',
transactionFile = 'sounds/beeps.mp3',
transactionVolume = 0.3
})
Wait(Config.bodycamBeepFrequency)
end
end
end
end)
Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

@@ -0,0 +1,84 @@
<html>
<head>
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
<script src="js/http.js" tyle="text/javascript"></script>
</head>
<body style="display: none">
<script>
// CRED: https://stackoverflow.com/questions/6150289/how-can-i-convert-an-image-into-base64-string-using-javascript/20285053#20285053
function toDataUrl(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onload = function () {
var reader = new FileReader();
reader.onloadend = function () {
callback(reader.result);
};
reader.readAsDataURL(xhr.response);
};
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.send();
}
var audioPlayer = null;
window.addEventListener("message", function (event) {
if (event.data.type === "convert_base64") {
toDataUrl(event.data.img, function (base64) {
fetch(`https://${GetParentResourceName()}/base64`, {
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify({
base64: base64,
handle: event.data.handle,
id: event.data.id,
}),
});
});
}
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.1.1/howler.min.js" type="text/javascript"></script>
<script>
var audioPlayer = null;
var showGif = false;
window.addEventListener("message", function (event) {
if (event.data.type == "playSound") {
if (audioPlayer != null) {
audioPlayer.pause();
}
audioPlayer = new Howl({
src: [event.data.transactionFile],
});
audioPlayer.volume(event.data.transactionVolume);
audioPlayer.play();
}
if (event.data.type == "toggleGif") {
showGif = !showGif;
if (showGif) {
document.body.style.display = "block";
switch (event.data.location) {
case "top-left":
document.getElementById("overlay").style.top = "0";
document.getElementById("overlay").style.left = "0";
break;
case "top-right":
document.getElementById("overlay").style.top = "0";
document.getElementById("overlay").style.right = "0";
break;
case "bottom-left":
document.getElementById("overlay").style.bottom = "0";
document.getElementById("overlay").style.left = "0";
break;
case "bottom-right":
document.getElementById("overlay").style.bottom = "0";
document.getElementById("overlay").style.right = "0";
break;
}
} else {
document.body.style.display = "none";
}
}
});
</script>
<img src="./img/logo.gif" alt="header" style="height: 6vh; position: absolute" id="overlay" />
</body>
</html>
@@ -0,0 +1,7 @@
$(function () {
window.addEventListener('message', function (event) {
if (event.data.type == "light_event") {
$.post("http://localhost:" + event.data.port + "/lighting", JSON.stringify({ state: event.data.event }))
}
});
});
@@ -0,0 +1,245 @@
--[[
SonoranCAD FiveM Integration
Commands Module
Provides /sonoran command for console control
]]
--[[ /sonoran
debugmode - old caddebug toggle
info - dump version info, configuration
support - dump useful data for support staff
verify - run hash checks to confirm all files are untampered
plugin <name> - show info about a plugin (config)
update - attempt to auto-update
]]
registerApiType("UPLOAD_LOGS", "support")
function dumpInfo()
local version = GetResourceMetadata(GetCurrentResourceName(), "version", 0)
local pluginList, loadedPlugins, disabledPlugins = GetPluginLists()
local pluginVersions = {}
local cadVariables = { ["netPort"] = GetConvar("netPort", "Unknown")}
local variableList = ""
for k, v in pairs(cadVariables) do
variableList = ("%s%s = %s\n"):format(variableList, k, v)
end
for k, v in pairs(pluginList) do
if Config.plugins[v] then
table.insert(pluginVersions, ("%s [%s/%s]"):format(v, Config.plugins[v].version, Config.plugins[v].latestVersion))
end
end
local coreConfig = {}
for k, v in pairs(Config) do
if (k == "plugins") then goto continue end
if type(v) == "function" then goto continue end
if type(v) == "table" then
table.insert(coreConfig, ("%s = %s"):format(k, json.encode(v)))
goto continue
end
if type(v) == "thread" then goto continue end
table.insert(coreConfig, ("%s = %s"):format(k, v))
coreConfig[k] = v
::continue::
end
return ([[
SonoranCAD
Version: %s - Latest: %s
FXS Version: %s
Available Submodules
%s
Loaded Submodules
%s
Disabled Submodules
%s
Relevant Variables
%s
Core Configuration
%s
]]):format(version, Config.latestVersion, getServerVersion(), table.concat(pluginVersions, ", "), table.concat(loadedPlugins, ", "), table.concat(disabledPlugins, ", "), variableList, table.concat(coreConfig, "\n"))
end
function dumpPlugin(name)
local pluginDetail = {}
if not Config.plugins[name] then
print("Bad plugin: "..name)
return nil
end
for k, v in pairs(Config.plugins[name]) do
table.insert(pluginDetail, ("%s = %s"):format(k, v))
end
return ([[
Plugin: %s
Version: %s
Configuration:
%s
]]):format(name, Config.plugins[name].version, table.concat(pluginDetail, "\n "))
end
local function sendSupportLogs(key)
infoLog("Please wait, gathering required data...")
local cadOutput = {}
cadOutput.key = tonumber(key)
if cadOutput.key == nil then
errorLog("Invalid support key.")
return
end
local plugins = {}
for name, config in pairs(Config.plugins) do
pluginData = {}
pluginData.name = name
pluginData.version = config.version
pluginData.config = config
table.insert(plugins, pluginData)
end
cadOutput.plugins = plugins
cadOutput.logs = ([[
SonoranCAD Support Output
---------------------------------------
Configuration Information
---
%s
---------------------------------------
Console Buffer
------
%s
---------------------------------------
Last 50 Debug Messages
----------------------
%s
]]):format(dumpInfo(), GetConsoleBuffer(), table.concat(getDebugBuffer(), "\n"))
Config.debugMode = false
performApiRequest({cadOutput}, "UPLOAD_LOGS", function(data)
if data == "LOGS UPDATED" then
infoLog("Support logs have been successfully uploaded. Debug mode was disabled during the upload.")
else
errorLog(("Failed to upload support logs: %s"):format(data))
end
end)
end
RegisterCommand("sonoran", function(source, args, rawCommand)
if source ~= 0 then
print("Console only command")
return
end
if not args[1] then
print("Missing command. Try \"sonoran help\" for help.")
return
end
if args[1] == "help" then
print([[
SonoranCAD Help
debugmode - Toggles debugging mode
info - dump version info, configuration
support - dump useful data for support staff
errors - display all error/warning messages since last startup
plugin <name> - show info about a plugin (config)
update - Run core updater
pluginupdate - Run plugin updater
viewcaches - View the current unit and call cache, for troubleshooting
getclientlog <playerId> - Get a log buffer from a given client
dumpconsole - Dumps current console buffer to file
]])
elseif args[1] == "debugmode" then
Config.debugMode = not Config.debugMode
local convarString = ""
if Config.debugMode then
convarString = "true"
else
convarString = "false"
end
SetConvar("sonoran_debugMode", convarString)
infoLog(("Debug mode toggled to %s"):format(convarString))
TriggerClientEvent("SonoranCAD::core:debugModeToggle", -1, Config.debugMode)
elseif args[1] == "info" then
print(dumpInfo())
elseif args[1] == "support" and args[2] ~= nil then
sendSupportLogs(args[2])
elseif args[1] == "plugin" and args[2] then
if Config.plugins[args[2]] then
print(dumpPlugin(args[2]))
else
errorLog("Invalid plugin")
end
elseif args[1] == "update" then --update - attempt to auto-update
infoLog("Checking for core update...")
RunAutoUpdater(true)
elseif args[1] == "dumpconsole" then
local savePath = GetResourcePath(GetCurrentResourceName()).."/buffer.log"
local f = assert(io.open(savePath, 'wb'))
f:write(GetConsoleBuffer())
f:close()
infoLog("Wrote buffer to "..savePath)
elseif args[1] == "pluginupdate" then
infoLog("Scanning for plugin updates...")
for k, v in pairs(Config.plugins) do
CheckForPluginUpdate(k, true)
end
elseif args[1] == "viewcaches" then
local units = GetUnitCache()
local calls = GetCallCache()
print(("Units: %s\r\nCalls: %s"):format(json.encode(units), json.encode(calls)))
print("Done")
elseif args[1] == "getclientlog" then
if args[2] then
if GetPlayerName(args[2]) ~= nil then
TriggerClientEvent("SonoranCAD::core:RequestLogBuffer", args[2])
infoLog("Requested log buffer. Please wait...")
else
errorLog("Invalid player ID")
end
else
errorLog("Invalid argument.")
end
elseif args[1] == "errors" then
print("----ERROR/WARNING BUFFER START----")
local buf = getErrorBuffer()
for i=1, #buf do
print(buf[i])
end
print("----ERROR/WARNING BUFFER END----")
else
print("Missing command. Try \"sonoran help\" for help.")
end
end, true)
function GetPluginLists()
local pluginList = {}
local loadedPlugins = {}
local disabledPlugins = {}
local disableFormatted = {}
for name, v in pairs(Config.plugins) do
table.insert(pluginList, name)
if v.enabled then
table.insert(loadedPlugins, name)
else
if v.disableReason == nil then
v.disableReason = "disabled in config"
end
disabledPlugins[name] = v.disableReason
end
end
for name, reason in pairs(disabledPlugins) do
table.insert(disableFormatted, ("%s (%s)"):format(name, reason))
end
return pluginList, loadedPlugins, disableFormatted
end
-- Support Push Event
AddEventHandler("SonoranCAD::pushevents:SendSupportLogs", function(key)
infoLog("Support has requested logs to be uploaded. Collecting now...")
sendSupportLogs(key)
end)
RegisterNetEvent("SonoranCAD::core:LogBuffer")
AddEventHandler("SonoranCAD::core:LogBuffer", function(buffer)
infoLog(("Incoming log buffer from player %s"):format(source))
for i=1, #buffer do
print((": %s"):format(buffer[i]))
end
infoLog("End of buffer")
end)
@@ -0,0 +1,571 @@
Config = {
communityID = nil,
apiKey = nil,
apiUrl = nil,
postTime = nil,
serverId = nil,
primaryIdentifier = nil,
apiSendEnabled = nil,
debugMode = nil,
updateBranch = nil,
enableCanary = false,
latestVersion = '',
apiVersion = -1,
plugins = {},
proxyUrl = ''
}
Config.RegisterPluginConfig = function(pluginName, configs)
Config.plugins[pluginName] = {}
for k, v in pairs(configs) do
Config.plugins[pluginName][k] = v
-- debugLog(("plugin %s set %s = %s"):format(pluginName, k, v))
end
table.insert(Plugins, pluginName)
end
local function CopyFile(old_path, new_path)
local old_file = io.open(old_path, 'rb')
local new_file = io.open(new_path, 'wb')
if not old_file then
warnLog('Failed to open source file: ' .. old_path ..
' - please check your folder permissions or rename file manually.')
return false
end
if not new_file then
warnLog('Failed to create target file: ' .. new_path ..
' - please check your folder permissions or rename file manually.')
old_file:close()
return false
end
local old_file_sz, new_file_sz
while true do
local block = old_file:read(2 ^ 13)
if not block then
old_file_sz = old_file:seek('end')
break
end
new_file:write(block)
end
old_file:close()
new_file_sz = new_file:seek('end')
new_file:close()
if new_file_sz ~= old_file_sz then
print('File copy size mismatch')
return false
end
return true
end
exports('GetPluginConfig', function(pluginName)
return Config.GetPluginConfig(pluginName)
end)
Config.GetPluginConfig = function(pluginName)
local correctConfig = nil
if Config.plugins[pluginName] ~= nil then
if Config.critError then
Config.plugins[pluginName].enabled = false
Config.plugins[pluginName].disableReason = 'startup aborted'
elseif Config.plugins[pluginName].enabled == nil then
Config.plugins[pluginName].enabled = true
end
return Config.plugins[pluginName]
else
if pluginName == 'yourpluginname' then
return {enabled = false, disableReason = 'Template plugin'}
end
if pluginName == 'apicheck' or pluginName == 'livemap' or pluginName ==
'smartsigns' then
return {enabled = false, disableReason = 'deprecated plugin'}
end
correctConfig = LoadResourceFile(GetCurrentResourceName(),
'/configuration/' .. pluginName ..
'_config.lua')
if not correctConfig then
infoLog(
('Submodule %s only has the default configurations file (%s_config.dist.lua)... Attempting to rename config to: %s_config.lua'):format(
pluginName, pluginName, pluginName))
if not CopyFile(GetResourcePath(GetCurrentResourceName()) ..
'/configuration/' .. pluginName ..
'_config.dist.lua',
GetResourcePath(GetCurrentResourceName()) ..
'/configuration/' .. pluginName .. '_config.lua') then
warnLog(
('Failed to rename %s_config.dist.lua to %s_config.lua'):format(
pluginName, pluginName))
warnLog(
('Using default configurations for %s. Please rename %s_config.dist.lua to %s_config.lua to avoid seeing this message'):format(
pluginName, pluginName, pluginName))
correctConfig = LoadResourceFile(GetCurrentResourceName(),
'/configuration/' .. pluginName ..
'_config.dist.lua')
else
correctConfig = LoadResourceFile(GetCurrentResourceName(),
'/configuration/' .. pluginName ..
'_config.lua')
end
end
if not correctConfig then
warnLog(
('Submodule %s is missing critical configuration. Please check our submodule install guide at https://info.sonorancad.com/integration-plugins/in-game-integration/fivem-installation/submodule-configuration#activating-a-submodule for steps to properly install.'):format(
pluginName))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Missing configuration file'
}
return {
enabled = false,
disableReason = 'Missing configuration file'
}
else
local configChunk = correctConfig:match("local config = {.-\n}") .. "\nreturn config"
if not configChunk then
errorLog("No config table found in the string.")
end
local tempEnv = {}
setmetatable(tempEnv, { __index = _G }) -- Allow access to global functions if needed
local loadedPlugin, pluginError = load(configChunk, 'config', 't', tempEnv)
if loadedPlugin then
-- Execute and capture the returned config table
local success, res = pcall(loadedPlugin)
if not success then
errorLog(('Submodule %s failed to load due to error: %s'):format(pluginName, res))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Failed to load'
}
return {enabled = false, disableReason = 'Failed to load'}
end
if res and type(res) == "table" then
-- Assign the extracted config to Config.plugins[pluginName]
Config.plugins[pluginName] = res
else
-- Handle case where config is not available
errorLog(
('Plugin %s did not define a valid config table.'):format(
pluginName))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Invalid or missing config'
}
return {
enabled = false,
disableReason = 'Invalid or missing config'
}
end
if Config.critError then
Config.plugins[pluginName].enabled = false
Config.plugins[pluginName].disableReason = 'startup aborted'
elseif Config.plugins[pluginName].enabled == nil then
Config.plugins[pluginName].enabled = true
elseif Config.plugins[pluginName].enabled == false then
Config.plugins[pluginName].disableReason = 'Disabled'
end
else
errorLog(('Plugin %s failed to load due to error: %s'):format(
pluginName, pluginError))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Failed to load'
}
return {enabled = false, disableReason = 'Failed to load'}
end
return Config.plugins[pluginName]
end
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'disabled'
}
return {enabled = false, disableReason = 'disabled'}
end
end
Config.LoadPlugin = function(pluginName, cb)
local correctConfig = nil
while Config.apiVersion == -1 do Wait(1) end
if Config.plugins[pluginName] ~= nil then
if Config.critError then
Config.plugins[pluginName].enabled = false
Config.plugins[pluginName].disableReason = 'startup aborted'
elseif Config.plugins[pluginName].enabled == nil then
Config.plugins[pluginName].enabled = true
end
return cb(Config.plugins[pluginName])
else
if pluginName == 'yourpluginname' then
return cb({enabled = false, disableReason = 'Template plugin'})
end
correctConfig = LoadResourceFile(GetCurrentResourceName(),
'/configuration/' .. pluginName ..
'_config.lua')
if not correctConfig then
infoLog(
('Plugin %s only has the default configurations file (%s_config.dist.lua)... Attempting to rename config to: %s_config.lua'):format(
pluginName, pluginName, pluginName))
if not CopyFile(GetResourcePath(GetCurrentResourceName()) ..
'/configuration/' .. pluginName ..
'_config.dist.lua',
GetResourcePath(GetCurrentResourceName()) ..
'/configuration/' .. pluginName .. '_config.lua') then
warnLog(
('Failed to rename %s_config.dist.lua to %s_config.lua'):format(
pluginName, pluginName))
warnLog(
('Using default configurations for %s. Please rename %s_config.dist.lua to %s_config.lua to avoid seeing this message'):format(
pluginName, pluginName, pluginName))
correctConfig = LoadResourceFile(GetCurrentResourceName(),
'/configuration/' .. pluginName ..
'_config.dist.lua')
else
correctConfig = LoadResourceFile(GetCurrentResourceName(),
'/configuration/' .. pluginName ..
'_config.lua')
end
end
if not correctConfig then
warnLog(
('Plugin %s is missing critical configuration. Please check our plugin install guide at https://info.sonorancad.com/integration-submodules/integration-submodules/plugin-installation for steps to properly install.'):format(
pluginName))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Missing configuration file'
}
return cb({
enabled = false,
disableReason = 'Missing configuration file'
})
else
local configChunk = correctConfig:match("local config = {.-\n}") .. "\nreturn config"
if not configChunk then
errorLog("No config table found in the string.")
end
local tempEnv = {}
setmetatable(tempEnv, { __index = _G }) -- Allow access to global functions if needed
local loadedPlugin, pluginError = load(configChunk, 'config', 't', tempEnv)
if loadedPlugin then
-- Execute and capture the returned config table
local success, res = pcall(loadedPlugin)
if not success then
errorLog(('Plugin %s failed to load due to error: %s'):format(pluginName, res))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Failed to load'
}
return {enabled = false, disableReason = 'Failed to load'}
end
if res and type(res) == "table" then
-- Assign the extracted config to Config.plugins[pluginName]
Config.plugins[pluginName] = res
else
-- Handle case where config is not available
errorLog(
('Plugin %s did not define a valid config table.'):format(
pluginName))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Invalid or missing config'
}
return {
enabled = false,
disableReason = 'Invalid or missing config'
}
end
if Config.critError then
Config.plugins[pluginName].enabled = false
Config.plugins[pluginName].disableReason = 'startup aborted'
elseif Config.plugins[pluginName].enabled == nil then
Config.plugins[pluginName].enabled = true
elseif Config.plugins[pluginName].enabled == false then
Config.plugins[pluginName].disableReason = 'Disabled'
end
else
errorLog(('Plugin %s failed to load due to error: %s'):format(
pluginName, pluginError))
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'Failed to load'
}
return cb({enabled = false, disableReason = 'Failed to load'})
end
return cb(Config.plugins[pluginName])
end
Config.plugins[pluginName] = {
enabled = false,
disableReason = 'disabled'
}
return cb({enabled = false, disableReason = 'disabled'})
end
end
local conf = LoadResourceFile(GetCurrentResourceName(),
'/configuration/config.json')
if conf == nil then
errorLog(
'CONFIG_ERROR: Unable to load configuration file. Ensure the file is named correctly (config.json). Check for extra extensions (like config.json.json).')
Config.critError = true
Config.apiSendEnabled = false
return
end
local parsedConfig = json.decode(conf)
if parsedConfig == nil then
errorLog(
'CONFIG_ERROR: Unable to parse configuration file. Ensure it is valid JSON.')
Config.critError = true
Config.apiSendEnabled = false
return
end
for k, v in pairs(json.decode(conf)) do
local cvar = GetConvar('sonoran_' .. k, 'NONE')
local cvar_setter = GetConvar('sonoran_' .. k .. '_setter', 'NONE')
local val = nil
if cvar ~= 'NONE' and cvar ~= 'statusLabels' then
if cvar_setter == 'NONE' or cvar_setter == 'server' then
infoLog(
('Configuration: Overriding config option %s with convar. New value: %s'):format(
k, cvar))
SetConvar('sonoran_' .. k .. '_setter', 'server')
cvar_setter = 'server'
else
infoLog(
('Configuration: Reusing config option %s from server boot. New value: %s, reboot the server if you made a change to this value...'):format(
k, cvar))
SetConvar('sonoran_' .. k .. '_setter', 'framework')
cvar_setter = 'framework'
end
if cvar == 'true' then
cvar = true
elseif cvar == 'false' then
cvar = false
end
Config[k] = cvar
val = cvar
else
Config[k] = v
val = v
end
if k ~= 'apiKey' then
SetConvar('sonoran_' .. k, tostring(val))
if cvar_setter == 'NONE' then
SetConvar('sonoran_' .. k .. '_setter', 'framework')
end
end
end
if Config.updateBranch == nil then Config.updateBranch = 'master' end
RegisterNetEvent('SonoranCAD::core:sendClientConfig')
AddEventHandler('SonoranCAD::core:sendClientConfig', function()
local config = {
communityID = Config.communityID,
postTime = Config.postTime,
serverId = Config.serverId,
primaryIdentifier = Config.primaryIdentifier,
apiSendEnabled = Config.apiSendEnabled,
debugMode = Config.debugMode,
devHiddenSwitch = Config.devHiddenSwitch,
statusLabels = Config.statusLabels,
bodycamEnabled = Config.bodycamEnabled,
bodycamBeepFrequency = Config.bodycamBeepFrequency,
bodycamScreenshotFrequency = Config.bodycamScreenshotFrequency,
bodycamPlayBeeps = Config.bodycamPlayBeeps,
bodycamOverlayEnabled = Config.bodycamOverlayEnabled,
bodycamOverlayLocation = Config.bodycamOverlayLocation,
bodycamCommandToggle = Config.bodycamCommandToggle,
bodycamCommandChangeFrequncy = Config.bodycamCommandChangeFrequncy,
apiVersion = Config.apiVersion,
mode = Config.mode
}
TriggerClientEvent('SonoranCAD::core:recvClientConfig', source, config)
end)
CreateThread(function()
Wait(2000) -- wait for server to settle
if Config.critError then return end
local serverId = Config.serverId
while Config.apiVersion == -1 do Wait(10) end
if not Config.apiSendEnabled or Config.apiVersion < 3 then
debugLog('Too low version or API disabled, ignore this')
return
end
performApiRequest({}, 'GET_SERVERS', function(response)
local info = json.decode(response)
for k, v in pairs(info.servers) do
if tostring(v.id) == tostring(serverId) then
ServerInfo = v
break
end
end
local needSetup = false
local serverObj = {}
if ServerInfo == nil then
needSetup = true
serverObj = {
id = serverId,
name = 'Server ' .. serverId,
description = 'Server ' .. serverId,
signal = '',
listenerPort = GetConvar('netPort', '0'),
mapIp = '',
differingOutbound = false,
outboundIp = '',
enableMap = true,
mapType = 'NORMAL'
}
else
serverObj = ServerInfo
end
if serverObj.name == '' then
serverObj.name = 'Server ' .. tostring(serverId)
end
if ServerInfo.listenerPort ~= GetConvar('netPort', '0') then
infoLog(
('Configuration information doesn\'t match, will attempt to auto-correct game port from %s to %s.'):format(
ServerInfo.listenerPort, GetConvar('netPort', '0')))
serverObj.listenerPort = GetConvar('netPort', '0')
needSetup = true
end
PerformHttpRequest('https://api.ipify.org?format=json',
function(errorCode, resultData, resultHeaders)
local r = json.decode(resultData)
if r ~= nil and r.ip ~= nil then
debugLog(
('IP DETECT - IP: %s - Detected: %s - Outbound set: %s - Outbound IP: %s'):format(
ServerInfo.mapIp, r.ip, ServerInfo.differingOutbound,
ServerInfo.outboundIp))
if serverObj.mapIp == '' or serverObj.mapIp == nil then
serverObj.mapIp = r.ip
needSetup = true
end
if ServerInfo.mapIp ~= r.ip then
if ServerInfo.differingOutbound and ServerInfo.outboundIp ==
r.ip then
infoLog(
'Detected proper differing outbound IP configuration.')
else
if ServerInfo.differingOutbound then
needSetup = true
serverObj.outboundIp = r.ip
else
needSetup = true
serverObj.outboundIp = r.ip
serverObj.differingOutbound = true
end
end
end
end
local disableOverride = (Config.disableOverride ~= nil and
Config.disableOverride or false)
if needSetup and not disableOverride then
local payload = nil
if ServerInfo == nil then
payload = {['servers'] = {serverObj}}
else
payload = info
for k, v in pairs(payload) do
if v.id == serverId then
payload[k] = serverObj
end
end
end
debugLog(('Send payload: %s'):format(json.encode(payload)))
performApiRequest(json.encode(payload), 'SET_SERVERS', function(
resp)
debugLog('SET_SERVERS: ' .. tostring(resp))
end)
elseif disableOverride and not needSetup then
warnLog(
'disableOverride is true or there is no additional setup required, skipping any potential auto-IP/port fixing')
end
end, 'GET', nil, nil)
end)
if isPluginLoaded('livemap') then
warnLog(
'The livemap plugin is no longer being used due to the map being native to the CAD. You can remove this plugin.')
end
end)
CreateThread(function()
-- attempt to fetch web_baseUrl
local baseUrl = ''
local counter = 0
if Config.bodycamEnabled then
local counter = 0
while baseUrl == '' do
Wait(1000)
baseUrl = GetConvar('web_baseUrl', '')
-- Every 60 seconds, log a warning
counter = counter + 1
if counter % 60 == 0 then
warnLog('Still waiting for web_baseUrl convar to be set...bodycam will not work until this is set.')
end
end
else
-- Run the loop once
baseUrl = GetConvar('web_baseUrl', '')
if baseUrl == '' then
warnLog('Bodycam is disabled and web_baseUrl is not set. Skipping loop.')
end
end
Config.proxyUrl = ('https://%s/sonorancad/'):format(GetConvar('web_baseUrl',''))
debugLog(('Set proxyUrl to %s'):format(Config.proxyUrl))
TriggerClientEvent('SonoranCAD::Core::InitBodycam', -1, 1, Config.apiVersion)
end)
RegisterNetEvent('SonoranCAD::Core::RequestBodycam', function()
if not Config.proxyUrl or Config.proxyUrl == '' then
-- tell client we're not ready
TriggerClientEvent('SonoranCAD::Core::InitBodycam', source, 0, Config.apiVersion)
else
-- tell client we're ready
if Config.apiVersion == -1 then
debugLog('API version not set, waiting for it to be set...')
while Config.apiVersion == -1 do Wait(1000) end
end
TriggerClientEvent('SonoranCAD::Core::InitBodycam', source, 1, Config.apiVersion)
end
end)
CreateThread(function()
while Config.apiVersion == -1 do Wait(100) end
if Config.critError then return end
if isPluginLoaded('wraithv2') then
if GetResourceState('wk_wars2x') ~= 'started' then
warnLog(
('Warning: wk_wars2x resource in bad start (%s). Ensure it is started to use the wraithv2 resource.'):format(
GetResourceState('wk_wars2x')))
end
if GetResourceState('pNotify') ~= 'started' then
warnLog(
('Warning: pNotify is required to see notifications from the wraithv2 plugin but the resource in bad start (%s). Ensure it is started'):format(
GetResourceState('pNotify')))
end
end
if isPluginLoaded('smartsigns') then
warnLog('smartsigns is now a standalone resource. Please update.')
end
-- smartsigns improper install check
if file_exists(('%s/submodules/smartsigns/sv_smartsigns.lua'):format(
GetResourcePath(GetCurrentResourceName()))) or
file_exists(
('%s/submodules/smartsigns/smartsigns/sv_smartsigns.lua'):format(
GetResourcePath(GetCurrentResourceName()))) then
errorLog('-----------------------')
errorLog(
'Smartsigns incorrect installation detected. This should be installed a standalone resource. If you still have the plugin, you MUST update! You will recieve a parse error in this state.')
errorLog('-----------------------')
end
end)
function file_exists(name)
local f = io.open(name, 'r')
if f ~= nil then
io.close(f)
return true
else
return false
end
end
@@ -0,0 +1,78 @@
-- source: https://github.com/loaf-scripts/loaf_headshot_base64/blob/main/client.lua
local requests = {}
local function GenerateId()
local id = ""
for i = 1, 15 do
id = id .. (math.random(1, 2) == 1 and string.char(math.random(97, 122)) or tostring(math.random(0,9)))
end
return id
end
local function ClearHeadshots()
for i = 1, 255 do
if IsPedheadshotValid(i) then
UnregisterPedheadshot(i)
end
end
end
function GetHeadshot(ped)
ClearHeadshots()
if not ped then ped = PlayerPedId() end
if DoesEntityExist(ped) then
local handle, timer = RegisterPedheadshot(ped), GetGameTimer() + 5000
while not IsPedheadshotReady(handle) or not IsPedheadshotValid(handle) do
Wait(50)
if GetGameTimer() >= timer then
return {success=false, error="Could not load ped headshot."}
end
end
local txd = GetPedheadshotTxdString(handle)
local url = string.format("https://nui-img/%s/%s", txd, txd)
return {success=true, url=url, txd=txd, handle=handle}
end
end
function GetBase64(ped)
if not ped then ped = PlayerPedId() end
local headshot = GetHeadshot(ped)
if headshot.success then
local requestId = GenerateId()
requests[requestId] = nil
SendNUIMessage({
type = "convert_base64",
img = headshot.url,
handle = headshot.handle,
id = requestId
})
local timer = GetGameTimer() + 5000
while not requests[requestId] do
Wait(250)
if GetGameTimer() >= timer then
return {success=false, error="Waiting for base64 conversion timed out."}
end
end
return {success=true, base64=requests[requestId]}
else
return headshot
end
end
RegisterNUICallback("base64", function(data, cb)
if data.handle then
UnregisterPedheadshot(data.handle)
end
if data.id then
requests[data.id] = data.base64
Wait(1500)
requests[data.id] = nil
end
cb({ok=true})
end)
exports("getBase64", GetBase64)
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<title>SonoranCAD Info Page</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
</head>
<body>
<div id="passwordDiv">
<form>
<p>API Key: <input type="text" id="password" name="password"/></p>
<input type="button" id="apisubmit" value="Get Configuration"/> <br/><br/>
Console Input: <input type="text" id="consoleinput" name="consoleinput" width="150"/> <input type="button" id="consolesubmit" value="Send Command"/>
</p>
</form>
</div>
<div id="errorDiv"></div>
<div id="infoDiv"></div>
<div id="consoleDiv"></div>
<div id="debugHeader" style="display:none"><p>LAST 50 DEBUG MESSAGES</p></div>
<div id="debugDiv"></div>
<script type="text/javascript">
const api = document.getElementById("apisubmit")
const console = document.getElementById("consolesubmit")
console.addEventListener('click', e => {
fetch('/sonorancad/console', {
method: 'post',
body: JSON.stringify({
password: document.querySelector("#password").value,
command: document.querySelector("#consoleinput").value
})
}).then(res => res.json()).then(res => {
let consoleDiv = document.getElementById("consoleDiv");
if (res.error) {
document.getElementById("errorDiv").innerHTML = res.error;
} else {
consoleDiv.innerHTML = res.output;
document.getElementById("errorDiv").innerHTML = "";
}
document.getElementById("#consoleinput").innerHTML = "";
})
});
api.addEventListener('click', e => {
fetch('/sonorancad/info', {
method: 'post',
body: JSON.stringify({
password: document.querySelector("#password").value
})
}).then(res => res.json()).then(res => {
let passDiv = document.getElementById("passwordDiv");
let infoDiv = document.getElementById("infoDiv");
let consoleDiv = document.getElementById("consoleDiv");
let debugDiv = document.getElementById("debugDiv");
let debugHeader = document.getElementById("debugHeader");
if (res.error) {
infoDiv.style.color = '#aa0000';
infoDiv.innerHTML = "An error occurred: " + res.error;
infoDiv.style.display == "none";
} else {
infoDiv.style.color = '#000000';
infoDiv.style.display == "block";
// passDiv.style.display == "none";
infoDiv.innerHTML = res.cadInfo
infoDiv.innerHTML += res.config
consoleDiv.innerHTML = res.console
debugDiv.innerHTML = res.debug
debugHeader.style = "display:block;"
}
})
})
</script>
</body>
</html>
@@ -0,0 +1,46 @@
const { format } = require("path");
function byteCount(s) {
return encodeURI(s).split(/%..|./).length - 1;
}
exports('HandleHttpRequest', (dest, callback, method, data, headers) => {
emit("SonoranCAD::core:writeLog", "debug", "[http] to: " + dest + " - data: " + dest, JSON.stringify(data));
const urlObj = url.parse(dest)
const options = {
hostname: urlObj.hostname,
path: urlObj.pathname,
method: method,
headers: headers
}
if (method == "POST") {
options.headers['Content-Type'] = 'application/json'
}
else if (method != "GET") {
console.error("Invalid request. Only GET/POST supported. Method: " + method);
callback(500, "", {});
return;
}
options.headers['X-SonoranCAD-Version'] = GetResourceMetadata(GetCurrentResourceName(), "version", 0)
//console.debug("send to: " + dest);
const req = https.request(options, (res) => {
let output = "";
res.on('data', (d) => {
output += d.toString()
}),
res.on('end', () => {
callback(res.statusCode, output, res.headers);
})
})
req.on('error', (error) => {
let ignore_ids = ["EAI_AGAIN", "ETIMEOUT", "ENOTFOUND"]
if (!ignore_ids.includes(error.code))
console.debug("HTTP error caught: " + JSON.stringify(error));
callback(error.errono, {}, {});
})
if (method == "POST") {
req.write(data);
}
req.end();
});
@@ -0,0 +1,508 @@
local PluginHttpHandlers = {}
local PluginFilePaths = {}
function RegisterPluginHttpEvent(eventName, func)
if PluginHttpHandlers[eventName] ~= nil then
errorLog('Failed to register plugin event ' .. eventName .. ': Already Exists')
return
end
PluginHttpHandlers[eventName] = func
end
local PushEventHandler = {
EVENT_UNIT_STATUS = function(body)
if (not body.data.identIds) then
return false, 'missing identIds'
end
if body.data.identIds ~= nil then
for i = 1, #body.data.identIds do
local unit = GetUnitObjectById(body.data.identIds[i])
if unit then
unit.status = body.data.status
SetUnitCache(body.data.identIds[i], unit)
TriggerEvent('SonoranCAD::pushevents:UnitUpdate', unit, unit.status)
TriggerEvent('SonoranCAD::pushevents:UnitStatusUpdate', unit, unit.status)
return true
end
debugLog(('EVENT_UNIT_STATUS: Unknown unit, idents: %s'):format(json.encode(body.data.identIds)))
return false, 'unknown unit'
end
else
return false, 'invalid, no idents'
end
return true
end,
EVENT_UNIT_LOGIN = function(body)
if (not body.data.unit.id) then
return false, 'missing ID'
end
local unit = body.data.unit
debugLog('Got a unit: ' .. json.encode(unit))
unit.isDispatch = body.data.isDispatch
SetUnitCache(unit.id, unit)
TriggerEvent('SonoranCAD::pushevents:UnitLogin', unit)
return true
end,
EVENT_UNIT_LOGOUT = function(body)
if (not body.data.identId) then
return false, 'missing identId'
end
debugLog('UNIT_LOGOUT: ' .. json.encode(body.data))
TriggerEvent('SonoranCAD::pushevents:UnitLogout', body.data.identId)
SetUnitCache(GetUnitById(body.data.identId), nil)
return true
end,
EVENT_DISPATCH_NEW = function(body)
SetCallCache(body.data.dispatch.callId, {
dispatch_type = 'CALL_NEW',
dispatch = body.data.dispatch ~= nil and body.data.dispatch or body.data
})
TriggerEvent('SonoranCAD::pushevents:DispatchEvent', GetCallCache()[body.data.dispatch.callId])
return true
end,
EVENT_DISPATCH_EDIT = function(body)
TriggerEvent('SonoranCAD::pushevents:DispatchEdit', GetCallCache()[body.data.dispatch.callId], body.data)
SetCallCache(body.data.dispatch.callId, {
dispatch_type = 'CALL_EDIT',
dispatch = body.data.dispatch ~= nil and body.data.dispatch or body.data
})
TriggerEvent('SonoranCAD::pushevents:DispatchEvent', GetCallCache()[body.data.dispatch.callId])
return true
end,
EVENT_DISPATCH_CLOSED = function(body)
for i = 1, #body.data.callIds do
local id = body.data.callIds[i]
if GetCallCache()[id] ~= nil then
local call = GetCallCache()[id].dispatch
local d = {
dispatch_type = 'CALL_CLOSE',
dispatch = call.dispatch ~= nil and call.dispatch or call
}
d.dispatch.status = 2 -- make sure its updated to closed status
SetCallCache(id, d)
TriggerEvent('SonoranCAD::pushevents:DispatchEvent', d)
return true
else
debugLog(('Unknown call close (call ID %s), current cache: %s'):format(id, json.encode(CallCache)))
return false, 'unknown call close'
end
end
end,
EVENT_DISPATCH_NOTE = function(body)
TriggerEvent('SonoranCAD::pushevents:DispatchNote', GetCallCache()[body.data.callId], body.data)
if GetCallCache()[body.data.callId] ~= nil then
local call = GetCallCache()[body.data.callId].dispatch
local newnotes = {}
table.insert(newnotes, body.data.note)
if call.notes ~= nil then
for k, v in pairs(call.notes) do
table.insert(newnotes, v)
end
end
call.notes = newnotes
SetCallCache(body.data.callId, {
dispatch_type = 'CALL_EDIT',
dispatch = call.dispatch ~= nil and call.dispatch or call
})
return true
else
debugLog(('Unknown call note update (call ID %s), current cache: %s'):format(body.data.callId, json.encode(CallCache)))
return false, 'unknown call note'
end
end,
EVENT_DISPATCH_UNIT_ATTACH = function(body)
-- fetch the call and unit data
local call = GetCallCache()[body.data.callId]
if body.data.idents ~= nil then
idents = body.data.idents
elseif body.data.ident ~= nil then
table.insert(idents, body.data.ident)
end
for i = 1, #idents do
local unit = GetUnitById(idents[i])
debugLog('UNIT: ' .. json.encode(unit))
if call and unit then
TriggerEvent('SonoranCAD::pushevents:UnitAttach', call, GetUnitCache()[unit])
local idx = nil
for x = 1, #call.dispatch.idents do
if call.dispatch.idents[x] == idents[i] then
idx = x
end
end
debugLog('INDEX VALUE: ' .. tostring(idx))
if idx == nil then
table.insert(call.dispatch.idents, idents[i])
SetCallCache(body.data.callId, {
dispatch_type = 'CALL_EDIT',
dispatch = call.dispatch ~= nil and call.dispatch or call
})
end
else
debugLog(('Attach failure, unknown call or unit (C: %s) (U: %s)'):format(json.encode(call), json.encode(unit)))
return false, 'invalid call or unit'
end
end
return true
end,
EVENT_DISPATCH_UNIT_DETACH = function(body)
local call = GetCallCache()[body.data.callId]
local idents = {}
if body.data.idents ~= nil then
idents = body.data.idents
elseif body.data.ident ~= nil then
table.insert(idents, body.data.ident)
end
for i = 1, #idents do
local unit = GetUnitById(idents[i])
if call and unit then
TriggerEvent('SonoranCAD::pushevents:UnitDetach', call, GetUnitCache()[unit])
local idx = nil
for x = 1, #call.dispatch.idents do
if call.dispatch.idents[x] == idents[i] then
idx = x
end
end
if unit ~= nil then
table.remove(call.dispatch.idents, idx)
SetCallCache(body.data.callId, {
dispatch_type = 'CALL_EDIT',
dispatch = call.dispatch ~= nil and call.dispatch or call
})
end
else
debugLog(('Detach failure, unknown call or unit (C: %s) (U: %s)'):format(json.encode(call), json.encode(unit)))
return false, 'invalid call or unit'
end
end
return true
end,
GET_LOGS = function(body)
TriggerEvent('SonoranCAD::pushevents:SendSupportLogs', body.logKey)
return true
end,
EVENT_911 = function(body)
SetEmergencyCache(body.data.call.callId, body.data.call)
TriggerEvent('SonoranCAD::pushevents:IncomingCadCall', body.data.call, body.data.call.metaData, body.data.apiIds)
return true
end,
EVENT_REMOVE_911 = function(body)
for i = 1, #body.data.callIds do
if body.data.callIds[i] then
SetEmergencyCache(body.data.callIds[i], nil)
TriggerEvent('SonoranCAD::pushevents:CadCallRemoved', body.data.callIds[i])
end
end
return true
end,
EVENT_UNIT_PANIC = function(body)
local identIds = body.data.identIds or {}
-- for legacy
if body.data.identId then
table.insert(identIds, body.data.identId)
end
for _, identId in ipairs(identIds) do
local unit = GetUnitById(identId)
if unit then
TriggerEvent('SonoranCAD::pushevents:UnitPanic', unit, identId, body.data.isPanic)
else
debugLog(('Ignore panic event, unit ident %s not found'):format(tostring(identId)))
end
end
end,
EVENT_STREETSIGN_UPDATED = function(body)
if body == nil or body.data == nil or body.data.signData == nil then
return false, 'invalid data'
end
TriggerEvent('SonoranCAD::pushevents:SmartSignUpdate', body.data.signData)
return true
end,
EVENT_RECORD_ADD = function(body)
TriggerEvent('SonoranCAD::pushevents:RecordAdded', body.data.record)
return true
end,
EVENT_RECORD_EDIT = function(body)
TriggerEvent('SonoranCAD::pushevents:RecordEdited', body.data.record)
return true
end,
EVENT_RECORD_REMOVE = function(body)
TriggerEvent('SonoranCAD::pushevents:RecordRemoved', body.data.record)
return true
end,
EVENT_UNIT_GROUP_ADD = function(body)
local idents = {}
if body.identId ~= nil then
table.insert(idents, body.identId)
elseif body.identIds ~= nil then
for _, v in pairs(body.identIds) do
table.insert(idents, v)
end
else
return false, 'invalid data'
end
local payload = {
groupName = body.data.groupName,
idents = idents
}
TriggerEvent('SonoranCAD::pushevents:UnitGroupAdd', payload)
return true
end,
EVENT_UNIT_GROUP_REMOVE = function(body)
TriggerEvent('SonoranCAD::pushevents:UnitGroupRemove', body.data)
return true
end,
EVENT_TONE = function(body)
TriggerEvent('SonoranCAD::pushevents:Tone', body.data)
return true
end
}
SetHttpHandler(function(req, res)
local path = req.path
local method = req.method
local base = ''
local file = ''
for word in path:gmatch('[^/]+') do
if base == '' then
base = word
elseif file == '' then
file = word
end
end
if method == 'POST' and path == '/info' then
req.setDataHandler(function(body)
if not body then
res.send(json.encode({
['error'] = 'bad request'
}))
return
end
local data = json.decode(body)
if not data then
res.send(json.encode({
['error'] = 'bad request'
}))
elseif Config.critError and not Config.apiKey then
res.send(json.encode({
['error'] = 'critical config error'
}))
elseif string.upper(data.password) ~= string.upper(Config.apiKey) then
res.send(json.encode({
['error'] = 'bad request'
}))
else
local pluginsFormatted = {}
for name, plugin in pairs(Config.plugins) do
local pl = plugin
for k, v in pairs(pl) do
if type(v) == 'function' then
debugLog('replacing a function')
pl[k] = 'function'
end
end
table.insert(pluginsFormatted, name .. ': ' .. json.encode(pl))
end
res.send(json.encode({
['status'] = 'ok',
['cadInfo'] = string.gsub(dumpInfo(), '\n', '<br />'),
['config'] = table.concat(pluginsFormatted, '<br /><br/>'),
['console'] = string.gsub(GetConsoleBuffer(), '\n', '<br />'),
['debug'] = string.gsub(table.concat(getDebugBuffer(), '\n'), '\n', '<br />')
}))
end
end)
elseif method == 'POST' and path == '/console' then
req.setDataHandler(function(body)
if not body then
res.send(json.encode({
['error'] = 'bad request'
}))
return
end
local data = json.decode(body)
if not data then
res.send(json.encode({
['error'] = 'bad request'
}))
elseif Config.critError and not Config.apiKey then
res.send(json.encode({
['error'] = 'critical config error'
}))
elseif string.upper(data.password) ~= string.upper(Config.apiKey) then
res.send(json.encode({
['error'] = 'bad request'
}))
else
local s = string.gmatch(data.command, '%S+')()
if s ~= 'sonoran' then
res.send(json.encode({
['error'] = 'not allowed'
}))
return
end
ExecuteCommand(data.command)
res.send(json.encode({
['status'] = 'ok',
['output'] = string.gsub(GetConsoleBuffer(), '\n', '<br />')
}))
end
end)
elseif method == 'POST' and path == '/event' then
req.setDataHandler(function(data)
if not data then
res.send(json.encode({
['error'] = 'bad request'
}))
return
end
local body = json.decode(data)
if not body then
res.send(json.encode({
['error'] = 'bad request'
}))
debugLog('Invalid event: ' .. tostring(body))
return
end
if body.key and body.key:upper() == Config.apiKey:upper() then
debugLog(('EVENT: %s - %s'):format(body.type, json.encode(body)))
if Config.enablePushEventForwarding then
PerformHttpRequest(Config.pushEventForwardUrl, function(statusCode, res, headers)
debugLog('Forward Response: ' .. tostring(res))
end, 'POST', data, {
['Content-Type'] = 'application/json'
})
end
if PushEventHandler[body.type:upper()] then
CreateThread(function()
body.res = res
local success, result = PushEventHandler[body.type:upper()](body)
if success then
res.send('ok')
else
if not result then
result = 'error'
end
res.send(result);
end
end)
else
TriggerEvent('SonoranCAD::pushevents:OtherEvent', body.type:upper(), body.data)
res.send('ok - custom')
end
end
end)
elseif method == 'POST' and path == '/pluginevent' then
req.setDataHandler(function(data)
if not data then
res.send(json.encode({
['error'] = 'bad request'
}))
return
end
local body = json.decode(data)
if not body then
res.send(json.encode({
['error'] = 'bad request'
}))
return
end
if body.key and body.key:upper() == Config.apiKey:upper() then
if not body.type or not PluginHttpHandlers[body.type] then
return res.send('error')
end
local resp = PluginHttpHandlers[body.type](body)
return res.send(json.encode(resp))
else
return res.send('error')
end
end)
elseif method == 'GET' and PluginFilePaths[base] ~= nil then
local data = LoadResourceFile(GetCurrentResourceName(), ('filestore/%s/%s'):format(base, file), 'r')
if not data then
warnLog('NOFILE: ' .. tostring(('%s/filestore/%s/%s'):format(GetResourcePath(GetCurrentResourceName()), base, file)))
res.writeHead(404)
res.send('404')
else
res.send(data)
end
elseif method == 'GET' and path:find('^/bodycam') then
-- Extract the query string from the path
local queryString = path:match('?.*$')
local params = {}
-- Parse the query string
if queryString then
for key, value in queryString:gmatch('([^&=?]-)=([^&=?]+)') do
-- URL decode the value
value = value:gsub('+', ' '):gsub('%%(%x%x)', function(h)
return string.char(tonumber(h, 16))
end)
params[key] = value
end
end
-- Extract the 'ident' and 'image' parameters
local ident = params['ident']
local image = params['image']
-- Check if 'ident' and 'image' parameters exist and proceed with your logic
if ident and image then
local imagePath = GetResourcePath(GetCurrentResourceName()) .. '/screenshots/' .. ident .. '/' .. image
-- Your logic here, for example, fetching and sending the image
local imageFile = io.open(imagePath, 'rb')
if not imageFile then
res.send(json.encode({
error = 'Image not found'
}))
return
else
local content = imageFile:read('*all')
res.send(content)
end
-- Respond to the request with the bodycam image or relevant information
else
-- Handle the case where the required parameters are missing
print('Missing \'ident\' or \'image\' parameter')
-- Respond with an error or a message indicating the missing parameters
end
elseif path == '/' then
local html = LoadResourceFile(GetCurrentResourceName(), '/core/html/index.html')
res.send(html)
else
res.send('If you\'re seeing this, sonorancad is loaded.')
end
end)
function AddPluginFilePath(path)
if PluginFilePaths[path] == nil then
PluginFilePaths[path] = true
exports[GetCurrentResourceName()]:CreateFolderIfNotExisting(('%s/filestore/%s'):format(GetResourcePath(GetCurrentResourceName()), path))
end
end
function SaveFileInPluginPath(path, filename, filedata)
if PluginFilePaths[path] ~= nil then
local file = assert(io.open(('%s/filestore/%s/%s'):format(GetResourcePath(GetCurrentResourceName()), path, filename), 'wb+'))
file:write(filedata)
file:close()
debugLog('Saved file: ' .. ('%s/filestore/%s/%s'):format(GetResourcePath(GetCurrentResourceName()), path, filename))
end
end
AddEventHandler('SonoranCAD::pushevents:shim', function(chunk)
local body = json.decode(chunk)
if not body then
debugLog('Invalid event: ' .. tostring(chunk))
return
end
if body.key and body.key:upper() == Config.apiKey:upper() then
if PushEventHandler[body.type:upper()] then
CreateThread(function()
body.res = res
local success, result = PushEventHandler[body.type:upper()](body)
end)
end
end
end)
-- AddPluginFilePath('images', function(path)
-- end)
@@ -0,0 +1,97 @@
const path = require("path");
exports("SaveBase64ToFile", function (base64String, filename) {
let base64Image = base64String.split(";base64,").pop();
fs.writeFile(filename, base64Image, { encoding: "base64" }, function (err) {
return true;
});
});
exports("createScreenshotDirectory", async function (apiID) {
let screenshotFolder = `${GetResourcePath(GetCurrentResourceName())}/screenshots`;
if (!fs.existsSync(screenshotFolder)) {
fs.mkdirSync(screenshotFolder);
}
let dir = `${GetResourcePath(GetCurrentResourceName())}/screenshots/${apiID}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return dir;
});
function deleteFileWithRetry(filePath, maxRetries = 50, interval = 100, attempt = 0) {
try {
fs.unlink(filePath, (err) => {
if (err) {
if (attempt < maxRetries) {
setTimeout(() => {
deleteFileWithRetry(filePath, maxRetries, interval, attempt + 1);
}, interval);
}
}
});
} catch (e) {}
}
async function deleteDirectoryWithRetry(dirPath) {
await fs.rm(dirPath, { recursive: true }, (err) => {
if (err) {
console.log(err);
}
});
}
exports("createScreenshotFilename", async function (directory) {
try {
// Get all jpg files with their full paths
let files = fs
.readdirSync(directory)
.filter((file) => file.endsWith(".jpg"))
.map((file) => ({
name: file,
time: fs.statSync(path.join(directory, file)).mtime.getTime(),
}))
.sort((a, b) => a.time - b.time); // Sort files by modification time, oldest first
let nextFileNumber;
if (files.length >= 10) {
// If we have 10 or more files, increment the highest number by 1 and delete the oldest file if more than 10 files exist
const highestNumber = Math.max(...files.map((file) => parseInt(file.name.replace(".jpg", ""), 10)));
nextFileNumber = highestNumber + 1;
if (files.length > 10) {
// Delete the oldest file
const oldestFile = files[0].name;
deleteFileWithRetry(path.join(directory, oldestFile));
}
} else {
// If less than 10 files, find the first number not used
let existingNumbers = files.map((file) => parseInt(file.name, 10));
nextFileNumber = 1;
for (let i = 1; i <= 10; i++) {
if (!existingNumbers.includes(i)) {
nextFileNumber = i;
break;
}
}
}
return `${nextFileNumber}.jpg`;
} catch (e) {
console.error(e);
}
});
exports("deleteDirectory", async function (dir) {
try {
if (fs.existsSync(dir)) {
deleteDirectoryWithRetry(dir);
}
} catch (e) {}
});
exports("clearScreenshotsFolder", async function () {
try {
let dir = `${GetResourcePath(GetCurrentResourceName())}/screenshots`;
if (fs.existsSync(dir)) {
deleteDirectoryWithRetry(dir);
}
} catch (e) {}
});
@@ -0,0 +1,74 @@
light_port = 9990
light_last_event = "restore"
local function runEvent(event)
if event == nil then
return
end
if event == light_last_event or (light_last_event == "panic" and event ~= "restore") then
return
end
light_last_event = event
debugLog("send light event "..json.encode({ type = "light_event", event = event, port = light_port }))
SendNUIMessage({ type = "light_event", event = event, port = light_port })
end
local function vehicleSignalState(veh)
local lights = GetVehicleIndicatorLights(veh)
if lights ~= nil then
if lights == 0 then
return "restore"
elseif lights == 1 then
return "left"
elseif lights == 2 then
return "right"
elseif lights == 3 then
return "hazard"
else
return "restore"
end
else
return "restore"
end
end
local function vehicleEmergencyState(veh)
return (IsVehicleSirenOn(veh) == 1)
end
CreateThread(function()
while not NetworkIsPlayerActive(PlayerId()) do
Wait(10)
end
while true do
local ped = GetPlayerPed(PlayerId())
if IsPedInAnyVehicle(ped, false) then
local veh = GetVehiclePedIsIn(ped, false)
if veh then
if vehicleEmergencyState(veh) then
runEvent("lights")
else
local state = vehicleSignalState(veh)
if state ~= light_last_event then
runEvent(state)
end
end
end
else
runEvent("restore")
end
Wait(1000)
end
end)
RegisterCommand("setlightport", function(source, args, rawCommand)
local port = args[1]
if args[1] == nil or not tonumber(args[1]) then
return print("Invalid argument.")
end
port = tonumber(port)
if port < 1 or port > 65535 then
return print("Invalid port")
end
light_port = port
end)
@@ -0,0 +1,146 @@
local MessageBuffer = {}
local DebugBuffer = {}
local ErrorBuffer = {}
local ErrorCodes = {
['STEAM_ERROR'] = "You have set SonoranCAD to Steam mode, but have not configured a Steam Web API key. Please see FXServer documentation. SonoranCAD will not function in Steam mode without this set.",
['PORT_MISSING_ERROR'] = "Could not find valid server information for server ID %s. Ensure you have configured your server in the CAD before using the map or push events.",
['PORT_CONFIG_ERROR'] = "CONFIGURATION PROBLEM: Your current game server port (%s) does not match your CAD configuration (%s). Please ensure they match.",
['MAP_CONFIG_ERROR'] = "CONFIGURATION PROBLEM: Map port on the server (%s) does not match your CAD configuration (%s) for server ID (%s). Please ensure they match.",
['PORT_OUTBOUND_ERROR'] = "CONFIGURATION PROBLEM: Detected outbound IP (%s), but (%s) is configured in the CAD. They must match!",
['PORT_OUTBOUND_MISMATCH'] = "CONFIGURATION PROBLEM: Detected IP (%s), but (%s) is configured in the CAD. They must match!",
['CONFIG_ERROR'] = "Failed to load core configuration. Ensure config.json is present and is the correct format.",
['API_ERROR'] = "Failed to get version information. Is the API down? Please restart sonorancad.",
['API_PAID_ONLY'] = "ERROR: Your community cannot use any plugins requiring the API. Please purchase a subscription of Standard or higher.",
['ERROR_ABORT'] = "Aborted startup due to critical errors reported. Review logs for troubleshooting.",
['PLUGIN_DEPENDENCY_ERROR'] = "Submodule %s requires %s, which is not loaded! Skipping.",
['PLUGIN_VERSION_MISMATCH'] = "PLUGIN ERROR: Plugin %s requires %s at version %s or higher, but only %s was found. Use the command \"sonoran pluginupdate\" to check for updates.",
['PLUGIN_CONFIG_OUTDATED'] = "Plugin Updater: %s has a new configuration version (%s ~= %s). You should look at the template configuration file (%s_config.dist.lua) and update your configuration before using this plugin.",
['PLUGIN_CORE_OUTDATED'] = "PLUGIN ERROR: Plugin %s requires Core Version %s, but you have %s. Please update SonoranCAD to use this plugin. Force disabled.",
['CUSTOM_POSTALS_FILE_NOT_FOUND'] = "Your custom postals file %s could not be found in the /sonorancad/submodules/postals/ directory. Please ensure it exists and is not corrupted.",
['POSTAL_RESOURCE_MISSING'] = "The configured postals resource (%s) does not exist. Please check the name.",
['POSTAL_RESOURCE_STOPPED'] = "The postals resource (%s) is not started. Please ensure it's started before clients connect. This is only a warning.",
['POSTAL_RESOURCE_BAD_STATE'] = "The configured postals resource (%s) is in a bad state (%s). Please check it.",
['POSTAL_FILE_READ_ERROR'] = "Failed to open postals file for reading",
['POSTAL_CUSTOM_RESOURCE_FILE_ERROR'] = "Failed to locate postal file from resource %s! Please ensure that it is defined in %s's fxmanifest.lua as 'postal_file'",
['IDCARD_RESOURCE_NOT_STARTED'] = "The sonoran_idcard resource seems to be stopped, this resource is required for the ID card UI to work. Attempting to start the resource. Please add it to your server.cfg to run by default.",
['IDCARD_RESOURCE_MISSING'] = "The sonoran_idcard resource seems to be missing, this resource is required for the the ID card UI to work. Please ensure the resource name is exactly: 'sonoran_idcard' or install it from our GitHub: https://github.com/Sonoran-Software/id_card_ui",
['IDCARD_RESOURCE_BAD_STATE'] = "The sonoran_idcard resource is in a bad state. Please check it.",
['INCORRECT_WKWARS2X_VERSION'] = "It appears that you are using an incorrect version of the resource wk_wars2x. Please ensure you install the version from our GitHub: https://github.com/Sonoran-Software/wk_wars2x. The version you are using is not compatible with SonoranCAD.",
}
function getErrorText(err)
return ErrorCodes[err]
end
local function LocalTime()
local _, _, _, h, m, s = GetLocalTime()
return '' .. h .. ':' .. m .. ':' .. s
end
local function sendConsole(level, color, message)
local debugging = true
if Config ~= nil then
debugging = (Config.debugMode == true and Config.debugMode ~= "false")
end
local time = os and os.date("%X") or LocalTime()
local info = debug.getinfo(3, 'S')
local source = "."
if info.source:find("@@sonorancad") then
source = info.source:gsub("@@sonorancad/","")..":"..info.linedefined
end
local msg = ("[%s][%s:%s%s^7]%s %s^0"):format(time, debugging and source or "SonoranCAD", color, level, color, message)
if (debugging and level == "DEBUG") or (not debugging and level ~= "DEBUG") then
print(msg)
end
if (level == "ERROR" or level == "WARNING") and IsDuplicityVersion() then
table.insert(ErrorBuffer, 1, msg)
end
if level == "DEBUG" and IsDuplicityVersion() then
if #DebugBuffer > 50 then
table.remove(DebugBuffer)
end
table.insert(DebugBuffer, 1, msg)
else
if not IsDuplicityVersion() then
if #MessageBuffer > 10 then
table.remove(MessageBuffer)
end
table.insert(MessageBuffer, 1, msg)
end
end
end
function getDebugBuffer()
return DebugBuffer
end
function getErrorBuffer()
return ErrorBuffer
end
function debugLog(message)
if Config == nil then
return
end
sendConsole("DEBUG", "^7", message)
end
function debugPrint(message)
debugLog(message)
end
function logError(err, msg)
local o = ""
if msg == nil then
o = ("ERR %s: %s - See https://sonoran.software/errorcodes for more information."):format(err, ErrorCodes[err])
else
o = ("ERR %s: %s - See https://sonoran.software/errorcodes for more information."):format(err, msg)
end
sendConsole("ERROR", "^1", o)
end
function errorLog(message)
sendConsole("ERROR", "^1", message)
end
function warnLog(message)
sendConsole("WARNING", "^3", message)
end
function infoLog(message)
sendConsole("INFO", "^5", message)
end
--RegisterServerEvent("SonoranCAD::core:writeLog")
AddEventHandler("SonoranCAD::core:writeLog", function(level, message)
if level == "debug" then
debugLog(message)
elseif level == "info" then
infoLog(message)
elseif level == "error" then
errorLog(message)
elseif level == "warn" then
warnLog(message)
else
debugLog(message)
end
end)
RegisterNetEvent("SonoranCAD::core:RequestLogBuffer")
AddEventHandler("SonoranCAD::core:RequestLogBuffer", function()
if not IsDuplicityVersion() then
TriggerServerEvent("SonoranCAD::core:LogBuffer", MessageBuffer)
print("log buffer requested")
end
end)
print(("^5%s^0"):format([[
_____ _________ ____
/ ___/____ ____ ____ _________ _____ / ____/ | / __ \
\__ \/ __ \/ __ \/ __ \/ ___/ __ `/ __ \/ / / /| | / / / /
___/ / /_/ / / / / /_/ / / / /_/ / / / / /___/ ___ |/ /_/ /
/____/\____/_/ /_/\____/_/ \__,_/_/ /_/\____/_/ |_/_____/
]]))
infoLog("Starting up...")
@@ -0,0 +1,148 @@
--[[
SonoranCAD FiveM Integration
Plugin Loader
Provides logic for checking loaded plugins after startup
]]
local function LoadVersionFile()
local f = LoadResourceFile(GetCurrentResourceName(), ("version.json"))
if f then
return f
else
warnLog(("Failed to load version file from /sonorancad/version.json Check to see if the file exists."))
return nil
end
end
function CheckForPluginUpdate(name)
local plugin = Config.plugins[name]
plugin.check_url = 'https://raw.githubusercontent.com/Sonoran-Software/SonoranCADFiveM/refs/heads/master/sonorancad/version.json'
if plugin == nil then
errorLog(("Submodule %s not found."):format(name))
return
elseif plugin.check_url == nil or plugin.check_url == "" then
debugLog(("Submodule %s does not have check_url set. Is it configured correctly?"):format(name))
return
end
PerformHttpRequestS(plugin.check_url, function(code, data, headers)
if code == 200 then
local remote = json.decode(data)
if remote == nil then
if plugin.enabled then
warnLog(("Failed to get a valid response for %s. Skipping."):format(k))
debugLog(("Raw output for %s: %s"):format(k, data))
end
elseif (remote.submoduleConfigs[name].version ~= nil and plugin.configVersion ~= nil) then
local configCompare = compareVersions(remote.submoduleConfigs[name].version, plugin.configVersion)
if configCompare.result and not Config.debugMode then
if plugin.enabled then
errorLog(("Submodule Updater: %s has a new configuration version. You should look at the template configuration file (%s_config.dist.lua) and update your configuration before using this submodule. Guide: https://sonoran.link/config-update"):format(name, name))
Config.plugins[name].enabled = false
Config.plugins[name].disableReason = "outdated config file"
end
else
debugLog(("Submodule %s has the same configuration version."):format(name))
local distConfig = LoadResourceFile(GetCurrentResourceName(), ("/configuration/%s_config.dist.lua"):format(name))
local normalConfig = LoadResourceFile(GetCurrentResourceName(), ("/configuration/%s_config.lua"):format(name))
if distConfig and normalConfig then
local filePath = ("%s/configuration/config-backup"):format(GetResourcePath(GetCurrentResourceName()))
exports['sonorancad']:CreateFolderIfNotExisting(filePath)
local backupFile = io.open(("%s/configuration/config-backup/%s_config.lua"):format(GetResourcePath(GetCurrentResourceName()), name), "w")
backupFile:write(distConfig)
backupFile:close()
os.remove(("%s/configuration/%s_config.dist.lua"):format(GetResourcePath(GetCurrentResourceName()), name))
debugLog(("Submodule %s configuration file is up to date. Backup saved."):format(name))
end
end
elseif name == 'locations' then
if (remote.submoduleConfigs[name].version ~= nil and plugin.pluginVersion ~= nil) then
local configCompare = compareVersions(remote.submoduleConfigs[name].version, plugin.pluginVersion)
if configCompare.result and not Config.debugMode then
if plugin.enabled then
errorLog(("Submodule Updater: %s has a new configuration version. You should look at the template configuration file (%s_config.dist.lua) and update your configuration before using this submodule."):format(name, name))
Config.plugins[name].enabled = false
Config.plugins[name].disableReason = "outdated config file"
end
else
debugLog(("Submodule %s has the same configuration version."):format(name))
local distConfig = LoadResourceFile(GetCurrentResourceName(), ("/configuration/%s_config.dist.lua"):format(name))
local normalConfig = LoadResourceFile(GetCurrentResourceName(), ("/configuration/%s_config.lua"):format(name))
if distConfig and normalConfig then
local filePath = ("%s/configuration/config-backup"):format(GetResourcePath(GetCurrentResourceName()))
exports['sonorancad']:CreateFolderIfNotExisting(filePath)
local backupFile = io.open(("%s/configuration/config-backup/%s_config.lua"):format(GetResourcePath(GetCurrentResourceName()), name), "w")
backupFile:write(distConfig)
backupFile:close()
os.remove(("%s/configuration/%s_config.dist.lua"):format(GetResourcePath(GetCurrentResourceName()), name))
debugLog(("Submodule %s configuration file is up to date. Backup saved."):format(name))
end
end
end
end
elseif plugin.enabled then
warnLog(("Failed to check submodule config updates for %s: %s %s"):format(name, code, data))
end
end, "GET")
end
CreateThread(function()
Wait(5000)
while Config.apiVersion == -1 do Wait(10) end
if Config.critError then logError("ERROR_ABORT") end
for k, v in pairs(Config.plugins) do
if Config.critError then
Config.plugins[k].enabled = false
Config.plugins[k].disableReason = "Startup aborted"
goto skip
end
local vfile = LoadVersionFile(k)
if vfile == nil then
goto skip
end
local versionFile = json.decode(vfile)
if Config.plugins[k].enabled then
if versionFile.submoduleConfigs[k].requiresPlugins ~= nil then
for _, plugin in pairs(versionFile.submoduleConfigs[k].requiresPlugins) do
local isCritical = plugin.critical
if Config.plugins[plugin.name] == nil or not Config.plugins[plugin.name].enabled then
if isCritical then
logError("PLUGIN_DEPENDENCY_ERROR", getErrorText("PLUGIN_DEPENDENCY_ERROR"):format(k, plugin.name))
Config.plugins[k].enabled = false
Config.plugins[k].disableReason = ("Missing dependency %s"):format(plugin.name)
elseif plugin.name ~= "esxsupport" then
warnLog(("[submodule loader] submodule %s requires %s, but it is not installed. Some features may not work properly."):format(k, plugin.name))
end
end
end
end
end
CheckForPluginUpdate(k)
end
::skip::
local pluginList = {}
local loadedPlugins = {}
local disabledPlugins = {}
local disableFormatted = {}
for name, v in pairs(Config.plugins) do
table.insert(pluginList, name)
if v.enabled then
table.insert(loadedPlugins, name)
else
if v.disableReason == nil then
v.disableReason = "disabled in config"
end
disabledPlugins[name] = v.disableReason
end
end
infoLog(("Available Submodules: %s"):format(table.concat(pluginList, ", ")))
infoLog(("Loaded Submodules: %s"):format(table.concat(loadedPlugins, ", ")))
for name, reason in pairs(disabledPlugins) do
table.insert(disableFormatted, ("%s (%s)"):format(name, reason))
end
if #disableFormatted > 0 then
infoLog(("Disabled Submodules: %s"):format(
table.concat(disableFormatted, ", ")))
end
end)
@@ -0,0 +1,33 @@
latestFrame = {};
RegisterNetEvent('SonoranCAD::core:TakeScreenshot', function()
local source = source
local unit = GetUnitByPlayerId(source)
if unit == nil then
debugLog('Unit not found')
-- TriggerClientEvent('SonoranCAD::core::ScreenshotOff', source)
return
end
local screenshotDirectory = exports['sonorancad']:createScreenshotDirectory(tostring(unit.id))
local screenshotName = exports['sonorancad']:createScreenshotFilename(screenshotDirectory)
local frameName = screenshotName:gsub("%.jpg$", "")
latestFrame[source] = tonumber(frameName)
exports['screenshot-basic']:requestClientScreenshot(source, {
fileName = screenshotDirectory .. '/' .. screenshotName,
quality = 0.5
}, function()
end)
end)
RegisterNetEvent('SonoranCAD::core::bodyCamOff', function()
local source = source
latestFrame[source] = nil
local unit = GetUnitByPlayerId(source)
if unit == nil then
debugLog('Unit not found')
-- TriggerClientEvent('SonoranCAD::core::ScreenshotOff', source)
return
end
local screenshotDirectory = exports['sonorancad']:createScreenshotDirectory(tostring(unit.id))
exports['sonorancad']:deleteDirectory(screenshotDirectory)
end)
@@ -0,0 +1,530 @@
Plugins = {}
ApiUrls = {
production = "https://api.sonorancad.com/",
development = "https://cadapi.dev.sonoransoftware.com/"
}
function getApiUrl()
if Config.mode == nil then
return ApiUrls.production
else
if ApiUrls[Config.mode] ~= nil then
return ApiUrls[Config.mode]
else
Config.critError = true
assert(false, "Invalid mode. Valid values are production, development")
end
end
end
CreateThread(function()
infoLog("Starting SonoranCAD from "..GetResourcePath("sonorancad"))
Config.apiUrl = getApiUrl()
exports['sonorancad']:clearScreenshotsFolder()
performApiRequest({}, "GET_VERSION", function(result, ok)
if not ok then
logError("API_ERROR")
Config.critError = true
return
end
Config.apiVersion = tonumber(string.sub(result, 1, 1))
if Config.apiVersion < 2 then
logError("API_PAID_ONLY")
Config.critError = true
end
debugLog(("Set version %s from response %s"):format(Config.apiVersion, result))
infoLog(("Loaded community ID %s with API URL: %s"):format(Config.communityID, Config.apiUrl))
end)
if Config.primaryIdentifier == "steam" and (GetConvar("steam_webapiKey", "none") == "none" or GetConvar("steam_webapiKey", "none") == "") then
logError("STEAM_ERROR")
Config.critError = true
end
local versionfile = json.decode(LoadResourceFile(GetCurrentResourceName(), "/version.json"))
local fxversion = versionfile.testedFxServerVersion
local currentFxVersion = getServerVersion()
if currentFxVersion ~= nil and fxversion ~= nil then
if tonumber(currentFxVersion) < tonumber(fxversion) then
warnLog(("SonoranCAD has been tested with FXServer version %s, but you're running %s. Please update ASAP."):format(fxversion, currentFxVersion))
end
end
if GetResourceState("sonoran_updatehelper") == "started" then
ExecuteCommand("stop sonoran_updatehelper")
end
manuallySetUnitCache() -- set unit cache on startup
end)
exports("getCadVersion", function()
return Config.apiVersion
end)
-- Toggles API sender.
RegisterServerEvent("cadToggleApi")
AddEventHandler("cadToggleApi", function()
Config.apiSendEnabled = not Config.apiSendEnabled
end)
--[[
Sonoran CAD API Handler - Core Wrapper
]]
ApiEndpoints = {
["UNIT_LOCATION"] = "emergency",
["CALL_911"] = "emergency",
["UNIT_PANIC"] = "emergency",
["GET_VERSION"] = "general",
["GET_SERVERS"] = "general",
["ATTACH_UNIT"] = "emergency",
["DETACH_UNIT"] = "emergency",
["ADD_CALL_NOTE"] = "emergency",
["RECORD_ADD"] = "general",
["RECORD_UPDATE"] = "general",
["SET_SERVERS"] = "general",
["GET_CHARACTERS"] = "civilian",
["EDIT_CHARACTER"] = "civilian",
["NEW_RECORD"] = "general",
["EDIT_RECORD"] = "general",
["REMOVE_RECORD"] = "general",
["GET_TEMPLATES"] = "general",
["LOOKUP_INT"] = "general",
}
EndpointsRequireId = {
["UNIT_STATUS"] = true,
["KICK_UNIT"] = true,
["UNIT_PANIC"] = true,
["UNIT_LOCATION"] = true,
["NEW_CHARACTER"] = true,
["REMOVE_CHARACTER"] = true,
["EDIT_CHARACTER"] = true,
["GET_CHARACTERS"] = true,
["CHECK_APIID"] = true,
["APPLY_PERMISSION_KEY"] = true,
["BAN_USER"] = true,
["KICK_USER"] = true
}
function registerApiType(type, endpoint)
ApiEndpoints[type] = endpoint
end
exports("registerApiType", registerApiType)
local rateLimitedEndpoints = {}
function performApiRequest(postData, type, cb)
-- apply required headers
local payload = {}
payload["id"] = Config.communityID
payload["key"] = Config.apiKey
payload["data"] = postData
payload["type"] = type
local endpoint = nil
if ApiEndpoints[type] ~= nil then
endpoint = ApiEndpoints[type]
else
return warnLog(("API request failed: endpoint %s is not registered. Use the registerApiType function to register this endpoint with the appropriate type."):format(type))
end
if not cb then
cb = function() end
end
local url = ""
if endpoint == "support" then
apiUrl = "https://api.sonoransoftware.com/"
url = apiUrl..tostring(endpoint).."/"
else
apiUrl = getApiUrl()
url = apiUrl..tostring(endpoint).."/"..tostring(type:lower())
end
assert(type ~= nil, "No type specified, invalid request.")
if Config.critError then
return
elseif not Config.apiSendEnabled then
warnLog("API sending is disabled, ignoring request.")
return
end
if rateLimitedEndpoints[type] == nil then
PerformHttpRequestS(url, function(statusCode, res, headers)
debugLog(("type %s called with post data %s to url %s"):format(type, json.encode(payload), url))
if statusCode == 200 and res ~= nil then
debugLog("result: "..tostring(res))
if res == "Sonoran CAD: Backend Service Reached" or res == "Backend Service Reached" then
errorLog(("API ERROR: Invalid endpoint (URL: %s). Ensure you're using a valid endpoint."):format(url))
else
if res == nil then
res = {}
debugLog("Warning: Response had no result, setting to empty table.")
end
cb(res, true)
end
elseif statusCode == 400 then
warnLog("Bad request was sent to the API. Enable debug mode and retry your request. Response: "..tostring(res))
-- additional safeguards
if res == "INVALID COMMUNITY ID"
or res == "API IS NOT ENABLED FOR THIS COMMUNITY"
or string.find(res, "IS NOT ENABLED FOR THIS COMMUNITY")
or res == "INVALID API KEY" then
errorLog("Fatal: Disabling API - an error was encountered that must be resolved. Please restart the resource after resolving: "..tostring(res))
Config.apiSendEnabled = false
end
cb(res, false)
elseif statusCode == 404 then -- handle 404 requests, like from CHECK_APIID
debugLog("404 response found")
cb(res, false)
elseif statusCode == 429 then -- rate limited :(
if rateLimitedEndpoints[type] then
-- don't warn again, it's spammy. Instead, just print a debug
debugLog(("Endpoint %s ratelimited. Dropping request."))
return
end
rateLimitedEndpoints[type] = true
warnLog(("WARN_RATELIMIT: You are being ratelimited (last request made to %s) - Ignoring all API requests to this endpoint for 60 seconds. If this is happening frequently, please review your configuration to ensure you're not sending data too quickly."):format(type))
SetTimeout(60000, function()
rateLimitedEndpoints[type] = nil
infoLog(("Endpoint %s no longer ignored."):format(type))
end)
elseif string.match(tostring(statusCode), "50") then
errorLog(("API error returned (%s). Check status.sonoransoftware.com or our Discord to see if there's an outage."):format(statusCode))
debugLog(("API_ERROR Error returned: %s %s"):format(statusCode, res))
else
errorLog(("CAD API ERROR (from %s): %s %s"):format(url, statusCode, res))
end
end, "POST", json.encode(payload), {["Content-Type"]="application/json"})
else
debugLog(("Endpoint %s is ratelimited. Dropped request: %s"):format(type, json.encode(payload)))
end
end
exports("performApiRequest", performApiRequest)
-- Metrics
CreateThread(function()
registerApiType("HEARTBEAT", "general")
while true do
-- Wait a few seconds for server startup
Wait(5000)
local coreVersion = GetResourceMetadata(GetCurrentResourceName(), "version", 0)
SetConvarServerInfo("SonoranCAD", coreVersion)
local plugins = {}
local playerCount = GetNumPlayerIndices()
for k, v in pairs(Config.plugins) do
table.insert(plugins, {["name"] = k, ["version"] = v.version, ["latest"] = v.latestVersion, ["enabled"] = v.enabled})
end
local payload = {
coreVersion = coreVersion,
commId = Config.communityID,
playerCount = playerCount,
serverId = Config.serverId,
fxVersion = getServerVersion(),
plugins = plugins,
ingressUrl = GetConvar("web_baseUrl", "")
}
performApiRequest(payload, "HEARTBEAT", function() end)
Wait(1000*60*60)
end
end)
if Config.devHiddenSwitch then
RegisterCommand("cc", function()
TriggerClientEvent("chat:clear", -1)
end)
end
-- Missing identifier detection
RegisterNetEvent("SonoranCAD::core:PlayerReady")
AddEventHandler("SonoranCAD::core:PlayerReady", function()
local ids = GetIdentifiers(source)
if ids[Config.primaryIdentifier] == nil then
warnLog(("Player %s connected, but did not have an %s ID."):format(source, Config.primaryIdentifier))
end
end)
-- Jordan - Add universal handler for 911 calls
--[[
SonoranCAD API Handler - 911 Calls
@param caller string
@param location string
@param description string
@param postal number
@param plate string (optional)
@param cb function
@param silenceAlert boolean
@param useCallLocation boolean
]]
function call911(caller, location, description, postal, plate, cb, silenceAlert, useCallLocation)
if not silenceAlert then
silenceAlert = false
end
if not useCallLocation then
useCallLocation = false
end
exports['sonorancad']:performApiRequest({
{
['serverId'] = GetConvar('sonoran_serverId', 1),
['isEmergency'] = true,
['caller'] = caller,
['location'] = location,
['description'] = description,
['metaData'] = {
['plate'] = plate,
['postal'] = postal,
['useCallLocation'] = useCallLocation,
['silenceAlert'] = silenceAlert
}
}
}, 'CALL_911', cb)
end
RegisterNetEvent('SonoranScripts::Call911', function(caller, location, description, postal, plate, cb, silenceAlert, useCallLocation)
call911(caller, location, description, postal, plate, function(response)
json.encode(response) -- Not, CB's can only be used on the server side, so we just print this here for you to see.
end, silenceAlert, useCallLocation)
end)
-- Jordan - CAD Utils
dispatchOnline = false
ActiveDispatchers = {}
registerEndpoints = function()
exports['sonorancad']:registerApiType('MODIFY_BLIP', 'emergency')
exports['sonorancad']:registerApiType('ADD_BLIP', 'emergency')
exports['sonorancad']:registerApiType('REMOVE_BLIP', 'emergency')
exports['sonorancad']:registerApiType('GET_BLIPS', 'emergency')
exports['sonorancad']:registerApiType('MODIFY_BLIP', 'emergency')
exports['sonorancad']:registerApiType('CALL_911', 'emergency')
exports['sonorancad']:registerApiType('ADD_CALL_NOTE', 'emergency')
exports['sonorancad']:registerApiType('REMOVE_911', 'emergency')
exports['sonorancad']:registerApiType('LOOKUP', 'general')
exports['sonorancad']:registerApiType('SET_CALL_POSTAL', 'emergency')
exports['sonorancad']:registerApiType('GET_ACTIVE_UNITS', 'emergency')
end
addBlip = function(coords, colorHex, subType, toolTip, icon, dataTable, cb)
local data = {
{
['serverId'] = GetConvar('sonoran_serverId', 1),
['blip'] = {
['id'] = -1,
['subType'] = subType,
['coordinates'] = {
['x'] = coords.x,
['y'] = coords.y
},
['icon'] = icon,
['color'] = colorHex,
['tooltip'] = toolTip,
['data'] = dataTable
}
}
}
exports['sonorancad']:performApiRequest(data, 'ADD_BLIP', function(res)
if cb ~= nil then
cb(res)
end
end)
end
addBlips = function(blips, cb)
exports['sonorancad']:performApiRequest(blips, 'ADD_BLIP', function(res)
if cb ~= nil then
cb(res)
end
end)
end
removeBlip = function(ids, cb)
exports['sonorancad']:performApiRequest({
{
['ids'] = ids
}
}, 'REMOVE_BLIP', function(res)
if cb ~= nil then
cb(res)
end
end)
end
modifyBlipd = function(blipId, dataTable)
exports['sonorancad']:performApiRequest({
{
['id'] = blipId,
['data'] = dataTable
}
}, 'MODIFY_BLIP', function(_)
end)
end
getBlips = function(cb)
local data = {
{
['serverId'] = GetConvar('sonoran_serverId', 1)
}
}
exports['sonorancad']:performApiRequest(data, 'GET_BLIPS', function(res)
if cb ~= nil then
cb(res)
end
end)
end
removeWithSubtype = function(subType, cb)
getBlips(function(res)
local dres = json.decode(res)
local ids = {}
if type(dres) == 'table' then
for _, v in ipairs(dres) do
if v.subType == subType then
table.insert(ids, #ids + 1, v.id)
end
end
if #ids > 0 then
removeBlip(ids, cb)
end
else
warnLog('No blips were returned.')
end
end)
end
call911 = function(caller, location, description, postal, plate, cb)
exports['sonorancad']:performApiRequest({
{
['serverId'] = GetConvar('sonoran_serverId', 1),
['isEmergency'] = true,
['caller'] = caller,
['location'] = location,
['description'] = description,
['metaData'] = {
['plate'] = plate,
['postal'] = postal
}
}
}, 'CALL_911', cb)
end
addTempBlipData = function(blipId, blipData, waitSeconds, returnToData)
exports['sonorancad']:performApiRequest({
{
['id'] = blipId,
['data'] = blipData
}
}, 'MODIFY_BLIP', function(_)
end)
Citizen.CreateThread(function()
Citizen.Wait(waitSeconds * 1000)
exports['sonorancad']:performApiRequest({
{
['id'] = blipId,
['data'] = returnToData
}
}, 'MODIFY_BLIP', function(_)
end)
end)
end
addTempBlipColor = function(blipId, color, waitSeconds, returnToColor)
exports['sonorancad']:performApiRequest({
{
['id'] = blipId,
['color'] = color
}
}, 'MODIFY_BLIP', function(_)
end)
Citizen.CreateThread(function()
Citizen.Wait(waitSeconds * 1000)
exports['sonorancad']:performApiRequest({
{
['id'] = blipId,
['color'] = returnToColor
}
}, 'MODIFY_BLIP', function(_)
end)
end)
end
remove911 = function(callId)
exports['sonorancad']:performApiRequest({
{
['serverId'] = GetConvar('sonoran_serverId', 1),
['callId'] = callId
}
}, 'REMOVE_911', function(_)
end)
end
addCallNote = function(callId, caller)
exports['sonorancad']:performApiRequest({
{
['serverId'] = GetConvar('sonoran_serverId', 1),
['callId'] = callId,
['note'] = caller
}
}, 'ADD_CALL_NOTE', function(_)
end)
end
setCallPostal = function(callId, postal)
exports['sonorancad']:performApiRequest({
{
['serverId'] = GetConvar('sonoran_serverId', 1),
['callId'] = callId,
['postal'] = postal
}
}, 'SET_CALL_POSTAL', function(_)
end)
end
performLookup = function(plate, cb)
exports['sonorancad']:performApiRequest({
{
['types'] = {
2,
3
},
['plate'] = plate,
['partial'] = false,
['first'] = '',
['last'] = '',
['mi'] = ''
}
}, 'LOOKUP', function(res)
if cb ~= nil then
cb(res)
end
end)
end
checkCADSubscriptionType = function()
while exports['sonorancad']:getCadVersion() == nil or exports['sonorancad']:getCadVersion() == -1 do
Citizen.Wait(100)
end
local version = exports['sonorancad']:getCadVersion()
if version ~= 4 and version == 3 then
errorLog('The live map blip feature require the Pro plan for the CAD. It will be disabled for this run.'
.. ' We recommend either upgrading your plan or disabling this feature in the config file.')
Config.integration.SonoranCAD_integration.addLiveMapBlips = false
Config.modified = true
TriggerClientEvent(GetCurrentResourceName() .. '::ModifiedConfig', -1, Config)
elseif version ~= 4 and version ~= 3 and version ~= 5 and version ~= 6 then
errorLog('SonoranCAD integration with this script requires at least a Plus plan for the CAD. It will be'
.. ' disabled for this run. We recommend either upgrading your plan or disabling this' .. ' feature in the config file.')
Config.integration.SonoranCAD_integration.use = false
Config.modified = true
TriggerClientEvent(GetCurrentResourceName() .. '::ModifiedConfig', -1, Config)
end
end
getDispatchStatus = function(_)
return dispatchOnline
end
exports('registerEndpoints', registerEndpoints)
exports('addBlip', addBlip)
exports('addBlips', addBlips)
exports('removeBlip', removeBlip)
exports('modifyBlipd', modifyBlipd)
exports('getBlips', getBlips)
exports('removeWithSubtype', removeWithSubtype)
exports('call911', call911)
exports('addTempBlipData', addTempBlipData)
exports('addTempBlipColor', addTempBlipColor)
exports('remove911', remove911)
exports('addCallNote', addCallNote)
exports('setCallPostal', setCallPostal)
exports('performLookup', performLookup)
exports('checkCADSubscriptionType', checkCADSubscriptionType)
exports('getDispatchStatus', getDispatchStatus)
-- Jordan - CAD Utils
@@ -0,0 +1,113 @@
function shallowcopy(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in pairs(orig) do
copy[orig_key] = orig_value
end
else -- number, string, boolean, etc
copy = orig
end
return copy
end
function stringsplit(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t={} ; i=1
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
t[i] = str
i = i + 1
end
return t
end
-- Helper function to determine index of given identifier
function findIndex(identifier)
for i,loc in ipairs(LocationCache) do
if loc.apiId == identifier then
return i
end
end
end
function GetIdentifiers(player)
local ids = {}
for _, id in ipairs(GetPlayerIdentifiers(player)) do
local split = stringsplit(id, ":")
ids[split[1]] = split[2]
end
--debugLog("Returning "..json.encode(ids))
return ids
end
function isPluginLoaded(pluginName)
for k, v in pairs(Plugins) do
if v == pluginName then
return true
end
end
return false
end
exports('isPluginLoaded', isPluginLoaded)
function PerformHttpRequestS(url, cb, method, data, headers)
if not data then
data = ""
end
if not headers then
headers = {["X-User-Agent"] = "SonoranCAD"}
end
exports["sonorancad"]:HandleHttpRequest(url, cb, method, data, headers)
end
function has_value(tab, val)
if tab == nil then
debugLog("nil passed to has_value, ignore")
return false
end
for index, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
function getServerVersion()
local s = GetConvar("version", "")
local v = s:find("v1.0.0.")
local e = string.gsub(s:sub(v),"v1.0.0.","")
local i = e:sub(1, string.len(e) - e:find(" "))
return i
end
function compareVersions(version1, version2)
local v1, v2, v3 = version1:match("(%d+)%.(%d*)%.?(%d*)")
local r1, r2, r3 = version2:match("(%d+)%.(%d*)%.?(%d*)")
-- Convert to numbers and default to 0 for minor and patch if missing
v1, v2, v3 = tonumber(v1) or 0, tonumber(v2) or 0, tonumber(v3) or 0
r1, r2, r3 = tonumber(r1) or 0, tonumber(r2) or 0, tonumber(r3) or 0
-- Calculate parsed versions with proper weights
local parsedVersion1 = v1 * 10000 + v2 * 100 + v3
local parsedVersion2 = r1 * 10000 + r2 * 100 + r3
-- Create debug log table
local tbl = {
result = (parsedVersion2 < parsedVersion1),
parsedVersion1 = parsedVersion1,
parsedVersion2 = parsedVersion2,
version1 = version1,
version2 = version2
}
debugLog(json.encode(tbl))
return tbl
end
@@ -0,0 +1,268 @@
local UnitCache = {}
local CallCache = {}
local EmergencyCache = {}
local PlayerUnitMapping = {}
local function findUnitById(identIds)
if identIds == nil then
return nil
end
for k, v in pairs(UnitCache) do
if type(identIds) == "number" then
if identIds == v.id then
return k
end
else
local ids = nil
if v.data ~= nil then
ids = v.data.apiIds
else
ids = v.apiIds
end
for _, id in pairs(ids) do
if has_value(identIds, id) then
return k
end
end
end
end
return nil
end
function GetSourceByApiId(apiIds)
if apiIds == nil then return nil end
for x=1, #apiIds do
for i=0, GetNumPlayerIndices()-1 do
local player = GetPlayerFromIndex(i)
if player then
local identifiers = GetIdentifiers(player)
for type, id in pairs(identifiers) do
if id == apiIds[x] then
return player
end
end
end
end
end
return nil
end
function GetUnitCache() return UnitCache end
function GetCallCache() return CallCache end
function GetEmergencyCache() return EmergencyCache end
function SetUnitCache(k, v)
local key = findUnitById(k)
if key ~= nil and UnitCache[key] ~= nil then
UnitCache[key] = v
else
table.insert(UnitCache, v)
end
end
function SetCallCache(k, v)
CallCache[k] = v
TriggerEvent('SonoranCAD::pushevents:CallCacheUpdated')
end
function SetEmergencyCache(k, v)
EmergencyCache[k] = v
TriggerEvent('SonoranCAD::pushevents:EmergencyCacheUpdated')
end
-- Global function wrapper
function GetUnitById(ids) return findUnitById(ids) end
function GetUnitObjectById(id)
if UnitCache[id] ~= nil then
return UnitCache[id]
else
return nil
end
end
function GetUnitByPlayerId(player)
local identifiers = GetIdentifiers(player)
local ids = {}
for k, v in pairs(identifiers) do
table.insert(ids, v)
end
local index = findUnitById(ids)
if index then
return UnitCache[index]
end
return nil
end
exports('GetUnitByPlayerId', GetUnitByPlayerId)
exports('GetUnitCache', GetUnitCache)
exports('GetCallCache', GetCallCache)
exports('GetEmergencyCache', GetEmergencyCache)
exports('GetUnitById', GetUnitById)
AddEventHandler("playerDropped", function()
local id = GetUnitByPlayerId(source)
local unit = findUnitById(id)
if unit then
TriggerEvent("SonoranCAD::core:RemovePlayer", source, UnitCache[unit])
UnitCache[unit] = nil
end
end)
AddEventHandler("SonoranCAD::pushevents:UnitLogin", function(unit)
local playerId = GetSourceByApiId(unit.data.apiIds)
if playerId then
PlayerUnitMapping[playerId] = unit.id
TriggerEvent("SonoranCAD::core:AddPlayer", playerId, unit)
TriggerClientEvent("SonoranCAD::core:AddPlayer", playerId, unit)
else
debugLog(("Unknown unit %s and player %s"):format(json.encode(unit), playerId))
end
end)
AddEventHandler("SonoranCAD::pushevents:UnitLogout", function(id)
if Config.noUnitTimer then
local key = findUnitById(id)
debugLog(("unitlogout key %s"):format(key))
if key then
local playerId = GetSourceByApiId(UnitCache[key].data.apiIds)
if playerId then
debugLog(("Triggering RemovePlayer on ID %s"):format(playerId))
TriggerEvent("SonoranCAD::core:RemovePlayer", playerId, UnitCache[key])
TriggerClientEvent("SonoranCAD::core:RemovePlayer", playerId)
PlayerUnitMapping[playerId] = nil
end
end
SetUnitCache(id, nil)
end
end)
registerApiType("GET_ACTIVE_UNITS", "emergency")
Citizen.CreateThread(function()
Wait(500)
while Config.apiVersion == -1 do
Wait(1000)
end
if not Config.apiSendEnabled or (Config.noUnitTimer == "true" or Config.noUnitTimer == true) or Config.apiVersion < 3 then
debugLog("Disabling active units routine")
return
end
while true do
local OldUnits = {}
local NewUnits = {}
for k, v in pairs(UnitCache) do
OldUnits[k] = v
end
if GetNumPlayerIndices() > 0 then
local payload = { serverId = Config.serverId, unitsOnly = false }
performApiRequest({payload}, "GET_ACTIVE_UNITS", function(runits)
local allUnits = json.decode(runits)
if allUnits ~= nil then
for k, v in pairs(allUnits) do
local playerId = GetSourceByApiId(v.data.apiIds)
if playerId then
PlayerUnitMapping[playerId] = v.id
table.insert(NewUnits, v)
TriggerEvent("SonoranCAD::core:AddPlayer", playerId, v)
else
debugLog(("Couldn't find unit, not adding %s (%s)"):format(playerId, json.encode(v.data.apiIds)))
end
end
end
for k, v in pairs(OldUnits) do
local exists = false
for _, n in pairs(NewUnits) do
if n.id == v.id then
exists = true
end
end
if not exists then
debugLog(("Removing player %s, not on units list"):format(k))
PlayerUnitMapping[k] = nil
TriggerEvent("SonoranCAD::core:RemovePlayer", k, v)
TriggerClientEvent("SonoranCAD::core:RemovePlayer", k, v)
end
end
UnitCache = {}
for k, v in pairs(NewUnits) do
debugLog("Insert unit "..json.encode(v))
table.insert(UnitCache, v)
end
end)
end
Citizen.Wait(60000)
end
end)
registerApiType("GET_CALLS", "emergency")
CreateThread(function()
Wait(1000)
while Config.apiVersion == -1 do
Wait(10)
end
if not Config.apiSendEnabled or Config.apiVersion < 3 then
debugLog("Too low version or API disabled, skip call caching")
return
end
local payload = { serverId = Config.serverId}
while true do
performApiRequest({payload},"GET_CALLS",function(response)
local calls = json.decode(response)
for k, v in pairs(calls.activeCalls) do
CallCache[v.callId] = { dispatch = v }
end
for k, v in pairs(calls.emergencyCalls) do
EmergencyCache[v.callId] = v
end
end)
Citizen.Wait(60 * 1000)
end
end)
function manuallySetUnitCache()
local OldUnits = {}
local NewUnits = {}
for k, v in pairs(UnitCache) do
OldUnits[k] = v
end
if GetNumPlayerIndices() > 0 then
local payload = { serverId = Config.serverId, unitsOnly = false }
performApiRequest({payload}, "GET_ACTIVE_UNITS", function(runits)
local allUnits = json.decode(runits)
if allUnits ~= nil then
for _, v in pairs(allUnits) do
local playerId = GetSourceByApiId(v.data.apiIds)
if playerId then
PlayerUnitMapping[playerId] = v.id
table.insert(NewUnits, v)
TriggerEvent("SonoranCAD::core:AddPlayer", playerId, v)
else
debugLog(("Couldn't find unit, not adding %s (%s)"):format(playerId, json.encode(v.data.apiIds)))
end
end
end
for k, v in pairs(OldUnits) do
local exists = false
for _, n in pairs(NewUnits) do
if n.id == v.id then
exists = true
end
end
if not exists then
debugLog(("Removing player %s, not on units list"):format(k))
PlayerUnitMapping[k] = nil
TriggerEvent("SonoranCAD::core:RemovePlayer", k, v)
TriggerClientEvent("SonoranCAD::core:RemovePlayer", k, v)
end
end
UnitCache = {}
for _, v in pairs(NewUnits) do
debugLog("Insert unit "..json.encode(v))
table.insert(UnitCache, v)
end
end)
end
end
exports('ManuallySetUnitCache', manuallySetUnitCache())
@@ -0,0 +1,92 @@
var unzipper = require("unzipper");
var fs = require("fs");
exports('UnzipFile', (file, dest) => {
try {
fs.createReadStream(file).pipe(unzipper.Extract({ path: dest}).on('close', () => {
emit("unzipCoreCompleted", true);
}).on('error', (error) => {
emit("unzipCoreCompleted", false, error);
}));
} catch(ex) {
console.error("Failed to unzip a file: " + ex);
return false;
}
});
function deleteDirR(dir) {
fs.rmdir(dir, {recursive:true}, (err) => {
if (err) {
console.log(err)
return false, err;
}
});
return true;
}
exports('UnzipFolder', (file, name, dest) => {
let firstDir = null;
let hasStreamFolder = false;
const rootPath = GetResourcePath(GetCurrentResourceName());
const streamPath = rootPath + "/stream/" + name + "/";
if (!fs.existsSync(file)) {
console.error("File " + file + " doesn't exist.");
return false;
}
fs.createReadStream(file).pipe(unzipper.Parse())
.on('entry', function(entry) {
var fileName = entry.path;
const type = entry.type;
if (type == "Directory") {
if (fileName.includes("stream") && !hasStreamFolder) {
hasStreamFolder = true;
deleteDirR(streamPath);
}
if (firstDir == null) {
firstDir = fileName;
}
else {
fileName = fileName.replace(firstDir, "");
if (!fs.existsSync(dest + fileName)) {
fs.mkdirSync(dest + fileName);
}
}
}
if (type == "File") {
fileName = fileName.replace(firstDir, "");
let finalPath = dest + fileName;
if (fileName.includes("stream")) {
let file = fileName.replace(/^.*[\\\/]/, '');
finalPath = `${rootPath}/stream/${name}/${file}`;
if (!fs.existsSync(`${rootPath}/stream/${name}/`)) {
fs.mkdirSync(`${rootPath}/stream/${name}/`);
}
}
emit("SonoranCAD::core:writeLog", "debug", "write: " + finalPath);
entry.pipe(fs.createWriteStream(finalPath));
} else {
entry.autodrain();
}
}).on('close', () => {
emit("unzipCompleted", true, name, file);
}).on('error', (error) => {
emit("unzipCompleted", false, name, file, error);
})
});
exports('CreateFolderIfNotExisting', (path) => {
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
});
exports('DeleteDirectoryRecursively', (dir) => {
fs.rmdir(dir, {recursive:true}, (err) => {
if (err) {
console.log(err)
return false, err;
}
});
return true
});
@@ -0,0 +1,118 @@
local pendingRestart = false
local function doUnzip(path)
local unzipPath = GetResourcePath(GetCurrentResourceName()).."/../../"
exports[GetCurrentResourceName()]:UnzipFile(path, unzipPath)
end
AddEventHandler("unzipCoreCompleted", function(success, error)
if success then
if not Config.allowUpdateWithPlayers and GetNumPlayerIndices() > 0 then
pendingRestart = true
infoLog("Delaying auto-update until server is empty.")
return
end
warnLog("Auto-restarting...")
local f = assert(io.open(GetResourcePath("sonoran_updatehelper").."/run.lock", "w+"))
f:write("core")
f:close()
Wait(5000)
ExecuteCommand("ensure sonoran_updatehelper")
else
errorLog("Failed to download core update. "..tostring(error))
end
end)
local function doUpdate(latest)
-- best way to do this...
local releaseUrl = ("https://github.com/Sonoran-Software/SonoranCADFiveM/releases/download/v%s/sonorancad-%s.zip"):format(latest, latest)
PerformHttpRequest(releaseUrl, function(code, data, headers)
if code == 200 then
local savePath = GetResourcePath(GetCurrentResourceName()).."/update.zip"
local f = assert(io.open(savePath, 'wb'))
f:write(data)
f:close()
infoLog("Saved file...")
doUnzip(savePath)
else
warnLog(("Failed to download from %s: %s %s"):format(releaseUrl, code, data))
end
end, "GET")
end
function RunAutoUpdater(manualRun)
if Config.updateBranch == nil then
return
end
local f = LoadResourceFile(GetCurrentResourceName(), "/update.zip")
if f ~= nil then
-- remove the update file and stop the helper
ExecuteCommand("stop sonoran_updatehelper")
os.remove(GetResourcePath(GetCurrentResourceName()).."/update.zip")
os.remove(GetResourcePath("sonoran_updatehelper").."/run.lock")
end
local versionFile = Config.autoUpdateUrl
if versionFile == "https://raw.githubusercontent.com/Sonoran-Software/SonoranCADLuaIntegration/{branch}/sonorancad/version.json" then
errorLog('It seems like you might be running a v2.X.X core configuration file. Please update from the config.CHANGEME.json file or reinstall the resource. Install guide: https://sonoran.link/v3')
versionFile = "https://raw.githubusercontent.com/Sonoran-Software/SonoranCADFiveM/{branch}/sonorancad/version.json"
end
if versionFile == nil then
versionFile = "https://raw.githubusercontent.com/Sonoran-Software/SonoranCADFiveM/{branch}/sonorancad/version.json"
end
versionFile = string.gsub(versionFile, "{branch}", Config.updateBranch)
local myVersion = GetResourceMetadata(GetCurrentResourceName(), "version", 0)
PerformHttpRequestS(versionFile, function(code, data, headers)
if code == 200 then
local remote = json.decode(data)
if remote == nil then
warnLog(("Failed to get a valid response for %s. Skipping."):format(k))
debugLog(("Raw output for %s: %s"):format(k, data))
else
Config.latestVersion = remote.resource
local compare = compareVersions(remote.resource, myVersion)
if compare.result then
if not Config.allowAutoUpdate then
print("^3|===========================================================================|")
print("^3| ^5SonoranCAD Update Available ^3|")
print("^3| ^8Current : " .. compare.version2 .. " ^3|")
print("^3| ^2Latest : " .. compare.version1 .. " ^3|")
print("^3| Download at: ^4https://github.com/Sonoran-Software/SonoranCADFiveM ^3|")
print("^3|===========================================================================|^7")
if Config.allowAutoUpdate == nil then
warnLog("You have not configured the automatic updater. Please set allowAutoUpdate in config.json to allow updates.")
end
else
infoLog("Running auto-update now...")
doUpdate(remote.resource)
end
else
if manualRun then
infoLog(("No updates available. Detected version %s, latest version is %s"):format(compare.version1, compare.version2))
end
end
end
end
end, "GET")
end
CreateThread(function()
while true do
if pendingRestart then
if GetNumPlayerIndices() > 0 then
warnLog("An update has been applied to SonoranCAD but requires a resource restart. Restart delayed until server is empty.")
else
infoLog("Server is empty, restarting resources...")
local f = assert(io.open(GetResourcePath("sonoran_updatehelper").."/run.lock", "w+"))
f:write("core")
f:close()
ExecuteCommand("ensure sonoran_updatehelper")
end
else
RunAutoUpdater()
end
Wait(60000*60)
end
end)
@@ -0,0 +1,50 @@
fx_version 'cerulean'
games {'gta5'}
author 'Sonoran CAD'
description 'Sonoran CAD FiveM Integration'
version '3.1.4'
server_scripts {
'core/http.js'
,'core/unzipper/unzip.js'
,'core/image.js'
,'core/logging.lua'
,'core/shared_functions.lua'
,'core/configuration.lua'
,'core/server.lua'
,'core/commands.lua'
,'core/httpd.lua'
,'core/unittracking.lua'
,'core/updater.lua'
,'core/apicheck.lua'
,'configuration/*_config.lua'
,'core/plugin_loader.lua'
,'submodules/**/sv_*.lua'
,'submodules/**/sv_*.js'
,'core/screenshot.lua'
}
client_scripts {
'core/logging.lua'
,'core/headshots.lua'
,'core/shared_functions.lua'
,'core/client.lua'
,'core/lighting.lua'
,'configuration/*_config.lua'
,'submodules/**/cl_*.lua'
,'submodules/**/cl_*.js'
}
ui_page 'core/client_nui/index.html'
files {
'stream/**/*.ytyp',
'core/client_nui/index.html',
'core/client_nui/js/*.js',
'core/client_nui/sounds/*.mp3',
'core/client_nui/img/logo.gif',
'submodules/**/*.mp3',
'submodules/postals/*.json'
}
data_file 'DLC_ITYP_REQUEST' 'stream/**/*.ytyp'
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@"
else
exec node "$basedir/../mkdirp/bin/cmd.js" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mkdirp\bin\cmd.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
} else {
& "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rimraf/bin.js" "$@"
else
exec node "$basedir/../rimraf/bin.js" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rimraf\bin.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rimraf/bin.js" $args
} else {
& "$basedir/node$exe" "$basedir/../rimraf/bin.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rimraf/bin.js" $args
} else {
& "node$exe" "$basedir/../rimraf/bin.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+344
View File
@@ -0,0 +1,344 @@
{
"name": "sonorancad",
"lockfileVersion": 2,
"requires": true,
"packages": {
"node_modules/asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/big-integer": {
"version": "1.6.49",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz",
"integrity": "sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
"dependencies": {
"buffers": "~0.1.1",
"chainsaw": "~0.1.0"
},
"engines": {
"node": "*"
}
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
"engines": {
"node": "*"
}
},
"node_modules/buffer-indexof-polyfill": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=",
"engines": {
"node": ">=0.2.0"
}
},
"node_modules/chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
"dependencies": {
"traverse": ">=0.3.0 <0.4"
},
"engines": {
"node": "*"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
"dependencies": {
"readable-stream": "^2.0.2"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"dependencies": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"engines": {
"node": ">=0.6"
}
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"node_modules/listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"node_modules/ssh2": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.5.0.tgz",
"integrity": "sha512-iUmRkhH9KGeszQwDW7YyyqjsMTf4z+0o48Cp4xOwlY5LjtbIAvyd3fwnsoUZW/hXmTCRA3yt7S/Jb9uVjErVlA==",
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.4",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "0.0.2",
"nan": "^2.15.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=",
"engines": {
"node": "*"
}
},
"node_modules/ts3-nodejs-library": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/ts3-nodejs-library/-/ts3-nodejs-library-3.4.0.tgz",
"integrity": "sha512-T9LV0zxvMmF6cl3GjRAUcVNrZnuCbVHS0HQw1qg7kYdrNOnXh1Eg4Ix5WpiBsuSQ0bVB45VEF85vgmmXO3S2AA==",
"dependencies": {
"buffer-crc32": "^0.2.13",
"ssh2": "^1.4.0"
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"node_modules/unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
"dependencies": {
"big-integer": "^1.6.17",
"binary": "~0.3.0",
"bluebird": "~3.4.1",
"buffer-indexof-polyfill": "~1.0.0",
"duplexer2": "~0.1.4",
"fstream": "^1.0.12",
"graceful-fs": "^4.2.2",
"listenercount": "~1.0.1",
"readable-stream": "~2.3.6",
"setimmediate": "~1.0.4"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}
+65
View File
@@ -0,0 +1,65 @@
@Library('jenkins-joylib@v1.0.8') _
pipeline {
agent none
options {
buildDiscarder(logRotator(numToKeepStr: '45'))
timestamps()
}
stages {
stage('top') {
parallel {
stage('v4-zone') {
agent {
label joyCommonLabels(image_ver: '15.4.1')
}
tools {
nodejs 'sdcnode-v4-zone'
}
stages {
stage('check') {
steps{
sh('make check')
}
}
stage('test') {
steps{
sh('make test')
}
}
}
}
stage('v6-zone64') {
agent {
label joyCommonLabels(image_ver: '18.4.0')
}
tools {
nodejs 'sdcnode-v6-zone64'
}
stages {
stage('check') {
steps{
sh('make check')
}
}
stage('test') {
steps{
sh('make test')
}
}
}
}
}
}
}
post {
always {
joySlackNotifications()
}
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2011 Mark Cavage, All rights reserved.
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
+50
View File
@@ -0,0 +1,50 @@
node-asn1 is a library for encoding and decoding ASN.1 datatypes in pure JS.
Currently BER encoding is supported; at some point I'll likely have to do DER.
## Usage
Mostly, if you're *actually* needing to read and write ASN.1, you probably don't
need this readme to explain what and why. If you have no idea what ASN.1 is,
see this: ftp://ftp.rsa.com/pub/pkcs/ascii/layman.asc
The source is pretty much self-explanatory, and has read/write methods for the
common types out there.
### Decoding
The following reads an ASN.1 sequence with a boolean.
var Ber = require('asn1').Ber;
var reader = new Ber.Reader(Buffer.from([0x30, 0x03, 0x01, 0x01, 0xff]));
reader.readSequence();
console.log('Sequence len: ' + reader.length);
if (reader.peek() === Ber.Boolean)
console.log(reader.readBoolean());
### Encoding
The following generates the same payload as above.
var Ber = require('asn1').Ber;
var writer = new Ber.Writer();
writer.startSequence();
writer.writeBoolean(true);
writer.endSequence();
console.log(writer.buffer);
## Installation
npm install asn1
## License
MIT.
## Bugs
See <https://github.com/joyent/node-asn1/issues>.
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.
module.exports = {
newInvalidAsn1Error: function (msg) {
var e = new Error();
e.name = 'InvalidAsn1Error';
e.message = msg || '';
return e;
}
};
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.
var errors = require('./errors');
var types = require('./types');
var Reader = require('./reader');
var Writer = require('./writer');
// --- Exports
module.exports = {
Reader: Reader,
Writer: Writer
};
for (var t in types) {
if (types.hasOwnProperty(t))
module.exports[t] = types[t];
}
for (var e in errors) {
if (errors.hasOwnProperty(e))
module.exports[e] = errors[e];
}
+262
View File
@@ -0,0 +1,262 @@
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.
var assert = require('assert');
var Buffer = require('safer-buffer').Buffer;
var ASN1 = require('./types');
var errors = require('./errors');
// --- Globals
var newInvalidAsn1Error = errors.newInvalidAsn1Error;
// --- API
function Reader(data) {
if (!data || !Buffer.isBuffer(data))
throw new TypeError('data must be a node Buffer');
this._buf = data;
this._size = data.length;
// These hold the "current" state
this._len = 0;
this._offset = 0;
}
Object.defineProperty(Reader.prototype, 'length', {
enumerable: true,
get: function () { return (this._len); }
});
Object.defineProperty(Reader.prototype, 'offset', {
enumerable: true,
get: function () { return (this._offset); }
});
Object.defineProperty(Reader.prototype, 'remain', {
get: function () { return (this._size - this._offset); }
});
Object.defineProperty(Reader.prototype, 'buffer', {
get: function () { return (this._buf.slice(this._offset)); }
});
/**
* Reads a single byte and advances offset; you can pass in `true` to make this
* a "peek" operation (i.e., get the byte, but don't advance the offset).
*
* @param {Boolean} peek true means don't move offset.
* @return {Number} the next byte, null if not enough data.
*/
Reader.prototype.readByte = function (peek) {
if (this._size - this._offset < 1)
return null;
var b = this._buf[this._offset] & 0xff;
if (!peek)
this._offset += 1;
return b;
};
Reader.prototype.peek = function () {
return this.readByte(true);
};
/**
* Reads a (potentially) variable length off the BER buffer. This call is
* not really meant to be called directly, as callers have to manipulate
* the internal buffer afterwards.
*
* As a result of this call, you can call `Reader.length`, until the
* next thing called that does a readLength.
*
* @return {Number} the amount of offset to advance the buffer.
* @throws {InvalidAsn1Error} on bad ASN.1
*/
Reader.prototype.readLength = function (offset) {
if (offset === undefined)
offset = this._offset;
if (offset >= this._size)
return null;
var lenB = this._buf[offset++] & 0xff;
if (lenB === null)
return null;
if ((lenB & 0x80) === 0x80) {
lenB &= 0x7f;
if (lenB === 0)
throw newInvalidAsn1Error('Indefinite length not supported');
if (lenB > 4)
throw newInvalidAsn1Error('encoding too long');
if (this._size - offset < lenB)
return null;
this._len = 0;
for (var i = 0; i < lenB; i++)
this._len = (this._len << 8) + (this._buf[offset++] & 0xff);
} else {
// Wasn't a variable length
this._len = lenB;
}
return offset;
};
/**
* Parses the next sequence in this BER buffer.
*
* To get the length of the sequence, call `Reader.length`.
*
* @return {Number} the sequence's tag.
*/
Reader.prototype.readSequence = function (tag) {
var seq = this.peek();
if (seq === null)
return null;
if (tag !== undefined && tag !== seq)
throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) +
': got 0x' + seq.toString(16));
var o = this.readLength(this._offset + 1); // stored in `length`
if (o === null)
return null;
this._offset = o;
return seq;
};
Reader.prototype.readInt = function () {
return this._readTag(ASN1.Integer);
};
Reader.prototype.readBoolean = function () {
return (this._readTag(ASN1.Boolean) === 0 ? false : true);
};
Reader.prototype.readEnumeration = function () {
return this._readTag(ASN1.Enumeration);
};
Reader.prototype.readString = function (tag, retbuf) {
if (!tag)
tag = ASN1.OctetString;
var b = this.peek();
if (b === null)
return null;
if (b !== tag)
throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) +
': got 0x' + b.toString(16));
var o = this.readLength(this._offset + 1); // stored in `length`
if (o === null)
return null;
if (this.length > this._size - o)
return null;
this._offset = o;
if (this.length === 0)
return retbuf ? Buffer.alloc(0) : '';
var str = this._buf.slice(this._offset, this._offset + this.length);
this._offset += this.length;
return retbuf ? str : str.toString('utf8');
};
Reader.prototype.readOID = function (tag) {
if (!tag)
tag = ASN1.OID;
var b = this.readString(tag, true);
if (b === null)
return null;
var values = [];
var value = 0;
for (var i = 0; i < b.length; i++) {
var byte = b[i] & 0xff;
value <<= 7;
value += byte & 0x7f;
if ((byte & 0x80) === 0) {
values.push(value);
value = 0;
}
}
value = values.shift();
values.unshift(value % 40);
values.unshift((value / 40) >> 0);
return values.join('.');
};
Reader.prototype._readTag = function (tag) {
assert.ok(tag !== undefined);
var b = this.peek();
if (b === null)
return null;
if (b !== tag)
throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) +
': got 0x' + b.toString(16));
var o = this.readLength(this._offset + 1); // stored in `length`
if (o === null)
return null;
if (this.length > 4)
throw newInvalidAsn1Error('Integer too long: ' + this.length);
if (this.length > this._size - o)
return null;
this._offset = o;
var fb = this._buf[this._offset];
var value = 0;
for (var i = 0; i < this.length; i++) {
value <<= 8;
value |= (this._buf[this._offset++] & 0xff);
}
if ((fb & 0x80) === 0x80 && i !== 4)
value -= (1 << (i * 8));
return value >> 0;
};
// --- Exported API
module.exports = Reader;
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.
module.exports = {
EOC: 0,
Boolean: 1,
Integer: 2,
BitString: 3,
OctetString: 4,
Null: 5,
OID: 6,
ObjectDescriptor: 7,
External: 8,
Real: 9, // float
Enumeration: 10,
PDV: 11,
Utf8String: 12,
RelativeOID: 13,
Sequence: 16,
Set: 17,
NumericString: 18,
PrintableString: 19,
T61String: 20,
VideotexString: 21,
IA5String: 22,
UTCTime: 23,
GeneralizedTime: 24,
GraphicString: 25,
VisibleString: 26,
GeneralString: 28,
UniversalString: 29,
CharacterString: 30,
BMPString: 31,
Constructor: 32,
Context: 128
};
+317
View File
@@ -0,0 +1,317 @@
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.
var assert = require('assert');
var Buffer = require('safer-buffer').Buffer;
var ASN1 = require('./types');
var errors = require('./errors');
// --- Globals
var newInvalidAsn1Error = errors.newInvalidAsn1Error;
var DEFAULT_OPTS = {
size: 1024,
growthFactor: 8
};
// --- Helpers
function merge(from, to) {
assert.ok(from);
assert.equal(typeof (from), 'object');
assert.ok(to);
assert.equal(typeof (to), 'object');
var keys = Object.getOwnPropertyNames(from);
keys.forEach(function (key) {
if (to[key])
return;
var value = Object.getOwnPropertyDescriptor(from, key);
Object.defineProperty(to, key, value);
});
return to;
}
// --- API
function Writer(options) {
options = merge(DEFAULT_OPTS, options || {});
this._buf = Buffer.alloc(options.size || 1024);
this._size = this._buf.length;
this._offset = 0;
this._options = options;
// A list of offsets in the buffer where we need to insert
// sequence tag/len pairs.
this._seq = [];
}
Object.defineProperty(Writer.prototype, 'buffer', {
get: function () {
if (this._seq.length)
throw newInvalidAsn1Error(this._seq.length + ' unended sequence(s)');
return (this._buf.slice(0, this._offset));
}
});
Writer.prototype.writeByte = function (b) {
if (typeof (b) !== 'number')
throw new TypeError('argument must be a Number');
this._ensure(1);
this._buf[this._offset++] = b;
};
Writer.prototype.writeInt = function (i, tag) {
if (typeof (i) !== 'number')
throw new TypeError('argument must be a Number');
if (typeof (tag) !== 'number')
tag = ASN1.Integer;
var sz = 4;
while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) &&
(sz > 1)) {
sz--;
i <<= 8;
}
if (sz > 4)
throw newInvalidAsn1Error('BER ints cannot be > 0xffffffff');
this._ensure(2 + sz);
this._buf[this._offset++] = tag;
this._buf[this._offset++] = sz;
while (sz-- > 0) {
this._buf[this._offset++] = ((i & 0xff000000) >>> 24);
i <<= 8;
}
};
Writer.prototype.writeNull = function () {
this.writeByte(ASN1.Null);
this.writeByte(0x00);
};
Writer.prototype.writeEnumeration = function (i, tag) {
if (typeof (i) !== 'number')
throw new TypeError('argument must be a Number');
if (typeof (tag) !== 'number')
tag = ASN1.Enumeration;
return this.writeInt(i, tag);
};
Writer.prototype.writeBoolean = function (b, tag) {
if (typeof (b) !== 'boolean')
throw new TypeError('argument must be a Boolean');
if (typeof (tag) !== 'number')
tag = ASN1.Boolean;
this._ensure(3);
this._buf[this._offset++] = tag;
this._buf[this._offset++] = 0x01;
this._buf[this._offset++] = b ? 0xff : 0x00;
};
Writer.prototype.writeString = function (s, tag) {
if (typeof (s) !== 'string')
throw new TypeError('argument must be a string (was: ' + typeof (s) + ')');
if (typeof (tag) !== 'number')
tag = ASN1.OctetString;
var len = Buffer.byteLength(s);
this.writeByte(tag);
this.writeLength(len);
if (len) {
this._ensure(len);
this._buf.write(s, this._offset);
this._offset += len;
}
};
Writer.prototype.writeBuffer = function (buf, tag) {
if (typeof (tag) !== 'number')
throw new TypeError('tag must be a number');
if (!Buffer.isBuffer(buf))
throw new TypeError('argument must be a buffer');
this.writeByte(tag);
this.writeLength(buf.length);
this._ensure(buf.length);
buf.copy(this._buf, this._offset, 0, buf.length);
this._offset += buf.length;
};
Writer.prototype.writeStringArray = function (strings) {
if ((!strings instanceof Array))
throw new TypeError('argument must be an Array[String]');
var self = this;
strings.forEach(function (s) {
self.writeString(s);
});
};
// This is really to solve DER cases, but whatever for now
Writer.prototype.writeOID = function (s, tag) {
if (typeof (s) !== 'string')
throw new TypeError('argument must be a string');
if (typeof (tag) !== 'number')
tag = ASN1.OID;
if (!/^([0-9]+\.){3,}[0-9]+$/.test(s))
throw new Error('argument is not a valid OID string');
function encodeOctet(bytes, octet) {
if (octet < 128) {
bytes.push(octet);
} else if (octet < 16384) {
bytes.push((octet >>> 7) | 0x80);
bytes.push(octet & 0x7F);
} else if (octet < 2097152) {
bytes.push((octet >>> 14) | 0x80);
bytes.push(((octet >>> 7) | 0x80) & 0xFF);
bytes.push(octet & 0x7F);
} else if (octet < 268435456) {
bytes.push((octet >>> 21) | 0x80);
bytes.push(((octet >>> 14) | 0x80) & 0xFF);
bytes.push(((octet >>> 7) | 0x80) & 0xFF);
bytes.push(octet & 0x7F);
} else {
bytes.push(((octet >>> 28) | 0x80) & 0xFF);
bytes.push(((octet >>> 21) | 0x80) & 0xFF);
bytes.push(((octet >>> 14) | 0x80) & 0xFF);
bytes.push(((octet >>> 7) | 0x80) & 0xFF);
bytes.push(octet & 0x7F);
}
}
var tmp = s.split('.');
var bytes = [];
bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10));
tmp.slice(2).forEach(function (b) {
encodeOctet(bytes, parseInt(b, 10));
});
var self = this;
this._ensure(2 + bytes.length);
this.writeByte(tag);
this.writeLength(bytes.length);
bytes.forEach(function (b) {
self.writeByte(b);
});
};
Writer.prototype.writeLength = function (len) {
if (typeof (len) !== 'number')
throw new TypeError('argument must be a Number');
this._ensure(4);
if (len <= 0x7f) {
this._buf[this._offset++] = len;
} else if (len <= 0xff) {
this._buf[this._offset++] = 0x81;
this._buf[this._offset++] = len;
} else if (len <= 0xffff) {
this._buf[this._offset++] = 0x82;
this._buf[this._offset++] = len >> 8;
this._buf[this._offset++] = len;
} else if (len <= 0xffffff) {
this._buf[this._offset++] = 0x83;
this._buf[this._offset++] = len >> 16;
this._buf[this._offset++] = len >> 8;
this._buf[this._offset++] = len;
} else {
throw newInvalidAsn1Error('Length too long (> 4 bytes)');
}
};
Writer.prototype.startSequence = function (tag) {
if (typeof (tag) !== 'number')
tag = ASN1.Sequence | ASN1.Constructor;
this.writeByte(tag);
this._seq.push(this._offset);
this._ensure(3);
this._offset += 3;
};
Writer.prototype.endSequence = function () {
var seq = this._seq.pop();
var start = seq + 3;
var len = this._offset - start;
if (len <= 0x7f) {
this._shift(start, len, -2);
this._buf[seq] = len;
} else if (len <= 0xff) {
this._shift(start, len, -1);
this._buf[seq] = 0x81;
this._buf[seq + 1] = len;
} else if (len <= 0xffff) {
this._buf[seq] = 0x82;
this._buf[seq + 1] = len >> 8;
this._buf[seq + 2] = len;
} else if (len <= 0xffffff) {
this._shift(start, len, 1);
this._buf[seq] = 0x83;
this._buf[seq + 1] = len >> 16;
this._buf[seq + 2] = len >> 8;
this._buf[seq + 3] = len;
} else {
throw newInvalidAsn1Error('Sequence too long');
}
};
Writer.prototype._shift = function (start, len, shift) {
assert.ok(start !== undefined);
assert.ok(len !== undefined);
assert.ok(shift);
this._buf.copy(this._buf, start + shift, start, start + len);
this._offset += shift;
};
Writer.prototype._ensure = function (len) {
assert.ok(len);
if (this._size - this._offset < len) {
var sz = this._size * this._options.growthFactor;
if (sz - this._offset < len)
sz += len;
var buf = Buffer.alloc(sz);
this._buf.copy(buf, 0, 0, this._offset);
this._buf = buf;
this._size = sz;
}
};
// --- Exported API
module.exports = Writer;
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.
// If you have no idea what ASN.1 or BER is, see this:
// ftp://ftp.rsa.com/pub/pkcs/ascii/layman.asc
var Ber = require('./ber/index');
// --- Exported API
module.exports = {
Ber: Ber,
BerReader: Ber.Reader,
BerWriter: Ber.Writer
};
+31
View File
@@ -0,0 +1,31 @@
{
"author": "Joyent (joyent.com)",
"contributors": [
"Mark Cavage <mcavage@gmail.com>",
"David Gwynne <loki@animata.net>",
"Yunong Xiao <yunong@joyent.com>",
"Alex Wilson <alex.wilson@joyent.com>"
],
"name": "asn1",
"description": "Contains parsers and serializers for ASN.1 (currently BER only)",
"version": "0.2.4",
"repository": {
"type": "git",
"url": "git://github.com/joyent/node-asn1.git"
},
"main": "lib/index.js",
"dependencies": {
"safer-buffer": "~2.1.0"
},
"devDependencies": {
"istanbul": "^0.3.6",
"faucet": "0.0.1",
"tape": "^3.5.0",
"eslint": "2.13.1",
"eslint-plugin-joyent": "~1.3.0"
},
"scripts": {
"test": "./node_modules/.bin/tape ./test/ber/*.test.js"
},
"license": "MIT"
}
@@ -0,0 +1,21 @@
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
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.
@@ -0,0 +1,97 @@
# balanced-match
Match balanced string pairs, like `{` and `}` or `<b>` and `</b>`. Supports regular expressions as well!
[![build status](https://secure.travis-ci.org/juliangruber/balanced-match.svg)](http://travis-ci.org/juliangruber/balanced-match)
[![downloads](https://img.shields.io/npm/dm/balanced-match.svg)](https://www.npmjs.org/package/balanced-match)
[![testling badge](https://ci.testling.com/juliangruber/balanced-match.png)](https://ci.testling.com/juliangruber/balanced-match)
## Example
Get the first matching pair of braces:
```js
var balanced = require('balanced-match');
console.log(balanced('{', '}', 'pre{in{nested}}post'));
console.log(balanced('{', '}', 'pre{first}between{second}post'));
console.log(balanced(/\s+\{\s+/, /\s+\}\s+/, 'pre { in{nest} } post'));
```
The matches are:
```bash
$ node example.js
{ start: 3, end: 14, pre: 'pre', body: 'in{nested}', post: 'post' }
{ start: 3,
end: 9,
pre: 'pre',
body: 'first',
post: 'between{second}post' }
{ start: 3, end: 17, pre: 'pre', body: 'in{nest}', post: 'post' }
```
## API
### var m = balanced(a, b, str)
For the first non-nested matching pair of `a` and `b` in `str`, return an
object with those keys:
* **start** the index of the first match of `a`
* **end** the index of the matching `b`
* **pre** the preamble, `a` and `b` not included
* **body** the match, `a` and `b` not included
* **post** the postscript, `a` and `b` not included
If there's no match, `undefined` will be returned.
If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `['{', 'a', '']` and `{a}}` will match `['', 'a', '}']`.
### var r = balanced.range(a, b, str)
For the first non-nested matching pair of `a` and `b` in `str`, return an
array with indexes: `[ <a index>, <b index> ]`.
If there's no match, `undefined` will be returned.
If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `[ 1, 3 ]` and `{a}}` will match `[0, 2]`.
## Installation
With [npm](https://npmjs.org) do:
```bash
npm install balanced-match
```
## Security contact information
To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.
## License
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
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.
+62
View File
@@ -0,0 +1,62 @@
'use strict';
module.exports = balanced;
function balanced(a, b, str) {
if (a instanceof RegExp) a = maybeMatch(a, str);
if (b instanceof RegExp) b = maybeMatch(b, str);
var r = range(a, b, str);
return r && {
start: r[0],
end: r[1],
pre: str.slice(0, r[0]),
body: str.slice(r[0] + a.length, r[1]),
post: str.slice(r[1] + b.length)
};
}
function maybeMatch(reg, str) {
var m = str.match(reg);
return m ? m[0] : null;
}
balanced.range = range;
function range(a, b, str) {
var begs, beg, left, right, result;
var ai = str.indexOf(a);
var bi = str.indexOf(b, ai + 1);
var i = ai;
if (ai >= 0 && bi > 0) {
if(a===b) {
return [ai, bi];
}
begs = [];
left = str.length;
while (i >= 0 && !result) {
if (i == ai) {
begs.push(i);
ai = str.indexOf(a, i + 1);
} else if (begs.length == 1) {
result = [ begs.pop(), bi ];
} else {
beg = begs.pop();
if (beg < left) {
left = beg;
right = bi;
}
bi = str.indexOf(b, i + 1);
}
i = ai < bi && ai >= 0 ? ai : bi;
}
if (begs.length) {
result = [ left, right ];
}
}
return result;
}
@@ -0,0 +1,48 @@
{
"name": "balanced-match",
"description": "Match balanced character pairs, like \"{\" and \"}\"",
"version": "1.0.2",
"repository": {
"type": "git",
"url": "git://github.com/juliangruber/balanced-match.git"
},
"homepage": "https://github.com/juliangruber/balanced-match",
"main": "index.js",
"scripts": {
"test": "tape test/test.js",
"bench": "matcha test/bench.js"
},
"devDependencies": {
"matcha": "^0.7.0",
"tape": "^4.6.0"
},
"keywords": [
"match",
"regexp",
"test",
"balanced",
"parse"
],
"author": {
"name": "Julian Gruber",
"email": "mail@juliangruber.com",
"url": "http://juliangruber.com"
},
"license": "MIT",
"testling": {
"files": "test/*.js",
"browsers": [
"ie/8..latest",
"firefox/20..latest",
"firefox/nightly",
"chrome/25..latest",
"chrome/canary",
"opera/12..latest",
"opera/next",
"safari/5.1..latest",
"ipad/6.0..latest",
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
}
}
@@ -0,0 +1,13 @@
# Contributing
This repository uses [cr.joyent.us](https://cr.joyent.us) (Gerrit) for new
changes. Anyone can submit changes. To get started, see the [cr.joyent.us user
guide](https://github.com/joyent/joyent-gerrit/blob/master/docs/user/README.md).
This repo does not use GitHub pull requests.
See the [Joyent Engineering
Guidelines](https://github.com/joyent/eng/blob/master/docs/index.md) for general
best practices expected in this repository.
If you're changing something non-trivial or user-facing, you may want to submit
an issue first.
+66
View File
@@ -0,0 +1,66 @@
The Blowfish portions are under the following license:
Blowfish block cipher for OpenBSD
Copyright 1997 Niels Provos <provos@physnet.uni-hamburg.de>
All rights reserved.
Implementation advice by David Mazieres <dm@lcs.mit.edu>.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The bcrypt_pbkdf portions are under the following license:
Copyright (c) 2013 Ted Unangst <tedu@openbsd.org>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Performance improvements (Javascript-specific):
Copyright 2016, Joyent Inc
Author: Alex Wilson <alex.wilson@joyent.com>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+45
View File
@@ -0,0 +1,45 @@
Port of the OpenBSD `bcrypt_pbkdf` function to pure Javascript. `npm`-ified
version of [Devi Mandiri's port](https://github.com/devi/tmp/blob/master/js/bcrypt_pbkdf.js),
with some minor performance improvements. The code is copied verbatim (and
un-styled) from Devi's work.
This product includes software developed by Niels Provos.
## API
### `bcrypt_pbkdf.pbkdf(pass, passlen, salt, saltlen, key, keylen, rounds)`
Derive a cryptographic key of arbitrary length from a given password and salt,
using the OpenBSD `bcrypt_pbkdf` function. This is a combination of Blowfish and
SHA-512.
See [this article](http://www.tedunangst.com/flak/post/bcrypt-pbkdf) for
further information.
Parameters:
* `pass`, a Uint8Array of length `passlen`
* `passlen`, an integer Number
* `salt`, a Uint8Array of length `saltlen`
* `saltlen`, an integer Number
* `key`, a Uint8Array of length `keylen`, will be filled with output
* `keylen`, an integer Number
* `rounds`, an integer Number, number of rounds of the PBKDF to run
### `bcrypt_pbkdf.hash(sha2pass, sha2salt, out)`
Calculate a Blowfish hash, given SHA2-512 output of a password and salt. Used as
part of the inner round function in the PBKDF.
Parameters:
* `sha2pass`, a Uint8Array of length 64
* `sha2salt`, a Uint8Array of length 64
* `out`, a Uint8Array of length 32, will be filled with output
## License
This source form is a 1:1 port from the OpenBSD `blowfish.c` and `bcrypt_pbkdf.c`.
As a result, it retains the original copyright and license. The two files are
under slightly different (but compatible) licenses, and are here combined in
one file. For each of the full license texts see `LICENSE`.
+556
View File
@@ -0,0 +1,556 @@
'use strict';
var crypto_hash_sha512 = require('tweetnacl').lowlevel.crypto_hash;
/*
* This file is a 1:1 port from the OpenBSD blowfish.c and bcrypt_pbkdf.c. As a
* result, it retains the original copyright and license. The two files are
* under slightly different (but compatible) licenses, and are here combined in
* one file.
*
* Credit for the actual porting work goes to:
* Devi Mandiri <me@devi.web.id>
*/
/*
* The Blowfish portions are under the following license:
*
* Blowfish block cipher for OpenBSD
* Copyright 1997 Niels Provos <provos@physnet.uni-hamburg.de>
* All rights reserved.
*
* Implementation advice by David Mazieres <dm@lcs.mit.edu>.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* The bcrypt_pbkdf portions are under the following license:
*
* Copyright (c) 2013 Ted Unangst <tedu@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* Performance improvements (Javascript-specific):
*
* Copyright 2016, Joyent Inc
* Author: Alex Wilson <alex.wilson@joyent.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// Ported from OpenBSD bcrypt_pbkdf.c v1.9
var BLF_J = 0;
var Blowfish = function() {
this.S = [
new Uint32Array([
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7,
0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee,
0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,
0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce,
0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e,
0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88,
0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e,
0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88,
0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6,
0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba,
0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f,
0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279,
0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,
0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0,
0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790,
0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7,
0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad,
0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477,
0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49,
0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41,
0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400,
0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a]),
new Uint32Array([
0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623,
0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6,
0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e,
0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff,
0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701,
0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf,
0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e,
0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16,
0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b,
0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f,
0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4,
0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802,
0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510,
0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50,
0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8,
0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128,
0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0,
0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3,
0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00,
0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735,
0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9,
0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7]),
new Uint32Array([
0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934,
0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45,
0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a,
0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42,
0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2,
0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33,
0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3,
0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b,
0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922,
0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,
0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804,
0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d,
0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350,
0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d,
0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f,
0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2,
0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e,
0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52,
0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5,
0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,
0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4,
0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0]),
new Uint32Array([
0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b,
0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8,
0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304,
0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9,
0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593,
0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b,
0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c,
0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb,
0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,
0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae,
0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5,
0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84,
0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8,
0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38,
0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c,
0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964,
0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8,
0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02,
0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,
0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0,
0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e,
0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6])
];
this.P = new Uint32Array([
0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
0x9216d5d9, 0x8979fb1b]);
};
function F(S, x8, i) {
return (((S[0][x8[i+3]] +
S[1][x8[i+2]]) ^
S[2][x8[i+1]]) +
S[3][x8[i]]);
};
Blowfish.prototype.encipher = function(x, x8) {
if (x8 === undefined) {
x8 = new Uint8Array(x.buffer);
if (x.byteOffset !== 0)
x8 = x8.subarray(x.byteOffset);
}
x[0] ^= this.P[0];
for (var i = 1; i < 16; i += 2) {
x[1] ^= F(this.S, x8, 0) ^ this.P[i];
x[0] ^= F(this.S, x8, 4) ^ this.P[i+1];
}
var t = x[0];
x[0] = x[1] ^ this.P[17];
x[1] = t;
};
Blowfish.prototype.decipher = function(x) {
var x8 = new Uint8Array(x.buffer);
if (x.byteOffset !== 0)
x8 = x8.subarray(x.byteOffset);
x[0] ^= this.P[17];
for (var i = 16; i > 0; i -= 2) {
x[1] ^= F(this.S, x8, 0) ^ this.P[i];
x[0] ^= F(this.S, x8, 4) ^ this.P[i-1];
}
var t = x[0];
x[0] = x[1] ^ this.P[0];
x[1] = t;
};
function stream2word(data, databytes){
var i, temp = 0;
for (i = 0; i < 4; i++, BLF_J++) {
if (BLF_J >= databytes) BLF_J = 0;
temp = (temp << 8) | data[BLF_J];
}
return temp;
};
Blowfish.prototype.expand0state = function(key, keybytes) {
var d = new Uint32Array(2), i, k;
var d8 = new Uint8Array(d.buffer);
for (i = 0, BLF_J = 0; i < 18; i++) {
this.P[i] ^= stream2word(key, keybytes);
}
BLF_J = 0;
for (i = 0; i < 18; i += 2) {
this.encipher(d, d8);
this.P[i] = d[0];
this.P[i+1] = d[1];
}
for (i = 0; i < 4; i++) {
for (k = 0; k < 256; k += 2) {
this.encipher(d, d8);
this.S[i][k] = d[0];
this.S[i][k+1] = d[1];
}
}
};
Blowfish.prototype.expandstate = function(data, databytes, key, keybytes) {
var d = new Uint32Array(2), i, k;
for (i = 0, BLF_J = 0; i < 18; i++) {
this.P[i] ^= stream2word(key, keybytes);
}
for (i = 0, BLF_J = 0; i < 18; i += 2) {
d[0] ^= stream2word(data, databytes);
d[1] ^= stream2word(data, databytes);
this.encipher(d);
this.P[i] = d[0];
this.P[i+1] = d[1];
}
for (i = 0; i < 4; i++) {
for (k = 0; k < 256; k += 2) {
d[0] ^= stream2word(data, databytes);
d[1] ^= stream2word(data, databytes);
this.encipher(d);
this.S[i][k] = d[0];
this.S[i][k+1] = d[1];
}
}
BLF_J = 0;
};
Blowfish.prototype.enc = function(data, blocks) {
for (var i = 0; i < blocks; i++) {
this.encipher(data.subarray(i*2));
}
};
Blowfish.prototype.dec = function(data, blocks) {
for (var i = 0; i < blocks; i++) {
this.decipher(data.subarray(i*2));
}
};
var BCRYPT_BLOCKS = 8,
BCRYPT_HASHSIZE = 32;
function bcrypt_hash(sha2pass, sha2salt, out) {
var state = new Blowfish(),
cdata = new Uint32Array(BCRYPT_BLOCKS), i,
ciphertext = new Uint8Array([79,120,121,99,104,114,111,109,97,116,105,
99,66,108,111,119,102,105,115,104,83,119,97,116,68,121,110,97,109,
105,116,101]); //"OxychromaticBlowfishSwatDynamite"
state.expandstate(sha2salt, 64, sha2pass, 64);
for (i = 0; i < 64; i++) {
state.expand0state(sha2salt, 64);
state.expand0state(sha2pass, 64);
}
for (i = 0; i < BCRYPT_BLOCKS; i++)
cdata[i] = stream2word(ciphertext, ciphertext.byteLength);
for (i = 0; i < 64; i++)
state.enc(cdata, cdata.byteLength / 8);
for (i = 0; i < BCRYPT_BLOCKS; i++) {
out[4*i+3] = cdata[i] >>> 24;
out[4*i+2] = cdata[i] >>> 16;
out[4*i+1] = cdata[i] >>> 8;
out[4*i+0] = cdata[i];
}
};
function bcrypt_pbkdf(pass, passlen, salt, saltlen, key, keylen, rounds) {
var sha2pass = new Uint8Array(64),
sha2salt = new Uint8Array(64),
out = new Uint8Array(BCRYPT_HASHSIZE),
tmpout = new Uint8Array(BCRYPT_HASHSIZE),
countsalt = new Uint8Array(saltlen+4),
i, j, amt, stride, dest, count,
origkeylen = keylen;
if (rounds < 1)
return -1;
if (passlen === 0 || saltlen === 0 || keylen === 0 ||
keylen > (out.byteLength * out.byteLength) || saltlen > (1<<20))
return -1;
stride = Math.floor((keylen + out.byteLength - 1) / out.byteLength);
amt = Math.floor((keylen + stride - 1) / stride);
for (i = 0; i < saltlen; i++)
countsalt[i] = salt[i];
crypto_hash_sha512(sha2pass, pass, passlen);
for (count = 1; keylen > 0; count++) {
countsalt[saltlen+0] = count >>> 24;
countsalt[saltlen+1] = count >>> 16;
countsalt[saltlen+2] = count >>> 8;
countsalt[saltlen+3] = count;
crypto_hash_sha512(sha2salt, countsalt, saltlen + 4);
bcrypt_hash(sha2pass, sha2salt, tmpout);
for (i = out.byteLength; i--;)
out[i] = tmpout[i];
for (i = 1; i < rounds; i++) {
crypto_hash_sha512(sha2salt, tmpout, tmpout.byteLength);
bcrypt_hash(sha2pass, sha2salt, tmpout);
for (j = 0; j < out.byteLength; j++)
out[j] ^= tmpout[j];
}
amt = Math.min(amt, keylen);
for (i = 0; i < amt; i++) {
dest = i * stride + (count - 1);
if (dest >= origkeylen)
break;
key[dest] = out[i];
}
keylen -= i;
}
return 0;
};
module.exports = {
BLOCKS: BCRYPT_BLOCKS,
HASHSIZE: BCRYPT_HASHSIZE,
hash: bcrypt_hash,
pbkdf: bcrypt_pbkdf
};
@@ -0,0 +1,15 @@
{
"name": "bcrypt-pbkdf",
"version": "1.0.2",
"description": "Port of the OpenBSD bcrypt_pbkdf function to pure JS",
"repository": {
"type": "git",
"url": "git://github.com/joyent/node-bcrypt-pbkdf.git"
},
"main": "index.js",
"dependencies": {
"tweetnacl": "^0.14.3"
},
"devDependencies": {},
"license": "BSD-3-Clause"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <http://unlicense.org>
+589
View File
@@ -0,0 +1,589 @@
# BigInteger.js [![Build Status][travis-img]][travis-url] [![Coverage Status][coveralls-img]][coveralls-url] [![Monthly Downloads][downloads-img]][downloads-url]
[travis-url]: https://travis-ci.org/peterolson/BigInteger.js
[travis-img]: https://travis-ci.org/peterolson/BigInteger.js.svg?branch=master
[coveralls-url]: https://coveralls.io/github/peterolson/BigInteger.js?branch=master
[coveralls-img]: https://coveralls.io/repos/peterolson/BigInteger.js/badge.svg?branch=master&service=github
[downloads-url]: https://www.npmjs.com/package/big-integer
[downloads-img]: https://img.shields.io/npm/dm/big-integer.svg
**BigInteger.js** is an arbitrary-length integer library for Javascript, allowing arithmetic operations on integers of unlimited size, notwithstanding memory and time limitations.
**Update (December 2, 2018):** [`BigInt` is being added as a native feature of JavaScript](https://tc39.github.io/proposal-bigint/). This library now works as a polyfill: if the environment supports the native `BigInt`, this library acts as a thin wrapper over the native implementation.
## Installation
If you are using a browser, you can download [BigInteger.js from GitHub](http://peterolson.github.com/BigInteger.js/BigInteger.min.js) or just hotlink to it:
<script src="https://peterolson.github.io/BigInteger.js/BigInteger.min.js"></script>
If you are using node, you can install BigInteger with [npm](https://npmjs.org/).
npm install big-integer
Then you can include it in your code:
var bigInt = require("big-integer");
## Usage
### `bigInt(number, [base], [alphabet], [caseSensitive])`
You can create a bigInt by calling the `bigInt` function. You can pass in
- a string, which it will parse as an bigInt and throw an `"Invalid integer"` error if the parsing fails.
- a Javascript number, which it will parse as an bigInt and throw an `"Invalid integer"` error if the parsing fails.
- another bigInt.
- nothing, and it will return `bigInt.zero`.
If you provide a second parameter, then it will parse `number` as a number in base `base`. Note that `base` can be any bigInt (even negative or zero). The letters "a-z" and "A-Z" will be interpreted as the numbers 10 to 35. Higher digits can be specified in angle brackets (`<` and `>`). The default `base` is `10`.
You can specify a custom alphabet for base conversion with the third parameter. The default `alphabet` is `"0123456789abcdefghijklmnopqrstuvwxyz"`.
The fourth parameter specifies whether or not the number string should be case-sensitive, i.e. whether `a` and `A` should be treated as different digits. By default `caseSensitive` is `false`.
Examples:
var zero = bigInt();
var ninetyThree = bigInt(93);
var largeNumber = bigInt("75643564363473453456342378564387956906736546456235345");
var googol = bigInt("1e100");
var bigNumber = bigInt(largeNumber);
var maximumByte = bigInt("FF", 16);
var fiftyFiveGoogol = bigInt("<55>0", googol);
Note that Javascript numbers larger than `9007199254740992` and smaller than `-9007199254740992` are not precisely represented numbers and will not produce exact results. If you are dealing with numbers outside that range, it is better to pass in strings.
### Method Chaining
Note that bigInt operations return bigInts, which allows you to chain methods, for example:
var salary = bigInt(dollarsPerHour).times(hoursWorked).plus(randomBonuses)
### Constants
There are three named constants already stored that you do not have to construct with the `bigInt` function yourself:
- `bigInt.one`, equivalent to `bigInt(1)`
- `bigInt.zero`, equivalent to `bigInt(0)`
- `bigInt.minusOne`, equivalent to `bigInt(-1)`
The numbers from -999 to 999 are also already prestored and can be accessed using `bigInt[index]`, for example:
- `bigInt[-999]`, equivalent to `bigInt(-999)`
- `bigInt[256]`, equivalent to `bigInt(256)`
### Methods
#### `abs()`
Returns the absolute value of a bigInt.
- `bigInt(-45).abs()` => `45`
- `bigInt(45).abs()` => `45`
#### `add(number)`
Performs addition.
- `bigInt(5).add(7)` => `12`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Addition)
#### `and(number)`
Performs the bitwise AND operation. The operands are treated as if they were represented using [two's complement representation](http://en.wikipedia.org/wiki/Two%27s_complement).
- `bigInt(6).and(3)` => `2`
- `bigInt(6).and(-3)` => `4`
#### `bitLength()`
Returns the number of digits required to represent a bigInt in binary.
- `bigInt(5)` => `3` (since 5 is `101` in binary, which is three digits long)
#### `compare(number)`
Performs a comparison between two numbers. If the numbers are equal, it returns `0`. If the first number is greater, it returns `1`. If the first number is lesser, it returns `-1`.
- `bigInt(5).compare(5)` => `0`
- `bigInt(5).compare(4)` => `1`
- `bigInt(4).compare(5)` => `-1`
#### `compareAbs(number)`
Performs a comparison between the absolute value of two numbers.
- `bigInt(5).compareAbs(-5)` => `0`
- `bigInt(5).compareAbs(4)` => `1`
- `bigInt(4).compareAbs(-5)` => `-1`
#### `compareTo(number)`
Alias for the `compare` method.
#### `divide(number)`
Performs integer division, disregarding the remainder.
- `bigInt(59).divide(5)` => `11`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Division)
#### `divmod(number)`
Performs division and returns an object with two properties: `quotient` and `remainder`. The sign of the remainder will match the sign of the dividend.
- `bigInt(59).divmod(5)` => `{quotient: bigInt(11), remainder: bigInt(4) }`
- `bigInt(-5).divmod(2)` => `{quotient: bigInt(-2), remainder: bigInt(-1) }`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Division)
#### `eq(number)`
Alias for the `equals` method.
#### `equals(number)`
Checks if two numbers are equal.
- `bigInt(5).equals(5)` => `true`
- `bigInt(4).equals(7)` => `false`
#### `geq(number)`
Alias for the `greaterOrEquals` method.
#### `greater(number)`
Checks if the first number is greater than the second.
- `bigInt(5).greater(6)` => `false`
- `bigInt(5).greater(5)` => `false`
- `bigInt(5).greater(4)` => `true`
#### `greaterOrEquals(number)`
Checks if the first number is greater than or equal to the second.
- `bigInt(5).greaterOrEquals(6)` => `false`
- `bigInt(5).greaterOrEquals(5)` => `true`
- `bigInt(5).greaterOrEquals(4)` => `true`
#### `gt(number)`
Alias for the `greater` method.
#### `isDivisibleBy(number)`
Returns `true` if the first number is divisible by the second number, `false` otherwise.
- `bigInt(999).isDivisibleBy(333)` => `true`
- `bigInt(99).isDivisibleBy(5)` => `false`
#### `isEven()`
Returns `true` if the number is even, `false` otherwise.
- `bigInt(6).isEven()` => `true`
- `bigInt(3).isEven()` => `false`
#### `isNegative()`
Returns `true` if the number is negative, `false` otherwise.
Returns `false` for `0` and `-0`.
- `bigInt(-23).isNegative()` => `true`
- `bigInt(50).isNegative()` => `false`
#### `isOdd()`
Returns `true` if the number is odd, `false` otherwise.
- `bigInt(13).isOdd()` => `true`
- `bigInt(40).isOdd()` => `false`
#### `isPositive()`
Return `true` if the number is positive, `false` otherwise.
Returns `false` for `0` and `-0`.
- `bigInt(54).isPositive()` => `true`
- `bigInt(-1).isPositive()` => `false`
#### `isPrime(strict?)`
Returns `true` if the number is prime, `false` otherwise.
Set "strict" boolean to true to force GRH-supported lower bound of 2*log(N)^2.
- `bigInt(5).isPrime()` => `true`
- `bigInt(6).isPrime()` => `false`
#### `isProbablePrime([iterations], [rng])`
Returns `true` if the number is very likely to be prime, `false` otherwise.
Supplying `iterations` is optional - it determines the number of iterations of the test (default: `5`). The more iterations, the lower chance of getting a false positive.
This uses the [Miller Rabin test](https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test).
- `bigInt(5).isProbablePrime()` => `true`
- `bigInt(49).isProbablePrime()` => `false`
- `bigInt(1729).isProbablePrime()` => `false`
Note that this function is not deterministic, since it relies on random sampling of factors, so the result for some numbers is not always the same - unless you pass a predictable random number generator as `rng`. The behavior and requirements are the same as with `randBetween`.
- `bigInt(1729).isProbablePrime(1, () => 0.1)` => `false`
- `bigInt(1729).isProbablePrime(1, () => 0.2)` => `true`
If the number is composite then the MillerRabin primality test declares the number probably prime with a probability at most `4` to the power `iterations`.
If the number is prime, this function always returns `true`.
#### `isUnit()`
Returns `true` if the number is `1` or `-1`, `false` otherwise.
- `bigInt.one.isUnit()` => `true`
- `bigInt.minusOne.isUnit()` => `true`
- `bigInt(5).isUnit()` => `false`
#### `isZero()`
Return `true` if the number is `0` or `-0`, `false` otherwise.
- `bigInt.zero.isZero()` => `true`
- `bigInt("-0").isZero()` => `true`
- `bigInt(50).isZero()` => `false`
#### `leq(number)`
Alias for the `lesserOrEquals` method.
#### `lesser(number)`
Checks if the first number is lesser than the second.
- `bigInt(5).lesser(6)` => `true`
- `bigInt(5).lesser(5)` => `false`
- `bigInt(5).lesser(4)` => `false`
#### `lesserOrEquals(number)`
Checks if the first number is less than or equal to the second.
- `bigInt(5).lesserOrEquals(6)` => `true`
- `bigInt(5).lesserOrEquals(5)` => `true`
- `bigInt(5).lesserOrEquals(4)` => `false`
#### `lt(number)`
Alias for the `lesser` method.
#### `minus(number)`
Alias for the `subtract` method.
- `bigInt(3).minus(5)` => `-2`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Subtraction)
#### `mod(number)`
Performs division and returns the remainder, disregarding the quotient. The sign of the remainder will match the sign of the dividend.
- `bigInt(59).mod(5)` => `4`
- `bigInt(-5).mod(2)` => `-1`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Division)
#### `modInv(mod)`
Finds the [multiplicative inverse](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse) of the number modulo `mod`.
- `bigInt(3).modInv(11)` => `4`
- `bigInt(42).modInv(2017)` => `1969`
#### `modPow(exp, mod)`
Takes the number to the power `exp` modulo `mod`.
- `bigInt(10).modPow(3, 30)` => `10`
#### `multiply(number)`
Performs multiplication.
- `bigInt(111).multiply(111)` => `12321`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Multiplication)
#### `neq(number)`
Alias for the `notEquals` method.
#### `next()`
Adds one to the number.
- `bigInt(6).next()` => `7`
#### `not()`
Performs the bitwise NOT operation. The operands are treated as if they were represented using [two's complement representation](http://en.wikipedia.org/wiki/Two%27s_complement).
- `bigInt(10).not()` => `-11`
- `bigInt(0).not()` => `-1`
#### `notEquals(number)`
Checks if two numbers are not equal.
- `bigInt(5).notEquals(5)` => `false`
- `bigInt(4).notEquals(7)` => `true`
#### `or(number)`
Performs the bitwise OR operation. The operands are treated as if they were represented using [two's complement representation](http://en.wikipedia.org/wiki/Two%27s_complement).
- `bigInt(13).or(10)` => `15`
- `bigInt(13).or(-8)` => `-3`
#### `over(number)`
Alias for the `divide` method.
- `bigInt(59).over(5)` => `11`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Division)
#### `plus(number)`
Alias for the `add` method.
- `bigInt(5).plus(7)` => `12`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Addition)
#### `pow(number)`
Performs exponentiation. If the exponent is less than `0`, `pow` returns `0`. `bigInt.zero.pow(0)` returns `1`.
- `bigInt(16).pow(16)` => `18446744073709551616`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Exponentiation)
#### `prev(number)`
Subtracts one from the number.
- `bigInt(6).prev()` => `5`
#### `remainder(number)`
Alias for the `mod` method.
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Division)
#### `shiftLeft(n)`
Shifts the number left by `n` places in its binary representation. If a negative number is provided, it will shift right. Throws an error if `n` is outside of the range `[-9007199254740992, 9007199254740992]`.
- `bigInt(8).shiftLeft(2)` => `32`
- `bigInt(8).shiftLeft(-2)` => `2`
#### `shiftRight(n)`
Shifts the number right by `n` places in its binary representation. If a negative number is provided, it will shift left. Throws an error if `n` is outside of the range `[-9007199254740992, 9007199254740992]`.
- `bigInt(8).shiftRight(2)` => `2`
- `bigInt(8).shiftRight(-2)` => `32`
#### `square()`
Squares the number
- `bigInt(3).square()` => `9`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Squaring)
#### `subtract(number)`
Performs subtraction.
- `bigInt(3).subtract(5)` => `-2`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Subtraction)
#### `times(number)`
Alias for the `multiply` method.
- `bigInt(111).times(111)` => `12321`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#Multiplication)
#### `toArray(radix)`
Converts a bigInt into an object with the properties "value" and "isNegative." "Value" is an array of integers modulo the given radix. "isNegative" is a boolean that represents the sign of the result.
- `bigInt("1e9").toArray(10)` => {
value: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
isNegative: false
}
- `bigInt("1e9").toArray(16)` => {
value: [3, 11, 9, 10, 12, 10, 0, 0],
isNegative: false
}
- `bigInt(567890).toArray(100)` => {
value: [56, 78, 90],
isNegative: false
}
Negative bases are supported.
- `bigInt(12345).toArray(-10)` => {
value: [2, 8, 4, 6, 5],
isNegative: false
}
Base 1 and base -1 are also supported.
- `bigInt(-15).toArray(1)` => {
value: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
isNegative: true
}
- `bigInt(-15).toArray(-1)` => {
value: [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
isNegative: false
}
Base 0 is only allowed for the number zero.
- `bigInt(0).toArray(0)` => {
value: [0],
isNegative: false
}
- `bigInt(1).toArray(0)` => `Error: Cannot convert nonzero numbers to base 0.`
#### `toJSNumber()`
Converts a bigInt into a native Javascript number. Loses precision for numbers outside the range `[-9007199254740992, 9007199254740992]`.
- `bigInt("18446744073709551616").toJSNumber()` => `18446744073709552000`
#### `xor(number)`
Performs the bitwise XOR operation. The operands are treated as if they were represented using [two's complement representation](http://en.wikipedia.org/wiki/Two%27s_complement).
- `bigInt(12).xor(5)` => `9`
- `bigInt(12).xor(-5)` => `-9`
### Static Methods
#### `fromArray(digits, base = 10, isNegative?)`
Constructs a bigInt from an array of digits in base `base`. The optional `isNegative` flag will make the number negative.
- `bigInt.fromArray([1, 2, 3, 4, 5], 10)` => `12345`
- `bigInt.fromArray([1, 0, 0], 2, true)` => `-4`
#### `gcd(a, b)`
Finds the greatest common denominator of `a` and `b`.
- `bigInt.gcd(42,56)` => `14`
#### `isInstance(x)`
Returns `true` if `x` is a BigInteger, `false` otherwise.
- `bigInt.isInstance(bigInt(14))` => `true`
- `bigInt.isInstance(14)` => `false`
#### `lcm(a,b)`
Finds the least common multiple of `a` and `b`.
- `bigInt.lcm(21, 6)` => `42`
#### `max(a,b)`
Returns the largest of `a` and `b`.
- `bigInt.max(77, 432)` => `432`
#### `min(a,b)`
Returns the smallest of `a` and `b`.
- `bigInt.min(77, 432)` => `77`
#### `randBetween(min, max, [rng])`
Returns a random number between `min` and `max`, optionally using `rng` to generate randomness.
- `bigInt.randBetween("-1e100", "1e100")` => (for example) `8494907165436643479673097939554427056789510374838494147955756275846226209006506706784609314471378745`
`rng` should take no arguments and return a `number` between 0 and 1. It defaults to `Math.random`.
- `bigInt.randBetween("-1e100", "1e100", () => 0.5)` => (always) `50000005000000500000050000005000000500000050000005000000500000050000005000000500000050000005000000`
### Override Methods
#### `toString(radix = 10, [alphabet])`
Converts a bigInt to a string. There is an optional radix parameter (which defaults to 10) that converts the number to the given radix. Digits in the range `10-35` will use the letters `a-z`.
- `bigInt("1e9").toString()` => `"1000000000"`
- `bigInt("1e9").toString(16)` => `"3b9aca00"`
You can use a custom base alphabet with the second parameter. The default `alphabet` is `"0123456789abcdefghijklmnopqrstuvwxyz"`.
- `bigInt("5").toString(2, "aA")` => `"AaA"`
**Note that arithmetical operators will trigger the `valueOf` function rather than the `toString` function.** When converting a bigInteger to a string, you should use the `toString` method or the `String` function instead of adding the empty string.
- `bigInt("999999999999999999").toString()` => `"999999999999999999"`
- `String(bigInt("999999999999999999"))` => `"999999999999999999"`
- `bigInt("999999999999999999") + ""` => `1000000000000000000`
Bases larger than 36 are supported. If a digit is greater than or equal to 36, it will be enclosed in angle brackets.
- `bigInt(567890).toString(100)` => `"<56><78><90>"`
Negative bases are also supported.
- `bigInt(12345).toString(-10)` => `"28465"`
Base 1 and base -1 are also supported.
- `bigInt(-15).toString(1)` => `"-111111111111111"`
- `bigInt(-15).toString(-1)` => `"101010101010101010101010101010"`
Base 0 is only allowed for the number zero.
- `bigInt(0).toString(0)` => `0`
- `bigInt(1).toString(0)` => `Error: Cannot convert nonzero numbers to base 0.`
[View benchmarks for this method](http://peterolson.github.io/BigInteger.js/benchmark/#toString)
#### `valueOf()`
Converts a bigInt to a native Javascript number. This override allows you to use native arithmetic operators without explicit conversion:
- `bigInt("100") + bigInt("200") === 300; //true`
## Contributors
To contribute, just fork the project, make some changes, and submit a pull request. Please verify that the unit tests pass before submitting.
The unit tests are contained in the `spec/spec.js` file. You can run them locally by opening the `spec/SpecRunner.html` or file or running `npm test`. You can also [run the tests online from GitHub](http://peterolson.github.io/BigInteger.js/spec/SpecRunner.html).
There are performance benchmarks that can be viewed from the `benchmarks/index.html` page. You can [run them online from GitHub](http://peterolson.github.io/BigInteger.js/benchmark/).
## License
This project is public domain. For more details, read about the [Unlicense](http://unlicense.org/).
+29
View File
@@ -0,0 +1,29 @@
{
"name": "big-integer",
"description": "An arbitrary length integer library for Javascript",
"main": "./BigInteger.js",
"authors": [
"Peter Olson"
],
"license": "Unlicense",
"keywords": [
"math",
"big",
"bignum",
"bigint",
"biginteger",
"integer",
"arbitrary",
"precision",
"arithmetic"
],
"homepage": "https://github.com/peterolson/BigInteger.js",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"coverage",
"tests"
]
}
@@ -0,0 +1,48 @@
{
"name": "big-integer",
"version": "1.6.49",
"author": "Peter Olson <peter.e.c.olson+npm@gmail.com>",
"description": "An arbitrary length integer library for Javascript",
"contributors": [],
"bin": {},
"scripts": {
"test": "tsc && karma start my.conf.js && node spec/tsDefinitions.js",
"minify": "uglifyjs BigInteger.js -o BigInteger.min.js"
},
"main": "./BigInteger",
"repository": {
"type": "git",
"url": "git@github.com:peterolson/BigInteger.js.git"
},
"keywords": [
"math",
"big",
"bignum",
"bigint",
"biginteger",
"integer",
"arbitrary",
"precision",
"arithmetic"
],
"devDependencies": {
"@types/lodash": "^4.14.118",
"@types/node": "^7.10.2",
"coveralls": "^3.0.6",
"jasmine": "3.5.0",
"jasmine-core": "^3.5.0",
"karma": "^4.3.0",
"karma-cli": "^2.0.0",
"karma-coverage": "^2.0.1",
"karma-jasmine": "^2.0.1",
"karma-phantomjs-launcher": "^1.0.4",
"lodash": "^4.17.21",
"typescript": "^3.6.3",
"uglifyjs": "^2.4.10"
},
"license": "Unlicense",
"engines": {
"node": ">=0.6"
},
"typings": "./BigInteger.d.ts"
}
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": [
"es6"
],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": false,
"baseUrl": "./",
"moduleResolution": "node",
"allowJs": true,
"typeRoots": [
"./"
],
"types": [
"node"
],
"forceConsistentCasingInFileNames": true
},
"files": [
"BigInteger.d.ts",
"spec/tsDefinitions.ts"
]
}
+1
View File
@@ -0,0 +1 @@
node_modules
+4
View File
@@ -0,0 +1,4 @@
language: node_js
node_js:
- 0.4
- 0.6

Some files were not shown because too many files have changed in this diff Show More