Resolve merge conflicts

merge-requests/22/head
Hiroyuki 2021-03-19 21:23:38 -04:00
commit 8d9a47b93e
No known key found for this signature in database
GPG Key ID: C15AC26538975A24
139 changed files with 14169 additions and 0 deletions

47
.eslintrc.json Normal file
View File

@ -0,0 +1,47 @@
{
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
},
"env": {
"es6": true,
"node": true
},
"extends": ["airbnb-base"],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"linebreak-style": "off",
"no-unused-vars": "off",
"max-len": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"class-methods-use-this": "off",
"no-restricted-syntax": "off",
"camelcase": "off",
"indent": "warn",
"object-curly-newline": "off",
"import/prefer-default-export": "off",
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": 2,
"import/extensions": "off",
"no-param-reassign": "off",
"no-underscore-dangle": "off",
"keyword-spacing": "off",
"no-multiple-empty-lines": "off",
"consistent-return": "off",
"no-continue": "off"
},
"ignorePatterns": "**/*.js"
}

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
# Package Management & Libraries
node_modules
# Configuration Files
config.yaml
configs/config.yaml
src/config.yaml
build/config.yaml
.vscode
yarn-error.log
google.json
src/google.json
build/google.json
# Build/Distribution Files
build
dist
# Storage/DB Files
localstorage

23
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,23 @@
stages:
- lint
- build
lint:
stage: lint
script: |
yarn install
yarn lint
only:
- pushes
- merge_requests
- web
tsc:
stage: build
script: |
yarn install
tsc -p tsconfig.json -noEmit
only:
- pushes
- merge_requests
- web

View File

@ -0,0 +1,13 @@
# BUG REPORT
**Brief Description:**
**Priority:** (1-5, 5 being the most urgent)
**Steps to Reproduce:** (separated with numbers)
**Expected Result:**
**Actual Result:**
**Notes:** (delete if none)

View File

@ -0,0 +1,5 @@
## FEATURE REQUEST / SUGGESTION
**Request:**
**Description:**

16
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,16 @@
# CONTRIBUTIONS
We accept contributions from the community, however there's a few steps you need to do first before you're able to fork the project.
1. Join the [Discord Server](https://loc.sh/discord).
2. Send a DM to @Ramirez in the server, provide your GitLab username.
3. We'll let you know when you'll be able to fork the project.
## Issues
If you're interested in tackling an issue, please comment on that particular issue that you're handling it so Maintainers can label it appropriately.
## Other Information
* Make sure your contributions match the current style of the code, run `yarn run lint` to find issues with the style. Requests will be denied if they do not comply with styling.
* Submit your merge requests to the **dev** branch only.
* If you can use TypeScript functionality, do it. For example, don't declare something as `any` if it can be typed.

661
LICENSE Normal file
View File

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
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.
Community Relations
Copyright (C) 2020 Engineering Management
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

10
Makefile Normal file
View File

@ -0,0 +1,10 @@
all: clean build
clean:
@-rm -rf build
build:
-npx tsc -p ./tsconfig.json
run:
cd build && node main

64
package.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "loccr",
"version": "1.0.0",
"description": "The official system for handling Community Relations in the LOC Discord server.",
"main": "build/main.js",
"scripts": {
"lint": "eslint -c ./.eslintrc.json src --ext ts"
},
"repository": "https://gitlab.libraryofcode.org/engineering/communityrelations.git",
"author": "Matthew R, AD, FSEN <matthew@staff.libraryofcode.org>",
"license": "AGPL-3.0",
"private": false,
"devDependencies": {
"@types/ari-client": "^2.2.2",
"@types/bull": "^3.14.4",
"@types/cron": "^1.7.2",
"@types/express": "^4.17.6",
"@types/helmet": "^0.0.47",
"@types/jsonwebtoken": "^8.5.0",
"@types/mathjs": "^6.0.7",
"@types/mongoose": "^5.7.19",
"@types/node": "^14.14.25",
"@types/nodemailer": "^6.4.0",
"@types/puppeteer": "^5.4.3",
"@types/signale": "^1.4.1",
"@types/uuid": "^7.0.3",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"eslint": "^7.19.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-plugin-import": "^2.20.2",
"tslib": "^2.1.0",
"typescript": "^3.9.2"
},
"dependencies": {
"@google-cloud/text-to-speech": "^3.1.2",
"ari-client": "^2.2.0",
"asterisk-manager": "^0.1.16",
"awesome-phonenumber": "^2.45.0",
"axios": "^0.19.2",
"body-parser": "^1.19.0",
"brain.js": "^2.0.0-beta.2",
"bull": "^3.20.1",
"cheerio": "^1.0.0-rc.5",
"cron": "^1.8.2",
"eris": "^0.14.0",
"eris-pagination": "github:bsian03/eris-pagination",
"express": "^4.17.1",
"helmet": "^3.22.0",
"jsonwebtoken": "^8.5.1",
"mathjs": "^7.6.0",
"moment": "^2.25.3",
"mongoose": "^5.11.15",
"nodemailer": "^6.4.8",
"pluris": "^0.2.5",
"puppeteer": "^5.5.0",
"sd-notify": "^2.8.0",
"signale": "^1.4.0",
"stock-info": "^1.2.0",
"stripe": "^8.120.0",
"uuid": "^8.0.0",
"yaml": "^1.9.2"
}
}

View File

@ -0,0 +1,3 @@
import { Server, ServerManagement } from '../../class';
export default (management: ServerManagement) => new Server(management, 3892, `${__dirname}/routes`);

View File

@ -0,0 +1 @@
export { default as Root } from './root';

View File

@ -0,0 +1,920 @@
import { Guild, GuildTextableChannel, Member, TextChannel } from 'eris';
import { v4 as genUUID } from 'uuid';
import { Request } from 'express';
import { RichEmbed, Route, Server } from '../../../class';
import { StaffInterface } from '../../../models';
export default class Root extends Route {
constructor(server: Server) {
super(server);
this.conf = {
path: '/api',
};
this.server.client.once('ready', async () => {
this.guild = this.server.client.guilds.get(this.server.client.config.guildID) || await this.server.client.getRESTGuild(this.server.client.config.guildID);
this.directorRole = '662163685439045632';
this.directorLogs = <TextChannel>this.guild.channels.get('807444198969835550');
this.chairman = '278620217221971968';
});
}
private guild: Guild;
private directorRole: string;
private directorLogs: GuildTextableChannel;
private chairman: string;
private async authenticate(req: Request) {
if (!req.headers.authorization) return false;
const users = await this.server.client.db.Score.find();
const director = users.find((user) => user.pin.join('-') === req.headers.authorization);
if (!director) return false;
const member = this.guild.members.get(director.userID);
if (!member) return false;
if (!member.roles.includes(this.directorRole)) return false;
const staffProfile = await this.server.client.db.Staff.findOne({ userID: member.id });
return { member, director: staffProfile };
}
private genEmbed(
id: string,
type: 'eo' | 'motion' | 'proc' | 'res' | 'confirmMotion',
director: {
user: StaffInterface,
member: Member
},
payload: {
subject: string,
body: string,
},
) {
let title: string;
let color: number;
switch (type) {
case 'eo':
title = 'Executive Order';
color = 0xff00a7;
break;
case 'motion':
title = 'Motion';
color = 0xffbb5f;
break;
case 'proc':
title = 'Proclamation';
color = 0x9b89ff;
break;
case 'res':
title = 'Resolution';
color = 0x27b17a;
break;
case 'confirmMotion':
title = 'Motion Confirmed';
color = 0x4a6cc5;
break;
default:
throw new TypeError('You\'ve specified an invalid type for this action log. Valid types: "eo", "motion", "proc" and "res"');
}
const embed = new RichEmbed();
embed.setTitle(title);
embed.setAuthor(`${director.member.username}#${director.member.discriminator}, ${director.user.pn.join(', ')}`, director.member.avatarURL);
embed.setDescription(`${id}\n\n_This action is available on the Board Register System Directory. You can make changes or edit it [here](https://board.ins/directory?id=${id})._`);
embed.addField('Subject', payload.subject);
embed.addField('Body', payload.body);
embed.setColor(color);
embed.setFooter('Library of Code sp-us | Board Register System', 'https://static.libraryofcode.org/library_of_code.png');
embed.setTimestamp();
return embed;
}
public bind() {
this.router.all('/', (_req, res, next) => {
res.setHeader('Control-Allow-Access-Origin', '*');
next();
});
this.router.get('/', async (_req, res) => {
const eo = await this.server.client.db.ExecutiveOrder.countDocuments();
const motion = await this.server.client.db.Motion.countDocuments();
const proc = await this.server.client.db.Proclamation.countDocuments();
const resolution = await this.server.client.db.Proclamation.countDocuments();
res.status(200).json({
code: this.constants.codes.SUCCESS,
counts: {
eo,
motion,
proc,
resolution,
total: eo + motion + proc + resolution,
},
});
});
this.router.get('/identify', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).send({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: `You have authenticated as a Director. Welcome ${authenticated.member.username}#${authenticated.member.discriminator}.`,
user: {
username: authenticated.member.username,
discriminator: authenticated.member.discriminator,
id: authenticated.member.id,
avatarURL: authenticated.member.avatarURL || authenticated.member.defaultAvatarURL,
},
});
});
this.router.post('/eo', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).send({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
if (typeof req.body.subject !== 'string' || typeof req.body.body !== 'string' || req.body.subject.length > 256 || req.body.body.length > 1024) {
return res.status(400).json({
code: this.constants.codes.CLIENT_ERROR,
message: this.constants.messages.CLIENT_ERROR,
});
}
const id = genUUID();
const embed = this.genEmbed(
id,
'eo',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: req.body.subject,
body: req.body.body,
},
);
const msg = await this.directorLogs.createMessage({ embed });
const eo = await this.server.client.db.ExecutiveOrder.create({
issuer: authenticated.director.userID,
subject: req.body.subject,
body: req.body.body,
at: Date.now(),
oID: id,
msg: msg.id,
});
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: `Created new Executive Order with ID ${eo.oID} by ${authenticated.member.username}#${authenticated.member.discriminator}`,
});
});
this.router.post('/motion', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).send({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
if (typeof req.body.subject !== 'string' || typeof req.body.body !== 'string' || req.body.subject.length > 256 || req.body.body.length > 1024) {
return res.status(400).json({
code: this.constants.codes.CLIENT_ERROR,
message: this.constants.messages.CLIENT_ERROR,
});
}
const id = genUUID();
const embed = this.genEmbed(
id,
'motion',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: req.body.subject,
body: req.body.body,
},
);
const msg = await this.directorLogs.createMessage({ embed });
const motion = await this.server.client.db.Motion.create({
issuer: authenticated.director.userID,
subject: req.body.subject,
body: req.body.body,
at: Date.now(),
oID: id,
processed: false,
msg: msg.id,
});
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: `Created new Motion with ID ${motion.oID} by ${authenticated.member.username}#${authenticated.member.discriminator}`,
});
});
this.router.post('/proc', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).send({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
if (typeof req.body.subject !== 'string' || typeof req.body.body !== 'string' || req.body.subject.length > 256 || req.body.body.length > 1024) {
return res.status(400).json({
code: this.constants.codes.CLIENT_ERROR,
message: this.constants.messages.CLIENT_ERROR,
});
}
const id = genUUID();
const embed = this.genEmbed(
id,
'proc',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: req.body.subject,
body: req.body.body,
},
);
const msg = await this.directorLogs.createMessage({ embed });
await msg.addReaction('modSuccess:578750988907970567');
await msg.addReaction('modError:578750737920688128');
await msg.addReaction('🙋');
const proc = await this.server.client.db.Proclamation.create({
issuer: authenticated.director.userID,
subject: req.body.subject,
body: req.body.body,
at: Date.now(),
oID: id,
processed: false,
msg: msg.id,
});
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: `Created new Proclamation with ID ${proc.oID} by ${authenticated.member.username}#${authenticated.member.discriminator}`,
});
});
this.router.delete('/eo/:id', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
const eo = await this.server.client.db.ExecutiveOrder.findOne({ oID: req.params.id });
if (!eo) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
if (eo.issuer !== authenticated.member.id && authenticated.member.id !== this.chairman) {
return res.status(403).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.codes.UNAUTHORIZED,
});
}
await eo.deleteOne();
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: 'Executive Order deleted.',
});
});
this.router.delete('/motion/:id', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
const motion = await this.server.client.db.Motion.findOne({ oID: req.params.id });
if (!motion) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
if (motion.issuer !== authenticated.member.id && authenticated.member.id !== this.chairman) {
return res.status(403).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.codes.UNAUTHORIZED,
});
}
await motion.deleteOne();
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: 'Motion deleted.',
});
});
this.router.delete('/proc/:id', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
const proc = await this.server.client.db.Proclamation.findOne({ oID: req.params.id });
if (!proc) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
if (proc.issuer !== authenticated.member.id && authenticated.member.id !== this.chairman) {
return res.status(403).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.codes.UNAUTHORIZED,
});
}
await proc.deleteOne();
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: 'Proclamation deleted.',
});
});
this.router.get('/eo/:id', async (req, res) => {
const eo = await this.server.client.db.ExecutiveOrder.findOne({ oID: req.params.id });
if (!eo) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
const issuer = this.guild.members.get(eo.issuer);
res.status(200).json({
code: this.constants.codes.SUCCESS,
issuer: {
username: issuer.username,
discriminator: issuer.discriminator,
avatarURL: issuer.avatarURL,
},
subject: eo.subject,
body: eo.body,
issuedAt: eo.at,
id: eo.oID,
});
});
this.router.get('/motion/:id', async (req, res) => {
const motion = await this.server.client.db.Motion.findOne({ oID: req.params.id });
if (!motion) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
const issuer = this.guild.members.get(motion.issuer);
res.status(200).json({
code: this.constants.codes.SUCCESS,
issuer: {
username: issuer.username,
discriminator: issuer.discriminator,
avatarURL: issuer.avatarURL,
},
subject: motion.subject,
body: motion.body,
issuedAt: motion.at,
id: motion.oID,
voteResults: motion.results || null,
});
});
this.router.get('/proc/:id', async (req, res) => {
const proc = await this.server.client.db.Proclamation.findOne({ oID: req.params.id });
if (!proc) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
const issuer = this.guild.members.get(proc.issuer);
res.status(200).json({
code: this.constants.codes.SUCCESS,
issuer: {
username: issuer.username,
discriminator: issuer.discriminator,
avatarURL: issuer.avatarURL,
},
subject: proc.subject,
body: proc.body,
issuedAt: proc.at,
id: proc.oID,
voteResults: proc.results || null,
});
});
this.router.get('/resolution/:id', async (req, res) => {
const resolution = await this.server.client.db.Resolution.findOne({ oID: req.params.id });
if (!resolution) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
const issuer = this.guild.members.get(resolution.issuer);
res.status(200).json({
code: this.constants.codes.SUCCESS,
issuer: {
username: issuer.username,
discriminator: issuer.discriminator,
avatarURL: issuer.avatarURL,
},
subject: resolution.subject,
body: resolution.body,
issuedAt: resolution.at,
id: resolution.oID,
voteResults: resolution.results || null,
});
});
this.router.patch('/eo/:id', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
const eo = await this.server.client.db.ExecutiveOrder.findOne({ oID: req.params.id });
if (!eo) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
if (eo.issuer !== authenticated.member.id && authenticated.member.id !== this.chairman) {
return res.status(403).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.codes.UNAUTHORIZED,
});
}
await eo.updateOne({
subject: req.body.subject || eo.subject,
body: req.body.body || eo.body,
});
if (eo.subject !== req.body.subject || eo.body !== req.body.body) {
const message = await this.directorLogs.getMessage(eo.msg);
const embed = this.genEmbed(
eo.oID,
'eo',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: req.body.subject,
body: req.body.body,
},
);
await message.edit({ embed });
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: 'Updated Executive Order.',
});
});
this.router.patch('/motion/:id', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
const motion = await this.server.client.db.Motion.findOne({ oID: req.params.id });
if (!motion) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
if (motion.issuer !== authenticated.member.id && authenticated.member.id !== this.chairman) {
return res.status(403).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.codes.UNAUTHORIZED,
});
}
await motion.updateOne({
subject: req.body.subject || motion.subject,
body: req.body.body || motion.body,
});
if (motion.subject !== req.body.subject || motion.body !== req.body.body) {
const message = await this.directorLogs.getMessage(motion.msg);
const embed = this.genEmbed(
motion.oID,
'motion',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: req.body.subject,
body: req.body.body,
},
);
await message.edit({ embed });
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: 'Updated Motion.',
});
});
this.router.patch('/proc/:id', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
const proc = await this.server.client.db.Proclamation.findOne({ oID: req.params.id });
if (!proc) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
if (proc.issuer !== authenticated.member.id && authenticated.member.id !== this.chairman) {
return res.status(403).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.codes.UNAUTHORIZED,
});
}
await proc.updateOne({
subject: req.body.subject || proc.subject,
body: req.body.body || proc.body,
});
if (proc.subject !== req.body.subject || proc.body !== req.body.body) {
const message = await this.directorLogs.getMessage(proc.msg);
const embed = this.genEmbed(
proc.oID,
'proc',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: req.body.subject,
body: req.body.body,
},
);
await message.edit({ embed });
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: 'Updated Proclamation.',
});
});
this.router.patch('/resolution/:id', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
const resolution = await this.server.client.db.Resolution.findOne({ oID: req.params.id });
if (!resolution) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
if (resolution.issuer !== authenticated.member.id && authenticated.member.id !== this.chairman) {
return res.status(403).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.codes.UNAUTHORIZED,
});
}
await resolution.updateOne({
subject: req.body.subject || resolution.subject,
body: req.body.body || resolution.body,
});
if (resolution.subject !== req.body.subject || resolution.body !== req.body.body) {
const message = await this.directorLogs.getMessage(resolution.msg);
const embed = this.genEmbed(
resolution.oID,
'res',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: req.body.subject,
body: req.body.body,
},
);
await message.edit({ embed });
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: 'Updated Resolution.',
});
});
this.router.get('/eo', async (req, res) => {
const page = !Number.isNaN(Number(req.query.page)) ? Number(req.query.page) : 0;
const skipped = page || page * 10;
const eo = await this.server.client.db.ExecutiveOrder.find().skip(skipped);
const returned: {
oID: string;
author: string;
issuedAt: number;
}[] = [];
for (const result of eo) {
const director = this.guild.members.get(result.issuer);
const author = director ? `${director.username}#${director.discriminator}` : 'Deleted User#0000';
returned.push({
oID: result.oID,
author,
issuedAt: result.at,
});
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
results: returned,
});
});
this.router.get('/motion', async (req, res) => {
const page = !Number.isNaN(Number(req.query.page)) ? Number(req.query.page) : 0;
const skipped = page || page * 10;
const motion = await this.server.client.db.Motion.find().skip(skipped);
const returned: {
oID: string;
author: string;
issuedAt: number;
}[] = [];
for (const result of motion) {
const director = this.guild.members.get(result.issuer);
const author = director ? `${director.username}#${director.discriminator}` : 'Deleted User#0000';
returned.push({
oID: result.oID,
author,
issuedAt: result.at,
});
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
results: returned,
});
});
this.router.get('/proc', async (req, res) => {
const page = !Number.isNaN(Number(req.query.page)) ? Number(req.query.page) : 0;
const skipped = page || page * 10;
const proc = await this.server.client.db.Proclamation.find().skip(skipped);
const returned: {
oID: string;
author: string;
issuedAt: number;
}[] = [];
for (const result of proc) {
const director = this.guild.members.get(result.issuer);
const author = director ? `${director.username}#${director.discriminator}` : 'Deleted User#0000';
returned.push({
oID: result.oID,
author,
issuedAt: result.at,
});
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
results: returned,
});
});
this.router.get('/resolution', async (req, res) => {
const page = !Number.isNaN(Number(req.query.page)) ? Number(req.query.page) : 0;
const skipped = page || page * 10;
const resolution = await this.server.client.db.Resolution.find().skip(skipped);
const returned: {
oID: string;
author: string;
issuedAt: number;
}[] = [];
for (const result of resolution) {
const director = this.guild.members.get(result.issuer);
const author = director ? `${director.username}#${director.discriminator}` : 'Deleted User#0000';
returned.push({
oID: result.oID,
author,
issuedAt: result.at,
});
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
results: returned,
});
});
this.router.put('/motion/confirm/:id', async (req, res) => {
const authenticated = await this.authenticate(req);
if (!authenticated) {
return res.status(401).json({
code: this.constants.codes.UNAUTHORIZED,
message: this.constants.messages.UNAUTHORIZED,
});
}
const motion = await this.server.client.db.Motion.findOne({ oID: req.params.id });
if (!motion) {
return res.status(404).json({
code: this.constants.codes.NOT_FOUND,
message: this.constants.messages.NOT_FOUND,
});
}
if (Number.isNaN(Number(req.body.yea)) || Number.isNaN(Number(req.body.nay)) || Number.isNaN(Number(req.body.present))) {
return res.status(400).json({
code: this.constants.codes.CLIENT_ERROR,
message: this.constants.messages.CLIENT_ERROR,
});
}
const yea = Number(req.body.yea);
const nay = Number(req.body.nay);
const present = Number(req.body.present);
const total = this.guild.members.filter((member) => member.roles.includes(this.directorRole));
const absent = total.length - (yea + nay + present);
await motion.updateOne({
processed: true,
results: {
yea,
nay,
present,
absent,
},
});
const confirmationEmbed = this.genEmbed(
motion.oID,
'confirmMotion',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: motion.subject,
body: motion.body,
},
);
confirmationEmbed.addField('Results', `**Total:** ${total.length}\n**Yea:** ${yea}\n**Nay:** ${nay}\n**Present:** ${present}\n**Absent:** ${absent}`);
await this.directorLogs.createMessage({ embed: confirmationEmbed });
const excludingYea = nay + present + absent;
if (yea > excludingYea) {
const resolutionEmbed = this.genEmbed(
motion.oID,
'res',
{
user: authenticated.director,
member: authenticated.member,
},
{
subject: motion.subject,
body: motion.body,
},
);
const resolutionMessage = await this.directorLogs.createMessage({ embed: resolutionEmbed });
await this.server.client.db.Resolution.create({
issuer: motion.issuer,
subject: motion.subject,
body: motion.body,
at: Date.now(),
oID: motion.oID,
results: {
yea,
nay,
present,
absent,
},
msg: resolutionMessage.id,
});
}
res.status(200).json({
code: this.constants.codes.SUCCESS,
message: 'Confirmed the results of the Motion.',
});
});
}
}

View File

@ -0,0 +1,3 @@
import { Server, ServerManagement } from '../../class';
export default (management: ServerManagement) => new Server(management, 3895, `${__dirname}/routes`);

View File

@ -0,0 +1 @@
export { default as report } from './report';

View File

@ -0,0 +1,605 @@
/* eslint-disable no-bitwise */
/* eslint-disable no-continue */
import jwt from 'jsonwebtoken';
import { TextChannel } from 'eris';
import { LocalStorage, Route, Server } from '../../../class';
import { ScoreHistoricalRaw } from '../../../models/ScoreHistorical';
import { getTotalMessageCount } from '../../../intervals/score';
export default class Report extends Route {
public timeout: Map<string, number>;
public acceptedOffers: LocalStorage;
constructor(server: Server) {
super(server);
this.timeout = new Map();
this.acceptedOffers = new LocalStorage('accepted-offers');
this.conf = {
path: '/report',
};
}
protected check(userID: string) {
if (this.timeout.has(userID)) {
if (this.timeout.get(userID) >= 3) {
return true;
}
this.timeout.set(userID, this.timeout.get(userID) + 1);
} else {
this.timeout.set(userID, 1);
}
setTimeout(() => {
if (this.timeout.has(userID)) {
this.timeout.set(userID, this.timeout.get(userID) - 1);
} else {
this.timeout.delete(userID);
}
}, 30000);
return false;
}
public bind() {
this.router.all('*', (_req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', '*');
res.setHeader('Access-Control-Allow-Headers', '*');
next();
});
this.router.post('/hard', async (req, res) => {
try {
if (!req.headers.authorization) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
if (!req.body.pin || !req.body.userID || !req.body.reason) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR, message: this.constants.messages.CLIENT_ERROR });
if (req.body.reason?.length < 1) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR, message: this.constants.codes.CLIENT_ERROR });
const merchant = await this.server.client.db.Merchant.findOne({ key: req.headers.authorization }).lean().exec();
if (!merchant) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const member = await this.server.client.db.Score.findOne({ userID: req.body.userID, 'pin.2': req.body.pin }).lean().exec();
if (!member) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const mem = this.server.client.util.resolveMember(member.userID, this.server.client.guilds.get(this.server.client.config.guildID));
if (!mem) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.codes.NOT_FOUND });
if (member.locked) return res.status(403).json({ code: this.constants.codes.PERMISSION_DENIED, message: this.constants.messages.PERMISSION_DENIED });
if (merchant?.type !== 1) return res.status(403).json({ code: this.constants.codes.PERMISSION_DENIED, message: this.constants.messages.PERMISSION_DENIED });
if (this.check(member.userID)) {
await this.server.client.db.Score.updateOne({ userID: member.userID }, { $set: { locked: true } });
const chan = await this.server.client.getDMChannel(member.userID);
try {
await chan.createMessage(`__**Community Report Locked**__\nWe've detected suspicious activity on your Community Report, for the integrity of your report we have automatically locked it. To unlock your report, please run \`${this.server.client.config.prefix}score pref unlock\` in <#468759629334183956>.`);
} catch (err) {
this.server.client.util.signale.error(`Unable to DM user: ${member.userID} | ${err}`);
}
return res.status(403).json({ code: this.constants.codes.PERMISSION_DENIED, message: this.constants.messages.PERMISSION_DENIED });
}
const flags = [];
if (mem.user.publicFlags) {
if ((mem.user.publicFlags & (1 << 0)) === 1 << 0) flags.push('DISCORD_EMPLOYEE');
if ((mem.user.publicFlags & (1 << 1)) === 1 << 1) flags.push('PARTNERED_SERVER_OWNER');
if ((mem.user.publicFlags & (1 << 2)) === 1 << 2) flags.push('HYPESQUAD_EVENTS');
if ((mem.user.publicFlags & (1 << 3)) === 1 << 3) flags.push('BUG_HUNTER_1');
if ((mem.user.publicFlags & (1 << 6)) === 1 << 6) flags.push('HOUSE_BRAVERY');
if ((mem.user.publicFlags & (1 << 7)) === 1 << 7) flags.push('HOUSE_BRILLIANCE');
if ((mem.user.publicFlags & (1 << 8)) === 1 << 8) flags.push('HOUSE_BALANCE');
if ((mem.user.publicFlags & (1 << 9)) === 1 << 9) flags.push('EARLY_SUPPORTER');
if ((mem.user.publicFlags & (1 << 10)) === 1 << 10) flags.push('TEAM_USER');
if ((mem.user.publicFlags & (1 << 12)) === 1 << 12) flags.push('SYSTEM');
if ((mem.user.publicFlags & (1 << 14)) === 1 << 14) flags.push('BUG_HUNTER_2');
if ((mem.user.publicFlags & (1 << 16)) === 1 << 16) flags.push('VERIFIED_BOT');
if ((mem.user.publicFlags & (1 << 17)) === 1 << 17) flags.push('EARLY_VERIFIED_BOT_DEVELOPER');
}
const set = [];
const accounts = await this.server.client.db.Score.find().lean().exec();
for (const sc of accounts) {
if (sc.total < 200) { continue; }
if (sc.total > 800) { set.push(800); continue; }
set.push(sc.total);
}
let totalScore: number;
let activityScore: number;
const moderationScore = Math.round(member.moderation);
let roleScore: number;
let cloudServicesScore: number;
const otherScore = Math.round(member.other);
let miscScore: number;
if (member.total < 200) totalScore = 0;
else if (member.total > 800) totalScore = 800;
else totalScore = Math.round(member.total);
if (member.activity < 10) activityScore = 0;
else if (member.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activityScore = Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12));
else activityScore = Math.round(member.activity);
if (member.roles <= 0) roleScore = 0;
else if (member.roles > 54) roleScore = 54;
else roleScore = Math.round(member.roles);
if (member.staff <= 0) miscScore = 0;
else miscScore = Math.round(member.staff);
if (member.cloudServices === 0) cloudServicesScore = 0;
else if (member.cloudServices > 10) cloudServicesScore = 10;
else cloudServicesScore = Math.round(member.cloudServices);
const inqs = await this.server.client.db.Inquiry.find({ userID: member.userID }).lean().exec();
const inquiries: [{ id?: string, name: string, date: Date }?] = [];
if (inqs?.length > 0) {
for (const inq of inqs) {
const testDate = (new Date(new Date(inq.date).setHours(1460)));
if (testDate > new Date()) inquiries.push({ id: inq.iid, name: inq.name, date: inq.date });
}
}
const historicalData = await this.server.client.db.ScoreHistorical.find({ userID: member.userID }).lean().exec();
const array: ScoreHistoricalRaw[] = [];
for (const data of historicalData) {
delete data.report?.softInquiries;
let total: number;
let activity: number;
const moderation = Math.round(data.report.moderation);
let role: number;
let cloud: number;
const other = Math.round(data.report.other);
let misc: number;
if (data.report.total < 200) total = 0;
else if (data.report.total > 800) total = 800;
else total = Math.round(data.report.total);
if (data.report.activity < 10) activity = 0;
else if (data.report.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activity = Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12));
else activity = Math.round(data.report.activity);
if (data.report.roles <= 0) role = 0;
else if (data.report.roles > 54) role = 54;
else role = Math.round(data.report.roles);
if (data.report.staff <= 0) role = 0;
else misc = Math.round(data.report.staff);
if (data.report.cloudServices === 0) cloud = 0;
else if (data.report.cloudServices > 10) cloud = 10;
else cloud = Math.round(data.report.cloudServices);
data.report.total = total; data.report.activity = activity; data.report.moderation = moderation; data.report.roles = role; data.report.cloudServices = cloud; data.report.other = other; data.report.staff = misc;
array.push(data);
}
const inq = await this.server.client.report.createInquiry(member.userID, merchant.name, 0, req.body.reason);
await this.server.client.db.Merchant.updateOne({ key: req.headers.authorization }, { $addToSet: { pulls: { type: 0, reason: req.body.reason, date: new Date() } } });
return res.status(200).json({
code: this.constants.codes.SUCCESS,
message: {
id: inq.id,
userID: member.userID,
memberInformation: {
username: mem.user.username,
discriminator: mem.user.discriminator,
joinedServerAt: new Date(mem.joinedAt),
createdAt: new Date(mem.createdAt),
avatarURL: mem.avatarURL,
flags,
nitroBoost: mem.premiumSince === null,
},
percentile: Math.round(this.server.client.util.percentile(set, member.total)),
totalScore,
activityScore,
roleScore,
moderationScore,
cloudServicesScore,
miscScore,
otherScore,
inquiries,
historical: array ?? [],
},
});
} catch (err) {
return this.handleError(err, res);
}
});
this.router.post('/v2/soft', async (req, res) => {
try {
if (!req.headers.authorization) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
if (!req.body.userID) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR, message: this.constants.messages.CLIENT_ERROR });
const merchant = await this.server.client.db.Merchant.findOne({ key: req.headers.authorization }).lean().exec();
if (!merchant) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const report = await this.server.client.db.Score.findOne({ userID: req.body.userID }).lean().exec();
if (!report) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
let totalScore: number;
if (report.total < 200) totalScore = 0;
else if (report.total > 800) totalScore = 800;
else totalScore = Math.round(report.total);
await this.server.client.db.Merchant.updateOne({ key: req.headers.authorization }, { $addToSet: { pulls: { type: 1, reason: 'N/A', date: new Date() } } });
const mem = this.server.client.util.resolveMember(report.userID, this.server.client.guilds.get(this.server.client.config.guildID));
await this.server.client.report.createInquiry(report.userID, merchant.name.toUpperCase(), 1);
if (!merchant.privileged) {
return res.status(200).json({
code: this.constants.codes.SUCCESS,
message: {
userID: report.userID,
totalScore,
},
});
}
const flags = [];
if (mem.user.publicFlags) {
if ((mem.user.publicFlags & (1 << 0)) === 1 << 0) flags.push('DISCORD_EMPLOYEE');
if ((mem.user.publicFlags & (1 << 1)) === 1 << 1) flags.push('PARTNERED_SERVER_OWNER');
if ((mem.user.publicFlags & (1 << 2)) === 1 << 2) flags.push('HYPESQUAD_EVENTS');
if ((mem.user.publicFlags & (1 << 3)) === 1 << 3) flags.push('BUG_HUNTER_1');
if ((mem.user.publicFlags & (1 << 6)) === 1 << 6) flags.push('HOUSE_BRAVERY');
if ((mem.user.publicFlags & (1 << 7)) === 1 << 7) flags.push('HOUSE_BRILLIANCE');
if ((mem.user.publicFlags & (1 << 8)) === 1 << 8) flags.push('HOUSE_BALANCE');
if ((mem.user.publicFlags & (1 << 9)) === 1 << 9) flags.push('EARLY_SUPPORTER');
if ((mem.user.publicFlags & (1 << 10)) === 1 << 10) flags.push('TEAM_USER');
if ((mem.user.publicFlags & (1 << 12)) === 1 << 12) flags.push('SYSTEM');
if ((mem.user.publicFlags & (1 << 14)) === 1 << 14) flags.push('BUG_HUNTER_2');
if ((mem.user.publicFlags & (1 << 16)) === 1 << 16) flags.push('VERIFIED_BOT');
if ((mem.user.publicFlags & (1 << 17)) === 1 << 17) flags.push('EARLY_VERIFIED_BOT_DEVELOPER');
}
const set = [];
if (req.query.p?.toString() === 'true') {
const accounts = await this.server.client.db.Score.find().lean().exec();
for (const sc of accounts) {
if (sc.total < 200) { continue; }
if (sc.total > 800) { set.push(800); continue; }
set.push(sc.total);
}
}
let activityScore: number;
const moderationScore = Math.round(report.moderation);
let roleScore: number;
let cloudServicesScore: number;
const otherScore = Math.round(report.other);
let miscScore: number;
if (report.activity < 10) activityScore = 0;
else if (report.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activityScore = Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12));
else activityScore = Math.round(report.activity);
if (report.roles <= 0) roleScore = 0;
else if (report.roles > 54) roleScore = 54;
else roleScore = Math.round(report.roles);
if (report.staff <= 0) miscScore = 0;
else miscScore = Math.round(report.staff);
if (report.cloudServices === 0) cloudServicesScore = 0;
else if (report.cloudServices > 10) cloudServicesScore = 10;
else cloudServicesScore = Math.round(report.cloudServices);
const historicalData = await this.server.client.db.ScoreHistorical.find({ userID: report.userID }).lean().exec();
const array: ScoreHistoricalRaw[] = [];
for (const data of historicalData) {
delete data.report?.softInquiries;
delete data.report?.inquiries;
let total: number;
let activity: number;
const moderation = Math.round(data.report.moderation);
let role: number;
let cloud: number;
const other = Math.round(data.report.other);
let misc: number;
if (data.report.total < 200) total = 0;
else if (data.report.total > 800) total = 800;
else total = Math.round(data.report.total);
if (data.report.activity < 10) activity = 0;
else if (data.report.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activity = Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12));
else activity = Math.round(data.report.activity);
if (data.report.roles <= 0) role = 0;
else if (data.report.roles > 54) role = 54;
else role = Math.round(data.report.roles);
if (data.report.staff <= 0) role = 0;
else misc = Math.round(data.report.staff);
if (data.report.cloudServices === 0) cloud = 0;
else if (data.report.cloudServices > 10) cloud = 10;
else cloud = Math.round(data.report.cloudServices);
data.report.total = total; data.report.activity = activity; data.report.moderation = moderation; data.report.roles = role; data.report.cloudServices = cloud; data.report.other = other; data.report.staff = misc;
array.push(data);
}
return res.status(200).json({
code: this.constants.codes.SUCCESS,
message: {
userID: report.userID,
memberInformation: {
username: mem.user.username,
discriminator: mem.user.discriminator,
joinedServerAt: new Date(mem.joinedAt),
createdAt: new Date(mem.createdAt),
avatarURL: mem.avatarURL,
flags,
nitroBoost: mem.premiumSince === null,
},
totalScore,
percentile: Math.round(this.server.client.util.percentile(set, report.total)),
activityScore,
moderationScore,
roleScore,
cloudServicesScore,
otherScore,
miscScore,
historical: array ?? [],
},
});
} catch (err) {
return this.handleError(err, res);
}
});
this.router.post('/soft', async (req, res) => {
try {
if (!req.headers.authorization) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
if (!req.body.pin || !req.body.userID) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR, message: this.constants.messages.CLIENT_ERROR });
const merchant = await this.server.client.db.Merchant.findOne({ key: req.headers.authorization }).lean().exec();
if (!merchant) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const member = await this.server.client.db.Score.findOne({ userID: req.body.userID, 'pin.2': req.body.pin }).lean().exec();
if (!member) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const mem = this.server.client.util.resolveMember(member.userID, this.server.client.guilds.get(this.server.client.config.guildID));
if (!mem) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.codes.NOT_FOUND });
let totalScore: number;
if (member.total < 200) totalScore = 0;
else if (member.total > 800) totalScore = 800;
else totalScore = Math.round(member.total);
await this.server.client.db.Merchant.updateOne({ key: req.headers.authorization }, { $addToSet: { pulls: { type: 1, reason: 'N/A', date: new Date() } } });
await this.server.client.report.createInquiry(member.userID, merchant.name.toUpperCase(), 1);
if (!merchant.privileged) {
return res.status(200).json({
code: this.constants.codes.SUCCESS,
message: {
userID: member.userID,
totalScore,
},
});
}
let activityScore: number;
const moderationScore = Math.round(member.moderation);
let roleScore: number;
let cloudServicesScore: number;
const otherScore = Math.round(member.other);
let miscScore: number;
if (member.activity < 10) activityScore = 0;
else if (member.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activityScore = Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12));
else activityScore = Math.round(member.activity);
if (member.roles <= 0) roleScore = 0;
else if (member.roles > 54) roleScore = 54;
else roleScore = Math.round(member.roles);
if (member.staff <= 0) miscScore = 0;
else miscScore = Math.round(member.staff);
if (member.cloudServices === 0) cloudServicesScore = 0;
else if (member.cloudServices > 10) cloudServicesScore = 10;
else cloudServicesScore = Math.round(member.cloudServices);
return res.status(200).json({
code: this.constants.codes.SUCCESS,
message: {
userID: member.userID,
totalScore,
activityScore,
moderationScore,
roleScore,
cloudServicesScore,
otherScore,
miscScore,
},
});
} catch (err) {
return this.handleError(err, res);
}
});
this.router.get('/web', async (req, res) => {
try {
res.setHeader('Access-Control-Allow-Origin', '*');
if (this.timeout.has(req.ip)) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
if (!req.query.pin) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const args = req.query.pin.toString();
this.timeout.set(req.ip, 1);
setTimeout(() => this.timeout.delete(req.ip), 1800000);
let score = await this.server.client.db.Score.findOne({ pin: [Number(args.split('-')[0]), Number(args.split('-')[1]), Number(args.split('-')[2])] }).lean().exec();
if (!score) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const member = await this.server.client.getRESTGuildMember(this.constants.discord.SERVER_ID, score.userID);
if (!member) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
let updated = false;
if (req.query.staff) {
// eslint-disable-next-line no-shadow
const args = req.query.staff.toString();
const staffScore = await this.server.client.db.Score.findOne({ pin: [Number(args.split('-')[0]), Number(args.split('-')[1]), Number(args.split('-')[2])] }).lean().exec();
if (!staffScore) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
if (!staffScore.staff) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
this.timeout.delete(req.ip);
if (staffScore.userID === score.userID) {
updated = true;
await this.server.client.report.createInquiry(member.user.id, `${member.username} via report.libraryofcode.org @ IP ${req.ip}`, 1);
} else {
await this.server.client.report.createInquiry(member.user.id, 'Library of Code sp-us | Staff Team via report.libraryofcode.org', 1);
}
} else if (!updated) {
await this.server.client.report.createInquiry(member.user.id, `${member.username} via report.libraryofcode.org @ IP ${req.ip}`, 1);
}
score = await this.server.client.db.Score.findOne({ pin: [Number(args.split('-')[0]), Number(args.split('-')[1]), Number(args.split('-')[2])] }).lean().exec();
let totalScore = '0';
let activityScore = '0';
let moderationScore = '0';
let roleScore = '0';
let cloudServicesScore = '0';
let otherScore = '0';
let miscScore = '0';
if (score.total < 200) totalScore = '---';
else if (score.total > 800) totalScore = '800';
else totalScore = `${score.total}`;
if (score.activity < 10) activityScore = '---';
else if (score.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activityScore = String(Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12)));
else activityScore = `${score.activity}`;
if (score.roles <= 0) roleScore = '---';
else if (score.roles > 54) roleScore = '54';
else roleScore = `${score.roles}`;
moderationScore = `${score.moderation}`;
if (score.other === 0) otherScore = '---';
else otherScore = `${score.other}`;
if (score.staff <= 0) miscScore = '---';
else miscScore = `${score.staff}`;
if (score.cloudServices === 0) cloudServicesScore = '---';
else if (score.cloudServices > 10) cloudServicesScore = '10';
else cloudServicesScore = `${score.cloudServices}`;
const moderations = await this.server.client.db.Moderation.find({ userID: score.userID }).lean().exec();
const historical = await this.server.client.db.ScoreHistorical.find({ userID: score.userID }).lean().exec();
for (const data of historical) {
let total: number;
let activity: number;
const moderation = Math.round(data.report.moderation);
let role: number;
let cloud: number;
const other = Math.round(data.report.other);
let misc: number;
if (data.report.total < 200) total = 0;
else if (data.report.total > 800) total = 800;
else total = Math.round(data.report.total);
if (data.report.activity < 10) activity = 0;
else if (data.report.activity > Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12))) activity = Math.floor((Math.log1p(getTotalMessageCount(this.server.client)) * 12));
else activity = Math.round(data.report.activity);
if (data.report.roles <= 0) role = 0;
else if (data.report.roles > 54) role = 54;
else role = Math.round(data.report.roles);
if (data.report.staff <= 0) role = 0;
else misc = Math.round(data.report.staff);
if (data.report.cloudServices === 0) cloud = 0;
else if (data.report.cloudServices > 10) cloud = 10;
else cloud = Math.round(data.report.cloudServices);
data.report.total = total; data.report.activity = activity; data.report.moderation = moderation; data.report.roles = role; data.report.cloudServices = cloud; data.report.other = other; data.report.staff = misc;
}
const inqs = await this.server.client.db.Inquiry.find({ userID: score.userID }).lean().exec();
const hardInquiries: [{ id?: string, name: string, reason: string, date: Date }?] = [];
const softInquiries: [{ id?: string, name: string, date: Date }?] = [];
for (const inq of inqs) {
if (inq.type === 0) {
hardInquiries.push({ id: inq.iid, name: inq.name, reason: inq.reason, date: inq.date });
} else if (inq.type === 1) {
softInquiries.push({ name: inq.name, date: inq.date });
}
}
return res.status(200).json({
name: `${member.username}#${member.discriminator}`,
avatarURL: member.avatarURL,
userID: score.userID,
pin: score.pin?.join('-'),
score: totalScore,
activityScore,
cloudServicesScore,
moderationScore,
roleScore,
otherScore,
miscScore,
notify: score.notify,
locked: !!score.locked,
totalModerations: moderations?.length > 0 ? moderations.length : 0,
inquiries: hardInquiries?.length > 0 ? hardInquiries.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) : [],
softInquiries: softInquiries?.length > 0 ? softInquiries.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) : [],
historical: historical ?? [],
lastUpdated: score.lastUpdate,
});
} catch (err) {
return this.handleError(err, res);
}
});
this.router.get('/offer', async (req, res) => {
try {
res.setHeader('Access-Control-Allow-Origin', '*');
if (!req.query.code) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
if (await this.acceptedOffers.get(req.query.code.toString())) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
let offer: {
userID?: string,
staffID?: string,
channelID?: string,
messageID?: string,
pin?: string,
name?: string,
department?: string,
date?: Date,
};
try {
offer = <{
userID?: string,
staffID?: string,
channelID?: string,
messageID?: string,
pin?: string,
name?: string,
department?: string,
date?: Date,
}>jwt.verify(req.query.code.toString(), this.server.client.config.internalKey);
} catch {
return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
}
const chan = <TextChannel>this.server.client.guilds.get(this.constants.discord.SERVER_ID).channels.get(offer.channelID);
await chan.createMessage(`__**PRE-APPROVED OFFER ACCEPTED**__\n<@${offer.staffID}>`);
const message = await chan.getMessage(offer.messageID);
const args = [];
args.push(offer.userID, 'hard');
`${offer.department}:${offer.name}`.split(' ').forEach((item) => args.push(item));
await this.server.client.commands.get('score').run(message, args);
await this.acceptedOffers.set(req.query.code.toString(), true);
return res.sendStatus(200);
} catch (err) {
return this.handleError(err, res);
}
});
}
}

3
src/api/cr.ins/main.ts Normal file
View File

@ -0,0 +1,3 @@
import { Server, ServerManagement } from '../../class';
export default (management: ServerManagement) => new Server(management, 3891, `${__dirname}/routes`);

View File

@ -0,0 +1 @@
export { default as root } from './root';

View File

@ -0,0 +1,32 @@
import { LocalStorage, Route, Server } from '../../../class';
export default class Root extends Route {
constructor(server: Server) {
super(server);
this.conf = {
path: '/',
};
}
public bind() {
this.router.get('/m/:id', async (req, res) => {
try {
const id = req.params.id.split('.')[0];
const file = await this.server.client.db.File.findOne({ identifier: id });
if (!file) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.messages.NOT_FOUND });
if (file.downloaded >= file.maxDownloads) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.messages.NOT_FOUND });
if (req.query.d === '1') {
res.contentType('text/html');
const decomp = await LocalStorage.decompress(file.data);
res.status(200).send(decomp);
} else {
res.contentType(file.mimeType);
res.status(200).send(file.data);
}
return await file.updateOne({ $inc: { downloaded: 1 } });
} catch (err) {
return this.handleError(err, res);
}
});
}
}

11
src/api/index.ts Normal file
View File

@ -0,0 +1,11 @@
import locsh from './loc.sh/main';
import crins from './cr.ins/main';
import commlibraryofcodeorg from './comm.libraryofcode.org/main';
import boardins from './board.ins/main';
export default {
'board.ins': boardins,
'loc.sh': locsh,
'cr.ins': crins,
'comm.libraryofcode.org': commlibraryofcodeorg,
};

6
src/api/loc.sh/main.ts Normal file
View File

@ -0,0 +1,6 @@
import { Server, ServerManagement } from '../../class';
export default (management: ServerManagement) => {
const server = new Server(management, 3890, `${__dirname}/routes`, false);
return server;
};

View File

@ -0,0 +1,2 @@
export { default as root } from './root';
export { default as internal } from './internal';

View File

@ -0,0 +1,119 @@
import axios from 'axios';
import bodyParser from 'body-parser';
import { Route, Server, LocalStorage } from '../../../class';
// import acknowledgements from '../../../configs/acknowledgements.json';
export default class Internal extends Route {
public timeout: Set<string>;
public acceptedOffers: LocalStorage;
constructor(server: Server) {
super(server);
this.timeout = new Set();
this.conf = {
path: '/int',
};
this.acceptedOffers = new LocalStorage('accepted-offers');
}
public bind() {
this.router.get('/directory', async (req, res) => {
try {
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.query.id) {
let member = this.server.client.guilds.get(this.server.client.config.guildID).members.get(req.query.id.toString());
if (!member) member = await this.server.client.getRESTGuildMember(this.server.client.config.guildID, req.query.id.toString());
if (!member) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.messages.NOT_FOUND });
const pagerNumber = await this.server.client.db.PagerNumber.findOne({ individualAssignID: member.id }).lean().exec();
let status = false;
if (member.roles.includes('446104438969466890') || member.roles.includes('701481967149121627')) status = true;
return res.status(200).json({ staff: status, username: member.user.username, discriminator: member.user.discriminator, nick: member.nick, avatarURL: member.user.avatarURL, pager: pagerNumber?.num });
}
const acknowledgements = await this.server.client.db.Staff.find().lean().exec();
return res.status(200).json(acknowledgements);
} catch (err) {
return this.handleError(err, res);
}
});
this.router.get('/offer', async (_req, res) => {
try {
return res.status(410).json({ code: this.constants.codes.DEPRECATED, message: this.constants.codes.DEPRECATED });
} catch (err) {
return this.handleError(err, res);
}
});
this.router.get('/score', async (_req, res) => {
try {
return res.status(410).json({ code: this.constants.codes.DEPRECATED, message: this.constants.codes.DEPRECATED });
} catch (err) {
return this.handleError(err, res);
}
});
this.router.get('/id', async (req, res) => {
try {
if (req.query?.auth?.toString() !== this.server.client.config.internalKey) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
if (!req.query.pin) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR });
const args = req.query.pin.toString();
const user = await this.server.client.db.Score.findOne({ pin: [Number(args.split('-')[0]), Number(args.split('-')[1]), Number(args.split('-')[2])] }).lean().exec();
if (!user) return res.status(404).json({ code: this.constants.codes.ACCOUNT_NOT_FOUND, message: this.constants.messages.NOT_FOUND });
return res.status(200).json({ userID: user.userID });
} catch (err) {
return this.handleError(err, res);
}
});
this.router.get('/pin', async (req, res) => {
try {
if (req.query?.auth?.toString() !== this.server.client.config.internalKey) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
if (!req.query.id) return res.status(400).json({ code: this.constants.codes.CLIENT_ERROR });
const report = await this.server.client.db.Score.findOne({ userID: req.query.id.toString() });
if (!report) return res.status(404).json({ code: this.constants.codes.ACCOUNT_NOT_FOUND, message: this.constants.messages.NOT_FOUND });
return res.status(200).json({ pin: report.pin });
} catch (err) {
return this.handleError(err, res);
}
});
this.router.post('/sub', bodyParser.raw({ type: 'application/json' }), async (req, res) => {
try {
const event = this.server.client.stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], this.server.client.config.stripeSubSigningSecret);
const data = <any> event.data.object;
switch (event.type) {
default:
return res.sendStatus(400);
case 'customer.subscription.created':
if (data.items.data[0].price.product === 'prod_Hi4EYmf2am5VZt') {
const customer = await this.server.client.db.Customer.findOne({ cusID: data.customer }).lean().exec();
if (!customer) return res.sendStatus(404);
await axios({
method: 'get',
url: `https://api.cloud.libraryofcode.org/wh/t3?userID=${customer.userID}&auth=${this.server.client.config.internalKey}`,
});
res.sendStatus(200);
}
break;
case 'customer.subscription.deleted':
if (data.items.data[0].price.product === 'prod_Hi4EYmf2am5VZt') {
const customer = await this.server.client.db.Customer.findOne({ cusID: data.customer }).lean().exec();
if (!customer) return res.sendStatus(404);
await axios({
method: 'get',
url: `https://api.cloud.libraryofcode.org/wh/t3-rm?userID=${customer.userID}&auth=${this.server.client.config.internalKey}`,
});
return res.sendStatus(200);
}
break;
}
return null;
} catch (err) {
return this.handleError(err, res);
}
});
}
}

View File

@ -0,0 +1,64 @@
import { Route, Server } from '../../../class';
import { RedirectRaw } from '../../../models';
export default class Root extends Route {
constructor(server: Server) {
super(server);
this.conf = {
path: '/',
};
}
public bind() {
this.router.get('/', (_req, res) => res.redirect('https://www.libraryofcode.org/'));
this.router.get('/dash', async (req, res) => {
try {
const lookup = await this.server.client.db.CustomerPortal.findOne({ key: req.query.q?.toString() });
if (!lookup) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.messages.NOT_FOUND });
if (new Date(lookup.expiresOn) < new Date()) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const customer = await this.server.client.db.Customer.findOne({ userID: lookup.userID });
if (!customer) {
const newCus = await this.server.client.stripe.customers.create({
email: lookup.emailAddress,
metadata: {
userID: lookup.userID,
username: lookup.username,
},
});
await (new this.server.client.db.Customer({
cusID: newCus.id,
userID: lookup.userID,
})).save();
const billingURL = await this.server.client.stripe.billingPortal.sessions.create({
customer: newCus.id,
return_url: 'https://www.libraryofcode.org',
});
res.redirect(302, billingURL.url);
return await lookup.updateOne({ $set: { used: true } });
}
const billingURL = await this.server.client.stripe.billingPortal.sessions.create({
customer: customer.cusID,
return_url: 'https://www.libraryofcode.org',
});
res.redirect(302, billingURL.url);
return await lookup.updateOne({ $set: { used: true } });
} catch (err) {
return this.handleError(err, res);
}
});
this.router.get('/:key', async (req, res) => {
try {
const link: RedirectRaw = await this.server.client.db.Redirect.findOne({ key: req.params.key }).lean().exec();
if (!link) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.messages.NOT_FOUND });
res.redirect(link.to);
return await this.server.client.db.Redirect.updateOne({ key: req.params.key }, { $inc: { visitedCount: 1 } });
} catch (err) {
this.server.client.util.handleError(err);
return res.status(500).json({ code: this.constants.codes.SERVER_ERROR, message: this.constants.messages.SERVER_ERROR });
}
});
}
}

137
src/class/Client.ts Normal file
View File

@ -0,0 +1,137 @@
import Stripe from 'stripe';
import eris from 'eris';
import pluris from 'pluris';
import mongoose from 'mongoose';
import { promises as fs } from 'fs';
import { Collection, Command, LocalStorage, Queue, Util, ServerManagement, Event } from '.';
import {
Customer, CustomerInterface,
CustomerPortal, CustomerPortalInterface,
ExecutiveOrder, ExecutiveOrderInterface,
File, FileInterface,
Inquiry, InquiryInterface,
Member, MemberInterface,
Merchant, MerchantInterface,
Moderation, ModerationInterface,
Motion, MotionInterface,
NNTrainingData, NNTrainingDataInterface,
Note, NoteInterface,
PagerNumber, PagerNumberInterface,
Proclamation, ProclamationInterface,
Promo, PromoInterface,
Rank, RankInterface,
Redirect, RedirectInterface,
Resolution, ResolutionInterface,
Score, ScoreInterface,
ScoreHistorical, ScoreHistoricalInterface,
Staff, StaffInterface,
Stat, StatInterface,
} from '../models';
import { Config } from '../../types'; // eslint-disable-line
pluris(eris);
export default class Client extends eris.Client {
public config: Config;
public commands: Collection<Command>;
public events: Collection<Event>;
public intervals: Collection<NodeJS.Timeout>;
public util: Util;
public serverManagement: ServerManagement;
public queue: Queue;
public stripe: Stripe;
public db: { Customer: mongoose.Model<CustomerInterface>, CustomerPortal: mongoose.Model<CustomerPortalInterface>, ExecutiveOrder: mongoose.Model<ExecutiveOrderInterface>, File: mongoose.Model<FileInterface>, Inquiry: mongoose.Model<InquiryInterface>, Member: mongoose.Model<MemberInterface>, Merchant: mongoose.Model<MerchantInterface>, Moderation: mongoose.Model<ModerationInterface>, Motion: mongoose.Model<MotionInterface>, NNTrainingData: mongoose.Model<NNTrainingDataInterface>, Note: mongoose.Model<NoteInterface>, PagerNumber: mongoose.Model<PagerNumberInterface>, Proclamation: mongoose.Model<ProclamationInterface>, Promo: mongoose.Model<PromoInterface>, Rank: mongoose.Model<RankInterface>, Redirect: mongoose.Model<RedirectInterface>, Resolution: mongoose.Model<ResolutionInterface>, Score: mongoose.Model<ScoreInterface>, ScoreHistorical: mongoose.Model<ScoreHistoricalInterface>, Staff: mongoose.Model<StaffInterface>, Stat: mongoose.Model<StatInterface>, local: { muted: LocalStorage } };
constructor(token: string, options?: eris.ClientOptions) {
super(token, options);
this.commands = new Collection<Command>();
this.events = new Collection<Event>();
this.intervals = new Collection<NodeJS.Timeout>();
this.queue = new Queue(this);
this.db = { Customer, CustomerPortal, ExecutiveOrder, File, Inquiry, Member, Merchant, Moderation, Motion, NNTrainingData, Note, PagerNumber, Promo, Proclamation, Rank, Redirect, Resolution, Score, ScoreHistorical, Staff, Stat, local: { muted: new LocalStorage('muted') } };
}
get report() {
return this.util.report;
}
get pbx() {
return this.util.pbx;
}
public async loadDatabase() {
await mongoose.connect(this.config.mongoDB, { useNewUrlParser: true, useUnifiedTopology: true, poolSize: 50 });
const statMessages = await this.db.Stat.findOne({ name: 'messages' });
const statCommands = await this.db.Stat.findOne({ name: 'commands' });
const statPages = await this.db.Stat.findOne({ name: 'pages' });
const statRequests = await this.db.Stat.findOne({ name: 'requests' });
if (!statMessages) {
await (new this.db.Stat({ name: 'messages', value: 0 }).save());
}
if (!statCommands) {
await (new this.db.Stat({ name: 'commands', value: 0 }).save());
}
if (!statPages) {
await (new this.db.Stat({ name: 'pages', value: 0 }).save());
}
if (!statRequests) {
await (new this.db.Stat({ name: 'requests', value: 0 }).save());
}
}
public loadPlugins() {
this.util = new Util(this);
this.serverManagement = new ServerManagement(this);
this.stripe = new Stripe(this.config.stripeKey, { apiVersion: null, typescript: true });
}
public async loadIntervals() {
const intervalFiles = await fs.readdir(`${__dirname}/../intervals`);
intervalFiles.forEach((file) => {
const intervalName = file.split('.')[0];
if (file === 'index.js') return;
const interval: NodeJS.Timeout = (require(`${__dirname}/../intervals/${file}`).default)(this);
this.intervals.add(intervalName, interval);
this.util.signale.success(`Successfully loaded interval: ${intervalName}`);
});
}
public async loadEvents(eventFiles: { [s: string]: typeof Event; } | ArrayLike<typeof Event>) {
const evtFiles = Object.entries<typeof Event>(eventFiles);
for (const [name, Ev] of evtFiles) {
const event = new Ev(this);
this.events.add(event.event, event);
this.on(event.event, event.run);
this.util.signale.success(`Successfully loaded event: ${name}`);
delete require.cache[require.resolve(`${__dirname}/../events/${name}`)];
}
}
public async loadCommands(commandFiles: { [s: string]: typeof Command; } | ArrayLike<typeof Command>) {
const cmdFiles = Object.values<typeof Command>(commandFiles);
for (const Cmd of cmdFiles) {
const command = new Cmd(this);
if (command.subcmds.length) {
command.subcmds.forEach((C) => {
const cmd: Command = new C(this);
command.subcommands.add(cmd.name, cmd);
this.util.signale.success(`Successfully loaded subcommand ${cmd.name} under ${command.name}`);
});
}
delete command.subcmds;
this.commands.add(command.name, command);
this.util.signale.success(`Successfully loaded command: ${command.name}`);
}
}
}

153
src/class/Collection.ts Normal file
View File

@ -0,0 +1,153 @@
/**
* Hold a bunch of something
*/
export default class Collection<V> extends Map<string, V> {
baseObject: new (...args: any[]) => V;
/**
* Creates an instance of Collection
*/
constructor(iterable: Iterable<[string, V]>|object = null) {
if (iterable && iterable instanceof Array) {
super(iterable);
} else if (iterable && iterable instanceof Object) {
super(Object.entries(iterable));
} else {
super();
}
}
/**
* Map to array
* ```js
* [value, value, value]
* ```
*/
toArray(): V[] {
return [...this.values()];
}
/**
* Map to object
* ```js
* { key: value, key: value, key: value }
* ```
*/
toObject(): { [key: string]: V } {
const obj: { [key: string]: V } = {};
for (const [key, value] of this.entries()) {
obj[key] = value;
}
return obj;
}
/**
* Add an object
*
* If baseObject, add only if instance of baseObject
*
* If no baseObject, add
* @param key The key of the object
* @param value The object data
* @param replace Whether to replace an existing object with the same key
* @return The existing or newly created object
*/
add(key: string, value: V, replace: boolean = false): V {
if (this.has(key) && !replace) {
return this.get(key);
}
if (this.baseObject && !(value instanceof this.baseObject)) return null;
this.set(key, value);
return value;
}
/**
* Return the first object to make the function evaluate true
* @param func A function that takes an object and returns something
* @return The first matching object, or `null` if no match
*/
find(func: Function): V {
for (const item of this.values()) {
if (func(item)) return item;
}
return null;
}
/**
* Return an array with the results of applying the given function to each element
* @param callbackfn A function that takes an object and returns something
*/
map<U>(callbackfn: (value?: V, index?: number, array?: V[]) => U): U[] {
const arr = [];
for (const item of this.values()) {
arr.push(callbackfn(item));
}
return arr;
}
/**
* Return all the objects that make the function evaluate true
* @param func A function that takes an object and returns true if it matches
*/
filter(func: (value: V) => boolean): V[] {
const arr = [];
for (const item of this.values()) {
if (func(item)) {
arr.push(item);
}
}
return arr;
}
/**
* Test if at least one element passes the test implemented by the provided function. Returns true if yes, or false if not.
* @param func A function that takes an object and returns true if it matches
*/
some(func: (value: V) => boolean) {
for (const item of this.values()) {
if (func(item)) {
return true;
}
}
return false;
}
/**
* Update an object
* @param key The key of the object
* @param value The updated object data
*/
update(key: string, value: V) {
return this.add(key, value, true);
}
/**
* Remove an object
* @param key The key of the object
* @returns The removed object, or `null` if nothing was removed
*/
remove(key: string): V {
const item = this.get(key);
if (!item) {
return null;
}
this.delete(key);
return item;
}
/**
* Get a random object from the Collection
* @returns The random object or `null` if empty
*/
random(): V {
if (!this.size) {
return null;
}
return Array.from(this.values())[Math.floor(Math.random() * this.size)];
}
toString() {
return `[Collection<${this.baseObject.name}>]`;
}
}

129
src/class/Command.ts Normal file
View File

@ -0,0 +1,129 @@
import { Member, Message, TextableChannel } from 'eris';
import { Client, Collection } from '.';
export default class Command {
public client: Client;
/**
* The name of the command
*/
public name: string;
/**
* The description for the command.
*/
public description: string;
/**
* Usage for the command.
*/
public usage: string;
/**
* The aliases for the command.
*/
public aliases: string[];
/**
* - **0:** Everyone
* - **1:** Associates+
* - **2:** Core Team+
* - **3:** Moderators, Supervisor, & Board of Directors
* - **4:** Technicians, Supervisor, & Board of Directors
* - **5:** Moderators, Technicians, Supervisor, & Board of Directors
* - **6:** Supervisor+
* - **7:** Board of Directors
*/
public permissions: number;
/**
* Determines if the command is only available in server.
*/
public guildOnly: boolean;
/**
* Determines if the command is enabled or not.
*/
public subcommands?: Collection<Command>;
public subcmds?: any[];
public enabled: boolean;
public run(message: Message, args: string[]): Promise<any> { return Promise.resolve(); }
constructor(client: Client) {
this.client = client;
this.aliases = [];
this.subcommands = new Collection<Command>();
this.subcmds = [];
}
get mainGuild() {
return this.client.guilds.get(this.client.config.guildID);
}
public checkPermissions(member: Member): boolean {
if (member.id === '278620217221971968' || member.id === '253600545972027394') return true;
switch (this.permissions) {
case 0:
return true;
case 1:
return member.roles.some((r) => ['701481967149121627', '453689940140883988', '455972169449734144', '701454780828221450', '701454855952138300', '662163685439045632'].includes(r));
case 2:
return member.roles.some((r) => ['453689940140883988', '455972169449734144', '701454780828221450', '701454855952138300', '662163685439045632'].includes(r));
case 3:
return member.roles.some((r) => ['455972169449734144', '701454855952138300', '662163685439045632'].includes(r));
case 4:
return member.roles.some((r) => ['701454780828221450', '701454855952138300', '662163685439045632'].includes(r));
case 5:
return member.roles.some((r) => ['455972169449734144', '701454780828221450', '701454855952138300', '662163685439045632'].includes(r));
case 6:
return member.roles.some((r) => ['701454855952138300', '662163685439045632'].includes(r));
case 7:
return member.roles.includes('662163685439045632');
default:
return false;
}
}
public checkCustomPermissions(member: Member, permission: number): boolean {
if (member.id === '278620217221971968' || member.id === '253600545972027394') return true;
switch (permission) {
case 0:
return true;
case 1:
return member.roles.some((r) => ['701481967149121627', '453689940140883988', '455972169449734144', '701454780828221450', '701454855952138300', '662163685439045632'].includes(r));
case 2:
return member.roles.some((r) => ['453689940140883988', '455972169449734144', '701454780828221450', '701454855952138300', '662163685439045632'].includes(r));
case 3:
return member.roles.some((r) => ['455972169449734144', '701454855952138300', '662163685439045632'].includes(r));
case 4:
return member.roles.some((r) => ['701454780828221450', '701454855952138300', '662163685439045632'].includes(r));
case 5:
return member.roles.some((r) => ['455972169449734144', '701454780828221450', '701454855952138300', '662163685439045632'].includes(r));
case 6:
return member.roles.some((r) => ['701454855952138300', '662163685439045632'].includes(r));
case 7:
return member.roles.includes('662163685439045632');
default:
return false;
}
}
public error(channel: TextableChannel, text: string): Promise<Message> {
return channel.createMessage(`***${this.client.util.emojis.ERROR} ${text}***`);
}
public success(channel: TextableChannel, text: string): Promise<Message> {
return channel.createMessage(`***${this.client.util.emojis.SUCCESS} ${text}***`);
}
public loading(channel: TextableChannel, text: string): Promise<Message> {
return channel.createMessage(`***${this.client.util.emojis.LOADING} ${text}***`);
}
}

15
src/class/Event.ts Normal file
View File

@ -0,0 +1,15 @@
import { Client } from '.';
export default class Event {
public client: Client
public event: string;
constructor(client: Client) {
this.client = client;
this.event = '';
this.run = this.run.bind(this);
}
public async run(...args: any[]): Promise<void> { return Promise.resolve(); }
}

28
src/class/Handler.ts Normal file
View File

@ -0,0 +1,28 @@
import ARI from 'ari-client';
import { PBX } from '.';
export default class Handler {
public pbx: PBX;
public app: string;
public options: {
available?: boolean,
}
constructor(pbx: PBX) {
this.pbx = pbx;
this.options = {};
}
get client() { return this.pbx.client; }
public run(event: ARI.Event, channel: ARI.Channel): Promise<any> { return Promise.resolve(); }
public async unavailable(event: ARI.Event, channel: ARI.Channel) {
const playback = await channel.play({
media: 'sound:all-outgoing-lines-unavailable',
}, undefined);
playback.once('PlaybackFinished', () => channel.hangup());
}
}

156
src/class/LocalStorage.ts Normal file
View File

@ -0,0 +1,156 @@
/* eslint-disable no-constant-condition */
import { promises as fs, accessSync, constants, writeFileSync } from 'fs';
import { promisify } from 'util';
import { join } from 'path';
import { gzip, gzipSync, unzip } from 'zlib';
type JSONData = [{ key: string, value: any }?];
/**
* Persistant local JSON-based storage.
* - auto-locking system to prevent corrupted data
* - uses gzip compression to keep DB storage space utilization low
* @author Matthew <matthew@staff.libraryofcode.org>
*/
export default class LocalStorage {
protected storagePath: string;
private locked: boolean = false;
constructor(dbName: string, dir = `${__dirname}/../../localstorage`) {
this.storagePath = join(__dirname, '../../localstorage') || dir;
this.init();
}
private init() {
try {
accessSync(this.storagePath, constants.F_OK);
} catch {
const setup = [];
const data = gzipSync(JSON.stringify(setup));
writeFileSync(this.storagePath, data);
}
return this;
}
/**
* Compresses data using gzip.
* @param data The data to be compressed.
* ```ts
* await LocalStorage.compress('hello!');
* ```
*/
static async compress(data: string): Promise<Buffer> {
const func = promisify(gzip);
const comp = <Buffer>await func(data);
return comp;
}
/**
* Decompresses data using gzip.
* @param data The data to be decompressed.
* ```ts
* const compressed = await LocalStorage.compress('data');
* const decompressed = await LocalStorage.decompress(compressed);
* console.log(decompressed); // logs 'data';
* ```
*/
static async decompress(data: Buffer): Promise<string> {
const func = promisify(unzip);
const uncomp = <Buffer>await func(data);
return uncomp.toString();
}
/**
* Retrieves one data from the store.
* If the store has multiple entries for the same key, this function will only return the first entry.
* ```ts
* await LocalStorage.get<type>('data-key');
* ```
* @param key The key for the data entry.
*/
public async get<T>(key: string): Promise<T> {
while (true) {
if (!this.locked) break;
}
this.locked = true;
const file = await fs.readFile(this.storagePath);
const uncomp = await LocalStorage.decompress(file);
this.locked = false;
const json: JSONData = JSON.parse(uncomp);
const result = json.filter((data) => data.key === key);
if (!result[0]) return null;
return result[0].value;
}
/**
* Retrieves multiple data keys/values from the store.
* This function will return all of the values matching the key you provided exactly. Use `LocalStorage.get();` if possible.
* ```ts
* await LocalStorage.get<type>('data-key');
* @param key The key for the data entry.
*/
public async getMany<T>(key: string): Promise<{ key: string, value: T }[]> {
while (true) {
if (!this.locked) break;
}
this.locked = true;
const file = await fs.readFile(this.storagePath);
const uncomp = await LocalStorage.decompress(file);
this.locked = false;
const json: JSONData = JSON.parse(uncomp);
const result = json.filter((data) => data.key === key);
if (result.length < 1) return null;
return result;
}
/**
* Sets a key/value pair and creates a new data entry.
* @param key The key for the data entry.
* @param value The value for the data entry, can be anything that is valid JSON.
* @param options.override [DEPRECATED] By default, this function will error if the key you're trying to set already exists. Set this option to true to override that setting.
* ```ts
* await LocalStorage.set('data-key', 'test');
* ```
*/
public async set(key: string, value: any): Promise<void> {
while (true) {
if (!this.locked) break;
}
this.locked = true;
const file = await fs.readFile(this.storagePath);
const uncomp = await LocalStorage.decompress(file);
const json: JSONData = JSON.parse(uncomp);
json.push({ key, value });
const comp = await LocalStorage.compress(JSON.stringify(json));
await fs.writeFile(this.storagePath, comp);
this.locked = false;
}
/**
* Deletes the data for the specified key.
* **Warning:** This function will delete ALL matching entries.
* ```ts
* await LocalStorage.del('data-key');
* ```
* @param key The key for the data entry.
*/
public async del(key: string): Promise<void> {
while (true) {
if (!this.locked) break;
}
this.locked = true;
const file = await fs.readFile(this.storagePath);
const uncomp = await LocalStorage.decompress(file);
const json: JSONData = JSON.parse(uncomp);
const filtered = json.filter((data) => data.key !== key);
const comp = await LocalStorage.compress(JSON.stringify(filtered));
await fs.writeFile(this.storagePath, comp);
this.locked = false;
}
}

220
src/class/Moderation.ts Normal file
View File

@ -0,0 +1,220 @@
/* eslint-disable no-bitwise */
import { Member, User } from 'eris';
import { randomBytes } from 'crypto';
import moment, { unitOfTime } from 'moment';
import { Client, RichEmbed } from '.';
import { Moderation as ModerationModel, ModerationInterface } from '../models';
import { moderation as channels } from '../configs/channels.json';
export default class Moderation {
public client: Client;
public logChannels: {
modlogs: string
};
constructor(client: Client) {
this.client = client;
this.logChannels = {
modlogs: channels.modlogs,
};
}
public checkPermissions(member: Member, moderator: Member): boolean {
if (member.id === moderator.id) return false;
if (member.roles.some((r) => ['662163685439045632', '455972169449734144', '453689940140883988'].includes(r))) return false;
const bit = member.permission.allow;
if ((bit | 8) === bit) return false;
if ((bit | 20) === bit) return false;
return true;
}
/**
* Converts some sort of time based duration to milliseconds based on length.
* @param time The time, examples: 2h, 1m, 1w
*/
public convertTimeDurationToMilliseconds(time: string): number {
const lockLength = time.match(/[a-z]+|[^a-z]+/gi);
const length = Number(lockLength[0]);
const unit = lockLength[1] as unitOfTime.Base;
return moment.duration(length, unit).asMilliseconds();
}
public async ban(user: User, moderator: Member, duration: number, reason?: string): Promise<ModerationInterface> {
if (reason && reason.length > 512) throw new Error('Ban reason cannot be longer than 512 characters');
await this.client.guilds.get(this.client.config.guildID).banMember(user.id, 7, reason);
const logID = randomBytes(2).toString('hex');
const mod = new ModerationModel({
userID: user.id,
logID,
moderatorID: moderator.id,
reason: reason || null,
type: 5,
date: new Date(),
});
const now: number = Date.now();
let date: Date;
let processed = true;
if (duration > 0) {
date = new Date(now + duration);
processed = false;
} else date = null;
const expiration = { date, processed };
mod.expiration = expiration;
const embed = new RichEmbed();
embed.setTitle(`Case ${logID} | Ban`);
embed.setColor('#e74c3c');
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
embed.addField('User', `<@${user.id}>`, true);
embed.addField('Moderator', `<@${moderator.id}>`, true);
if (reason) {
embed.addField('Reason', reason, true);
}
if (date) {
embed.addField('Expiration', moment(date).calendar(), true);
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
this.client.createMessage(this.logChannels.modlogs, { embed });
return mod.save();
}
public async unban(userID: string, moderator: Member, reason?: string): Promise<ModerationInterface> {
this.client.unbanGuildMember(this.client.config.guildID, userID, reason);
const user = await this.client.getRESTUser(userID);
if (!user) throw new Error('Cannot get user.');
const logID = randomBytes(2).toString('hex');
const mod = new ModerationModel({
userID,
logID,
moderatorID: moderator.id,
reason: reason || null,
type: 4,
date: new Date(),
});
const embed = new RichEmbed();
embed.setTitle(`Case ${logID} | Unban`);
embed.setColor('#1abc9c');
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
embed.addField('User', `<@${user.id}>`, true);
embed.addField('Moderator', `<@${moderator.id}>`, true);
if (reason) {
embed.addField('Reason', reason, true);
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
this.client.createMessage(this.logChannels.modlogs, { embed });
return mod.save();
}
public async mute(user: User, moderator: Member, duration: number, reason?: string): Promise<ModerationInterface> {
if (reason && reason.length > 512) throw new Error('Mute reason cannot be longer than 512 characters');
const member = await this.client.getRESTGuildMember(this.client.config.guildID, user.id);
if (!member) throw new Error('Cannot find member.');
await member.addRole('478373942638149643', `Muted by ${moderator.username}#${moderator.discriminator}`);
const logID = randomBytes(2).toString('hex');
const mod = new ModerationModel({
userID: user.id,
logID,
moderatorID: moderator.id,
reason: reason || null,
type: 2,
date: new Date(),
});
const now: number = Date.now();
let date: Date;
let processed = true;
if (duration > 0) {
date = new Date(now + duration);
processed = false;
} else date = null;
const expiration = { date, processed };
mod.expiration = expiration;
await this.client.db.local.muted.set(`muted-${member.id}`, true);
const embed = new RichEmbed();
embed.setTitle(`Case ${logID} | Mute`);
embed.setColor('#ffff00');
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
embed.addField('User', `<@${user.id}>`, true);
embed.addField('Moderator', `<@${moderator.id}>`, true);
if (reason) {
embed.addField('Reason', reason, true);
}
if (date) {
embed.addField('Expiration', moment(date).calendar(), true);
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
this.client.createMessage(this.logChannels.modlogs, { embed });
return mod.save();
}
public async unmute(userID: string, moderator: Member, reason?: string): Promise<ModerationInterface> {
const member = await this.client.getRESTGuildMember(this.client.config.guildID, userID);
const user = await this.client.getRESTUser(userID);
if (member) {
await member.removeRole('478373942638149643');
}
const logID = randomBytes(2).toString('hex');
const mod = new ModerationModel({
userID,
logID,
moderatorID: moderator.id,
reason: reason || null,
type: 1,
date: new Date(),
});
await this.client.db.local.muted.del(`muted-${member.id}`);
const embed = new RichEmbed();
embed.setTitle(`Case ${logID} | Unmute`);
embed.setColor('#1abc9c');
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
embed.addField('User', `<@${user.id}>`, true);
embed.addField('Moderator', `<@${moderator.id}>`, true);
if (reason) {
embed.addField('Reason', reason, true);
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
this.client.createMessage(this.logChannels.modlogs, { embed });
return mod.save();
}
public async kick(user: Member | User, moderator: Member, reason?: string): Promise<ModerationInterface> {
if (reason && reason.length > 512) throw new Error('Kick reason cannot be longer than 512 characters');
await this.client.guilds.get(this.client.config.guildID).kickMember(user.id, reason);
const logID = randomBytes(2).toString('hex');
const mod = new ModerationModel({
userID: user.id,
logID,
moderatorID: moderator.id,
reason: reason || null,
type: 5,
date: new Date(),
});
const embed = new RichEmbed();
embed.setTitle(`Case ${logID} | Kick`);
embed.setColor('#e74c3c');
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
embed.addField('User', `<@${user.id}>`, true);
embed.addField('Moderator', `<@${moderator.id}>`, true);
if (reason) {
embed.addField('Reason', reason, true);
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
this.client.createMessage(this.logChannels.modlogs, { embed });
return mod.save();
}
}

61
src/class/PBX.ts Normal file
View File

@ -0,0 +1,61 @@
/* eslint-disable no-continue */
import ARIClient from 'ari-client';
import AMIClient from 'asterisk-manager';
import GoogleTTS, { TextToSpeechClient } from '@google-cloud/text-to-speech';
import { Client, Collection, Handler } from '.';
export default class PBX {
public client: Client;
public handlers: Collection<Handler>;
public ari: ARIClient.Client;
public ami: any;
public tts: TextToSpeechClient;
constructor(client: Client) {
this.client = client;
this.handlers = new Collection<Handler>();
this.load();
}
private async load() {
this.ari = await ARIClient.connect('http://10.8.0.1:8088/ari', 'cr0', this.client.config.ariClientKey);
this.ari.start(['cr-zero', 'page-dtmf']);
this.ami = new AMIClient(5038, '10.8.0.1', 'cr', this.client.config.amiClientKey);
process.env.GOOGLE_APPLICATION_CREDENTIALS = `${__dirname}/../../google.json`;
this.tts = new GoogleTTS.TextToSpeechClient();
this.start();
}
public start() {
const handlers = Object.values<typeof Handler>(require(`${__dirname}/../pbx`));
for (const HandlerFile of handlers) {
const handler = new HandlerFile(this);
if (!handler.app) continue;
if (!handler.options?.available) {
this.ari.on('StasisStart', async (event, channel) => {
if (event.application !== handler.app) return;
await handler.unavailable(event, channel);
});
} else {
this.ari.on('StasisStart', async (event, channel) => {
if (event.application !== handler.app) return;
try {
await handler.run(event, channel);
} catch (err) {
this.client.util.handleError(err);
}
});
}
this.handlers.add(handler.app, handler);
this.client.util.signale.success(`Successfully loaded PBX Handler ${handler.app}`);
}
}
}

170
src/class/Queue.ts Normal file
View File

@ -0,0 +1,170 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-eval */
import Bull from 'bull';
import cron from 'cron';
import { TextableChannel, TextChannel } from 'eris';
import { Client, RichEmbed } from '.';
import { ScoreInterface, InqType as InquiryType } from '../models';
import { apply as Apply } from '../commands';
export default class Queue {
public client: Client;
public queues: { score: Bull.Queue };
constructor(client: Client) {
this.client = client;
this.queues = {
score: new Bull('score', { prefix: 'queue::score' }),
};
this.setProcessors();
this.setCronJobs();
}
protected setCronJobs() {
const historialCommunityReportJob = new cron.CronJob('0 20 * * *', async () => {
try {
const reports = await this.client.db.Score.find().lean().exec();
const startDate = new Date();
for (const report of reports) {
const inqs = await this.client.db.Inquiry.find({ userID: report.userID });
const data = new this.client.db.ScoreHistorical({
userID: report.userID,
report,
inquiries: inqs.map((inq) => inq._id),
date: startDate,
});
await data.save();
}
} catch (err) {
this.client.util.handleError(err);
}
});
historialCommunityReportJob.start();
}
public async jobCounts() {
const data = {
waiting: 0,
active: 0,
completed: 0,
failed: 0,
delayed: 0,
};
for (const entry of Object.entries(this.queues)) {
// eslint-disable-next-line no-await-in-loop
const counts = await entry[1].getJobCounts();
data.waiting += counts.waiting;
data.active += counts.active;
data.completed += counts.completed;
data.failed += counts.failed;
data.delayed += counts.delayed;
}
return data;
}
protected listeners() {
this.queues.score.on('active', (job) => {
this.client.util.signale.pending(`${job.id} has become active.`);
});
this.queues.score.on('completed', (job) => {
this.client.util.signale.success(`Job with id ${job.id} has been completed`);
});
this.queues.score.on('error', async (err) => {
this.client.util.handleError(err);
});
}
protected setProcessors() {
this.queues.score.process('score::inquiry', async (job: Bull.Job<{ inqID: string, userID: string, name: string, type: InquiryType, reason?: string }>) => {
const member = this.client.util.resolveMember(job.data.userID, this.client.guilds.get(this.client.config.guildID));
const report = await this.client.db.Score.findOne({ userID: job.data.userID }).lean().exec();
const embed = new RichEmbed();
if (job.data.type === InquiryType.HARD) {
embed.setTitle('Inquiry Notification');
embed.setDescription(job.data.inqID);
embed.setColor('#800080');
embed.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${job.data.userID}>`, true);
embed.addField('Type', 'HARD', true);
embed.addField('Department/Service', job.data.name.toUpperCase(), true);
embed.addField('Reason', job.data.reason ?? 'N/A', true);
embed.setTimestamp();
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
if (report.notify === true) {
await this.client.getDMChannel(job.data.userID).then((chan) => {
chan.createMessage(`__**Community Score - Hard Pull Notification**__\n*You have signed up to be notified whenever your hard score has been pulled. See \`?score\` for more information.*\n\n**Department/Service:** ${job.data.name.toUpperCase()}`);
}).catch(() => {});
}
} else {
embed.setTitle('Inquiry Notification');
embed.setColor('#00FFFF');
embed.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${job.data.userID}>`, true);
embed.addField('Type', 'SOFT', true);
embed.addField('Department/Service', job.data.name.toUpperCase(), true);
embed.setTimestamp();
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
}
const log = <TextChannel> this.client.guilds.get(this.client.config.guildID).channels.get('611584771356622849');
log.createMessage({ embed }).catch(() => {});
});
this.queues.score.process('score::update', async (job: Bull.Job<{ score: ScoreInterface, total: number, activity: number, roles: number, moderation: number, cloudServices: number, other: number, staff: number }>) => {
await this.client.db.Score.updateOne({ userID: job.data.score.userID }, { $set: { total: job.data.total, activity: job.data.activity, roles: job.data.roles, moderation: job.data.moderation, cloudServices: job.data.cloudServices, other: job.data.other, staff: job.data.staff, lastUpdate: new Date() } });
if (!job.data.score.pin || job.data.score.pin?.length < 1) {
await this.client.db.Score.updateOne({ userID: job.data.score.userID }, { $set: { pin: [this.client.util.randomNumber(100, 999), this.client.util.randomNumber(10, 99), this.client.util.randomNumber(1000, 9999)] } });
}
});
this.queues.score.process('score::apply', async (job: Bull.Job<{ channelInformation: { messageID: string, guildID: string, channelID: string }, url: string, userID: string, func?: string }>) => {
const application = await Apply.apply(this.client, job.data.url, job.data.userID);
const guild = this.client.guilds.get(job.data.channelInformation.guildID);
const channel = <TextableChannel> guild.channels.get(job.data.channelInformation.channelID);
const message = await channel.getMessage(job.data.channelInformation.messageID);
const member = guild.members.get(job.data.userID);
await message.delete();
const embed = new RichEmbed();
embed.setTitle('Application Decision');
if (member) {
embed.setAuthor(member.username, member.avatarURL);
}
if (application.decision === 'APPROVED') {
embed.setColor('#50c878');
} else if (application.decision === 'DECLINED') {
embed.setColor('#fe0000');
} else if (application.decision === 'PRE-DECLINE') {
embed.setColor('#ffa500');
} else {
embed.setColor('#eeeeee');
}
const chan = await this.client.getDMChannel(job.data.userID);
if (chan && application.token) {
chan.createMessage(`__**Application Decision Document**__\n*This document provides detailed information about the decision given to you by EDS.*\n\nhttps://eds.libraryofcode.org/dec/${application.token}`);
}
embed.setDescription(`This application was processed by __${application.processedBy}__ on behalf of the vendor, department, or service who operates this application. Please contact the vendor for further information about your application if needed.`);
embed.addField('Status', application.decision, true);
embed.addField('User ID', job.data.userID, true);
embed.addField('Application ID', application.id, true);
embed.addField('Job ID', job.id.toString(), true);
embed.setFooter(`${this.client.user.username} via Electronic Decision Service [EDS]`, this.client.user.avatarURL);
embed.setTimestamp();
await channel.createMessage({ content: `<@${job.data.userID}>`, embed });
if (job.data.func) {
const func = eval(job.data.func);
if (application.status === 'SUCCESS' && application.decision === 'APPROVED') await func(this.client, job.data.userID);
}
});
}
public addInquiry(inqID: string, userID: string, name: string, type: InquiryType, reason?: string) {
return this.queues.score.add('score::inquiry', { inqID, userID, name, type, reason });
}
public updateScore(score: ScoreInterface, total: number, activity: number, roles: number, moderation: number, cloudServices: number, other: number, staff: number) {
return this.queues.score.add('score::update', { score, total, activity, roles, moderation, cloudServices, other, staff });
}
public processApplication(channelInformation: { messageID: string, guildID: string, channelID: string }, url: string, userID: string, func?: string) {
return this.queues.score.add('score::apply', { channelInformation, url, userID, func });
}
}

97
src/class/Report.ts Normal file
View File

@ -0,0 +1,97 @@
import { v4 as uuid } from 'uuid';
import { Client } from '.';
import { InqType } from '../models';
export default class Report {
public client: Client;
constructor(client: Client) {
this.client = client;
}
public async createInquiry(userID: string, name: string, type: InqType, reason?: string) {
const report = await this.client.db.Score.findOne({ userID }).lean().exec();
const member = this.client.util.resolveMember(userID, this.client.guilds.get(this.client.config.guildID));
if (!report || !member) return null;
if (type === InqType.HARD && report.locked) return null;
const mod = await (new this.client.db.Inquiry({
iid: uuid(),
userID,
name,
type,
reason,
date: new Date(),
report,
}).save());
this.client.queue.addInquiry(mod.iid, userID, name, type, reason);
return mod;
}
/* public async soft(userID: string) {
const report = await this.client.db.Score.findOne({ userID });
if (!report) return null;
let totalScore: number;
let activityScore: number;
const moderationScore = Math.round(report.moderation);
let roleScore: number;
let cloudServicesScore: number;
const otherScore = Math.round(report.other);
let miscScore: number;
if (report.total < 200) totalScore = 0;
else if (report.total > 800) totalScore = 800;
else totalScore = Math.round(report.total);
if (report.activity < 10) activityScore = 0;
else if (report.activity > Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12))) activityScore = Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12));
else activityScore = Math.round(report.activity);
if (report.roles <= 0) roleScore = 0;
else if (report.roles > 54) roleScore = 54;
else roleScore = Math.round(report.roles);
if (report.staff <= 0) miscScore = 0;
else miscScore = Math.round(report.staff);
if (report.cloudServices === 0) cloudServicesScore = 0;
else if (report.cloudServices > 10) cloudServicesScore = 10;
else cloudServicesScore = Math.round(report.cloudServices);
const historicalData = await this.client.db.ScoreHistorical.find({ userID: member.userID }).lean().exec();
const array: ScoreHistoricalRaw[] = [];
for (const data of historicalData) {
let total: number;
let activity: number;
const moderation = Math.round(data.report.moderation);
let role: number;
let cloud: number;
const other = Math.round(data.report.other);
let misc: number;
if (data.report.total < 200) total = 0;
else if (data.report.total > 800) total = 800;
else total = Math.round(data.report.total);
if (data.report.activity < 10) activity = 0;
else if (data.report.activity > Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12))) activity = Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12));
else activity = Math.round(data.report.activity);
if (data.report.roles <= 0) role = 0;
else if (data.report.roles > 54) role = 54;
else role = Math.round(data.report.roles);
if (data.report.staff <= 0) role = 0;
else misc = Math.round(data.report.staff);
if (data.report.cloudServices === 0) cloud = 0;
else if (data.report.cloudServices > 10) cloud = 10;
else cloud = Math.round(data.report.cloudServices);
data.report.total = total; data.report.activity = activity; data.report.moderation = moderation; data.report.roles = role; data.report.cloudServices = cloud; data.report.other = other; data.report.staff = misc;
array.push(data);
}
} */
}

164
src/class/RichEmbed.ts Normal file
View File

@ -0,0 +1,164 @@
/* eslint-disable no-param-reassign */
import { EmbedOptions } from 'eris';
export default class RichEmbed implements EmbedOptions {
title?: string
type?: string
description?: string
url?: string
timestamp?: string | Date
color?: number
footer?: { text: string, icon_url?: string, proxy_icon_url?: string}
image?: { url?: string, proxy_url?: string, height?: number, width?: number }
thumbnail?: { url?: string, proxy_url?: string, height?: number, width?: number }
video?: { url: string, height?: number, width?: number }
provider?: { name: string, url?: string}
author?: { name: string, url?: string, proxy_icon_url?: string, icon_url?: string}
fields?: {name: string, value: string, inline?: boolean}[]
constructor(data: EmbedOptions = {}) {
this.title = data.title;
this.description = data.description;
this.url = data.url;
this.color = data.color;
this.author = data.author;
this.timestamp = data.timestamp;
this.fields = data.fields || [];
this.thumbnail = data.thumbnail;
this.image = data.image;
this.footer = data.footer;
}
/**
* Sets the title of this embed.
*/
public setTitle(title: string) {
if (typeof title !== 'string') throw new TypeError('RichEmbed titles must be a string.');
if (title.length > 256) throw new RangeError('RichEmbed titles may not exceed 256 characters.');
this.title = title;
return this;
}
/**
* Sets the description of this embed.
*/
public setDescription(description: string) {
if (typeof description !== 'string') throw new TypeError('RichEmbed descriptions must be a string.');
if (description.length > 2048) throw new RangeError('RichEmbed descriptions may not exceed 2048 characters.');
this.description = description;
return this;
}
/**
* Sets the URL of this embed.
*/
public setURL(url: string) {
if (typeof url !== 'string') throw new TypeError('RichEmbed URLs must be a string.');
if (!url.startsWith('http://') && !url.startsWith('https://')) url = `https://${url}`;
this.url = encodeURI(url);
return this;
}
/**
* Sets the color of this embed.
*/
public setColor(color: string | number) {
if (typeof color === 'string' || typeof color === 'number') {
if (typeof color === 'string') {
const regex = /[^a-f0-9]/gi;
color = color.replace(/#/g, '');
if (regex.test(color)) throw new RangeError('Hexadecimal colours must not contain characters other than 0-9 and a-f.');
color = parseInt(color, 16);
} else if (color < 0 || color > 16777215) throw new RangeError('Base 10 colours must not be less than 0 or greater than 16777215.');
this.color = color;
return this;
}
throw new TypeError('RichEmbed colours must be hexadecimal as string or number.');
}
/**
* Sets the author of this embed.
*/
public setAuthor(name: string, icon_url?: string, url?: string) {
if (typeof name !== 'string') throw new TypeError('RichEmbed Author names must be a string.');
if (url && typeof url !== 'string') throw new TypeError('RichEmbed Author URLs must be a string.');
if (icon_url && typeof icon_url !== 'string') throw new TypeError('RichEmbed Author icons must be a string.');
this.author = { name, icon_url, url };
return this;
}
/**
* Sets the timestamp of this embed.
*/
public setTimestamp(timestamp = new Date()) {
if (Number.isNaN(timestamp.getTime())) throw new TypeError('Expecting ISO8601 (Date constructor)');
this.timestamp = timestamp;
return this;
}
/**
* Adds a field to the embed (max 25).
*/
public addField(name: string, value: string, inline = false) {
if (typeof name !== 'string') throw new TypeError('RichEmbed Field names must be a string.');
if (typeof value !== 'string') throw new TypeError('RichEmbed Field values must be a string.');
if (typeof inline !== 'boolean') throw new TypeError('RichEmbed Field inlines must be a boolean.');
if (this.fields.length >= 25) throw new RangeError('RichEmbeds may not exceed 25 fields.');
if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.');
if (!/\S/.test(name)) throw new RangeError('RichEmbed field names may not be empty.');
if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.');
if (!/\S/.test(value)) throw new RangeError('RichEmbed field values may not be empty.');
this.fields.push({ name, value, inline });
return this;
}
/**
* Convenience function for `<RichEmbed>.addField('\u200B', '\u200B', inline)`.
*/
public addBlankField(inline = false) {
return this.addField('\u200B', '\u200B', inline);
}
/**
* Set the thumbnail of this embed.
*/
public setThumbnail(url: string) {
if (typeof url !== 'string') throw new TypeError('RichEmbed Thumbnail URLs must be a string.');
this.thumbnail = { url };
return this;
}
/**
* Set the image of this embed.
*/
public setImage(url: string) {
if (typeof url !== 'string') throw new TypeError('RichEmbed Image URLs must be a string.');
if (!url.startsWith('http://') || !url.startsWith('https://')) url = `https://${url}`;
this.image = { url };
return this;
}
/**
* Sets the footer of this embed.
*/
public setFooter(text: string, icon_url?: string) {
if (typeof text !== 'string') throw new TypeError('RichEmbed Footers must be a string.');
if (icon_url && typeof icon_url !== 'string') throw new TypeError('RichEmbed Footer icon URLs must be a string.');
if (text.length > 2048) throw new RangeError('RichEmbed footer text may not exceed 2048 characters.');
this.footer = { text, icon_url };
return this;
}
}

77
src/class/Route.ts Normal file
View File

@ -0,0 +1,77 @@
/* eslint-disable consistent-return */
import { Router, Request, Response } from 'express';
import { Server } from '.';
export default class Route {
public server: Server;
public conf: { path: string; deprecated?: boolean; maintenance?: boolean; };
public router: Router;
constructor(server: Server) {
this.server = server;
this.conf = { path: '' };
this.router = Router();
}
public bind() {}
public init() {
this.router.all('*', (req, res, next) => {
this.server.client.util.signale.log(`'${req.method}' request from '${req.ip}' to '${req.hostname}${req.path}'.`);
this.server.client.db.Stat.updateOne({ name: 'requests' }, { $inc: { value: 1 } }).exec();
if (this.conf.maintenance === true) res.status(503).json({ code: this.constants.codes.MAINTENANCE_OR_UNAVAILABLE, message: this.constants.messages.MAINTENANCE_OR_UNAVAILABLE });
else if (this.conf.deprecated === true) res.status(501).json({ code: this.constants.codes.DEPRECATED, message: this.constants.messages.DEPRECATED });
else next();
});
}
public deprecated(): void {
this.router.all('*', (_req, res) => {
res.status(501).json({ code: this.constants.codes.DEPRECATED, message: this.constants.messages.DEPRECATED });
});
}
public maintenance(): void {
this.router.all('*', (_req, res) => {
res.status(503).json({ code: this.constants.codes.MAINTENANCE_OR_UNAVAILABLE, message: this.constants.messages.MAINTENANCE_OR_UNAVAILABLE });
});
}
public handleError(error: Error, res: Response) {
res.status(500).json({ code: this.constants.codes.SERVER_ERROR, message: this.constants.messages.SERVER_ERROR });
this.server.parent.client.util.handleError(error);
}
get constants() {
return {
codes: {
SUCCESS: 100,
UNAUTHORIZED: 101,
PERMISSION_DENIED: 104,
ENDPOINT_NOT_FOUND: 104,
NOT_FOUND: 1041,
ACCOUNT_NOT_FOUND: 1041,
CLIENT_ERROR: 1044,
SERVER_ERROR: 105,
DEPRECATED: 1051,
MAINTENANCE_OR_UNAVAILABLE: 1053,
},
messages: {
UNAUTHORIZED: ['CREDENTIALS_INVALID', 'The credentials you supplied are invalid.'],
BEARER_TOKEN_INVALID: ['BEARER_TOKEN_INVALID', 'The Bearer token you supplied is invalid.'],
PERMISSION_DENIED: ['PERMISSION_DENIED', 'You do not have valid credentials to access this resource.'],
NOT_FOUND: ['NOT_FOUND', 'The resource you requested cannot be located.'],
ENDPOINT_NOT_FOUND: ['ENDPOINT_NOT_FOUND', 'The endpoint you requested does not exist or cannot be located.'],
CLIENT_ERROR: ['CLIENT_ERROR', 'The information provided to this endpoint via headers, body, query, or parameters are invalid.'],
SERVER_ERROR: ['INTERNAL_ERROR', 'An internal error has occurred, Engineers have been notified.'],
DEPRECATED: ['ENDPOINT_OR_RESOURCE_DEPRECATED', 'The endpoint or resource you\'re trying to access has been deprecated.'],
MAINTENANCE_OR_UNAVAILABLE: ['SERVICE_UNAVAILABLE', 'The endpoint or resource you\'re trying to access is either in maintenance or is not available.'],
},
discord: {
SERVER_ID: '446067825673633794',
},
};
}
}

76
src/class/Server.ts Normal file
View File

@ -0,0 +1,76 @@
import express from 'express';
import bodyParser from 'body-parser';
import helmet from 'helmet';
import { Server as HTTPServer } from 'http';
import { Collection, ServerManagement, Route } from '.';
export default class Server {
public app: express.Application;
public routes: Collection<Route>;
public parent: ServerManagement;
public port: number;
private root: string;
protected parse: boolean;
constructor(parent: ServerManagement, port: number, routeRoot: string, parse = true) {
this.parent = parent;
this.app = express();
this.routes = new Collection<Route>();
this.port = port;
this.root = routeRoot;
this.parse = parse;
this.init();
this.loadRoutes();
}
get client() {
return this.parent.client;
}
public async loadRoutes() {
const routes = Object.values<typeof Route>(require(this.root));
for (const RouteFile of routes) {
const route = new RouteFile(this);
if (route.conf.deprecated) {
route.deprecated();
} else if (route.conf.maintenance) {
route.maintenance();
} else {
route.init();
route.bind();
}
this.parent.client.util.signale.success(`Successfully loaded route 'http://localhost:${this.port}/${route.conf.path}'.`);
this.routes.add(route.conf.path, route);
this.app.use(route.conf.path, route.router);
}
this.app.listen(this.port);
}
public init() {
if (this.parse) {
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({ extended: true }));
}
this.app.set('trust proxy', 'loopback');
this.app.use(helmet({
hsts: false,
hidePoweredBy: false,
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
},
},
}));
}
public listen(port: number): HTTPServer {
return this.app.listen(port);
}
}

View File

@ -0,0 +1,22 @@
import { Client, Collection, Server } from '.';
import serverSetup from '../api';
export default class ServerManagement {
public client: Client;
public servers: Collection<Server>;
constructor(client: Client) {
this.client = client;
this.servers = new Collection<Server>();
this.loadServers();
}
public async loadServers() {
const apiRoot = Object.entries<(management: ServerManagement) => Server>(serverSetup);
for (const [api, server] of apiRoot) {
this.servers.add(api, server(this));
this.client.util.signale.success(`Successfully loaded server '${api}'.`);
}
}
}

310
src/class/Util.ts Normal file
View File

@ -0,0 +1,310 @@
/* eslint-disable no-bitwise */
import nodemailer from 'nodemailer';
import childProcess from 'child_process';
import { promisify } from 'util';
import signale from 'signale';
import { Member, Message, Guild, PrivateChannel, GroupChannel, Role, AnyGuildChannel, WebhookPayload } from 'eris';
import { Client, Command, Moderation, PBX, Report, RichEmbed } from '.';
import { statusMessages as emotes } from '../configs/emotes.json';
export default class Util {
public client: Client;
public moderation: Moderation;
public signale: signale.Signale;
public transporter: nodemailer.Transporter;
public pbx: PBX;
public report: Report;
constructor(client: Client) {
this.client = client;
this.moderation = new Moderation(this.client);
this.signale = signale;
this.signale.config({
displayDate: true,
displayTimestamp: true,
displayFilename: true,
});
this.transporter = nodemailer.createTransport({
host: 'staff.libraryofcode.org',
port: 587,
auth: { user: 'internal', pass: this.client.config.emailPass },
});
this.pbx = new PBX(this.client);
this.report = new Report(this.client);
}
get emojis() {
return {
SUCCESS: emotes.success,
LOADING: emotes.loading,
ERROR: emotes.error,
};
}
public hrn(number: any, fixed: number, formatter: any | any[]) {
const builtInFormatters = {
en: ['KMGTPEZY'.split(''), 1e3],
zh_CN: ['百千万亿兆京垓秭'.split(''), [100, 10, 10, 1e4, 1e4, 1e4, 1e4, 1e4]],
};
number = Math.abs(number);
if (!fixed && fixed !== 0) fixed = 1;
if (typeof formatter === 'object') {
const name = `${new Date().getTime()}`;
builtInFormatters[name] = formatter;
formatter = name;
}
if (!builtInFormatters[formatter]) formatter = 'en';
formatter = builtInFormatters[formatter];
let power = 1;
const texts = formatter[0];
const powers = formatter[1];
let loop = 0;
let is_array = false;
if (typeof powers === 'object') is_array = true;
// eslint-disable-next-line no-constant-condition
while (1) {
if (is_array) power = powers[loop];
else power = powers;
if (number >= power && loop < texts.length) number /= power;
else {
number = number.toFixed(fixed);
return loop === 0 ? number : `${number} ${texts[loop - 1]}`;
}
// eslint-disable-next-line no-plusplus
++loop;
}
}
/**
* Resolves a command
* @param query Command input
* @param message Only used to check for errors
*/
/* public resolveCommand(query: string | string[]): Promise<{cmd: Command, args: string[] }> {
try {
if (typeof query === 'string') query = query.split(' ');
const commands = this.client.commands.toArray();
const resolvedCommand = commands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase()));
if (!resolvedCommand) return Promise.resolve(null);
query.shift();
return Promise.resolve({ cmd: resolvedCommand, args: query });
} catch (error) {
return Promise.reject(error);
}
}
*/
public dataConversion(bytes: number): string {
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
if (bytes === 0) {
return '0 KB';
}
return `${(bytes / 1024 ** i).toFixed(2)} ${sizes[i]}`;
}
public async exec(command: string, _options: childProcess.ExecOptions = {}): Promise<string> {
const ex = promisify(childProcess.exec);
try {
return (await ex(command)).stdout;
} catch (err) {
return err;
}
/* return new Promise((res, rej) => {
let output = '';
const writeFunction = (data: string|Buffer|Error) => {
output += `${data}`;
};
const cmd = childProcess.exec(command, options);
cmd.stdout.on('data', writeFunction);
cmd.stderr.on('data', writeFunction);
cmd.on('error', writeFunction);
cmd.once('close', (code, signal) => {
cmd.stdout.off('data', writeFunction);
cmd.stderr.off('data', writeFunction);
cmd.off('error', writeFunction);
setTimeout(() => {}, 1000);
if (code !== 0) rej(new Error(`Command failed: ${command}\n${output}`));
res(output);
});
}); */
}
/**
* Resolves a command
* @param query Command input
* @param message Only used to check for errors
*/
public resolveCommand(query: string | string[], message?: Message): Promise<{ cmd: Command, args: string[] }> {
try {
let resolvedCommand: Command;
if (typeof query === 'string') query = query.split(' ');
const commands = this.client.commands.toArray();
resolvedCommand = commands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase()));
if (!resolvedCommand) return Promise.resolve(null);
query.shift();
while (resolvedCommand.subcommands.size && query.length) {
const subCommands = resolvedCommand.subcommands.toArray();
const found = subCommands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase()));
if (!found) break;
resolvedCommand = found;
query.shift();
}
return Promise.resolve({ cmd: resolvedCommand, args: query });
} catch (error) {
if (message) this.handleError(error, message);
else this.handleError(error);
return Promise.reject(error);
}
}
public resolveGuildChannel(query: string, { channels }: Guild, categories = false): AnyGuildChannel | undefined {
const ch: AnyGuildChannel[] = channels.filter((c) => (!categories ? c.type !== 4 : true));
return ch.find((c) => c.id === query.replace(/[<#>]/g, '') || c.name === query)
|| ch.find((c) => c.name.toLowerCase() === query.toLowerCase())
|| ch.find((c) => c.name.toLowerCase().startsWith(query.toLowerCase()));
}
public resolveRole(query: string, { roles }: Guild): Role | undefined {
return roles.find((r) => r.id === query.replace(/[<@&>]/g, '') || r.name === query)
|| roles.find((r) => r.name.toLowerCase() === query.toLowerCase())
|| roles.find((r) => r.name.toLowerCase().startsWith(query.toLowerCase()));
}
public resolveMember(query: string, { members }: Guild): Member | undefined {
return members.find((m) => `${m.username}#${m.discriminator}` === query || m.username === query || m.id === query.replace(/[<@!>]/g, '') || m.nick === query) // Exact match for mention, username+discrim, username and user ID
|| members.find((m) => `${m.username.toLowerCase()}#${m.discriminator}` === query.toLowerCase() || m.username.toLowerCase() === query.toLowerCase() || (m.nick && m.nick.toLowerCase() === query.toLowerCase())) // Case insensitive match for username+discrim, username
|| members.find((m) => m.username.toLowerCase().startsWith(query.toLowerCase()) || (m.nick && m.nick.toLowerCase().startsWith(query.toLowerCase())));
}
public async handleError(error: Error, message?: Message, command?: Command, disable = true): Promise<void> {
try {
this.signale.error(error);
const info: WebhookPayload = { content: `\`\`\`js\n${error.stack || error}\n\`\`\``, embeds: [] };
if (message) {
const embed = new RichEmbed();
embed.setColor('FF0000');
embed.setAuthor(`Error caused by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL);
embed.setTitle('Message content');
embed.setDescription(message.content);
embed.addField('User', `${message.author.mention} (\`${message.author.id}\`)`, true);
embed.addField('Channel', message.channel.mention, true);
let guild: string;
if (message.channel instanceof PrivateChannel || message.channel instanceof GroupChannel) guild = '@me';
else guild = message.channel.guild.id;
embed.addField('Message link', `[Click here](https://discordapp.com/channels/${guild}/${message.channel.id}/${message.id})`, true);
embed.setTimestamp(new Date(message.timestamp));
info.embeds.push(embed);
}
await this.client.executeWebhook(this.client.config.webhookID, this.client.config.webhookToken, info);
const msg = message ? message.content.slice(this.client.config.prefix.length).trim().split(/ +/g) : [];
if (command && disable) this.resolveCommand(msg).then((c) => { c.cmd.enabled = false; });
if (message) message.channel.createMessage(`***${this.emojis.ERROR} An unexpected error has occured - please contact a Staff member.${command && disable ? ' This command has been disabled.' : ''}***`);
} catch (err) {
this.signale.error(err);
}
}
public splitString(string: string, length: number): string[] {
if (!string) return [];
if (Array.isArray(string)) string = string.join('\n');
if (string.length <= length) return [string];
const arrayString: string[] = [];
let str: string = '';
let pos: number;
while (string.length > 0) {
pos = string.length > length ? string.lastIndexOf('\n', length) : string.length;
if (pos > length) pos = length;
str = string.substr(0, pos);
string = string.substr(pos);
arrayString.push(str);
}
return arrayString;
}
public splitFields(fields: { name: string, value: string, inline?: boolean }[]): { name: string, value: string, inline?: boolean }[][] {
let index = 0;
const array: { name: string, value: string, inline?: boolean }[][] = [[]];
while (fields.length) {
if (array[index].length >= 25) { index += 1; array[index] = []; }
array[index].push(fields[0]); fields.shift();
}
return array;
}
public splitArray<T>(array: T[], count: number) {
const finalArray: T[][] = [];
while (array.length) {
finalArray.push(array.splice(0, count));
}
return finalArray;
}
public decimalToHex(int: number): string {
const hex = int.toString(16);
return '#000000'.substring(0, 7 - hex.length) + hex;
}
public randomNumber(min: number, max: number): number {
return Math.round(Math.random() * (max - min) + min);
}
public encode(arg: string) {
return arg.split('').map((x) => x.charCodeAt(0) / 400);
}
public normalize(string) {
const input = [];
// eslint-disable-next-line no-plusplus
for (let i = 0; i < string.length; i++) {
input.push(string.charCodeAt(i) / 1000);
}
return input;
}
public convert_ascii(ascii: []) {
let string = '';
// eslint-disable-next-line no-plusplus
for (let i = 0; i < ascii.length; i++) {
string += String.fromCharCode(ascii[i] * 4000);
}
return string;
}
public percentile(arr: number[], val: number) {
return (100 * arr.reduce((acc, v) => acc + (v < val ? 1 : 0) + (v === val ? 0.5 : 0), 0)) / arr.length;
}
public ordinal(i: number) {
const j = i % 10;
const k = i % 100;
if (j === 1 && k !== 11) {
return `${i}st`;
}
if (j === 2 && k !== 12) {
return `${i}nd`;
}
if (j === 3 && k !== 13) {
return `${i}rd`;
}
return `${i}th`;
}
public capsFirstLetter(string?: string): string | void {
if (typeof string !== 'string') return undefined;
return string.substring(0, 1).toUpperCase() + string.substring(1);
}
}

15
src/class/index.ts Normal file
View File

@ -0,0 +1,15 @@
export { default as Client } from './Client';
export { default as Collection } from './Collection';
export { default as Command } from './Command';
export { default as Event } from './Event';
export { default as Handler } from './Handler';
export { default as LocalStorage } from './LocalStorage';
export { default as Moderation } from './Moderation';
export { default as PBX } from './PBX';
export { default as Queue } from './Queue';
export { default as Report } from './Report';
export { default as RichEmbed } from './RichEmbed';
export { default as Route } from './Route';
export { default as Server } from './Server';
export { default as ServerManagement } from './ServerManagement';
export { default as Util } from './Util';

60
src/commands/additem.ts Normal file
View File

@ -0,0 +1,60 @@
import { Message } from 'eris';
import { Client, Command, RichEmbed } from '../class';
export default class AddItem extends Command {
constructor(client: Client) {
super(client);
this.name = 'additem';
this.description = 'Adds information to your whois embed.';
this.usage = 'additem [code]';
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (args.length < 1) {
const embed = new RichEmbed();
embed.setTitle('Whois Data Codes');
embed.addField('Languages', '**Assembly Language:** lang-asm\n**C/C++:** lang-cfam\n**C#:** lang-csharp\n**Go:** lang-go\n**Java:** lang-java\n**JavaScript:** lang-js\n**Kotlin:** lang-kt\n**Python:** lang-py\n**Ruby:** lang-rb\n**Rust:** lang-rs\n**Swift:** lang-swift\n**TypeScript:** lang-ts');
embed.addField('Operating Systems', '**Arch:** os-arch\n**Debian:** os-deb\n**CentOS:** os-cent\n**Fedora:** os-fedora\n**macOS:** os-mdarwin\n**Manjaro:** os-manjaro\n**RedHat:** os-redhat\n**Ubuntu:** os-ubuntu\n**Windows:** os-win');
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
}
if (args[0].split('-')[0] === 'os' && ['arch', 'deb', 'cent', 'fedora', 'manjaro', 'mdarwin', 'redhat', 'ubuntu', 'win'].includes(args[0].split('-')[1])) {
const account = await this.client.db.Member.findOne({ userID: message.member.id });
if (!account) {
const newAccount = new this.client.db.Member({
userID: message.member.id,
additional: {
operatingSystems: [args[0].split('-')[1]],
},
});
await newAccount.save();
} else {
await account.updateOne({ $addToSet: { 'additional.operatingSystems': args[0].split('-')[1] } });
}
return message.channel.createMessage(`***${this.client.util.emojis.SUCCESS} Added OS code ${args[0]} to profile.***`);
}
if (args[0].split('-')[0] === 'lang' && ['js', 'py', 'rb', 'ts', 'rs', 'go', 'cfam', 'csharp', 'swift', 'java', 'kt', 'asm'].includes(args[0].split('-')[1])) {
const account = await this.client.db.Member.findOne({ userID: message.member.id });
if (!account) {
const newAccount = new this.client.db.Member({
userID: message.member.id,
additional: {
langs: [args[0].split('-')[1]],
},
});
await newAccount.save();
} else {
await account.updateOne({ $addToSet: { 'additional.langs': args[0].split('-')[1] } });
}
return message.channel.createMessage(`***${this.client.util.emojis.SUCCESS} Added language code ${args[0]} to profile.***`);
}
return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Invalid data code.***`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,35 @@
import { Message } from 'eris';
import { randomBytes } from 'crypto';
import { Client, Command } from '../class';
export default class AddMerchant extends Command {
constructor(client: Client) {
super(client);
this.name = 'addmerchant';
this.description = 'Creates a new merchant.';
this.usage = `${this.client.config.prefix}addmerchant <privileged: 1 for yes | 0 for no> <type: 0 for only soft | 1 for soft and hard> <merchant name>`;
this.aliases = ['am'];
this.permissions = 6;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[1]) return this.client.commands.get('help').run(message, [this.name]);
if ((Number(args[0]) !== 0) && (Number(args[0]) !== 1)) return this.error(message.channel, 'Invalid permissions.');
if ((Number(args[1]) !== 0) && (Number(args[1]) !== 1)) return this.error(message.channel, 'Invalid permissions.');
const key = randomBytes(20).toString('hex');
const merchant = await (new this.client.db.Merchant({
name: args.slice(2).join(' '),
privileged: Number(args[0]),
type: Number(args[1]),
key,
pulls: [],
})).save();
return this.success(message.channel, `Created merchant (${merchant._id}). \`${args.slice(2).join(' ')}\`\n\n\`${key}\``);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

40
src/commands/addnote.ts Normal file
View File

@ -0,0 +1,40 @@
import { Message } from 'eris';
import { Command, Client } from '../class';
export default class AddNote extends Command {
constructor(client: Client) {
super(client);
this.name = 'addnote';
this.description = 'Adds a note to a member.';
this.usage = `${this.client.config.prefix}addnote <member> <text> [category: comm | cs | edu]`;
this.permissions = 1;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0] || args.length < 1) return this.client.commands.get('help').run(message, [this.name]);
let user = this.client.util.resolveMember(args[0], this.mainGuild)?.user;
if (!user) user = await this.client.getRESTUser(args[0]);
if (!user) return this.error(message.channel, 'The member you specified could not be found.');
const note: { userID?: string, staffID: string, date: Date, category?: string, text?: string } = {
userID: user.id,
date: new Date(),
staffID: message.author.id,
};
if (args[args.length - 1] !== 'edu' && args[args.length - 1] !== 'comm' && args[args.length - 1] !== 'cs') {
note.category = '';
note.text = args.slice(1).join(' ');
} else {
note.category = args[args.length - 1];
note.text = args.slice(0, args.length - 1).join(' ');
}
const saved = await (new this.client.db.Note(note).save());
return this.success(message.channel, `Successfully created Note # \`${saved._id}\`.`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

45
src/commands/addrank.ts Normal file
View File

@ -0,0 +1,45 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class AddRank extends Command {
constructor(client: Client) {
super(client);
this.name = 'addrank';
this.description = 'Makes a role self-assignable.';
this.usage = `${this.client.config.prefix}addrank <role> <permissions, pass 0 for everyone. separate role IDs with ':'> <description>`;
this.permissions = 6;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
if (!args[1]) return this.error(message.channel, 'Permissions are required.');
if (!args[2]) return this.error(message.channel, 'A description is required');
const role = this.client.util.resolveRole(args[0], this.mainGuild);
if (!role) return this.error(message.channel, 'The role you specified doesn\'t appear to exist.');
const check = await this.client.db.Rank.findOne({ roleID: role.id });
if (check) return this.error(message.channel, 'This role is already self-assignable.');
let permissions: string[];
if (args[1] === '0') {
permissions = ['0'];
} else {
permissions = args[1].split(':');
}
const entry = new this.client.db.Rank({
name: role.name,
roleID: role.id,
permissions,
description: args.slice(2).join(' '),
});
await entry.save();
return this.success(message.channel, `Role ${role.name} is now self-assignable.`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,38 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class AddRedirect extends Command {
constructor(client: Client) {
super(client);
this.name = 'addredirect';
this.description = 'Adds a redirect link for \'loc.sh\'';
this.usage = 'addredirect <redirect to url> <key>';
this.aliases = ['ar'];
this.permissions = 6;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const check = await this.client.db.Redirect.findOne({ key: args[1].toLowerCase() });
if (check) return this.error(message.channel, `Redirect key ${args[1].toLowerCase()} already exists. Linked to: ${check.to}`);
try {
const test = new URL(args[0]);
if (test.protocol !== 'https:') return this.error(message.channel, 'Protocol must be HTTPS.');
} catch {
return this.error(message.channel, 'This doesn\'t appear to be a valid URL.');
}
if ((/^[a-zA-Z0-9]+$/gi.test(args[1].toLowerCase().replace('-', '').trim()) === false) || args[1].toLowerCase().length > 15) return this.error(message.channel, 'Invalid key. The key must be alphanumeric and less than 16 characters.');
const redirect = new this.client.db.Redirect({
key: args[1].toLowerCase(),
to: args[0],
visitedCount: 0,
});
await redirect.save();
return this.success(message.channel, `Redirect https://loc.sh/${args[1].toLowerCase()} -> ${args[0]} is now active.`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

158
src/commands/apply.ts Normal file
View File

@ -0,0 +1,158 @@
/* eslint-disable no-continue */
import type { AxiosError, AxiosStatic } from 'axios';
import axios from 'axios';
import { Member, Message } from 'eris';
import { Client, Command, RichEmbed } from '../class';
export default class Apply extends Command {
public services: Map<string, { description: string, type: 'HARD' | 'SOFT', url: string, validation: (...cond: any) => Promise<boolean> | boolean, func?: Function }>;
constructor(client: Client) {
super(client);
this.name = 'apply';
this.description = 'apply';
this.usage = `${this.client.config.prefix}apply [serviceName]\n${this.client.config.prefix}apply full`;
this.permissions = 0;
this.guildOnly = true;
this.enabled = true;
this.setServices();
}
protected setServices() {
this.services = new Map();
this.services.set('role::constants', {
description: 'Constants role assignment.',
type: 'HARD',
url: 'https://eds.libraryofcode.org/roles/constants',
validation: (member: Member) => !member.roles.includes('511771731891847168'),
func: async (client: Client, ...data: any[]) => {
const member = await client.guilds.get(client.config.guildID).getRESTMember(data[0]);
await member.addRole('511771731891847168', 'Constants Approval from EDS');
},
});
this.services.set('cs::t2', {
description: 'Tier 2 upgrade for Cloud Services account.',
type: 'HARD',
url: 'https://eds.libraryofcode.org/cs/t2',
validation: (member: Member) => member.roles.includes('546457886440685578'),
func: async (client: Client, ...data: any[]) => {
const member = await client.guilds.get(client.config.guildID).getRESTMember(data[0]);
const ax = <AxiosStatic>require('axios');
await ax({
method: 'get',
url: `https://api.cloud.libraryofcode.org/wh/t2?userID=${member.id}&auth=${client.config.internalKey}`,
});
},
});
this.services.set('cs::promot3', {
description: 'Receive 25% off your first purchase of Tier 3.',
type: 'SOFT',
url: 'https://eds.libraryofcode.org/cs/t3-promo',
validation: async (member: Member) => {
if (!member.roles.includes('546457886440685578')) return false;
const customer = await this.client.db.Customer.findOne({ userID: member.user.id }).lean().exec();
if (!customer) return false;
return true;
},
func: async (client: Client, ...data: any[]) => {
const member = await client.guilds.get(client.config.guildID).getRESTMember(data[0]);
const customer = await client.db.Customer.findOne({ userID: member.user.id }).lean().exec();
const coupon = await client.stripe.coupons.create({
percent_off: 25,
duration: 'once',
max_redemptions: 1,
name: 'Tier 3 - EDS Discount',
metadata: {
userID: member.user.id,
},
});
const promo = await client.stripe.promotionCodes.create({
coupon: coupon.id,
customer: customer.cusID,
max_redemptions: 1,
restrictions: {
first_time_transaction: true,
},
});
const doc = new client.db.Promo({
code: promo.code,
pID: promo.id,
});
await doc.save();
const chan = await client.getDMChannel(customer.userID);
chan.createMessage(`__**Tier 3 Coupon Code**__\n\`${promo.code}\`\n\n*Do not share this promotional code with anyone else. This promo code is good for your first purchase of Tier 2, 25% off applied. Will apply to your first invoice only, for more questions contact support.*`);
},
});
this.services.set('p::role::constants', {
description: 'Pre-approval for Constants role assignment.',
type: 'SOFT',
url: 'https://eds.libraryofcode.org/roles/preconstants',
validation: (member: Member) => !member.roles.includes('511771731891847168'),
});
this.services.set('p::cs::t2', {
description: 'Pre-approval for Tier 2.',
type: 'SOFT',
url: 'https://eds.libraryofcode.org/cs/t2pre',
validation: (member: Member) => member.roles.includes('546457886440685578'),
});
}
public async run(message: Message, args: string[]) {
try {
if (!args[0] || args[0] === 'full') {
const embed = new RichEmbed();
embed.setTitle('Instant Application Service [IAS]');
embed.setColor('#556cd6');
if (args[0] !== 'full') {
embed.setDescription(`*These applications are specifically targeted to you based on validation conditions. Run \`${this.client.config.prefix}apply full\` for a full list of all applications.*`);
embed.setThumbnail(message.member.avatarURL);
embed.setAuthor(message.member.username, message.member.avatarURL);
}
for (const service of this.services) {
// eslint-disable-next-line no-await-in-loop
const test = await service[1].validation(message.member);
if (!test && args[0] !== 'full') continue;
embed.addField(service[0], `**Description**: ${service[1].description}\n**Inquiry Type:** ${service[1].type}\n\n*Run \`${this.client.config.prefix}apply ${service[0]}\` to apply.*`);
}
if (embed.fields?.length <= 0) embed.setDescription(`*We have no offers for you at this time. To see a full list of offers, please run \`${this.client.config.prefix}apply full\`.*`);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
}
if (!this.services.has(args[0])) return this.error(message.channel, 'Invalid service/product name.');
const service = this.services.get(args[0]);
const test = await this.services.get(args[0]).validation(message.member);
if (!test) return this.error(message.channel, 'A condition exists which prevents you from applying, please try again later.');
const msg = await this.loading(message.channel, 'Thank you for submitting an application. We are currently processing it, you will be pinged here shortly with the decision.');
return await this.client.queue.processApplication({ channelID: message.channel.id, guildID: this.mainGuild.id, messageID: msg.id }, service.url, message.author.id, service.func ? service.func.toString() : undefined);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
public static async apply(client: Client, url: string, userID: string) {
try {
const { data } = await axios({
method: 'get',
url: `${url}?userID=${userID}&auth=${client.config.internalKey}`,
});
return {
status: 'SUCCESS',
decision: data.decision,
id: data.id,
processedBy: data.processedBy,
token: data.token,
};
} catch (err) {
const error = <AxiosError>err;
if (error.response?.status === 404 || error.response.status === 400 || error.response.status === 401) return { id: 'N/A', processedBy: 'N/A', status: 'CLIENT_ERROR', decision: 'PRE-DECLINED' };
return { id: 'N/A', processedBy: 'N/A', status: 'SERVER_ERROR', decision: 'PRE-DECLINED' };
}
}
}

53
src/commands/ban.ts Normal file
View File

@ -0,0 +1,53 @@
import moment, { unitOfTime } from 'moment';
import { Message, User } from 'eris';
import { Client, Command } from '../class';
export default class Ban extends Command {
constructor(client: Client) {
super(client);
this.name = 'ban';
this.description = 'Bans a member from the guild.';
this.usage = 'ban <member> [time] [reason]';
this.permissions = 3;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const member = this.client.util.resolveMember(args[0], this.mainGuild);
let user: User;
if (!member) {
try {
user = await this.client.getRESTUser(args[0]);
} catch {
return this.error(message.channel, 'Cannot find user.');
}
} else {
user = member.user;
}
try {
await this.mainGuild.getBan(args[0]);
return this.error(message.channel, 'This user is already banned.');
} catch {} // eslint-disable-line no-empty
if (member && !this.client.util.moderation.checkPermissions(member, message.member)) return this.error(message.channel, 'Permission Denied.');
message.delete();
let momentMilliseconds: number;
let reason: string;
if (args.length > 1) {
const lockLength = args[1].match(/[a-z]+|[^a-z]+/gi);
const length = Number(lockLength[0]);
const unit = lockLength[1] as unitOfTime.Base;
momentMilliseconds = moment.duration(length, unit).asMilliseconds();
reason = momentMilliseconds ? args.slice(2).join(' ') : args.slice(1).join(' ');
if (reason.length > 512) return this.error(message.channel, 'Ban reasons cannot be longer than 512 characters.');
}
await this.client.util.moderation.ban(user, message.member, momentMilliseconds, reason);
return this.success(message.channel, `${user.username}#${user.discriminator} has been banned.`);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

55
src/commands/billing.ts Normal file
View File

@ -0,0 +1,55 @@
import axios from 'axios';
import moment from 'moment';
import { Message } from 'eris';
import { randomBytes } from 'crypto';
import { v4 as uuid } from 'uuid';
import { Client, Command } from '../class';
import Billing_T3 from './billing_t3';
export default class Billing extends Command {
constructor(client: Client) {
super(client);
this.name = 'billing';
this.description = 'Pulls up your Billing Portal. You must have a CS Account to continue.';
this.usage = `${this.client.config.prefix}billing`;
this.subcmds = [Billing_T3];
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message) {
try {
const response = <{
found: boolean,
emailAddress?: string,
tier?: number,
supportKey?: string,
}> (await axios.get(`https://api.cloud.libraryofcode.org/wh/info?id=${message.author.id}&authorization=${this.client.config.internalKey}`)).data;
if (!response.found) return this.error(message.channel, 'CS Account not found.');
const portalKey = randomBytes(50).toString('hex');
const uid = uuid();
const redirect = new this.client.db.Redirect({
key: uid,
to: `https://loc.sh/dash?q=${portalKey}`,
});
const portal = new this.client.db.CustomerPortal({
key: portalKey,
username: message.author.username,
userID: message.author.id,
emailAddress: response.emailAddress,
expiresOn: moment().add(5, 'minutes').toDate(),
used: false,
});
await portal.save();
await redirect.save();
const chan = await this.client.getDMChannel(message.author.id);
await chan.createMessage(`__***Billing Account Portal***__\nClick here: https://loc.sh/${uid}\n\nYou will be redirected to your billing portal, please note the link expires after 5 minutes.`);
return await this.success(message.channel, 'Your Billing Portal information has been DMed to you.');
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,62 @@
import axios from 'axios';
import { Message } from 'eris';
import type { Stripe } from 'stripe';
import { Client, Command } from '../class';
import type { PromoInterface } from '../models';
export default class Billing_T3 extends Command {
constructor(client: Client) {
super(client);
this.name = 't3';
this.description = 'Subscription to CS Tier 3.';
this.usage = `${this.client.config.prefix}billing t3 [promoCode]`;
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
await message.delete();
const response = <{
found: boolean,
emailAddress?: string,
tier?: number,
supportKey?: string,
}>(await axios.get(`https://api.cloud.libraryofcode.org/wh/info?id=${message.author.id}&authorization=${this.client.config.internalKey}`)).data;
if (!response.found) return this.error(message.channel, 'CS Account not found.');
const customer = await this.client.db.Customer.findOne({ userID: message.author.id });
if (!customer) return this.error(message.channel, `You do not have a Customer Account. Please run \`${this.client.config.prefix}billing\`, once you visit the Billing Portal via the URL given to you, please try again.`);
let promoCode: PromoInterface;
if (args[0]) {
promoCode = await this.client.db.Promo.findOne({ code: args[0].toUpperCase() });
}
let subscription: Stripe.Response<Stripe.Subscription>;
try {
subscription = await this.client.stripe.subscriptions.create({
customer: customer.cusID,
payment_behavior: 'allow_incomplete',
items: [{ price: 'price_1H8e6ODatwI1hQ4WFVvX6Nda' }],
days_until_due: 1,
collection_method: 'send_invoice',
default_tax_rates: ['txr_1HlAadDatwI1hQ4WRHu14S2I'],
promotion_code: promoCode ? promoCode.id : undefined,
});
} catch (err) {
return this.error(message.channel, `Error creating subscription.\n\n${err}`);
}
await this.client.stripe.invoices.finalizeInvoice(subscription.latest_invoice.toString());
const invoice = await this.client.stripe.invoices.retrieve(subscription.latest_invoice.toString());
const chan = await this.client.getDMChannel(message.author.id);
await chan.createMessage(`__**Invoice for New Subscription**__\n${invoice.hosted_invoice_url}\n\n*Please click on the link above to pay for your subscription.*`);
return this.success(message.channel, 'Transaction processed.');
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

52
src/commands/callback.ts Normal file
View File

@ -0,0 +1,52 @@
import PhoneNumber from 'awesome-phonenumber';
import axios from 'axios';
import { Message, TextChannel } from 'eris';
import { Client, Command, RichEmbed } from '../class';
export default class Callback extends Command {
constructor(client: Client) {
super(client);
this.name = 'callback';
this.description = 'Requests a Callback from a Technican.\nPlease use `-` to separate the number if needed. E.x. 202-750-2585.\nDo note, we are unable to dial international numbers outside of the US and Canada.\n\n*We recommend you run this command in your DMs for privacy.*';
this.usage = 'callback <the number you want us to call>';
this.aliases = ['cb'];
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
if (message.channel.type === 0) await message.delete();
const member = this.mainGuild.members.get(message.author.id);
if (!member) return this.error(message.channel, 'Unable to fetch member.');
const phone = new PhoneNumber(args.join(' '), 'US');
if (!phone.isValid()) return this.error(message.channel, 'The number you have entered is invalid.');
const embed = new RichEmbed();
embed.setTitle('Callback Request');
embed.setDescription('Please dial `9` first to reach an the external trunk. For example, to dial 202-750-2585 you would dial 92027502585 on your device.\n\n*Please react with <:modSuccess:578750988907970567> on this message if you are taking the call.*');
embed.addField('Member', `${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`, true);
embed.addField('Phone Number', phone.getNumber('national'), true);
embed.addField('Phone Number Type', phone.getType(), true);
const communityReport = await this.client.db.Score.findOne({ userID: message.author.id }).lean().exec();
if (communityReport) {
await this.client.report.createInquiry(member.user.id, 'Library of Code sp-us | VOIP/PBX Member Support SVCS', 1);
embed.addField('PIN', `${communityReport.pin[0]}-${communityReport.pin[1]}-${communityReport.pin[2]}`, true);
}
try {
const d = await axios.get(`https://api.cloud.libraryofcode.org/wh/info/?id=${message.author.id}&authorization=${this.client.config.internalKey}`);
embed.addField('Email Address', d.data.emailAddress, true);
embed.addField('Support Key', d.data.supportKey, true);
} catch {
this.client.util.signale.warn('No CS Account found for user.');
}
const chan = <TextChannel> this.mainGuild.channels.get('780513128240382002');
const msg = await chan.createMessage({ content: '<@&780519428873781298>', embed });
await msg.addReaction('modSuccess:578750988907970567');
return message.channel.createMessage('__**Callback Request**__\nYour callback request has been sent to our agents, you should receive a callback within 1-24 hours from the date of this request. The number you will be called from is listed below.\n\n**Callback Number:** +1 202-750-2585\n**Callback Region:** Washington, D.C., United States\n\n\n*Your number is never stored on our systems at any time, as soon as your call is taken by an agent your number is deleted from notification channels.*');
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

46
src/commands/delitem.ts Normal file
View File

@ -0,0 +1,46 @@
import { Message } from 'eris';
import { Client, Command, RichEmbed } from '../class';
export default class DelItem extends Command {
constructor(client: Client) {
super(client);
this.name = 'delitem';
this.description = 'Removes information to your whois embed.';
this.usage = 'delitem [code]';
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (args.length < 1) {
const embed = new RichEmbed();
embed.setTitle('Whois Data Codes');
embed.addField('Languages', '**Assembly Language:** lang-asm\n**C/C++:** lang-cfam\n**C#:** lang-csharp\n**Go:** lang-go\n**Java:** lang-java\n**JavaScript:** lang-js\n**Kotlin:** lang-kt\n**Python:** lang-py\n**Ruby:** lang-rb\n**Rust:** lang-rs\n**Swift:** lang-swift\n**TypeScript:** lang-ts');
embed.addField('Operating Systems', '**Arch:** os-arch\n**Debian:** os-deb\n**CentOS:** os-cent\n**Fedora:** os-fedora\n**macOS:** os-mdarwin\n**Manjaro:** os-manjaro\n**RedHat:** os-redhat\n**Ubuntu:** os-ubuntu\n**Windows:** os-win');
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
}
if (args[0].split('-')[0] === 'os' && ['arch', 'deb', 'cent', 'fedora', 'manjaro', 'mdarwin', 'redhat', 'ubuntu', 'win'].includes(args[0].split('-')[1])) {
const account = await this.client.db.Member.findOne({ userID: message.member.id });
if (account?.additional.operatingSystems.length < 1) {
return message.channel.createMessage(`***${this.client.util.emojis.ERROR} You don't have any operating systems to remove.***`);
}
await account.updateOne({ $pull: { 'additional.operatingSystems': args[0].split('-')[1] } });
return message.channel.createMessage(`***${this.client.util.emojis.SUCCESS} Removed OS code ${args[0]} from profile.***`);
}
if (args[0].split('-')[0] === 'lang' && ['js', 'py', 'rb', 'ts', 'rs', 'go', 'cfam', 'csharp', 'swift', 'java', 'kt', 'asm'].includes(args[0].split('-')[1])) {
const account = await this.client.db.Member.findOne({ userID: message.member.id });
if (account?.additional.langs.length < 1) {
return message.channel.createMessage(`***${this.client.util.emojis.ERROR} You don't have any languages to remove.***`);
}
await account.updateOne({ $pull: { 'additional.langs': args[0].split('-')[1] } });
return message.channel.createMessage(`***${this.client.util.emojis.SUCCESS} Removed language code ${args[0]} from profile.***`);
}
return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Invalid data code.***`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,26 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class DelMerchant extends Command {
constructor(client: Client) {
super(client);
this.name = 'delmerchant';
this.description = 'Deletes a merchant.';
this.usage = `${this.client.config.prefix}delmerchant <merchant id>`;
this.aliases = ['dm'];
this.permissions = 6;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const merchant = await this.client.db.Merchant.findOne({ key: args[0] });
if (!merchant) return this.error(message.channel, 'Merchant specified does not exist.');
return this.success(message.channel, `Deleted merchant \`${merchant._id}\`.`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

26
src/commands/delnote.ts Normal file
View File

@ -0,0 +1,26 @@
import { Message } from 'eris';
import { Command, Client } from '../class';
export default class DelNote extends Command {
constructor(client: Client) {
super(client);
this.name = 'delnote';
this.description = 'Deletes a note.';
this.usage = `${this.client.config.prefix}delnote <note id>`;
this.permissions = 1;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const note = await this.client.db.Note.findOne({ _id: args[0] }).lean().exec().catch(() => {});
if (!note) return this.error(message.channel, 'Could not locate that note.');
await this.client.db.Note.deleteOne({ _id: note._id });
return this.success(message.channel, `Note # \`${note._id}\` has been deleted.`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

30
src/commands/delrank.ts Normal file
View File

@ -0,0 +1,30 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class DelRank extends Command {
constructor(client: Client) {
super(client);
this.name = 'delrank';
this.description = 'Deletes an existing self-assignable role. This doesn\'t delete the role itself.';
this.usage = `${this.client.config.prefix}delrank <role>`;
this.permissions = 6;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const role = this.client.util.resolveRole(args[0], this.mainGuild);
if (!role) return this.error(message.channel, 'The role you specified doesn\'t appear to exist.');
const check = await this.client.db.Rank.findOne({ roleID: role.id });
if (!check) return this.error(message.channel, 'The entry doesn\'t appear to exist.');
await this.client.db.Rank.deleteOne({ roleID: role.id });
return this.success(message.channel, `Role ${role.name} is no longer self-assignable.`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,26 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class DelRedirect extends Command {
constructor(client: Client) {
super(client);
this.name = 'delredirect';
this.description = 'Delete a redirect link for \'loc.sh\'';
this.usage = 'delredirect <key>';
this.aliases = ['dr'];
this.permissions = 6;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const check = await this.client.db.Redirect.findOne({ key: args[0].toLowerCase() });
if (!check) return this.error(message.channel, `Redirect key ${args[0].toLowerCase()} doesn't exist.`);
await this.client.db.Redirect.deleteOne({ key: args[0].toLowerCase() });
return this.success(message.channel, `Deleted redirect https://loc.sh/${args[0].toLowerCase()}.`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

37
src/commands/djs.ts Normal file
View File

@ -0,0 +1,37 @@
import { Message, EmbedOptions } from 'eris';
import axios, { AxiosResponse } from 'axios';
import { Client, Command, RichEmbed } from '../class';
export default class DJS extends Command {
constructor(client: Client) {
super(client);
this.name = 'djs';
this.description = 'Get information about Discord.js.';
this.usage = 'djs <query>';
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
let res: AxiosResponse<EmbedOptions>;
try {
res = await axios.get(`https://djsdocs.sorta.moe/v2/embed?src=master&q=${args[0]}`);
} catch (err) {
return this.error(message.channel, 'Please try again later, something unexpected happened.');
}
if (!res.data) return this.error(message.channel, 'Could not find information. Try something else.');
const embed = new RichEmbed(res.data);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

33
src/commands/eris.ts Normal file
View File

@ -0,0 +1,33 @@
import { Message, EmbedOptions } from 'eris';
import axios, { AxiosResponse } from 'axios';
import { Client, Command } from '../class';
export default class Eris extends Command {
constructor(client: Client) {
super(client);
this.name = 'eris';
this.description = 'Get information about Eris.';
this.usage = 'eris <query>';
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
let res: AxiosResponse<{embed: EmbedOptions}>;
try {
res = await axios.get('https://erisdocs.cloud.libraryofcode.org/docs', { params: { search: args[0] } });
} catch (err) {
if (err.code === 404) return this.error(message.channel, 'Could not find information. Try something else.');
return this.error(message.channel, 'Please try again later, something unexpected happened.');
}
return message.channel.createMessage(res.data);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

68
src/commands/eval.ts Normal file
View File

@ -0,0 +1,68 @@
import axios from 'axios';
import { inspect } from 'util';
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Eval extends Command {
constructor(client: Client) {
super(client);
this.name = 'eval';
this.description = 'Evaluates native JS code';
this.aliases = ['e'];
this.permissions = 7;
this.enabled = true;
this.guildOnly = false;
}
public async run(message: Message, args: string[]) {
try {
const evalMessage = message.content.slice(this.client.config.prefix.length).trim().split(' ').slice(1);
let evalString = evalMessage.join(' ').trim();
let evaled: any;
let depth = 0;
if (args[0] && args[0].startsWith('-d')) {
depth = Number(args[0].replace('-d', ''));
if (!depth || depth < 0) depth = 0;
const index = evalMessage.findIndex((v) => v.startsWith('-d')) + 1;
evalString = evalMessage.slice(index).join(' ').trim();
}
if (args[0] === '-a') {
const index = evalMessage.findIndex((v) => v === '-a') + 1;
evalString = `(async () => { ${evalMessage.slice(index).join(' ').trim()} })()`;
}
try {
// eslint-disable-next-line no-eval
evaled = await eval(evalString);
if (typeof evaled !== 'string') {
evaled = inspect(evaled, { depth });
}
if (evaled === undefined) {
evaled = 'undefined';
}
} catch (error) {
evaled = error.stack;
}
evaled = evaled.replace(new RegExp(this.client.config.token, 'gi'), 'juul');
// evaled = evaled.replace(new RegExp(this.client.config.emailPass, 'gi'), 'juul');
// evaled = evaled.replace(new RegExp(this.client.config.cloudflare, 'gi'), 'juul');
const display = this.client.util.splitString(evaled, 1975);
if (display[5]) {
try {
const { data } = await axios.post('https://snippets.cloud.libraryofcode.org/documents', display.join(''));
return this.success(message.channel, `Your evaluation evaled can be found on https://snippets.cloud.libraryofcode.org/${data.key}`);
} catch (error) {
return this.error(message.channel, `${error}`);
}
}
return display.forEach((m) => message.channel.createMessage(`\`\`\`js\n${m}\n\`\`\``));
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

64
src/commands/game.ts Normal file
View File

@ -0,0 +1,64 @@
/* eslint-disable prefer-destructuring */
import { Activity, Member, Message } from 'eris';
import { Client, Command, RichEmbed } from '../class';
enum ActivityType {
PLAYING,
STREAMING,
LISTENING,
WATCHING,
CUSTOM_STATUS
}
export default class Game extends Command {
constructor(client: Client) {
super(client);
this.name = 'game';
this.description = 'Displays information about the member\'s game.';
this.usage = 'game [member]';
this.permissions = 0;
this.aliases = ['activity'];
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
let member: Member;
if (!args[0]) member = message.member;
else {
member = this.client.util.resolveMember(args.join(' '), this.mainGuild);
if (!member) {
return this.error(message.channel, 'Member not found.');
}
}
if (!member.activities || member.activities.length <= 0) return this.error(message.channel, 'Cannot find a game for this member.');
const embed = new RichEmbed();
let mainStatus: Activity;
if (member.activities[0]?.type === ActivityType.CUSTOM_STATUS) {
mainStatus = member.activities[1];
embed.setDescription(`*${member.activities[0].state}*`);
} else {
mainStatus = member.activities[0];
}
embed.setAuthor(member.user.username, member.user.avatarURL);
if (mainStatus?.type === ActivityType.LISTENING) {
embed.setTitle('Spotify');
embed.setColor('#1ed760');
embed.addField('Song', mainStatus.details, true);
embed.addField('Artist', mainStatus.state, true);
embed.addField('Album', mainStatus.assets.large_text);
embed.addField('Start', `${new Date(mainStatus.timestamps.start).toLocaleTimeString('en-us')} ET`, true);
embed.addField('End', `${new Date(mainStatus.timestamps.end).toLocaleTimeString('en-us')} ET`, true);
embed.setThumbnail(`https://i.scdn.co/image/${mainStatus.assets.large_image.split(':')[1]}`);
embed.setFooter(`Listening to Spotify | ${this.client.user.username}`, 'https://media.discordapp.net/attachments/358674161566220288/496894273304920064/2000px-Spotify_logo_without_text.png');
embed.setTimestamp();
} else {
return this.error(message.channel, 'Only Spotify games are supported at this time.');
}
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

124
src/commands/help.ts Normal file
View File

@ -0,0 +1,124 @@
import { createPaginationEmbed } from 'eris-pagination';
import { Message } from 'eris';
import { Client, Command, RichEmbed } from '../class';
export default class Help extends Command {
constructor(client: Client) {
super(client);
this.name = 'help';
this.description = 'Information about commands.';
this.usage = 'help [command]';
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (args.length > 0) {
const resolved = await this.client.util.resolveCommand(args, message);
if (!resolved) return this.error(message.channel, 'The command you provided doesn\'t exist.');
const { cmd } = resolved;
const embed = new RichEmbed();
const subcommands = cmd.subcommands.size ? `\n**Subcommands:** ${cmd.subcommands.map((s) => `${cmd.name} ${s.name}`).join(', ')}` : '';
embed.setTitle(`${this.client.config.prefix}${cmd.name}`);
embed.addField('Description', cmd.description ?? '-');
embed.addField('Usage', cmd.usage ?? '-');
if (subcommands) embed.addField('Sub-commands', subcommands);
if (cmd.aliases.length > 0) {
embed.addField('Aliases', cmd.aliases.map((alias) => `${this.client.config.prefix}${alias}`).join(', '));
}
let description: string = '';
if (!cmd.enabled) {
description += 'This command is disabled.';
}
if (cmd.guildOnly) {
description += 'This command can only be ran in a guild.';
}
embed.setDescription(description);
switch (cmd.permissions) {
case 0:
break;
case 1:
embed.addField('Permissions', 'Associates+');
break;
case 2:
embed.addField('Permissions', 'Core Team+');
break;
case 3:
embed.addField('Permissions', 'Moderators, Supervisor, & Board of Directors');
break;
case 4:
embed.addField('Permissions', 'Technicians, Supervisor, & Board of Directors');
break;
case 5:
embed.addField('Permissions', 'Moderators, Technicians, Supervisor, & Board of Directors');
break;
case 6:
embed.addField('Permissions', 'Supervisor+');
break;
case 7:
embed.addField('Permissions', 'Board of Directors');
break;
default:
break;
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
}
const cmdList: Command[] = [];
this.client.commands.forEach((c) => {
if (c.permissions !== 0 && c.guildOnly) {
const check = c.checkCustomPermissions(message.member, c.permissions);
if (!check) return;
}
cmdList.push(c);
});
const commands = this.client.commands.map((c) => {
const aliases = c.aliases.map((alias) => `${this.client.config.prefix}${alias}`).join(', ');
let perm: string;
switch (c.permissions) {
case 0:
break;
case 1:
perm = 'Associates+';
break;
case 2:
perm = 'Core Team+';
break;
case 3:
perm = 'Moderators, Supervisor, & Board of Directors';
break;
case 4:
perm = 'Technicians, Supervisor, & Board of Directors';
break;
case 5:
perm = 'Moderators, Technicians, Supervisor, & Board of Directors';
break;
case 6:
perm = 'Supervisor+';
break;
case 7:
perm = 'Board of Directors';
break;
default:
break;
}
return { name: `${this.client.config.prefix}${c.name}`, value: `**Description:** ${c.description}\n**Aliases:** ${aliases}\n**Usage:** ${c.usage}\n**Permissions:** ${perm ?? ''}`, inline: false };
});
const splitCommands = this.client.util.splitFields(commands);
const cmdPages: RichEmbed[] = [];
splitCommands.forEach((splitCmd) => {
const embed = new RichEmbed();
embed.setTimestamp(); embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setDescription(`Command list for ${this.client.user.username}`);
splitCmd.forEach((c) => embed.addField(c.name, c.value, c.inline));
return cmdPages.push(embed);
});
if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] });
return createPaginationEmbed(message, cmdPages);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

48
src/commands/index.ts Normal file
View File

@ -0,0 +1,48 @@
export { default as additem } from './additem';
export { default as addmerchant } from './addmerchant';
export { default as addnote } from './addnote';
export { default as addrank } from './addrank';
export { default as addredirect } from './addredirect';
export { default as apply } from './apply';
export { default as ban } from './ban';
export { default as billing } from './billing';
export { default as callback } from './callback';
export { default as delitem } from './delitem';
export { default as delmerchant } from './delmerchant';
export { default as delnote } from './delnote';
export { default as delrank } from './delrank';
export { default as delredirect } from './delredirect';
export { default as djs } from './djs';
export { default as eris } from './eris';
export { default as eval } from './eval';
export { default as game } from './game';
export { default as help } from './help';
export { default as info } from './info';
export { default as intercom } from './intercom';
export { default as kick } from './kick';
export { default as listredirects } from './listredirects';
export { default as market } from './market';
export { default as members } from './members';
export { default as mute } from './mute';
export { default as notes } from './notes';
export { default as npm } from './npm';
export { default as offer } from './offer';
export { default as page } from './page';
export { default as ping } from './ping';
export { default as profile } from './profile';
export { default as pulldata } from './pulldata';
export { default as rank } from './rank';
export { default as role } from './role';
export { default as roleinfo } from './roleinfo';
export { default as score } from './score';
export { default as sip } from './sip';
export { default as site } from './site';
export { default as slowmode } from './slowmode';
export { default as stats } from './stats';
export { default as storemessages } from './storemessages';
export { default as sysinfo } from './sysinfo';
export { default as train } from './train';
export { default as tts } from './tts';
export { default as unban } from './unban';
export { default as unmute } from './unmute';
export { default as whois } from './whois';

37
src/commands/info.ts Normal file
View File

@ -0,0 +1,37 @@
import { Message } from 'eris';
import { totalmem } from 'os';
import { Client, Command, RichEmbed } from '../class';
import { version as tsVersion } from '../../node_modules/typescript/package.json';
export default class Info extends Command {
constructor(client: Client) {
super(client);
this.name = 'info';
this.description = 'Information about this application.';
this.usage = 'info';
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message) {
try {
const embed = new RichEmbed();
embed.setTitle('Information');
embed.setThumbnail(this.client.user.avatarURL);
embed.setDescription(`*See \`${this.client.config.prefix}sysinfo\` for more information on libraries used by this application.*`);
embed.addField('Version', 'Rolling Release', true);
embed.addField('Language(s)', '<:TypeScript:703451285789343774> TypeScript', true);
embed.addField('Runtime', `Node (${process.version})`, true);
embed.addField('Compilers/Transpilers', `TypeScript [tsc] (${tsVersion})`, true);
embed.addField('Memory Usage', `${Math.round(process.memoryUsage().rss / 1024 / 1024)} MB / ${Math.round(totalmem() / 1024 / 1024 / 1024)} GB`, true);
embed.addField('Repository', 'https://loc.sh/crgit | Licensed under GNU Affero General Public License V3', true);
embed.addField('Branch', await this.client.util.exec('git rev-parse --abbrev-ref HEAD'), true);
embed.addField('Commit', `[${await this.client.util.exec('git rev-parse --short HEAD')}](${await this.client.util.exec('git rev-parse HEAD')})`, true);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
message.channel.createMessage({ embed });
} catch (err) {
this.client.util.handleError(err, message, this);
}
}
}

45
src/commands/intercom.ts Normal file
View File

@ -0,0 +1,45 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
import { Misc as MiscPBXActions } from '../pbx';
export default class Intercom extends Command {
constructor(client: Client) {
super(client);
this.name = 'intercom';
this.description = 'Will synthesize inputted text to a recording and dial an intercom to the extension specified, then play the recording.';
this.usage = `${this.client.config.prefix}intercom <extension> <text>`;
this.permissions = 1;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const loading = await this.loading(message.channel, 'Synthesizing text...');
const recordingLocation = await MiscPBXActions.TTS(this.client.util.pbx, `Hello, this is the Library of Code Private Branch Exchange dialing you at the request of ${message.author.username} to deliver you a message. Playing message: ${args.slice(1).join(' ')}`, 'MALE');
await loading.edit(`***${this.client.util.emojis.LOADING} Preparing to dial...***`);
this.client.util.pbx.ami.action({
action: 'originate',
channel: `PJSIP/${args[0]}`,
exten: args[0],
context: 'from-internal',
CallerID: `TTS INTC FRM ${message.author.username} <000>`,
application: 'PlayBack',
priority: '1',
data: `beep&${recordingLocation.split(':')[1]}`,
variable: {
'PJSIP_HEADER(add,Call-Info)': '<uri>;answer-after=0',
'PJSIP_HEADER(add,Alert-Info)': 'Ring Answer',
},
}, async (err: Error) => {
if (err) return loading.edit(`***${this.client.util.emojis.ERROR} Failed to dial extension.***`);
return loading.edit(`***${this.client.util.emojis.SUCCESS} Successfully queued intercom message to EXT \`${args[0]}\`.***`);
});
return undefined;
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

37
src/commands/kick.ts Normal file
View File

@ -0,0 +1,37 @@
import { Member, Message } from 'eris';
import { Client, Command } from '../class';
export default class Kick extends Command {
constructor(client: Client) {
super(client);
this.name = 'kick';
this.description = 'Kicks a member from the guild.';
this.usage = 'kick <member> [reason]';
this.permissions = 3;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
let user: Member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!user) {
try {
user = await this.client.getRESTGuildMember(this.mainGuild.id, args[0]);
} catch {
return this.error(message.channel, 'Cannot find user.');
}
}
if (user && !this.client.util.moderation.checkPermissions(user, message.member)) return this.error(message.channel, 'Permission Denied.');
message.delete();
const reason: string = args.slice(1).join(' ');
if (reason.length > 512) return this.error(message.channel, 'Kick reasons cannot be longer than 512 characters.');
await this.client.util.moderation.kick(user, message.member, reason);
return this.success(message.channel, `${user.username}#${user.discriminator} has been kicked.`);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

View File

@ -0,0 +1,52 @@
import { Message } from 'eris';
import { createPaginationEmbed } from 'eris-pagination';
import { Client, Command, RichEmbed } from '../class';
export default class DelRedirect extends Command {
constructor(client: Client) {
super(client);
this.name = 'listredirects';
this.description = 'Delete a redirect link for \'loc.sh\'';
this.usage = 'listredirects [key || redirect to]';
this.aliases = ['getredirect', 'lsredirects', 'listlinks', 'lsr', 'gr'];
this.permissions = 6;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (args[0]) {
const redirects = await this.client.db.Redirect.find({ $or: [{ key: args[0].toLowerCase() }, { to: args[0].toLowerCase() }] });
if (redirects.length <= 0) return this.error(message.channel, 'Could not find an entry matching that query.');
const embed = new RichEmbed();
embed.setTitle('Redirect Information');
for (const redirect of redirects) {
embed.addField(`${redirect.key} | visited ${redirect.visitedCount} times`, redirect.to);
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
}
const redirects = await this.client.db.Redirect.find();
if (!redirects) return this.error(message.channel, 'No redirect links found.');
const redirectArray: [{ name: string, value: string }?] = [];
for (const redirect of redirects) {
redirectArray.push({ name: `${redirect.key} | visited ${redirect.visitedCount} times`, value: redirect.to });
}
const splitRedirects = this.client.util.splitFields(redirectArray);
const cmdPages: RichEmbed[] = [];
splitRedirects.forEach((split) => {
const embed = new RichEmbed();
embed.setTitle('Redirect Information');
embed.setTimestamp();
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
split.forEach((c) => embed.addField(c.name, c.value));
return cmdPages.push(embed);
});
if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] });
return createPaginationEmbed(message, cmdPages);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

59
src/commands/market.ts Normal file
View File

@ -0,0 +1,59 @@
import { Message } from 'eris';
import stockInfo from 'stock-info';
import { Client, Command, RichEmbed } from '../class';
export default class Market extends Command {
constructor(client: Client) {
super(client);
this.name = 'market';
this.description = 'Fetches information from a ticker for a stock or fund.';
this.usage = `${this.client.config.prefix}market <ticker>`;
this.permissions = 0;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
let stock: stockInfo.Stock;
try {
stock = await stockInfo.getSingleStockInfo(args[0]);
} catch (err) {
return this.error(message.channel, `Unable to fetch information for that ticker. | ${err}`);
}
const embed = new RichEmbed();
embed.setTitle(stock.longName ?? stock.symbol);
let type: string;
switch (stock.quoteType) {
case 'EQUITY':
type = 'Common/Preferred Stock';
break;
case 'ETF':
type = 'Exchange Traded Fund (ETF)';
break;
case 'MUTUALFUND':
type = 'Mutual Fund';
break;
default:
type = 'N/A or Unknown';
break;
}
embed.addField('Type', type, true);
embed.addField('Market Cap', `${stock.marketCap ? this.client.util.hrn(stock.marketCap, undefined, undefined) : 'N/A'}`, true);
embed.addField('Day Quote Price', stock.regularMarketPrice ? `$${Number(stock.regularMarketPrice).toFixed(2)}` : 'N/A', true);
embed.addField('Day G/L', stock.regularMarketChange ? `$${Number(stock.regularMarketChange.toFixed(2))}` : 'N/A', true);
embed.addField('Day Range', stock.regularMarketDayRange ?? 'N/A', true);
embed.addField('Bid/Ask', `Bid: $${stock.bid?.toFixed(2) ?? 'N/A'} | Ask: $${stock.ask?.toFixed(2) ?? 'N/A'}`, true);
embed.addField('Forward P/E (Price/Earnings)', `${stock.forwardPE?.toFixed(2) ?? 'N/A'}`, true);
embed.addField('Forward EPS (Earnings Per Share)', `${stock.epsForward?.toFixed(2) ?? 'N/A'}`, true);
embed.addField('Exchange', `${stock.fullExchangeName?.toUpperCase() ?? 'N/A'}`, true);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

85
src/commands/members.ts Normal file
View File

@ -0,0 +1,85 @@
import { Message } from 'eris';
import { createPaginationEmbed } from 'eris-pagination';
import { Client, Command, RichEmbed } from '../class';
export default class Members extends Command {
constructor(client: Client) {
super(client);
this.name = 'members';
this.description = 'Gets a list of members in the server or members in a specific role.';
this.usage = `${this.client.config.prefix}members [role name]`;
this.permissions = 0;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
await this.mainGuild.fetchAllMembers();
if (!args[0]) {
const embed = new RichEmbed();
const membersOnline = this.mainGuild.members.filter((member) => member.status === 'online');
const membersIdle = this.mainGuild.members.filter((member) => member.status === 'idle');
const membersDnd = this.mainGuild.members.filter((member) => member.status === 'dnd');
const membersOffline = this.mainGuild.members.filter((member) => member.status === 'offline' || member.status === undefined);
const membersBots = this.mainGuild.members.filter((member) => member.user.bot === true);
const membersHuman = this.mainGuild.members.filter((member) => member.user.bot === false);
embed.setTitle('Members');
embed.setDescription(`**Total:** ${this.mainGuild.members.size}\n**Humans:** ${membersHuman.length}\n**Bots:** ${membersBots.length}\n\n**<:online:732025023547834369> Online:** ${membersOnline.length}\n**<:idle:732025087896715344> Idle:** ${membersIdle.length}\n**<:dnd:732024861853089933> Do Not Disturb:** ${membersDnd.length}\n**<:offline:732024920518688849> Offline:** ${membersOffline.length}`);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
}
const role = this.client.util.resolveRole(args.join(' '), this.mainGuild);
if (!role) return this.error(message.channel, 'The role you specified doesn\'t exist.');
const statusArray: string[] = [];
const membersOnline: string[] = [];
const membersIdle: string[] = [];
const membersDnd: string[] = [];
const membersOffline: string[] = [];
for (const member of this.mainGuild.members.filter((m) => m.roles.includes(role.id)).sort((a, b) => a.username.localeCompare(b.username))) {
switch (member.status) {
case 'online':
membersOnline.push(`<:online:732025023547834369> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`);
break;
case 'idle':
membersIdle.push(`<:idle:732025087896715344> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`);
break;
case 'dnd':
membersDnd.push(`<:dnd:732024861853089933> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`);
break;
case 'offline':
membersOffline.push(`<:offline:732024920518688849> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`);
break;
case undefined:
membersOffline.push(`<:offline:732024920518688849> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`);
break;
default:
break;
}
}
if (membersOnline.length > 0) statusArray.push(membersOnline.join('\n'));
if (membersIdle.length > 0) statusArray.push(membersIdle.join('\n'));
if (membersDnd.length > 0) statusArray.push(membersDnd.join('\n'));
if (membersOffline.length > 0) statusArray.push(membersOffline.join('\n'));
const statusSplit = this.client.util.splitString(statusArray.join('\n'), 2000);
const cmdPages: RichEmbed[] = [];
statusSplit.forEach((split) => {
const embed = new RichEmbed();
embed.setTitle(`Members in ${role.name}`);
embed.setDescription(`Members in Role: ${membersOnline.length + membersIdle.length + membersDnd.length + membersOffline.length}\n\n${split}`);
embed.setColor(role.color);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return cmdPages.push(embed);
});
if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] });
return createPaginationEmbed(message, cmdPages);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

45
src/commands/mute.ts Normal file
View File

@ -0,0 +1,45 @@
import moment, { unitOfTime } from 'moment';
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Mute extends Command {
constructor(client: Client) {
super(client);
this.name = 'mute';
this.description = 'Mutes a member.';
this.usage = 'mute <member> [time] [reason]';
this.permissions = 2;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) return this.error(message.channel, 'Cannot find user.');
try {
const res1 = await this.client.db.local.muted.get<boolean>(`muted-${member.id}`);
if (res1 || this.mainGuild.members.get(member.id).roles.includes('478373942638149643')) return this.error(message.channel, 'This user is already muted.');
} catch {} // eslint-disable-line no-empty
if (member && !this.client.util.moderation.checkPermissions(member, message.member)) return this.error(message.channel, 'Permission Denied.');
message.delete();
let momentMilliseconds: number;
let reason: string;
if (args.length > 1) {
const lockLength = args[1].match(/[a-z]+|[^a-z]+/gi);
const length = Number(lockLength[0]);
const unit = lockLength[1] as unitOfTime.Base;
momentMilliseconds = moment.duration(length, unit).asMilliseconds();
reason = momentMilliseconds ? args.slice(2).join(' ') : args.slice(1).join(' ');
if (reason.length > 512) return this.error(message.channel, 'Mute reasons cannot be longer than 512 characters.');
}
await this.client.util.moderation.mute(member.user, message.member, momentMilliseconds, reason);
return this.success(message.channel, `${member.user.username}#${member.user.discriminator} has been muted.`);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

91
src/commands/notes.ts Normal file
View File

@ -0,0 +1,91 @@
import { Message, Member, User } from 'eris';
import { createPaginationEmbed } from 'eris-pagination';
import { Command, Client, RichEmbed } from '../class';
export default class Notes extends Command {
constructor(client: Client) {
super(client);
this.name = 'notes';
this.description = 'Pulls up notes for a member, with an optional categorical filter.';
this.usage = `${this.client.config.prefix}notes <member> [filter: comm | cs | edu]`;
this.permissions = 1;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
let member: Member | User = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) {
try {
member = await this.client.getRESTUser(args[0]);
} catch {
member = undefined;
}
}
if (!member) return this.error(message.channel, 'User specified could not be found.');
const notes = await this.client.db.Note.find({ userID: member.id });
if (!notes || notes?.length < 1) return this.error(message.channel, 'No notes exist for this user.');
const noteArray: [{ name: string, value: string, inline: boolean }?] = [];
if (args[1] === 'comm' || args[1] === 'cs' || args[1] === 'edu') {
switch (args[1]) {
case 'comm':
for (const note of notes.sort((a, b) => b.date.getTime() - a.date.getTime()).filter((r) => r.category === 'comm')) {
noteArray.push({
name: `${note._id}${note.category === '' ? '' : `, ${note.category.toUpperCase()}`} | ${note.date.toLocaleString('en-us')} ET | Staff: ${this.client.users.get(note.staffID) ? `${this.client.users.get(note.staffID).username}#${this.client.users.get(note.staffID).discriminator}` : 'N/A'}`,
value: note.text,
inline: true,
});
}
break;
case 'cs':
for (const note of notes.sort((a, b) => b.date.getTime() - a.date.getTime()).filter((r) => r.category === 'cs')) {
noteArray.push({
name: `${note._id}${note.category === '' ? '' : `, ${note.category.toUpperCase()}`} | ${note.date.toLocaleString('en-us')} ET | Staff: ${this.client.users.get(note.staffID) ? `${this.client.users.get(note.staffID).username}#${this.client.users.get(note.staffID).discriminator}` : 'N/A'}`,
value: note.text,
inline: true,
});
}
break;
case 'edu':
for (const note of notes.sort((a, b) => b.date.getTime() - a.date.getTime()).filter((r) => r.category === 'edu')) {
noteArray.push({
name: `${note._id}${note.category === '' ? '' : `, ${note.category.toUpperCase()}`}} | ${note.date.toLocaleString('en-us')} ET | Staff: Staff: ${this.client.users.get(note.staffID) ? `${this.client.users.get(note.staffID).username}#${this.client.users.get(note.staffID).discriminator}` : 'N/A'}`,
value: note.text,
inline: true,
});
}
break;
default:
break;
}
} else {
for (const note of notes.sort((a, b) => b.date.getTime() - a.date.getTime())) {
noteArray.push({
name: `${note._id}${note.category === '' ? '' : `, ${note.category}`} | ${note.date.toLocaleString('en-us')} ET | Staff: ${this.client.users.get(note.staffID) ? `${this.client.users.get(note.staffID).username}#${this.client.users.get(note.staffID).discriminator}` : 'N/A'}`,
value: note.text,
inline: true,
});
}
}
const noteSplit = this.client.util.splitFields(noteArray);
const cmdPages: RichEmbed[] = [];
noteSplit.forEach((split) => {
const embed = new RichEmbed();
embed.setColor('#0000FF');
embed.setAuthor(`${member.username}#${member.discriminator}`, member.avatarURL);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
split.forEach((c) => embed.addField(c.name, c.value, c.inline));
return cmdPages.push(embed);
});
if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] });
return createPaginationEmbed(message, cmdPages);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

65
src/commands/npm.ts Normal file
View File

@ -0,0 +1,65 @@
import { Message } from 'eris';
import axios from 'axios';
import { Client, Command, RichEmbed } from '../class';
export default class NPM extends Command {
constructor(client: Client) {
super(client);
this.name = 'npm';
this.description = 'Get information about npm modules.';
this.usage = 'npm <module name>';
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const res = await axios.get(`https://registry.npmjs.com/${args[0]}`, { validateStatus: (_) => true });
if (res.status === 404) return this.error(message.channel, 'Could not find the library, try something else.');
const { data } = res;
const bugs: string = data.bugs?.url || '';
const description: string = data.description || 'None';
const version: string = data['dist-tags']?.latest || 'Unknown';
const homepage: string = data.homepage || '';
let license: string = 'None';
if (typeof data.license === 'object') {
license = data.license.type;
} else if (typeof data.license === 'string') {
license = data.license;
}
let dependencies: string = 'None';
if (version !== 'Unknown' && data.versions[version].dependencies !== undefined && Object.keys(data.versions[version].dependencies).length > 0) {
dependencies = Object.keys(data.versions[version].dependencies).join(', ');
if (dependencies.length > 1024) dependencies = `${dependencies.substr(0, 1021)}...`;
}
const name: string = data.name || 'None';
const repository: string = bugs.replace('/issues', '') || '';
const creation: string = data?.time.created ? new Date(data.time.created).toLocaleString('en') : 'None';
const modification: string = data?.time.modified ? new Date(data.time.modified).toLocaleString('en') : 'None';
const embed = new RichEmbed();
embed.setColor(0xCC3534);
embed.setTimestamp();
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setAuthor('NPM', 'https://i.imgur.com/ErKf5Y0.png', 'https://www.npmjs.com/');
embed.setDescription(`[NPM](https://www.npmjs.com/package/${args[0]}) | [Homepage](${homepage}) | [Repository](${repository}) | [Bugs](${bugs})`);
embed.addField('Name', name, true);
embed.addField('Latest version', version, true);
embed.addField('License', license, true);
embed.addField('Description', description, false);
embed.addField('Dependencies', dependencies, false);
embed.addField('Creation Date', creation, true);
embed.addField('Modification Date', modification, true);
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

50
src/commands/offer.ts Normal file
View File

@ -0,0 +1,50 @@
/* eslint-disable default-case */
import jwt from 'jsonwebtoken';
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Offer extends Command {
constructor(client: Client) {
super(client);
this.name = 'offer';
this.description = 'Pre-qualifies a member for an offer. Will run a hard-pull automatically on acceptance.';
this.usage = `${this.client.config.prefix}offer <member> <name of offer>:<department/service for hard pull>`;
this.permissions = 4;
this.aliases = ['qualify'];
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) return this.error(message.channel, 'Could not find member.');
const score = await this.client.db.Score.findOne({ userID: member.user.id }).lean().exec();
if (!score) return this.error(message.channel, 'Could not find score report for this user.');
if (score.locked) return this.error(message.channel, 'This user\'s score report is locked.');
const name = args.slice(1).join(' ').split(':')[0];
const dept = args.slice(1).join(' ').split(':')[1];
if (!name || !dept) return this.error(message.channel, 'Invalid arguments.');
const token = jwt.sign({
userID: member.user.id,
staffID: message.author.id,
channelID: message.channel.id,
messageID: message.id,
pin: score.pin.join('-'),
name,
department: dept,
date: new Date(),
}, this.client.config.internalKey, { expiresIn: '12h' });
this.client.getDMChannel(member.user.id).then((chan) => {
chan.createMessage(`__**Offer Pre-Qualified**__\nYou have been pre-approved for an offer! If you wish to accept this offer, please enter the offer code at https://report.libraryofcode.org/. Do not share this code with anyone else. This offer automatically expires in 12 hours. Your report will be Hard Inquiried immediately after accepting this offer.\n\n**Department:** ${dept.toUpperCase()}\n**Offer for:** ${name}\n\n\`${token}\``);
}).catch(() => this.error(message.channel, 'Could not DM member.'));
return this.success(message.channel, `Offer sent.\n\n\`${token}\``);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

240
src/commands/page.ts Normal file
View File

@ -0,0 +1,240 @@
/* eslint-disable no-case-declarations */
/* eslint-disable no-continue */
/* eslint-disable no-await-in-loop */
import { promises as fs } from 'fs';
import { randomBytes } from 'crypto';
import { Message, TextableChannel } from 'eris';
import { Client, Command, RichEmbed } from '../class';
export default class Page extends Command {
public local: { emergencyNumbers: string[], departmentNumbers: string[], validPagerCodes: string[], codeDict: Map<string, string>, };
constructor(client: Client) {
super(client);
this.name = 'page';
this.description = 'Pages the specified emergency number, department number, or individual number with the specified pager code.';
this.usage = `${this.client.config.prefix}page <pager number> <pager code> [optional message]\n${this.client.config.prefix}page settings <type: email | phone> <options: [email: on/off | phone: on/off]>`;
this.aliases = ['p'];
this.permissions = 1;
this.enabled = true;
this.guildOnly = true;
this.local = {
emergencyNumbers: ['#0', '#1', '#2', '#3'],
departmentNumbers: ['00', '01', '10', '20', '21', '22'],
validPagerCodes: ['911', '811', '210', '265', '411', '419', '555', '556', '557', '558'],
codeDict: new Map(),
};
this.init();
}
public init() {
this.local.codeDict.set('911', 'Sender is requesting EMERGENCY assistance.');
this.local.codeDict.set('811', 'Sender is requesting immediate/ASAP assistance.');
this.local.codeDict.set('210', 'Sender is informing you they acknowledged your request, usually sent in response to OK the initial page. (10-4)');
this.local.codeDict.set('265', 'Sender is requesting that you check your email.');
this.local.codeDict.set('411', 'Sender is requesting information/counsel from you.');
this.local.codeDict.set('419', 'Sender didn\'t recognize your request.');
this.local.codeDict.set('555', 'Sender is requesting that you contact them.');
this.local.codeDict.set('556', 'Sender is requesting that you contact them via DMs.');
this.local.codeDict.set('557', 'Sender is requesting that you contact them via PBX/their extension.');
this.local.codeDict.set('558', 'Sender is requesting if they are able to call you via PBX. If so, please send the sender back a 210 page.');
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) {
this.client.commands.get('help').run(message, [this.name]);
const embed = new RichEmbed();
embed.setTitle('Special Emergency/Department Numbers & Pager Codes');
embed.addField('Special Emergency Numbers', '`#0` | Broadcast - all Staff/Associates\n`#1` | Authoritative Broadcast - all Directors, Supervisors, Technicians, and Moderators\n`#2` | Systems Administrators/Technicians Broadcast - Matthew, Bsian, NightRaven, and all Technicians\n`#3` | Community/Moderation Team Broadcast - all Directors, Supervisors, Moderators, and Core Team');
embed.addField('Department Numbers', '`00` | Board of Directors\n`01` | Supervisors\n`10` | Technicians\n`20` | Moderators\n`21` | Core Team\n`22` | Associates');
embed.addField('Pager Codes', '"Pager" term in this field refers to the Staff member that initially paged. This is a list of valid codes you can send via a page.\n\n`911` - Pager is requesting EMERGENCY assistance\n`811` - Pager is requesting immediate/ASAP assistance\n`210` - Pager is informing you they acknowledged your request, usually sent in response to OK the initial page.\n`265` - Pager is requesting that you check your email\n`411` - Pager is requesting information/counsel from you\n`419` - Pager didn\'t recognize your request\n`555` - Pager is requesting that you contact them\n`556` - Pager is requesting that you contact them via DMs\n`557` - Pager is requesting that you contact them via PBX/their extension.\n`558` - Pager is requesting if they are able to call you via PBX. If so, please send the pager back a 210 page.');
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
}
if (args[0] === 'settings') {
const pager = await this.client.db.PagerNumber.findOne({ individualAssignID: message.author.id });
if (!pager) return this.error(message.channel, 'You do not have a Pager Number.');
switch (args[1]) {
case 'email':
if (args[2] === 'off') {
if (pager.receiveEmail === false) return this.error(message.channel, 'You are already set to not receive email notifications.');
await pager.updateOne({ $set: { receiveEmail: false } });
return this.success(message.channel, 'You will no longer receive notifications by email for pages.');
}
if (args[2] === 'on') {
if (pager.receiveEmail === true) return this.error(message.channel, 'You are already set to receive email notifications.');
await pager.updateOne({ $set: { receiveEmail: true } });
return this.success(message.channel, 'You will now receive notifications by email for pages.');
}
break;
case 'phone':
if (args[2] === 'off') {
if (pager.receivePhone === false) return this.error(message.channel, 'You are already set to not receive PBX calls.');
await pager.updateOne({ $set: { receivePhone: false } });
return this.success(message.channel, 'You will no longer receive PBX calls for pages.');
}
if (args[2] === 'on') {
if (pager.receivePhone === true) return this.error(message.channel, 'You are already set to receive PBX calls.');
await pager.updateOne({ $set: { receivePhone: true } });
return this.success(message.channel, 'You will now receive PBX calls for pages.');
}
break;
default:
this.error(message.channel, 'Invalid response provided.');
break;
}
}
message.delete();
const loading = await this.loading(message.channel, 'Paging...');
const sender = await this.client.db.PagerNumber.findOne({ individualAssignID: message.author.id });
if (!sender) return this.error(message.channel, 'You do not have a Pager Number.');
const page = await this.page(args[0], sender.num, args[1], message, args[2] ? args.slice(2).join(' ') : undefined);
if (page.status === true) {
loading.delete();
return this.success(message.channel, page.message);
}
loading.delete();
return this.error(message.channel, page.message);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
public logPage(sender: { number: string, user?: string }, recipient: { number: string, user?: string }, type: 'discord' | 'email' | 'phone', code: string): void {
const chan = <TextableChannel> this.mainGuild.channels.get('722636436716781619');
chan.createMessage(`***[${type.toUpperCase()}] \`${sender.number} (${sender.user ? sender.user : ''})\` sent a page to \`${recipient.number} (${recipient.user ? recipient.user : ''})\` with code \`${code}\`.***`);
this.client.util.signale.log(`PAGE (${type.toUpperCase()})| TO: ${recipient.number}, FROM: ${sender.number}, CODE: ${code}`);
}
public async page(recipientNumber: string, senderNumber: string, code: string, message: Message, txt?: string, options?: { emergencyNumber: string }): Promise<{status: boolean, message: string}> {
try {
if (txt?.length >= 140) return { status: false, message: 'Your message must be less than 141 characters.' };
const senderEntry = await this.client.db.PagerNumber.findOne({ num: senderNumber });
if (!senderEntry) {
return { status: false, message: 'You do not have a Pager Number.' };
}
if (this.local.emergencyNumbers.includes(recipientNumber)) {
switch (recipientNumber) {
case '#0':
this.local.departmentNumbers.forEach(async (num) => {
await this.page(num, senderNumber, code, message, txt, { emergencyNumber: '0' });
});
break;
case '#1':
await this.page('00', senderNumber, code, message, txt, { emergencyNumber: '1' });
await this.page('01', senderNumber, code, message, txt, { emergencyNumber: '1' });
await this.page('10', senderNumber, code, message, txt, { emergencyNumber: '1' });
await this.page('20', senderNumber, code, message, txt, { emergencyNumber: '1' });
break;
case '#2':
const matthew = await this.client.db.PagerNumber.findOne({ individualAssignID: '278620217221971968' });
const bsian = await this.client.db.PagerNumber.findOne({ individualAssignID: '253600545972027394' });
const nightraven = await this.client.db.PagerNumber.findOne({ individualAssignID: '239261547959025665' });
await this.page(matthew?.num, senderNumber, code, message, txt, { emergencyNumber: '2' });
await this.page(bsian?.num, senderNumber, code, message, txt, { emergencyNumber: '2' });
await this.page(nightraven?.num, senderNumber, code, message, txt, { emergencyNumber: '2' });
await this.page('10', senderNumber, code, message, txt, { emergencyNumber: '2' });
break;
case '#3':
await this.page('00', senderNumber, code, message, txt, { emergencyNumber: '3' });
await this.page('01', senderNumber, code, message, txt, { emergencyNumber: '3' });
await this.page('20', senderNumber, code, message, txt, { emergencyNumber: '3' });
await this.page('21', senderNumber, code, message, txt, { emergencyNumber: '3' });
break;
default:
break;
}
return { status: true, message: `Page to \`${recipientNumber}\` sent.` };
}
const recipientEntry = await this.client.db.PagerNumber.findOne({ num: recipientNumber });
if (!recipientEntry) {
return { status: false, message: `Pager Number \`${recipientNumber}\` does not exist.` };
}
if (!this.local.validPagerCodes.includes(code)) {
return { status: false, message: 'The Pager Code you provided is invalid.' };
}
for (const id of recipientEntry.discordIDs) {
const recipient = this.mainGuild.members.get(recipientEntry.individualAssignID);
const sender = this.mainGuild.members.get(senderEntry.individualAssignID);
const chan = await this.client.getDMChannel(id);
if (!chan) continue;
if (!recipient || !sender) {
this.logPage({ number: senderNumber, user: 'N/A' }, { number: recipientNumber, user: 'N/A' }, 'discord', code);
} else {
this.logPage({ number: senderNumber, user: `${sender.username}#${sender.discriminator}` }, { number: recipientNumber, user: `${recipient.username}#${recipient.discriminator}` }, 'discord', code);
}
chan.createMessage(`${options?.emergencyNumber ? `[SEN#${options.emergencyNumber}] ` : ''}__**Page**__\n**Recipient PN:** ${recipientNumber}\n**Sender PN:** ${senderNumber} (${sender ? `${sender.username}#${sender.discriminator}` : ''})\n**Initial Command:** https://discordapp.com/channels/${this.mainGuild.id}/${message.channel.id}/${message.id} (<#${message.channel.id}>)\n\n**Pager Code:** ${code} (${this.local.codeDict.get(code)})${txt ? `\n**Message:** ${txt}` : ''}`);
}
for (const email of recipientEntry.emailAddresses) {
const recipient = this.mainGuild.members.get(recipientEntry.individualAssignID);
const sender = this.mainGuild.members.get(senderEntry.individualAssignID);
if (recipientEntry.receiveEmail === false) continue;
if (!recipient || !sender) {
this.logPage({ number: senderNumber, user: 'N/A' }, { number: recipientNumber, user: 'N/A' }, 'email', code);
} else {
this.logPage({ number: senderNumber, user: `${sender.username}#${sender.discriminator}` }, { number: recipientNumber, user: `${recipient.username}#${recipient.discriminator}` }, 'email', code);
}
await this.client.util.transporter.sendMail({
from: '"LOC Paging System" <internal@libraryofcode.org>',
to: email,
subject: `PAGE FROM ${senderNumber}`,
html: `<h1>Page</h1>${options?.emergencyNumber ? `<h2>[SEN#${options.emergencyNumber}]` : ''}<strong>Recipient PN:</strong> ${recipientNumber}<br><strong>Sender PN:</strong> ${senderNumber} (${sender ? `${sender.username}#${sender.discriminator}` : ''})<br><strong>Initial Command:</strong> https://discordapp.com/channels/${this.mainGuild.id}/${message.channel.id}/${message.id} (<#${message.channel.id}>)<br><br><strong>Pager Code:</strong> ${code} (${this.local.codeDict.get(code)})${txt ? `<br><strong>Message:</strong> ${txt}` : ''}`,
});
}
for (const id of recipientEntry.discordIDs) {
const pager = await this.client.db.PagerNumber.findOne({ individualAssignID: id });
if (!pager || !pager.receivePhone) continue;
const member = await this.client.db.Staff.findOne({ userID: pager.individualAssignID });
if (!member || !member.extension) continue;
const fileExtension = `${randomBytes(10).toString('hex')}`;
const [response] = await this.client.util.pbx.tts.synthesizeSpeech({
input: { text: `Hello, this call was automatically dialed by the Library of Code Private Branch Exchange. You have received a page from Pager Number, ${senderNumber}. The Pager Code is ${code}. Please check your Direct Messages on Discord for further information.` },
voice: { languageCode: 'en-US', ssmlGender: 'MALE' },
audioConfig: { audioEncoding: 'OGG_OPUS' },
});
await fs.writeFile(`/tmp/${fileExtension}.ogg`, response.audioContent, 'binary');
await this.client.util.exec(`ffmpeg -i /tmp/${fileExtension}.ogg -af "highpass=f=300, lowpass=f=3400" -ar 8000 -ac 1 -ab 64k -f mulaw /tmp/${fileExtension}.ulaw`);
try {
const chan = await this.client.util.pbx.ari.channels.originate({
endpoint: `PJSIP/${member.extension}`,
extension: `${member.extension}`,
callerId: `PAGE FRM ${senderNumber} <000>`,
context: 'from-internal',
priority: 1,
app: 'cr-zero',
});
chan.on('StasisStart', async (_event, channel) => {
const playback = await channel.play({
media: `sound:/tmp/${fileExtension}`,
}, undefined);
playback.on('PlaybackFinished', () => {
channel.hangup();
});
});
const recipient = this.mainGuild.members.get(recipientEntry.individualAssignID);
const sender = this.mainGuild.members.get(senderEntry.individualAssignID);
if (!recipient || !sender) {
this.logPage({ number: senderNumber, user: 'N/A' }, { number: recipientNumber, user: 'N/A' }, 'phone', code);
} else {
this.logPage({ number: senderNumber, user: `${sender.username}#${sender.discriminator}` }, { number: recipientNumber, user: `${recipient.username}#${recipient.discriminator}` }, 'phone', code);
}
} catch (err) {
this.client.util.signale.log(`Unable to Dial ${member.extension} | ${err}`);
}
}
this.client.db.Stat.updateOne({ name: 'pages' }, { $inc: { value: 1 } }).exec();
return { status: true, message: `Page to \`${recipientNumber}\` sent.` };
} catch (err) {
this.client.util.signale.error(err);
return { status: false, message: `Error during Processing: ${err}` };
}
}
}

23
src/commands/ping.ts Normal file
View File

@ -0,0 +1,23 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Ping extends Command {
constructor(client: Client) {
super(client);
this.name = 'ping';
this.description = 'Pings the bot';
this.usage = 'ping';
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message) {
try {
const clientStart: number = Date.now();
const msg: Message = await message.channel.createMessage('🏓 Pong!');
msg.edit(`🏓 Pong!\nClient: \`${Date.now() - clientStart}ms\`\nResponse: \`${msg.createdAt - message.createdAt}ms\``);
} catch (err) {
this.client.util.handleError(err, message, this);
}
}
}

26
src/commands/profile.ts Normal file
View File

@ -0,0 +1,26 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
import Profile_Bio from './profile_bio';
import Profile_GitHub from './profile_github';
import Profile_Gitlab from './profile_gitlab';
export default class Profile extends Command {
constructor(client: Client) {
super(client);
this.name = 'profile';
this.description = 'Manages your profile on CR.';
this.usage = 'profile <bio/github/gitlab> <new value>\n*Provide no value in subcommand to clear data.*';
this.permissions = 0;
this.enabled = true;
this.subcmds = [Profile_Bio, Profile_GitHub, Profile_Gitlab];
}
public async run(message: Message) {
if (!await this.client.db.Member.exists({ userID: message.author.id })) {
await this.client.db.Member.create({ userID: message.author.id });
}
this.error(message.channel, `Please specify a valid option to change. Choose from \`github\`, \`bio\` and \`gitlab\`. You can view your profile with \`${this.client.config.prefix}whois\`.`);
}
}

View File

@ -0,0 +1,39 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Profile_Bio extends Command {
constructor(client: Client) {
super(client);
this.name = 'bio';
this.description = 'Updates your bio on your profile.';
this.usage = `${this.client.config.prefix}bio <new bio>`;
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
if (!await this.client.db.Member.exists({ userID: message.author.id })) {
await this.client.db.Member.create({ userID: message.author.id });
}
const member = await this.client.db.Member.findOne({ userID: message.author.id });
if (!args[0]) {
await member.updateOne({
additional: {
...member.additional,
bio: null,
},
});
return message.addReaction('modSuccess:578750988907970567');
}
const bio = args.join(' ');
if (bio.length >= 256) return this.error(message.channel, 'Bio too long. It must be less than or equal to 256 characters.');
await member.updateOne({
additional: {
...member.additional,
bio,
},
});
return message.addReaction('modSuccess:578750988907970567');
}
}

View File

@ -0,0 +1,47 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Profile_GitHub extends Command {
constructor(client: Client) {
super(client);
this.name = 'github';
this.description = 'Updates your GitHub information on your profile.';
this.usage = `${this.client.config.prefix}github <GitHub profile URL>`;
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
if (!await this.client.db.Member.exists({ userID: message.author.id })) {
await this.client.db.Member.create({ userID: message.author.id });
}
const member = await this.client.db.Member.findOne({ userID: message.author.id });
if (!args[0]) {
await member.updateOne({
additional: {
...member.additional,
github: null,
},
});
return message.addReaction('modSuccess:578750988907970567');
}
const urlRegex = new RegExp(
'^(https?:\\/\\/)?'
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'
+ '((\\d{1,3}\\.){3}\\d{1,3}))'
+ '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'
+ '(\\?[;&a-z\\d%_.~+=-]*)?'
+ '(\\#[-a-z\\d_]*)?$',
'i',
);
if (!urlRegex.test(args[0]) || !args[0].startsWith('https://github.com/')) return this.error(message.channel, 'Invalid GitHub profile URL.');
await member.updateOne({
additional: {
...member.additional,
github: args[0],
},
});
return message.addReaction('modSuccess:578750988907970567');
}
}

View File

@ -0,0 +1,48 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Profile_GitLab extends Command {
constructor(client: Client) {
super(client);
this.name = 'gitlab';
this.description = 'Updates your GitLab information on your profile.';
this.usage = `${this.client.config.prefix}gitlab <GitLab profile URL>`;
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
if (!await this.client.db.Member.exists({ userID: message.author.id })) {
await this.client.db.Member.create({ userID: message.author.id });
}
if (!args[0]) return this.error(message.channel, 'No GitLab profile URL was provided.');
const member = await this.client.db.Member.findOne({ userID: message.author.id });
if (!args[0]) {
await member.updateOne({
additional: {
...member.additional,
gitlab: null,
},
});
return message.addReaction('modSuccess:578750988907970567');
}
const urlRegex = new RegExp(
'^(https?:\\/\\/)?'
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'
+ '((\\d{1,3}\\.){3}\\d{1,3}))'
+ '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'
+ '(\\?[;&a-z\\d%_.~+=-]*)?'
+ '(\\#[-a-z\\d_]*)?$',
'i',
);
if (!urlRegex.test(args[0])) return this.error(message.channel, 'Invalid GitLab profile URL.');
await member.updateOne({
additional: {
...member.additional,
gitlab: args[0],
},
});
return message.addReaction('modSuccess:578750988907970567');
}
}

97
src/commands/pulldata.ts Normal file
View File

@ -0,0 +1,97 @@
/* eslint-disable no-continue */
/* eslint-disable default-case */
import { Message, TextChannel } from 'eris';
import { Client, Command, RichEmbed } from '../class';
import { getTotalMessageCount } from '../intervals/score';
export default class Score extends Command {
constructor(client: Client) {
super(client);
this.name = 'pulldata';
this.description = 'Retrieves information about a hard inquiry that was performed on a member.';
this.usage = `${this.client.config.prefix}pulldata <userID> <reportID>`;
this.aliases = ['inq'];
this.permissions = 5;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[1]) return this.client.commands.get('help').run(message, [this.name]);
const member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) return this.error(message.channel, 'Could not locate member.');
const score = await this.client.db.Score.findOne({ userID: member.id });
if (!score) return this.error(message.channel, 'Could not find Community Report for this user.');
const report = score.inquiries.find((inq) => inq.id === args[1]);
if (!report) return this.error(message.channel, 'Could not find inquiry information.');
await this.client.report.createInquiry(member.id, 'Library of Code sp-us | Bureau of Community Reports', 1);
const embed = new RichEmbed();
embed.setTitle(`Hard Inquiry Information - ${report.id}`);
embed.setAuthor(member.username, member.user.avatarURL);
let currentScore = '0';
if (score.total < 200) currentScore = '---';
else if (score.total > 800) currentScore = '800';
else currentScore = `${score.total}`;
embed.setDescription(`**Current Community Score:** ${currentScore}\n\n**Department/Service:** ${report.name || 'N/A'}\n**Reason:** ${report.reason || 'N/A'}`);
let totalScore = '0';
let activityScore = '0';
let moderationScore = '0';
let roleScore = '0';
let cloudServicesScore = '0';
let otherScore = '0';
let miscScore = '0';
if (score.total < 200) totalScore = '---';
else if (score.total > 800) totalScore = '800';
else totalScore = `${score.total}`;
if (score.activity < 10) activityScore = '---';
else if (score.activity > Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12))) activityScore = String(Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12)));
else activityScore = `${score.activity}`;
if (score.roles <= 0) roleScore = '---';
else if (score.roles > 54) roleScore = '54';
else roleScore = `${score.roles}`;
moderationScore = `${score.moderation}`;
if (score.other === 0) otherScore = '---';
else otherScore = `${score.other}`;
if (score.staff <= 0) miscScore = '---';
else miscScore = `${score.staff}`;
if (score.cloudServices === 0) cloudServicesScore = '---';
else if (score.cloudServices > 10) cloudServicesScore = '10';
else cloudServicesScore = `${score.cloudServices}`;
let color = '🔴';
let additionalText = 'POOR';
embed.setColor('FF0000');
if (score.total >= 550) { color = '🟠'; additionalText = 'FAIR'; embed.setColor('FFA500'); }
if (score.total >= 630) { color = '🟡'; additionalText = 'GOOD'; embed.setColor('FFFF00'); }
if (score.total >= 700) { color = '🟢'; additionalText = 'EXCELLENT'; embed.setColor('66FF66'); }
if (score.total >= 770) { color = '✨'; additionalText = 'EXCEPTIONAL'; embed.setColor('#99FFFF'); }
embed.addField('Total | 200 to 800', score ? `${color} ${totalScore} | ${additionalText}` : 'N/C', true);
embed.addField(`Activity | 10 to ${Math.floor(Math.log1p(getTotalMessageCount(this.client)) * 12)}`, activityScore || 'N/C', true);
embed.addField('Roles | 1 to N/A', roleScore || 'N/C', true);
embed.addField('Moderation | N/A to 2' || 'N/C', moderationScore, true);
embed.addField('Cloud Services | N/A to 10+', cloudServicesScore || 'N/C', true);
embed.addField('Other', otherScore || 'N/C', true);
embed.addField('Misc', miscScore || 'N/C', true);
if (score.pin?.length > 0) {
embed.addField('PIN', score.pin.join('-'), true);
}
embed.setTimestamp(report.date);
embed.setFooter('Inquiry performed on', this.client.user.avatarURL);
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

73
src/commands/rank.ts Normal file
View File

@ -0,0 +1,73 @@
import { Message, Role } from 'eris';
import { createPaginationEmbed } from 'eris-pagination';
import { Client, Command, RichEmbed } from '../class';
export default class Rank extends Command {
constructor(client: Client) {
super(client);
this.name = 'rank';
this.description = 'Joins/leaves a self-assignable rank. Run this command without arguments to get a list of self-assignable roles.';
this.usage = `${this.client.config.prefix}rank <rank>`;
this.permissions = 0;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) {
const roles = await this.client.db.Rank.find();
const rankArray: [{ name: string, value: string }?] = [];
for (const rank of roles.sort((a, b) => a.name.localeCompare(b.name))) {
let perms: string;
if (rank.permissions.includes('0')) {
perms = 'Everyone';
} else {
const rolesArray: Role[] = [];
rank.permissions.forEach((r) => {
rolesArray.push(this.mainGuild.roles.get(r));
});
perms = rolesArray.map((r) => this.mainGuild.roles.get(r.id)).sort((a, b) => b.position - a.position).map((r) => `<@&${r.id}>`).join(', ');
}
let hasRank = false;
if (message.member.roles.includes(rank.roleID)) hasRank = true;
rankArray.push({ name: rank.name, value: `${hasRank ? '*You have this role.*\n' : ''}__Description:__ ${rank.description}\n__Permissions:__ ${perms}` });
}
const ranksSplit = this.client.util.splitFields(rankArray);
const cmdPages: RichEmbed[] = [];
ranksSplit.forEach((split) => {
const embed = new RichEmbed();
embed.setTitle('Ranks');
embed.setDescription(`Use \`${this.client.config.prefix}rank <rank name>\` to join/leave the rank.`);
embed.setFooter(`Requested by: ${message.author.username}#${message.author.discriminator} | ${this.client.user.username}`, message.author.avatarURL);
embed.setTimestamp();
split.forEach((c) => embed.addField(c.name, c.value));
return cmdPages.push(embed);
});
if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] });
return createPaginationEmbed(message, cmdPages);
}
const role = this.client.util.resolveRole(args.join(' '), this.client.guilds.get(this.client.config.guildID));
if (!role) return this.error(message.channel, 'The role you specified doesn\'t exist.');
const entry = await this.client.db.Rank.findOne({ roleID: role.id }).lean().exec();
if (!entry) return this.error(message.channel, 'The rank you specified doesn\'t exist.');
if (!message.member.roles.includes(entry.roleID)) {
let permCheck: boolean;
if (entry.permissions.includes('0')) {
permCheck = true;
} else {
permCheck = entry.permissions.some((item) => message.member.roles.includes(item));
}
if (!permCheck) return this.error(message.channel, 'Permission denied.');
await message.member.addRole(entry.roleID, 'User self-assigned this role.');
this.success(message.channel, `You have self-assigned ${entry.name}.`);
} else if (message.member.roles.includes(entry.roleID)) {
await message.member.removeRole(entry.roleID, 'User has removed a self-assignable role.');
this.success(message.channel, `You have removed ${entry.name}.`);
}
return null;
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

73
src/commands/role.ts Normal file
View File

@ -0,0 +1,73 @@
import { Message, Role as DRole } from 'eris';
import { Client, Command } from '../class';
export default class Role extends Command {
constructor(client: Client) {
super(client);
this.name = 'role';
this.description = 'Manage the roles of a member.';
this.usage = `${this.client.config.prefix}role <user> <roles>`;
this.permissions = 6;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (args.length < 2) return this.client.commands.get('help').run(message, [this.name]);
const member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) return this.error(message.channel, 'Member not found');
// if (!this.client.util.moderation.checkPermissions(member, message.member)) return this.error(message.channel, 'Permission denied.');
const rolesList = args.slice(1).join(' ').split(', ');
const rolesToAdd = [];
const rolesToRemove = [];
let stop = false;
for (const arg of rolesList) {
const action = arg[0];
let role: DRole;
if (action !== '+' && action !== '-') {
role = this.client.util.resolveRole(arg, this.mainGuild);
if (!role) {
stop = true;
return this.error(message.channel, `Role \`${arg}\` not found.`);
}
if (member.roles.includes(role.id)) return rolesToRemove.push(role);
rolesToAdd.push(role);
continue;
}
if (action === '+') {
role = this.client.util.resolveRole(arg.slice(1), this.mainGuild);
if (!role) {
stop = true;
return this.error(message.channel, `Role \`${arg.slice(1)}\` not found.`);
}
if (member.roles.includes(role.id)) {
stop = true;
return this.error(message.channel, `You already have the role \`${role.name}\`.`);
}
rolesToAdd.push(role);
continue;
}
if (action === '-') {
role = this.client.util.resolveRole(arg.slice(1), this.mainGuild);
if (!role) {
stop = true;
return this.error(message.channel, `Role \`${arg.slice(1)}\` not found.`);
}
if (!member.roles.includes(role.id)) {
stop = true;
return this.error(message.channel, `You don't have the role \`${role.name}\``);
}
rolesToRemove.push(role);
continue;
}
}
// eslint-disable-next-line
// if (stop) return;
rolesToAdd.forEach((role) => member.addRole(role.id));
rolesToRemove.forEach((role) => member.removeRole(role.id));
return this.success(message.channel, `Changed the roles for ${member.username}#${member.discriminator}${rolesToAdd.length > 0 ? `, added \`${rolesToAdd.map((r) => r.name).join('`, `')}\`` : ''}${rolesToRemove.length > 0 ? `, removed \`${rolesToRemove.map((r) => r.name).join('`, `')}\`` : ''}`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

57
src/commands/roleinfo.ts Normal file
View File

@ -0,0 +1,57 @@
import moment from 'moment';
import { Message, Role } from 'eris';
import { Client, Command, RichEmbed } from '../class';
export default class Roleinfo extends Command {
constructor(client: Client) {
super(client);
this.name = 'roleinfo';
this.description = 'Get information about a role.';
this.usage = 'roleinfo [role ID or role name]';
this.permissions = 0;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const role = this.client.util.resolveRole(args.join(' '), this.mainGuild);
if (!role) return this.error(message.channel, 'Could not find role.');
const perms = role.permissions;
const permsArray: string[] = [];
if (perms.has('administrator')) permsArray.push('Administrator');
if (perms.has('manageGuild')) permsArray.push('Manage Server');
if (perms.has('manageChannels')) permsArray.push('Manage Channels');
if (perms.has('manageRoles')) permsArray.push('Manage Roles');
if (perms.has('manageMessages')) permsArray.push('Manage Messages');
if (perms.has('manageNicknames')) permsArray.push('Manage Nicknames');
if (perms.has('manageEmojis')) permsArray.push('Manage Emojis');
if (perms.has('banMembers')) permsArray.push('Ban Members');
if (perms.has('kickMembers')) permsArray.push('Kick Members');
const embed = new RichEmbed();
embed.setTitle('Role Information');
embed.setDescription(`<@&${role.id}> ID: \`${role.id}\``);
embed.setColor(role.color);
embed.addField('Name', role.name, true);
embed.addField('Color', role.color ? this.client.util.decimalToHex(role.color) : 'None', true);
embed.addField('Members', String(this.client.guilds.get(this.client.config.guildID).members.filter((m) => m.roles.includes(role.id)).length), true);
embed.addField('Hoisted', role.hoist ? 'Yes' : 'No', true);
embed.addField('Position', String(role.position), true);
embed.addField('Creation Date', `${moment(new Date(role.createdAt)).format('dddd, MMMM Do YYYY, h:mm:ss A')} ET`, true);
embed.addField('Mentionable', role.mentionable ? 'Yes' : 'No', true);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
if (permsArray.length > 0) {
embed.addField('Permissions', permsArray.join(', '), true);
}
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

183
src/commands/score.ts Normal file
View File

@ -0,0 +1,183 @@
/* eslint-disable no-plusplus */
/* eslint-disable no-continue */
/* eslint-disable default-case */
import moment from 'moment';
import { Message, User } from 'eris';
import { Client, Command, RichEmbed } from '../class';
import { getTotalMessageCount } from '../intervals/score';
import Score_Hist from './score_hist';
import Score_Notify from './score_notify';
import Score_Pref from './score_pref';
export default class Score extends Command {
constructor(client: Client) {
super(client);
this.name = 'score';
this.description = 'Retrieves your Community Report';
this.usage = `${this.client.config.prefix}score\n${this.client.config.prefix}score <member> <type: 'hard' | 'soft'> <reporting department: ex. Library of Code sp-us | Cloud Account Services>:<reason>`;
this.aliases = ['report'];
this.subcmds = [Score_Hist, Score_Notify, Score_Pref];
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
let check = false;
let user: User;
if (!args[0] || !this.checkCustomPermissions(this.client.util.resolveMember(message.author.id, this.mainGuild), 4)) {
user = message.author;
if (!user) return this.error(message.channel, 'Member not found.');
await this.client.report.createInquiry(user.id, `${user.username} via Discord`, 1);
check = true;
} else {
user = this.client.util.resolveMember(args[0], this.mainGuild)?.user;
if (!user) {
const sc = await this.client.db.Score.findOne({ pin: [Number(args[0].split('-')[0]), Number(args[0].split('-')[1]), Number(args[0].split('-')[2])] });
if (!sc) return this.error(message.channel, 'Member not found.');
user = this.client.util.resolveMember(sc.userID, this.mainGuild)?.user;
}
if (!user) return this.error(message.channel, 'Member not found.');
if (message.channel.type !== 0) return this.error(message.channel, 'Hard Inquiries must be initiated in a guild.');
if (args[1] === 'hard') {
if (args.length < 3) return this.client.commands.get('help').run(message, [this.name]);
const name = args.slice(2).join(' ').split(':')[0];
const reason = args.slice(2).join(' ').split(':')[1];
const score = await this.client.db.Score.findOne({ userID: user.id });
if (!score) return this.error(message.channel, 'Score not calculated yet.');
if (score.locked) return this.error(message.channel, 'The score report you have requested has been locked.');
await this.client.report.createInquiry(score.userID, name, 0, reason);
}
}
if (!user) return this.error(message.channel, 'Member not found.');
const score = await this.client.db.Score.findOne({ userID: user.id });
const inqs = await this.client.db.Inquiry.find({ userID: user.id, type: 0 });
if (!score) return this.error(message.channel, 'Community Report has not been generated yet.');
let totalScore = '0';
let activityScore = '0';
let moderationScore = '0';
let roleScore = '0';
let cloudServicesScore = '0';
let otherScore = '0';
let miscScore = '0';
if (score) {
if (score.total < 200) totalScore = '---';
else if (score.total > 800) totalScore = '800';
else totalScore = `${score.total}`;
if (score.activity < 10) activityScore = '---';
else if (score.activity > Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12))) activityScore = String(Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12)));
else activityScore = `${score.activity}`;
if (score.roles <= 0) roleScore = '---';
else if (score.roles > 54) roleScore = '54';
else roleScore = `${score.roles}`;
moderationScore = `${score.moderation}`;
if (score.other === 0) otherScore = '---';
else otherScore = `${score.other}`;
if (score.staff <= 0) miscScore = '---';
else miscScore = `${score.staff}`;
if (score.cloudServices === 0) cloudServicesScore = '---';
else if (score.cloudServices > 10) cloudServicesScore = '10';
else cloudServicesScore = `${score.cloudServices}`;
} // else return this.error(message.channel, 'Community Score has not been calculated yet.');
const set = [];
const accounts = await this.client.db.Score.find().lean().exec();
for (const sc of accounts) {
if (sc.total < 200) { continue; }
if (sc.total > 800) { set.push(800); continue; }
set.push(sc.total);
}
const percentile = this.client.util.percentile(set, score.total);
const embed = new RichEmbed();
embed.setTitle('Community Report');
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
/* for (const role of member.roles.map((r) => this.mainGuild.roles.get(r)).sort((a, b) => b.position - a.position)) {
if (role?.color !== 0) {
embed.setColor(role.color);
break;
}
} */
let inqAdded = 0;
if (inqs?.length > 0) {
let desc = '__**Hard Inquiries**__\n*These inquiries will fall off your report within 2 months, try to keep your hard inquiries to a minimum. If you want to file a dispute, please DM Ramirez.*\n\n';
inqs.forEach((inq) => {
const testDate = (new Date(new Date(inq.date).setHours(1460)));
// eslint-disable-next-line no-useless-escape
if (testDate > new Date()) {
desc += `${inq.iid ? `__[${inq.iid}]__\n` : ''}**Department/Service:** ${inq.name.replace(/\*/gmi, '')}\n**Reason:** ${inq.reason}\n**Date:** ${moment(inq.date).format('MMMM Do YYYY, h:mm:ss')}\n**Expires:** ${moment(testDate).calendar()}\n\n`;
inqAdded++;
}
});
if (inqAdded <= 0) desc = '';
if (desc.length >= 1900) {
embed.setDescription('__***Too many Hard Inquiries to show, please see https://report.libraryofcode.org***__');
} else {
embed.setDescription(desc);
}
}
let color = '🔴';
let additionalText = 'POOR';
embed.setColor('FF0000');
if (score.total >= 550) { color = '🟠'; additionalText = 'FAIR'; embed.setColor('FFA500'); }
if (score.total >= 630) { color = '🟡'; additionalText = 'GOOD'; embed.setColor('FFFF00'); }
if (score.total >= 700) { color = '🟢'; additionalText = 'EXCELLENT'; embed.setColor('66FF66'); }
if (score.total >= 770) { color = '✨'; additionalText = 'EXCEPTIONAL'; embed.setColor('#99FFFF'); }
embed.addField('CommScore™ | 200 to 800', score ? `${color} ${totalScore} | ${additionalText} | ${this.client.util.ordinal(Math.round(percentile))} Percentile` : 'N/C', true);
embed.addField(`Activity | 10 to ${Math.floor(Math.log1p(getTotalMessageCount(this.client)) * 12)}`, activityScore || 'N/C', true);
embed.addField('Roles | 1 to N/A', roleScore || 'N/C', true);
embed.addField('Moderation | N/A to 2' || 'N/C', moderationScore, true);
embed.addField('Cloud Services | N/A to 10+', cloudServicesScore || 'N/C', true);
embed.addField('Other', otherScore || 'N/C', true);
embed.addField('Misc', miscScore || 'N/C', true);
if (score.locked) {
embed.addField('Status', 'Score Report Locked');
}
if (score.pin?.length > 0 && message.channel.type === 1) {
embed.addField('PIN', score.pin.join('-'), true);
}
if (score.lastUpdate) {
embed.setFooter('Report last updated', this.client.user.avatarURL);
embed.setTimestamp(score.lastUpdate);
} else {
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
}
// eslint-disable-next-line no-mixed-operators
if ((args[1] === 'hard' || args[1] === 'soft') && this.checkCustomPermissions(this.client.util.resolveMember(message.author.id, this.mainGuild), 4)) {
if (score.pin?.length > 0) embed.addField('PIN', score.pin.join('-'), true);
if (args[1] === 'soft' && !check) {
let name = '';
for (const role of this.client.util.resolveMember(message.author.id, this.mainGuild).roles.map((r) => this.mainGuild.roles.get(r)).sort((a, b) => b.position - a.position)) {
name = `Library of Code sp-us | ${role.name}`;
break;
}
await this.client.report.createInquiry(user.id, name, 1);
}
}
if (args[1] === 'hard' && this.checkCustomPermissions(this.client.util.resolveMember(message.author.id, this.mainGuild), 6)) {
await message.channel.createMessage({ embed });
await message.channel.createMessage('***===BEGIN ADDITIONAL INFORMATION===***');
await this.client.commands.get('score').subcommands.get('hist').run(message, [user.id]);
await this.client.commands.get('whois').run(message, [user.id]);
await this.client.commands.get('notes').run(message, [user.id]);
const whoisMessage = await message.channel.createMessage(`=whois ${user.id} --full`);
await whoisMessage.delete();
const modlogsMessage = await message.channel.createMessage(`=modlogs ${user.id}`);
await modlogsMessage.delete();
return await message.channel.createMessage('***===END ADDITIONAL INFORMATION===***');
}
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

334
src/commands/score_hist.ts Normal file
View File

@ -0,0 +1,334 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-continue */
/* eslint-disable default-case */
import moment from 'moment';
import { median, mode, mean } from 'mathjs';
import { createPaginationEmbed } from 'eris-pagination';
import { Message, User, TextChannel } from 'eris';
import { Client, Command, RichEmbed } from '../class';
import { getTotalMessageCount } from '../intervals/score';
import { InquiryInterface } from '../models';
export default class Score_Hist extends Command {
constructor(client: Client) {
super(client);
this.name = 'hist';
this.description = 'Pulls your Community Report history.';
this.usage = `${this.client.config.prefix}score hist <member>\n${this.client.config.prefix}score hist`;
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
let user: User;
if (!args[0] || !this.checkCustomPermissions(this.client.util.resolveMember(message.author.id, this.mainGuild), 4)) {
user = message.author;
if (!user) return this.error(message.channel, 'Member not found.');
const hists = await this.client.db.ScoreHistorical.find({ userID: user.id }).lean().exec();
if (!hists) return this.error(message.channel, 'No history found.');
if (hists.length < 1) return this.error(message.channel, 'No history found.');
const histArray: [{ name: string, value: string }?] = [];
const totalArray: number[] = [];
const activityArray: number[] = [];
const moderationArray: number[] = [];
const roleArray: number[] = [];
const cloudServicesArray: number[] = [];
const otherArray: number[] = [];
const miscArray: number[] = [];
for (const hist of hists.reverse()) {
totalArray.push(hist.report.total);
activityArray.push(hist.report.activity);
moderationArray.push(hist.report.moderation);
roleArray.push(hist.report.roles);
cloudServicesArray.push(hist.report.cloudServices);
otherArray.push(hist.report.other);
miscArray.push(hist.report.staff);
let totalScore = '0';
let activityScore = '0';
let moderationScore = '0';
let roleScore = '0';
let cloudServicesScore = '0';
let otherScore = '0';
let miscScore = '0';
if (hist.report.total < 200) totalScore = '---';
else if (hist.report.total > 800) totalScore = '800';
else totalScore = `${hist.report.total}`;
if (hist.report.activity < 10) activityScore = '---';
else if (hist.report.activity > Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12))) activityScore = String(Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12)));
else activityScore = `${hist.report.activity}`;
if (hist.report.roles <= 0) roleScore = '---';
else if (hist.report.roles > 54) roleScore = '54';
else roleScore = `${hist.report.roles}`;
moderationScore = `${hist.report.moderation}`;
if (hist.report.other === 0) otherScore = '---';
else otherScore = `${hist.report.other}`;
if (hist.report.staff <= 0) miscScore = '---';
else miscScore = `${hist.report.staff}`;
if (hist.report.cloudServices === 0) cloudServicesScore = '---';
else if (hist.report.cloudServices > 10) cloudServicesScore = '10';
else cloudServicesScore = `${hist.report.cloudServices}`;
let data = '';
let hardInquiries = 0;
const inquiries: InquiryInterface[] = [];
if (hist.inquiries?.length > 0) {
for (const h of hist.inquiries) {
const inq = await this.client.db.Inquiry.findOne({ _id: h });
inquiries.push(inq);
}
}
if (inquiries?.length > 0) {
inquiries.forEach((inq) => {
const testDate = (new Date(new Date(inq.date).setHours(1460)));
// eslint-disable-next-line no-plusplus
if (testDate > new Date()) hardInquiries++;
});
data += `__CommScore™:__ ${totalScore}\n__Activity:__ ${activityScore}\n__Roles:__ ${roleScore}\n__Moderation:__ ${moderationScore}\n__Cloud Services:__ ${cloudServicesScore}\n__Other:__ ${otherScore}\n__Misc:__ ${miscScore}\n\n__Hard Inquiries:__ ${hardInquiries}\n__Soft Inquiries:__ ${hist.report.softInquiries?.length ?? '0'}`;
histArray.push({ name: moment(hist.date).calendar(), value: data });
}
}
const stat = {
totalMean: mean(totalArray),
totalMode: mode(totalArray),
totalMedian: median(totalArray),
activityMean: mean(activityArray),
rolesMean: mean(roleArray),
moderationMean: mean(moderationArray),
cloudServicesMean: mean(cloudServicesArray),
otherMean: mean(otherArray),
miscMean: mean(miscArray),
};
const splitHist = this.client.util.splitFields(histArray);
const cmdPages: RichEmbed[] = [];
splitHist.forEach((split) => {
const embed = new RichEmbed();
embed.setTitle('Historical Community Report');
let totalMean = '0';
let totalMedian = '0';
let totalMode = '0';
let activityMean = '0';
let moderationMean = '0';
let roleMean = '0';
let cloudServicesMean = '0';
let otherMean = '0';
let miscMean = '0';
if (stat.totalMean < 200) totalMean = '---';
else if (stat.totalMean > 800) totalMean = '800';
else totalMean = `${stat.totalMean}`;
if (stat.totalMedian < 200) totalMedian = '---';
else if (stat.totalMedian > 800) totalMedian = '800';
else totalMedian = `${stat.totalMedian}`;
if (stat.totalMode < 200) totalMode = '---';
else if (stat.totalMode > 800) totalMode = '800';
else totalMode = `${stat.totalMode}`;
if (stat.activityMean < 10) activityMean = '---';
else if (stat.activityMean > Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12))) activityMean = String(Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12)));
else activityMean = `${stat.activityMean}`;
if (stat.rolesMean <= 0) roleMean = '---';
else if (stat.rolesMean > 54) roleMean = '54';
else roleMean = `${stat.rolesMean}`;
moderationMean = `${stat.moderationMean}`;
if (stat.otherMean === 0) otherMean = '---';
else otherMean = `${stat.otherMean}`;
if (stat.miscMean <= 0) miscMean = '---';
else miscMean = `${stat.miscMean}`;
if (stat.cloudServicesMean === 0) cloudServicesMean = '---';
else if (stat.cloudServicesMean > 10) cloudServicesMean = '10';
else cloudServicesMean = `${stat.cloudServicesMean}`;
embed.setDescription(`__**Statistical Averages**__\n**CommScore™ Mean:** ${totalMean} | **CommScore™ Mode:** ${totalMode} | **CommScore™ Median:** ${totalMedian}\n\n**Activity Mean:** ${activityMean}\n**Roles Mean:** ${roleMean}\n**Moderation Mean:** ${moderationMean}\n**Cloud Services Mean:** ${cloudServicesMean}\n**Other Mean:** ${otherMean}\n**Misc Mean:** ${miscMean}`);
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
embed.setTimestamp();
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
split.forEach((c) => embed.addField(c.name, c.value));
return cmdPages.push(embed);
});
const name = `${user.username} VIA DISCORD - [HISTORICAL]`;
await this.client.report.createInquiry(user.id, name, 1);
if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] });
return createPaginationEmbed(message, cmdPages);
} if (args[0] && this.checkCustomPermissions(this.client.util.resolveMember(message.author.id, this.mainGuild), 4)) {
user = this.client.util.resolveMember(args[0], this.mainGuild)?.user;
if (!user) {
const sc = await this.client.db.Score.findOne({ pin: [Number(args[0].split('-')[0]), Number(args[0].split('-')[1]), Number(args[0].split('-')[2])] });
user = this.client.util.resolveMember(sc.userID, this.mainGuild)?.user;
}
if (!user) return this.error(message.channel, 'Member not found.');
const hists = await this.client.db.ScoreHistorical.find({ userID: user.id }).lean().exec();
if (!hists) return this.error(message.channel, 'No history found.');
if (hists.length < 1) return this.error(message.channel, 'No history found.');
const histArray: [{ name: string, value: string }?] = [];
const totalArray: number[] = [];
const activityArray: number[] = [];
const moderationArray: number[] = [];
const roleArray: number[] = [];
const cloudServicesArray: number[] = [];
const otherArray: number[] = [];
const miscArray: number[] = [];
for (const hist of hists.reverse()) {
totalArray.push(hist.report.total);
activityArray.push(hist.report.activity);
moderationArray.push(hist.report.moderation);
roleArray.push(hist.report.roles);
cloudServicesArray.push(hist.report.cloudServices);
otherArray.push(hist.report.other);
miscArray.push(hist.report.staff);
let totalScore = '0';
let activityScore = '0';
let moderationScore = '0';
let roleScore = '0';
let cloudServicesScore = '0';
let otherScore = '0';
let miscScore = '0';
if (hist.report.total < 200) totalScore = '---';
else if (hist.report.total > 800) totalScore = '800';
else totalScore = `${hist.report.total}`;
if (hist.report.activity < 10) activityScore = '---';
else if (hist.report.activity > Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12))) activityScore = String(Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12)));
else activityScore = `${hist.report.activity}`;
if (hist.report.roles <= 0) roleScore = '---';
else if (hist.report.roles > 54) roleScore = '54';
else roleScore = `${hist.report.roles}`;
moderationScore = `${hist.report.moderation}`;
if (hist.report.other === 0) otherScore = '---';
else otherScore = `${hist.report.other}`;
if (hist.report.staff <= 0) miscScore = '---';
else miscScore = `${hist.report.staff}`;
if (hist.report.cloudServices === 0) cloudServicesScore = '---';
else if (hist.report.cloudServices > 10) cloudServicesScore = '10';
else cloudServicesScore = `${hist.report.cloudServices}`;
let data = '';
let hardInquiries = 0;
const inquiries: InquiryInterface[] = [];
if (hist.inquiries?.length > 0) {
for (const h of hist.inquiries) {
const inq = await this.client.db.Inquiry.findOne({ _id: h });
inquiries.push(inq);
}
}
if (inquiries?.length > 0) {
inquiries.forEach((inq) => {
const testDate = (new Date(new Date(inq.date).setHours(1460)));
// eslint-disable-next-line no-plusplus
if (testDate > new Date()) hardInquiries++;
});
data += `__CommScore™:__ ${totalScore}\n__Activity:__ ${activityScore}\n__Roles:__ ${roleScore}\n__Moderation:__ ${moderationScore}\n__Cloud Services:__ ${cloudServicesScore}\n__Other:__ ${otherScore}\n__Misc:__ ${miscScore}\n\n__Hard Inquiries:__ ${hardInquiries}\n__Soft Inquiries:__ ${hist.report.softInquiries?.length ?? '0'}`;
histArray.push({ name: moment(hist.date).calendar(), value: data });
}
}
const stat = {
totalMean: mean(totalArray),
totalMode: mode(totalArray),
totalMedian: median(totalArray),
activityMean: mean(activityArray),
rolesMean: mean(roleArray),
moderationMean: mean(moderationArray),
cloudServicesMean: mean(cloudServicesArray),
otherMean: mean(otherArray),
miscMean: mean(miscArray),
};
const splitHist = this.client.util.splitFields(histArray);
const cmdPages: RichEmbed[] = [];
splitHist.forEach((split) => {
const embed = new RichEmbed();
embed.setTitle('Historical Community Report');
let totalMean = '0';
let totalMedian = '0';
let totalMode = '0';
let activityMean = '0';
let moderationMean = '0';
let roleMean = '0';
let cloudServicesMean = '0';
let otherMean = '0';
let miscMean = '0';
if (stat.totalMean < 200) totalMean = '---';
else if (stat.totalMean > 800) totalMean = '800';
else totalMean = `${stat.totalMean}`;
if (stat.totalMedian < 200) totalMedian = '---';
else if (stat.totalMedian > 800) totalMedian = '800';
else totalMedian = `${stat.totalMedian}`;
if (stat.totalMode < 200) totalMode = '---';
else if (stat.totalMode > 800) totalMode = '800';
else totalMode = `${stat.totalMode}`;
if (stat.activityMean < 10) activityMean = '---';
else if (stat.activityMean > Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12))) activityMean = String(Math.floor((Math.log1p(getTotalMessageCount(this.client)) * 12)));
else activityMean = `${stat.activityMean}`;
if (stat.rolesMean <= 0) roleMean = '---';
else if (stat.rolesMean > 54) roleMean = '54';
else roleMean = `${stat.rolesMean}`;
moderationMean = `${stat.moderationMean}`;
if (stat.otherMean === 0) otherMean = '---';
else otherMean = `${stat.otherMean}`;
if (stat.miscMean <= 0) miscMean = '---';
else miscMean = `${stat.miscMean}`;
if (stat.cloudServicesMean === 0) cloudServicesMean = '---';
else if (stat.cloudServicesMean > 10) cloudServicesMean = '10';
else cloudServicesMean = `${stat.cloudServicesMean}`;
embed.setDescription(`__**Statistical Averages**__\n**CommScore™ Mean:** ${totalMean} | **CommScore™ Mode:** ${totalMode} | **CommScore™ Median:** ${totalMedian}\n\n**Activity Mean:** ${activityMean}\n**Roles Mean:** ${roleMean}\n**Moderation Mean:** ${moderationMean}\n**Cloud Services Mean:** ${cloudServicesMean}\n**Other Mean:** ${otherMean}\n**Misc Mean:** ${miscMean}`);
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
embed.setTimestamp();
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
split.forEach((c) => embed.addField(c.name, c.value));
return cmdPages.push(embed);
});
let name = '';
for (const role of this.client.util.resolveMember(message.author.id, this.mainGuild).roles.map((r) => this.mainGuild.roles.get(r)).sort((a, b) => b.position - a.position)) {
name = `Library of Code sp-us | ${role.name} - [HISTORICAL]`;
break;
}
await this.client.report.createInquiry(user.id, name, 1);
if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] });
return createPaginationEmbed(message, cmdPages);
}
return null;
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,36 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Score_Notify extends Command {
constructor(client: Client) {
super(client);
this.name = 'notify';
this.description = 'Edits your notification preferences for Hard Inquiries.';
this.usage = `${this.client.config.prefix}score notify <on | off>`;
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
const user = message.author;
if (!user) return this.error(message.channel, 'Member not found.');
const score = await this.client.db.Score.findOne({ userID: message.author.id });
if (!score) return this.error(message.channel, 'Score not calculated yet.');
if (!score.notify) await this.client.db.Score.updateOne({ userID: message.author.id }, { $set: { notify: false } });
switch (args[0]) {
case 'on':
await this.client.db.Score.updateOne({ userID: message.author.id }, { $set: { notify: true } });
return this.success(message.channel, 'You will now be sent notifications whenever your score is hard-pulled.');
case 'off':
await this.client.db.Score.updateOne({ userID: message.author.id }, { $set: { notify: false } });
return this.success(message.channel, 'You will no longer be sent notifications when your score is hard-pulled.');
default:
return this.error(message.channel, 'Invalid option. Valid options are `on` and `off`.');
}
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,35 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Score_Pref extends Command {
constructor(client: Client) {
super(client);
this.name = 'pref';
this.description = 'Locks or unlocks your Community Report.';
this.usage = `${this.client.config.prefix}score pref <lock | unlock>`;
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!message.author) return this.error(message.channel, 'Member not found.');
const score = await this.client.db.Score.findOne({ userID: message.author.id });
if (!score) return this.error(message.channel, 'Score not calculated yet.');
if (!score.locked) await this.client.db.Score.updateOne({ userID: message.author.id }, { $set: { locked: false } });
switch (args[0]) {
case 'lock':
await this.client.db.Score.updateOne({ userID: message.author.id }, { $set: { locked: true } });
return this.success(message.channel, 'Your report is now locked.');
case 'unlock':
await this.client.db.Score.updateOne({ userID: message.author.id }, { $set: { locked: false } });
return this.success(message.channel, 'Your report is now unlocked.');
default:
return this.error(message.channel, 'Invalid input');
}
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

29
src/commands/setnick.ts Normal file
View File

@ -0,0 +1,29 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Setnick extends Command {
constructor(client: Client) {
super(client);
this.name = 'setnick';
this.description = 'Changes the nickname of a member';
this.usage = 'setnick <member> [new nickname]';
this.permissions = 2;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) return this.error(message.channel, 'Cannot find user.');
let nickname = args.slice(1).join(' ');
if (args.length === 1) nickname = null;
if (nickname?.length > 32) return this.error(message.channel, 'New nickname may not be more than 32 characters long.');
await member.edit({ nick: nickname });
return this.success(message.channel, `Updated the nickname of ${member.user.username}#${member.user.discriminator}.`);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

87
src/commands/sip.ts Normal file
View File

@ -0,0 +1,87 @@
/* eslint-disable consistent-return */
import { Message } from 'eris';
import type { Channel } from 'ari-client';
import { Client, Command } from '../class';
import { Misc } from '../pbx';
export default class SIP extends Command {
constructor(client: Client) {
super(client);
this.name = 'sip';
this.description = 'Dials a SIP URI.';
this.usage = `${this.client.config.prefix}sip <uri>`;
this.permissions = 1;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
const staff = await this.client.db.Staff.findOne({ userID: message.author.id });
if (!staff || !staff?.extension) return this.error(message.channel, 'You must have an extension to complete this action.');
this.success(message.channel, 'Queued call.');
const bridge = await this.client.pbx.ari.Bridge().create();
let receiver: Channel = this.client.pbx.ari.Channel();
let sender: Channel = this.client.pbx.ari.Channel();
try {
sender = await this.client.pbx.ari.channels.originate({
endpoint: `PJSIP/${staff.extension}`,
extension: staff.extension,
callerId: 'LOC PBX OPERATOR <operator>',
context: 'from-internal',
priority: 1,
app: 'cr-zero',
});
} catch {
return this.error(message.channel, 'Unable to dial your extension.');
}
sender.once('StasisStart', async () => {
await Misc.play(this.client.pbx, sender, 'sound:pls-hold-while-try');
await sender.ring();
try {
receiver = await receiver.originate({
endpoint: `SIP/${args.join(' ')}`,
callerId: 'LOC PBX OPERATOR <operator>',
context: 'from-internal',
priority: 1,
app: 'cr-zero',
});
} catch {
await Misc.play(this.client.pbx, sender, 'sound:discon-or-out-of-service');
await sender.hangup().catch(() => {});
return false;
}
// receiver.once('StasisStart', )
});
receiver.once('StasisStart', async () => {
await sender.ringStop();
await bridge.addChannel({ channel: [receiver.id, sender.id] });
await bridge.play({ media: 'sound:beep' });
});
receiver.once('ChannelDestroyed', async () => {
if (!sender.connected) return;
await Misc.play(this.client.pbx, sender, ['sound:the-number-u-dialed', 'sound:T-is-not-available', 'sound:please-try-again-later']);
await sender.hangup().catch(() => {});
await bridge.destroy().catch(() => {});
});
receiver.once('StasisEnd', async () => {
await sender.hangup().catch(() => {});
await bridge.destroy().catch(() => {});
});
sender.once('StasisEnd', async () => {
await receiver.hangup().catch(() => {});
await bridge.destroy().catch(() => {});
});
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

302
src/commands/site.ts Normal file
View File

@ -0,0 +1,302 @@
/* eslint-disable dot-notation */
/* eslint-disable no-plusplus */
import cheerio from 'cheerio';
import https from 'https';
import dns from 'dns';
import puppeteer from 'puppeteer';
import { createPaginationEmbed } from 'eris-pagination';
import { promisify } from 'util';
import axios, { AxiosError, AxiosResponse } from 'axios';
import { Message } from 'eris';
import { Client, Command, RichEmbed } from '../class';
interface TLSResponse {
status: boolean,
message?: string,
subject: {
commonName: string,
organization: string[],
organizationalUnit: string[],
locality: string[],
country: string[],
},
issuer: {
commonName: string,
organization: string[],
organizationalUnit: string[],
locality: string[],
country: string[],
},
root: {
commonName: string,
organization: string[],
organizationalUnit: string[],
locality: string[],
country: string[],
},
notBefore: Date,
notAfter: Date,
validationType: 'DV' | 'OV' | 'EV',
signatureAlgorithm: string,
publicKeyAlgorithm: string,
serialNumber: string,
keyUsage: number[],
keyUsageAsText: ['CRL Signing'?, 'Certificate Signing'?, 'Content Commitment'?, 'Data Encipherment'?, 'Decipher Only'?, 'Digital Signature'?, 'Encipher Only'?, 'Key Agreement'?, 'Key Encipherment'?],
extendedKeyUsage: number[],
extendedKeyUsageAsText: ['All/Any Usages'?, 'TLS Web Server Authentication'?, 'TLS Web Client Authentication'?, 'Code Signing'?, 'E-mail Protection (S/MIME)'?],
san: string[],
fingerprint: string,
connection: {
cipherSuite: string,
tlsVersion: 'SSLv3' | 'TLSv1' | 'TLSv1.1' | 'TLSv1.2' | 'TLSv1.3',
},
}
export default class SiteInfo extends Command {
constructor(client: Client) {
super(client);
this.name = 'site';
this.description = 'Retrieves various information about a site. Includes web, host, and TLS information.';
this.usage = `${this.client.config.prefix}tls <domain>\n*Only raw domain, no protocols or URLs.*`;
this.aliases = ['ssl', 'cert', 'certinfo', 'ci', 'tls', 'si', 'siteinfo'];
this.permissions = 0;
this.guildOnly = true;
this.enabled = false;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const loading = await this.loading(message.channel, 'Loading...');
let author: { name?: string, icon?: string, url?: string } = {};
let s: AxiosResponse;
try {
s = await axios.get(`https://${args[0]}`, {
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
});
} catch (err) {
loading.delete().catch(() => {});
return this.error(message.channel, `Unable to retrieve information from site. | ${err}`);
}
try {
const site = cheerio.load(s.data);
let iconURI: string;
try {
await axios.get(`https://${args[0]}/favicon.ico`, {
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
});
iconURI = `https://${args[0]}/favicon.ico`;
} catch {
const att = site('link').toArray().filter((a) => a.attribs.rel === 'icon');
if (att?.length > 0) {
if (att[0].attribs.href.startsWith('/') || !att[0].attribs.href.startsWith('http')) {
iconURI = `https://${args[0]}${att[0].attribs.href}`;
} else {
iconURI = att[0].attribs.href;
}
}
}
author = {
name: site('title').text(),
icon: iconURI,
url: `https://${args[0]}`,
};
} catch {
author = {
name: `https://${args[0]}`,
icon: `https://${args[0]}/favicon.ico`,
url: `https://${args[0]}`,
};
}
const embeds: RichEmbed[] = [];
try {
const server = await this.getServerInformation(args[0]);
if (server) {
const em = new RichEmbed();
em.setTitle('Web Information');
server.forEach((f) => em.addField(f.name, f.value, f.inline));
embeds.push(em);
}
const host = await this.getHostInformation(args[0]);
if (host) {
const em = new RichEmbed();
em.setTitle('Host Information');
host.forEach((f) => em.addField(f.name, f.value, f.inline));
embeds.push(em);
}
const tls = await this.getTLSInformation(args[0]);
if (tls) {
const em = new RichEmbed();
em.setTitle('TLS Information');
tls.forEach((f) => em.addField(f.name, f.value, f.inline));
embeds.push(em);
}
} catch {
loading.delete().catch(() => {});
return this.error(message.channel, 'Unable to receive information.');
}
let screenshotData: string;
try {
const browser = await puppeteer.launch({
ignoreHTTPSErrors: true,
args: [
'--ignore-certificate-errors',
'--ignore-certificate-errors-spki-list',
],
});
const page = await browser.newPage();
await page.goto(`https://${args[0]}`);
screenshotData = await page.screenshot();
browser.close();
} catch (e) {
this.client.util.signale.error(e);
}
embeds.forEach((embed) => {
embed.setAuthor(author.name, author.icon, author.url);
embed.setColor('#4870fe');
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
});
if (screenshotData) {
if (embeds.length === 1) return message.channel.createMessage({ embed: embeds[0] }, { name: 'img.png', file: screenshotData });
} else {
await message.channel.createMessage('', { name: 'img.png', file: screenshotData });
}
loading.delete().catch(() => {});
return await createPaginationEmbed(message, embeds, {
cycling: true,
extendedButtons: true,
});
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
public async getHostInformation(domain: string) {
const getDNS = promisify(dns.resolve4);
try {
const dnsd = await getDNS(domain);
if (dnsd?.length < 1) return null;
const r = await axios.get(`http://ip-api.com/json/${dnsd[0]}`);
const embed = new RichEmbed();
embed.addField('IPv4 Address', dnsd[0], true);
embed.addBlankField();
embed.addField('Organization/Customer', r.data.org, true);
embed.addField('Internet Service Provider', r.data.isp, true);
embed.addField('Location', `${r.data.city}, ${r.data.regionName}, ${r.data.country}`, true);
return embed.fields;
} catch (err) {
this.client.util.signale.error(err);
return null;
}
}
public async getServerInformation(domain: string) {
let r: AxiosResponse;
try {
r = await axios.get(`https://${domain}`, {
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
});
} catch (err) {
this.client.util.signale.error(err);
return null;
}
const embed = new RichEmbed();
embed.addField('Web Software', r.headers['server'] ?? 'N/A', true);
embed.addField('Content Type', r.headers['content-type'] ? r.headers['content-type'].split(';')[0] : 'N/A', true);
embed.addField('Content Length', r.headers['content-length'] ? this.client.util.dataConversion(r.headers['content-length']) : 'N/A', true);
return embed.fields;
}
public async getTLSInformation(domain: string) {
https.globalAgent.options.rejectUnauthorized = false;
let r: AxiosResponse;
try {
r = await axios.get(`https://certapi.libraryofcode.org?q=${domain}`, {
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
});
} catch (err) {
const error = <AxiosError>err;
return null;
}
const resp: TLSResponse = r.data;
const embed = new RichEmbed();
let subjectString = `**Common Name:** ${resp.subject.commonName}\n`;
if (resp.subject.organization?.length > 0) subjectString += `**Organization:** ${resp.subject.organization[0]}\n`;
if (resp.subject.organizationalUnit?.length > 0) subjectString += `**Organizational Unit:** ${resp.subject.organizationalUnit[0]}\n`;
if (resp.subject.locality?.length > 0) subjectString += `**Locality:** ${resp.subject.locality[0]}\n`;
if (resp.subject.country?.length > 0) subjectString += `**Country:** ${resp.subject.country[0]}`;
embed.addField('Subject', subjectString, true);
let issuerString = `**Common Name:** ${resp.subject.commonName}\n`;
if (resp.issuer.organization?.length > 0) issuerString += `**Organization:** ${resp.issuer.organization[0]}\n`;
if (resp.issuer.organizationalUnit?.length > 0) issuerString += `**Organizational Unit:** ${resp.issuer.organizationalUnit[0]}\n`;
if (resp.issuer.locality?.length > 0) issuerString += `**Locality:** ${resp.issuer.locality[0]}\n`;
if (resp.issuer.country?.length > 0) issuerString += `**Country:** ${resp.issuer.country[0]}`;
const rootString = `**Root Certificate:** ${resp.root.organization?.length > 0 ? resp.root.organization[0] : ''} (${resp.root.commonName ?? ''} : ${resp.root.organizationalUnit?.length > 0 ? resp.root.organizationalUnit[0] : ''})`;
if (rootString?.length > 0) issuerString += `\n\n${rootString}`;
embed.addField('Issuer', issuerString, true);
embed.addBlankField();
embed.addField('Issued On', new Date(resp.notBefore).toLocaleString('en-us'), true);
embed.addField('Expiry', new Date(resp.notAfter).toLocaleString('en-us'), true);
embed.addBlankField();
let sanString = '';
for (let i = 0; i < resp.san.length; i++) {
if (i >= 10) {
sanString += '...';
break;
}
sanString += `${resp.san[i]}\n`;
}
embed.addField('Subject Alternative Names', sanString, true);
embed.addBlankField();
embed.addField('Key Usage', resp.keyUsageAsText.join(', '), true);
embed.addField('Extended Key Usage', resp.extendedKeyUsageAsText.join(', '), true);
embed.addField('Validation Type', resp.validationType, true);
embed.addBlankField();
embed.addField('Serial Number', String(resp.serialNumber), true);
embed.addField('Fingerprint (SHA1)', resp.fingerprint, true);
embed.addField('Signature Algorithm', resp.signatureAlgorithm, true);
embed.addField('Public Key Algorithm', resp.publicKeyAlgorithm, true);
embed.addBlankField();
embed.addField('TLS Cipher Suite', resp.connection.cipherSuite, true);
embed.addField('TLS Version', resp.connection.tlsVersion, true);
https.globalAgent.options.rejectUnauthorized = true;
return embed.fields;
}
}

34
src/commands/slowmode.ts Normal file
View File

@ -0,0 +1,34 @@
import { Message, GuildTextableChannel } from 'eris';
import moment, { unitOfTime } from 'moment';
import { Client, Command } from '../class';
export default class Slowmode extends Command {
regex: RegExp;
constructor(client: Client) {
super(client);
this.name = 'slowmode';
this.description = 'Set slowmode to a channel.';
this.usage = 'slowmode <length[unit]>';
this.permissions = 1;
this.guildOnly = true;
this.enabled = true;
this.regex = /[a-z]+|[^a-z]+/gi;
}
public async run(message: Message<GuildTextableChannel>, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const [length, unit] = args[0].match(this.regex);
if (Number.isNaN(Number(length))) return this.error(message.channel, 'Could not determine the slowmode time.');
const momentSeconds: number = Math.round(moment.duration(length, unit as unitOfTime.Base || 's').asSeconds());
if (momentSeconds > 21600 || momentSeconds < 0) return this.error(message.channel, 'Slowmode must be between 0 seconds and 6 hours.');
return message.channel.edit({ rateLimitPerUser: momentSeconds }).then((c) => message.addReaction(':success:477618704155410452'));
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

37
src/commands/stats.ts Normal file
View File

@ -0,0 +1,37 @@
import { Message } from 'eris';
import { Client, Command, RichEmbed } from '../class';
export default class Stats extends Command {
constructor(client: Client) {
super(client);
this.name = 'stats';
this.description = 'Provides system statistics.';
this.usage = `${this.client.config.prefix}stats`;
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message) {
try {
const messages = await this.client.db.Stat.findOne({ name: 'messages' });
const commands = await this.client.db.Stat.findOne({ name: 'commands' });
const pages = await this.client.db.Stat.findOne({ name: 'pages' });
const requests = await this.client.db.Stat.findOne({ name: 'requests' });
const embed = new RichEmbed();
embed.setTitle('Statistics');
embed.setThumbnail(this.client.user.avatarURL);
embed.addField('Messages Seen', `${messages.value}`, true);
embed.addField('Commands Executed', `${commands.value}`, true);
embed.addField('HTTP Requests Served', `${requests.value}`, true);
embed.addField('Pages Sent', `${pages.value}`, true);
embed.addField('Jobs Processed', `${(await this.client.queue.jobCounts()).completed}`, true);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,52 @@
import { randomBytes } from 'crypto';
import { Message, TextChannel } from 'eris';
import { Client, Command, LocalStorage } from '../class';
export default class StoreMessages extends Command {
constructor(client: Client) {
super(client);
this.name = 'storemessages';
this.description = 'Fetches 1000 messages from the specified channel and stores them in a HTML file.';
this.usage = `${this.client.config.prefix}storemessages <channel> [member ID]`;
this.aliases = ['sm'];
this.permissions = 7;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const check = this.client.util.resolveGuildChannel(args[0], this.mainGuild, false);
if (!check || check.type !== 0) return this.error(message.channel, 'The channel you specified either doesn\'t exist or isn\'t a textable guild channel.');
const chan = <TextChannel> this.mainGuild.channels.get(check.id);
const loadingMessage = await this.loading(message.channel, 'Fetching messages...');
let messages = await chan.getMessages(10000);
if (args[1]) {
messages = messages.filter((m) => m.author.id === args[1]);
}
let html = `<strong><i>CLASSIFIED RESOURCE: CL-GEN/AD</i></strong><br><h3>Library of Code sp-us</h3><strong>Channel:</strong> ${chan.name} (${chan.id})<br><strong>Generated by:</strong> ${message.author.username}#${message.author.discriminator}<br><strong>Generated at:</strong> ${new Date().toLocaleString('en-us')}<br><br>`;
for (const msg of messages) {
html += `(<i>${new Date(msg.timestamp).toLocaleString('en-us')}</i>) [<strong>${msg.author.username}#${msg.author.discriminator} - ${msg.author.id}</strong>]: ${msg.cleanContent}<br>`;
}
message.delete();
const identifier = randomBytes(20).toString('hex');
const comp = await LocalStorage.compress(html);
const file = new this.client.db.File({
name: `${chan.name}-${new Date().toLocaleString('en-us')}.html.gz`,
identifier,
mimeType: 'application/gzip',
data: comp,
downloaded: 0,
maxDownloads: 20,
});
await file.save();
loadingMessage.delete();
this.client.getDMChannel(message.author.id).then((c) => c.createMessage(`https://cr.ins/m/${identifier}.html.gz || https://cr.ins/m/${identifier}.html?d=1`)).catch(() => this.error(message.channel, 'Could not send a DM to you.'));
return this.success(message.channel, `Fetched messages for <#${chan.id}>. Check your DMs for link to access.`);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

54
src/commands/sysinfo.ts Normal file
View File

@ -0,0 +1,54 @@
// ?e require('mongoose').connections[0].db.command({ buildInfo: 1 })
import { Message } from 'eris';
import mongoose from 'mongoose';
import { Client, Command, RichEmbed } from '../class';
import { version as erisVersion } from '../../node_modules/eris/package.json';
import { version as mongooseVersion } from '../../node_modules/mongoose/package.json';
import { version as ariVersion } from '../../node_modules/ari-client/package.json';
import { version as amiVersion } from '../../node_modules/asterisk-manager/package.json';
import { version as nodeMailerVersion } from '../../node_modules/nodemailer/package.json';
import { version as ioredisVersion } from '../../node_modules/ioredis/package.json';
import { version as stripeVersion } from '../../node_modules/stripe/package.json';
import { version as cronVersion } from '../../node_modules/cron/package.json';
import { version as ttsVersion } from '../../node_modules/@google-cloud/text-to-speech/package.json';
import { version as bullVersion } from '../../node_modules/bull/package.json';
export default class SysInfo extends Command {
constructor(client: Client) {
super(client);
this.name = 'sysinfo';
this.description = 'Information about the various services/system we use to run.';
this.usage = 'sysinfo';
this.permissions = 0;
this.enabled = true;
}
public async run(message: Message) {
try {
const embed = new RichEmbed();
embed.setTitle('System & Service Information');
embed.setDescription('__Format of Services__\n- {Server/Service Name} + {various server stats} | {Node Library Used for Communication w/ Server or Service}');
const mongoBuild = await mongoose.connections[0].db.command({ buildInfo: 1 });
const mongoDatabase: {
collections: number,
objects: number,
dataSize: number,
} = await mongoose.connections[0].db.command({ dbStats: 1 });
const asteriskInformation = await this.client.util.pbx.ari.asterisk.getInfo();
const redisVersion = await this.client.util.exec('redis-cli info | grep "redis_version"');
embed.addField('Database', `- [MongoDB v${mongoBuild.version}](https://www.mongodb.com/) + Documents: ${mongoDatabase.objects} | [Mongoose ODM v${mongooseVersion}](https://github.com/Automattic/mongoose)\n- CR Local Storage w/ GZIP Compression | Internal\n- [Redis v${redisVersion.split(':')[1]}](https://redis.io/) | [IORedis v${ioredisVersion}](https://github.com/luin/ioredis)`, true);
embed.addField('Telephony/PBX', `- [Asterisk v${asteriskInformation.system.version}](https://www.asterisk.org/) | [Asterisk ARI Node.js Client v${ariVersion}](https://github.com/asterisk/node-ari-client) & [Asterisk AMI Node.js Client v${amiVersion}](https://github.com/danjenkins/node-asterisk-ami)`, true);
embed.addField('Email', `- [Postfix SMTP v3.1.12](http://www.postfix.org/) | [Nodemailer v${nodeMailerVersion}](https://github.com/nodemailer/nodemailer)`, true);
embed.addField('Discord', `- N/A | [Eris v${erisVersion}](https://github.com/abalabahaha/eris)`, true);
embed.addField('Payments', `- [Stripe API](https://stripe.com/) | [Stripe Node.js Client v${stripeVersion}](https://github.com/stripe/stripe-node)`, true);
embed.addField('Scheduling', `- [Cron](https://systemd.io/) | [Cron Scheduling Node.js v${cronVersion}](https://github.com/node-cron/node-cron)\n- Internal Queue | [Bull v${bullVersion}](https://github.com/OptimalBits/bull) w/ IORedis`, true);
embed.addField('Audio & Voice', `- [Google Cloud ML](https://cloud.google.com/text-to-speech) | [Google Cloud Node.js Text to Speech Synthesizer v${ttsVersion}](https://github.com/googleapis/nodejs-text-to-speech)\n - [FFMPEG](https://ffmpeg.org/) | Internal`, true);
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
message.channel.createMessage({ embed });
} catch (err) {
this.client.util.handleError(err, message, this);
}
}
}

40
src/commands/train.ts Normal file
View File

@ -0,0 +1,40 @@
import { Message, TextChannel } from 'eris';
import { Client, Command } from '../class';
export default class Train extends Command {
constructor(client: Client) {
super(client);
this.name = 'train';
this.description = 'Trains a neural network.';
this.usage = `${this.client.config.prefix}train <channel> <message id> <1: good | 0: bad>`;
this.permissions = 1;
this.guildOnly = false;
this.enabled = false;
}
public async run(message: Message, args: string[]) {
try {
if (args?.length < 3) return this.client.commands.get('help').run(message, [this.name]);
if (args[2] !== '0' && args[2] !== '1') return this.error(message.channel, 'Result must be either 0 or 1.');
const channel = <TextChannel> this.client.util.resolveGuildChannel(args[0], this.mainGuild);
if (!channel) return this.error(message.channel, 'Channel could not be found.');
if (channel.type !== 0) return this.error(message.channel, 'Invalid channel type.');
let msg: Message;
try {
msg = await channel.getMessage(args[1]);
} catch {
return this.error(message.channel, 'Could not find message.');
}
if (!msg) return this.error(message.channel, 'Message could not be found.');
await this.client.db.NNTrainingData.updateOne({ name: 'tc' }, { $addToSet: { data: { input: this.client.util.encode(msg.content), output: { res: Number(args[2]) } } } });
await message.delete();
const done = await this.success(message.channel, 'Neural Network trained successfully.');
return setTimeout(async () => {
await done.delete();
}, 3000);
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

36
src/commands/tts.ts Normal file
View File

@ -0,0 +1,36 @@
import axios from 'axios';
import { v4 as uuid } from 'uuid';
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class TTS extends Command {
constructor(client: Client) {
super(client);
this.name = 'tts';
this.description = 'Uses Google Text to Speech engines to synthesize input to a MP3 file. Only supports English at this time.';
this.usage = `${this.client.config.prefix}tts <text>`;
this.permissions = 0;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
if (args.length > 200) return this.error(message.channel, 'Cannot synthesize more than 200 characters.');
const msg = await this.loading(message.channel, 'Synthesizing...');
const d = await axios({
method: 'GET',
url: `https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q=${encodeURIComponent(args.join(' '))}&tl=en`,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
},
responseType: 'arraybuffer',
});
msg.delete();
return message.channel.createMessage(undefined, { name: `${uuid()}.mp3`, file: d.data });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

37
src/commands/unban.ts Normal file
View File

@ -0,0 +1,37 @@
import { Message, User } from 'eris';
import { Client, Command } from '../class';
export default class Unban extends Command {
constructor(client: Client) {
super(client);
this.name = 'unban';
this.description = 'Unbans a member from the guild.';
this.usage = 'unban <user id> [reason]';
this.permissions = 3;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
let user: User;
try {
user = await this.client.getRESTUser(args[0]);
} catch {
return this.error(message.channel, 'Could find find user.');
}
try {
await this.mainGuild.getBan(args[0]);
} catch {
return this.error(message.channel, 'This user is not banned.');
}
message.delete();
await this.client.util.moderation.unban(user.id, message.member, args.slice(1).join(' '));
return this.success(message.channel, `${user.username}#${user.discriminator} has been unbanned.`);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

34
src/commands/unmute.ts Normal file
View File

@ -0,0 +1,34 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Unmute extends Command {
constructor(client: Client) {
super(client);
this.name = 'unmute';
this.description = 'Unmutes a member.';
this.usage = 'unmute <member> [reason]';
this.permissions = 2;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) return this.error(message.channel, 'Cannot find user.');
try {
const res1 = await this.client.db.local.muted.get<boolean>(`muted-${member.id}`);
if (!res1 || !this.mainGuild.members.get(member.id).roles.includes('478373942638149643')) return this.error(message.channel, 'This user is already unmuted.');
} catch {} // eslint-disable-line no-empty
if (member && !this.client.util.moderation.checkPermissions(member, message.member)) return this.error(message.channel, 'Permission Denied.');
message.delete();
await this.client.util.moderation.unmute(member.user.id, message.member, args.slice(1).join(' '));
return this.success(message.channel, `${member.user.username}#${member.user.discriminator} has been unmuted.`);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

227
src/commands/whois.ts Normal file
View File

@ -0,0 +1,227 @@
/* eslint-disable no-bitwise */
import moment from 'moment';
import { Message, Member } from 'eris';
import { Client, Command, RichEmbed } from '../class';
import { whois as emotes } from '../configs/emotes.json';
export default class Whois extends Command {
constructor(client: Client) {
super(client);
this.name = 'whois';
this.description = 'Provides information on a member.';
this.usage = 'whois [member]';
this.permissions = 0;
this.guildOnly = true;
this.enabled = true;
}
public async run(message: Message, args: string[]) {
try {
let member: Member;
if (!args[0]) member = message.member;
else {
member = this.client.util.resolveMember(args.join(' '), this.mainGuild);
try {
if (!member) member = await this.mainGuild.getRESTMember(args[0]);
} catch {
return this.error(message.channel, 'Member not found.');
}
}
if (!member) {
return this.error(message.channel, 'Member not found.');
}
const embed = new RichEmbed();
embed.setThumbnail(member.avatarURL);
const ackResolve = await this.client.db.Staff.findOne({ userID: member.id }).lean().exec();
let title = `${member.user.username}#${member.user.discriminator}`;
if (ackResolve?.pn?.length > 0) title += `, ${ackResolve.pn.join(', ')}`;
embed.setAuthor(title, member.user.avatarURL);
let description = '';
let titleAndDepartment = '';
if (ackResolve?.title && ackResolve?.dept) {
titleAndDepartment += `${emotes.titleAndDepartment} __**${ackResolve.title}**__, __${ackResolve.dept}__\n\n`;
} else if (ackResolve?.dept) {
titleAndDepartment += `${emotes.titleAndDepartment} __${ackResolve.dept}__\n\n`;
}
if (titleAndDepartment.length > 0) description += titleAndDepartment;
if (ackResolve?.emailAddress) {
description += `${emotes.email} ${ackResolve.emailAddress}\n`;
}
const pager = await this.client.db.PagerNumber.findOne({ individualAssignID: member.user.id }).lean().exec();
if (pager?.num) {
description += `📟 ${pager.num}\n`;
}
if (ackResolve?.extension) {
description += `☎️ ${ackResolve.extension}\n`;
}
const memberProfile = await this.client.db.Member.findOne({ userID: member.id }).lean().exec();
if (memberProfile?.additional?.gitlab) {
description += `${emotes.gitlab} ${memberProfile?.additional.gitlab}\n`;
}
if (memberProfile?.additional?.github) {
description += `${emotes.github} ${memberProfile?.additional.github}\n`;
}
if (memberProfile?.additional?.bio) {
description += `${emotes.bio} *${memberProfile?.additional.bio}*\n`;
}
description += `\n<@${member.id}>`;
embed.setDescription(description);
for (const role of member.roles.map((r) => this.mainGuild.roles.get(r)).sort((a, b) => b.position - a.position)) {
if (role?.color !== 0) {
embed.setColor(role.color);
break;
}
}
embed.addField('Status', member.status === 'dnd' ? 'Do Not Disturb' : this.client.util.capsFirstLetter(member.status) || 'Offline', true);
embed.addField('Joined At', `${moment(new Date(member.joinedAt)).format('dddd, MMMM Do YYYY, h:mm:ss A')} ET`, true);
embed.addField('Created At', `${moment(new Date(member.user.createdAt)).format('dddd, MMMM Do YYYY, h:mm:ss A')} ET`, true);
const score = await this.client.db.Score.findOne({ userID: member.id }).lean().exec();
if (score) {
await this.client.report.createInquiry(member.id, 'Library of Code sp-us | Bureau of Community Reports', 1);
let totalScore = '0';
if (score.total < 200) totalScore = '---';
else if (score.total > 800) totalScore = '800';
else totalScore = `${score.total}`;
embed.addField('CommScore™', totalScore, true);
} else embed.addField('CommScore™', 'N/C', true);
if (member.roles.length > 0) {
embed.addField(`Roles [${member.roles.length}]`, member.roles.map((r) => this.mainGuild.roles.get(r)).sort((a, b) => b.position - a.position).map((r) => `<@&${r.id}>`).join(', '));
}
const flags: string[] = [];
if (member.user.publicFlags) {
if ((member.user.publicFlags & (1 << 12)) === 1 << 12) flags.push('<:System:768370601265201152>');
if ((member.user.publicFlags & (1 << 0)) === 1 << 0) flags.push('<:DiscordStaff:768370601882025985>');
if ((member.user.publicFlags & (1 << 1)) === 1 << 1) flags.push('<:Partnered:768370601395879936>');
if ((member.user.publicFlags & (1 << 3)) === 1 << 3) flags.push('<:BugHunter:768370601105555467>');
if ((member.user.publicFlags & (1 << 14)) === 1 << 14) flags.push('<:BugHunter:768370601105555467>');
if ((member.user.publicFlags & (1 << 2)) === 1 << 2) flags.push('<:HypeSquadEvents:768370600745762846>');
if ((member.user.publicFlags & (1 << 6)) === 1 << 6) flags.push('<:HypeSquadBravery:768370601328640011> ');
if ((member.user.publicFlags & (1 << 7)) === 1 << 7) flags.push('<:HypeSquadBrilliance:768370600842362900>');
if ((member.user.publicFlags & (1 << 8)) === 1 << 8) flags.push('<:HypeSquadBalance:768370599584071751> ');
if ((member.user.publicFlags & (1 << 9)) === 1 << 9) flags.push('<:EarlySupporter:768370601873768468>');
if ((member.user.publicFlags & (1 << 16)) === 1 << 16) flags.push('<:VerifiedBot:768370599252197396>');
if ((member.user.publicFlags & (1 << 17)) === 1 << 17) flags.push('<:VerifiedBotDeveloper:768370601701933077>');
}
if (flags.length > 0) embed.addField('Flags', flags.join(' '));
const permissions: string[] = [];
const serverAcknowledgements: string[] = [];
const bit = member.permission.allow;
if (this.mainGuild.ownerID === member.id) serverAcknowledgements.push('Server Owner');
if (bit & 8) { permissions.push('Administrator'); serverAcknowledgements.push('Server Admin'); }
if (bit & 32) { permissions.push('Manage Server'); serverAcknowledgements.push('Server Manager'); }
if (bit & 16) permissions.push('Manage Channels');
if (bit & 268435456) permissions.push('Manage Roles');
if (bit & 8192) { permissions.push('Manage Messages'); serverAcknowledgements.push('Server Moderator'); }
if (bit & 134217728) permissions.push('Manage Nicknames');
if (bit & 1073741824) permissions.push('Manage Emojis');
if (bit & 4) permissions.push('Ban Members');
if (bit & 2) permissions.push('Kick Members');
const account = await this.client.db.Member.findOne({ userID: member.id }).lean().exec();
if (account?.additional?.langs?.length > 0) {
const langs: string[] = [];
for (const lang of account.additional.langs.sort((a, b) => a.localeCompare(b))) {
switch (lang) {
case 'asm':
langs.push('<:AssemblyLanguage:703448714248716442> Assembly Language');
break;
case 'cfam':
langs.push('<:clang:553684262193332278> C/C++');
break;
case 'csharp':
langs.push('<:csharp:553684277280112660> C#');
break;
case 'go':
langs.push('<:Go:703449475405971466> Go');
break;
case 'java':
langs.push('<:Java:703449725181100135> Java');
break;
case 'js':
langs.push('<:JavaScriptECMA:703449987916496946> JavaScript');
break;
case 'kt':
langs.push('<:Kotlin:703450201838321684> Kotlin');
break;
case 'py':
langs.push('<:python:553682965482176513> Python');
break;
case 'rb':
langs.push('<:ruby:604812470451699712> Ruby');
break;
case 'rs':
langs.push('<:Rust:703450901960196206> Rust');
break;
case 'swift':
langs.push('<:Swift:703451096093294672> Swift');
break;
case 'ts':
langs.push('<:TypeScript:703451285789343774> TypeScript');
break;
default:
break;
}
}
embed.addField('Known Languages', langs.join(', '));
}
if (account?.additional?.operatingSystems?.length > 0) {
const operatingSystems: string[] = [];
for (const os of account.additional.operatingSystems.sort((a, b) => a.localeCompare(b))) {
switch (os) {
case 'arch':
operatingSystems.push('<:arch:707694976523304960> Arch');
break;
case 'deb':
operatingSystems.push('<:debian:707695042617147589> Debian');
break;
case 'cent':
operatingSystems.push('<:centos:707702165816213525> CentOS');
break;
case 'fedora':
operatingSystems.push('<:fedora:707695073151680543> Fedora');
break;
case 'manjaro':
operatingSystems.push('<:manjaro:707701473680556062> Manjaro');
break;
case 'mdarwin':
operatingSystems.push('<:mac:707695427754917919> macOS');
break;
case 'redhat':
operatingSystems.push('<:redhat:707695102159749271> RedHat Enterprise Linux');
break;
case 'ubuntu':
operatingSystems.push('<:ubuntu:707695136888586300> Ubuntu');
break;
case 'win':
operatingSystems.push('<:windows10:707695160259248208> Windows');
break;
default:
break;
}
}
embed.addField('Used Operating Systems', operatingSystems.join(', '));
}
if (permissions.length > 0) {
embed.addField('Permissions', permissions.join(', '));
}
if (serverAcknowledgements.length > 0) {
embed.addField('Acknowledgements', serverAcknowledgements[0]);
}
if (ackResolve?.additionalRoles?.length > 0) {
embed.addField('Additional Acknowledgements', ackResolve.additionalRoles.join(', '));
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,6 @@
{
"moderation": {
"modlogs": "446080867065135115",
"automod": ""
}
}

14
src/configs/emotes.json Normal file
View File

@ -0,0 +1,14 @@
{
"whois": {
"titleAndDepartment": "<:loc:607695848612167700>",
"email": "<:email:699786452267040878>",
"gitlab": "<:gitlab:699788655748841492>",
"github": "<:github:699786469404835939>",
"bio": "<:bio:699786408193294416>"
},
"statusMessages": {
"success": "<:modSuccess:578750988907970567>",
"loading": "<a:modloading:588607353935364106>",
"error": "<:modError:578750737920688128>"
}
}

View File

@ -0,0 +1,23 @@
import { Emoji, Message, TextChannel } from 'eris';
import { Client, Event } from '../class';
export default class CallBackHandler extends Event {
public client: Client;
constructor(client: Client) {
super(client);
this.event = 'messageReactionAdd';
}
public async run(message: Message, emoji: Emoji, member: string) {
try {
if (emoji.id !== '578750988907970567' || message.channel.id !== '780513128240382002') return;
if (member === this.client.user.id) return;
const chan = <TextChannel> this.client.guilds.get(this.client.config.guildID).channels.get('780513128240382002');
const msg = await chan.getMessage(message.id);
await msg.delete();
} catch (err) {
this.client.util.handleError(err);
}
}
}

View File

@ -0,0 +1,34 @@
/* eslint-disable no-useless-return */
import { Message, TextChannel, NewsChannel } from 'eris';
import { Client, Event } from '../class';
export default class CommandHandler extends Event {
public client: Client;
constructor(client: Client) {
super(client);
this.event = 'messageCreate';
}
public async run(message: Message) {
try {
this.client.db.Stat.updateOne({ name: 'messages' }, { $inc: { value: 1 } }).exec();
if (message.author.bot) return;
if (message.content.indexOf(this.client.config.prefix) !== 0) return;
const noPrefix: string[] = message.content.slice(this.client.config.prefix.length).trim().split(/ +/g);
const resolved = await this.client.util.resolveCommand(noPrefix);
if (!resolved) return;
if (resolved.cmd.guildOnly && !(message.channel instanceof TextChannel || message.channel instanceof NewsChannel)) return;
if (!resolved.cmd.enabled) { message.channel.createMessage(`***${this.client.util.emojis.ERROR} This command has been disabled***`); return; }
if (!resolved.cmd.checkPermissions(this.client.util.resolveMember(message.author.id, this.client.guilds.get('446067825673633794')))) return;
if ((message.channel.type === 0) && !message.channel.guild.members.get(message.author.id)) {
message.channel.guild.members.add(await message.channel.guild.getRESTMember(message.author.id));
}
this.client.util.signale.log(`User '${message.author.username}#${message.author.discriminator}' ran command '${resolved.cmd.name}' in '${message.channel.id}'.`);
await resolved.cmd.run(message, resolved.args);
this.client.db.Stat.updateOne({ name: 'commands' }, { $inc: { value: 1 } }).exec();
} catch (err) {
this.client.util.handleError(err, message);
}
}
}

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