mirror of https://github.com/aididan20/dyno
aididan20
4 years ago
commit
c1db61fecd
147 changed files with 13306 additions and 0 deletions
-
4.dockerignore
-
9.eslintignore
-
108.eslintrc.json
-
50.gitignore
-
3.gitmodules
-
1.npmrc
-
23Dockerfile
-
402LICENSE.md
-
1README.md
-
123config/default.yaml
-
34config/development.yaml
-
131config/production.yaml
-
37gulpfile.js
-
79package.json
-
11pm2/dyno.bot.json
-
17scripts/convertConfig.js
-
8scripts/execAll.sh
-
105scripts/loadActivity.js
-
101scripts/reshard.js
-
37src/commands/Admin/Avatar.js
-
28src/commands/Admin/Changelog.js
-
65src/commands/Admin/CommandStats.js
-
730src/commands/Admin/Data.js
-
130src/commands/Admin/DisablePremium.js
-
107src/commands/Admin/EnablePremium.js
-
74src/commands/Admin/Eval.js
-
50src/commands/Admin/Exec.js
-
50src/commands/Admin/Git.js
-
50src/commands/Admin/GlobalDisable.js
-
50src/commands/Admin/GlobalEnable.js
-
167src/commands/Admin/Guild.js
-
40src/commands/Admin/ListingBlacklist.js
-
53src/commands/Admin/LoadCommand.js
-
23src/commands/Admin/LoadController.js
-
35src/commands/Admin/LoadIPC.js
-
27src/commands/Admin/LoadModule.js
-
47src/commands/Admin/MoveCluster.js
-
39src/commands/Admin/Partner.js
-
68src/commands/Admin/RLReset.js
-
70src/commands/Admin/RemoteDebug.js
-
121src/commands/Admin/RemoteDiagnose.js
-
63src/commands/Admin/Restart.js
-
69src/commands/Admin/Sessions.js
-
33src/commands/Admin/Speedtest.js
-
27src/commands/Admin/UnloadModule.js
-
53src/commands/Admin/Update.js
-
94src/commands/Admin/UpdateTeam.js
-
25src/commands/Admin/Username.js
-
76src/commands/Info/Info.js
-
30src/commands/Info/Ping.js
-
47src/commands/Info/Premium.js
-
107src/commands/Info/Stats.js
-
37src/commands/Info/Uptime.js
-
40src/commands/Misc/Avatar.js
-
75src/commands/Misc/Botlist.js
-
41src/commands/Misc/Color.js
-
35src/commands/Misc/Discrim.js
-
64src/commands/Misc/Distance.js
-
41src/commands/Misc/DynoAvatar.js
-
52src/commands/Misc/Emotes.js
-
94src/commands/Misc/InviteInfo.js
-
65src/commands/Misc/MemberCount.js
-
37src/commands/Misc/RandomColor.js
-
68src/commands/Misc/ServerInfo.js
-
128src/commands/Misc/Whois.js
-
60src/commands/Roles/RoleInfo.js
-
64src/commands/Roles/Roles.js
-
575src/core/Dyno.js
-
15src/core/RPCClient.js
-
125src/core/RPCServer.js
-
145src/core/cluster/Cluster.js
-
194src/core/cluster/Events.js
-
129src/core/cluster/Logger.js
-
185src/core/cluster/Manager.js
-
164src/core/cluster/Server.js
-
226src/core/cluster/Sharding.js
-
161src/core/clusterManager/Commands.js
-
121src/core/clusterManager/Logger.js
-
184src/core/clusterManager/Manager.js
-
111src/core/collections/CommandCollection.js
-
424src/core/collections/GuildCollection.js
-
203src/core/collections/ModuleCollection.js
-
108src/core/config.js
-
24src/core/database.js
-
98src/core/logger.js
-
278src/core/managers/EventManager.js
-
146src/core/managers/IPCManager.js
-
348src/core/managers/LangManager.js
-
37src/core/managers/PagerManager.js
-
105src/core/managers/PermissionsManager.js
-
147src/core/managers/RPCManager.js
-
93src/core/managers/WebhookManager.js
-
497src/core/matomo.js
-
182src/core/metrics.js
-
146src/core/processManager/Manager.js
-
99src/core/processManager/Process.js
-
36src/core/redis.js
-
14src/core/rpc/Client.js
-
44src/core/rpc/LogServer.js
-
13src/core/rpc/Server.js
@ -0,0 +1,4 @@ |
|||
.env |
|||
node_modules |
|||
npm-debug.logs |
|||
config/local* |
@ -0,0 +1,9 @@ |
|||
node_modules/* |
|||
test/* |
|||
build/* |
|||
public/* |
|||
private/* |
|||
views/* |
|||
*.md |
|||
*.json |
|||
commands/Admin/Eval.js |
@ -0,0 +1,108 @@ |
|||
|
|||
{ |
|||
"extends": "eslint:recommended", |
|||
"parserOptions": { |
|||
"ecmaVersion": 2017 |
|||
}, |
|||
"env": { |
|||
"node": true |
|||
}, |
|||
"globals": { |
|||
"Promise": true |
|||
}, |
|||
"rules": { |
|||
"accessor-pairs": "warn", |
|||
"array-callback-return": "error", |
|||
// "complexity": "warn", |
|||
"dot-location": ["error", "property"], |
|||
"dot-notation": "error", |
|||
"eqeqeq": "error", |
|||
"no-empty-function": "error", |
|||
"no-floating-decimal": "error", |
|||
"no-implied-eval": "error", |
|||
"no-invalid-this": "error", |
|||
"no-lone-blocks": "error", |
|||
"no-new-func": "error", |
|||
"no-new-wrappers": "error", |
|||
"no-new": "error", |
|||
"no-octal-escape": "error", |
|||
"no-return-assign": "error", |
|||
"no-self-compare": "error", |
|||
"no-sequences": "error", |
|||
"no-throw-literal": "error", |
|||
"no-unmodified-loop-condition": "error", |
|||
"no-unused-expressions": "error", |
|||
"no-useless-call": "error", |
|||
"no-useless-escape": "error", |
|||
"no-void": "error", |
|||
"no-warning-comments": "warn", |
|||
"wrap-iife": "error", |
|||
"yoda": "error", |
|||
|
|||
"no-label-var": "error", |
|||
"no-undef-init": "error", |
|||
|
|||
"callback-return": "error", |
|||
"handle-callback-err": "error", |
|||
"no-mixed-requires": "error", |
|||
"no-new-require": "error", |
|||
"no-path-concat": "error", |
|||
|
|||
"array-bracket-spacing": "error", |
|||
"block-spacing": "error", |
|||
"brace-style": ["error", "1tbs", { "allowSingleLine": true }], |
|||
"comma-dangle": ["error", "always-multiline"], |
|||
"comma-spacing": "error", |
|||
"comma-style": "error", |
|||
"computed-property-spacing": "error", |
|||
"consistent-this": ["error", "$this"], |
|||
"eol-last": "error", |
|||
"func-names": "error", |
|||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }], |
|||
"keyword-spacing": "error", |
|||
"max-depth": "error", |
|||
// "max-len": ["error", 120, 2], |
|||
"max-nested-callbacks": ["error", { "max": 4 }], |
|||
"max-statements-per-line": ["error", { "max": 2 }], |
|||
"new-cap": "error", |
|||
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 3 }], |
|||
"no-array-constructor": "error", |
|||
// "no-inline-comments": "error", |
|||
"no-lonely-if": "error", |
|||
"no-mixed-operators": "error", |
|||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], |
|||
"no-new-object": "error", |
|||
"no-spaced-func": "error", |
|||
"no-trailing-spaces": "error", |
|||
"no-unneeded-ternary": "error", |
|||
"no-whitespace-before-property": "error", |
|||
"object-curly-spacing": ["error", "always"], |
|||
"operator-assignment": "error", |
|||
"operator-linebreak": ["error", "after"], |
|||
"padded-blocks": ["error", "never"], |
|||
"quote-props": ["error", "as-needed"], |
|||
"quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], |
|||
"semi-spacing": "error", |
|||
"semi": "error", |
|||
"space-before-blocks": "error", |
|||
"space-before-function-paren": ["error", "never"], |
|||
"space-in-parens": "error", |
|||
"space-infix-ops": "error", |
|||
"space-unary-ops": "error", |
|||
"spaced-comment": "error", |
|||
"unicode-bom": "error", |
|||
|
|||
"arrow-body-style": "error", |
|||
"arrow-spacing": "error", |
|||
"no-duplicate-imports": "error", |
|||
"no-useless-computed-key": "error", |
|||
"no-useless-constructor": "error", |
|||
"prefer-arrow-callback": "error", |
|||
"prefer-rest-params": "error", |
|||
"prefer-spread": "error", |
|||
// "prefer-template": "error", |
|||
"rest-spread-spacing": "error", |
|||
"template-curly-spacing": "error", |
|||
"yield-star-spacing": "error" |
|||
} |
|||
} |
@ -0,0 +1,50 @@ |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
|
|||
# Runtime data |
|||
pids |
|||
*.pid |
|||
*.seed |
|||
|
|||
# Directory for instrumented libs generated by jscoverage/JSCover |
|||
lib-cov |
|||
|
|||
# Coverage directory used by tools like istanbul |
|||
coverage |
|||
|
|||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) |
|||
.grunt |
|||
|
|||
# node-waf configuration |
|||
.lock-wscript |
|||
|
|||
# Compiled binary addons (http://nodejs.org/api/addons.html) |
|||
build/Release |
|||
|
|||
# Dependency directory |
|||
node_modules |
|||
|
|||
# Optional npm cache directory |
|||
.npm |
|||
|
|||
# Optional REPL history |
|||
.node_repl_history |
|||
|
|||
# Ignore local database files |
|||
*.db |
|||
|
|||
.env |
|||
private |
|||
.idea |
|||
.vscode |
|||
.DS_Store |
|||
jsconfig.json |
|||
pm2.json |
|||
Apollo.js |
|||
build |
|||
package-lock.json |
|||
config/local* |
|||
src/core/crashreport* |
|||
report.* |
@ -0,0 +1,3 @@ |
|||
[submodule "translations"] |
|||
path = translations |
|||
url = [email protected]:FlexLabs/dyno-translations.git |
@ -0,0 +1 @@ |
|||
|
@ -0,0 +1,23 @@ |
|||
FROM node:carbon-alpine |
|||
|
|||
WORKDIR /app |
|||
ARG NPM_TOKEN |
|||
|
|||
COPY package*.json ./ |
|||
COPY .npmrc .npmrc |
|||
|
|||
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc |
|||
RUN apk add --no-cache --update python build-base git \ |
|||
&& rm -rf /var/cache/apk/* |
|||
|
|||
RUN yarn cache clean |
|||
RUN yarn install --production |
|||
|
|||
COPY . . |
|||
|
|||
RUN rm -f .npmrc |
|||
RUN apk del python build-base git |
|||
|
|||
ENV NODE_ENV production |
|||
|
|||
CMD [ "yarn", "start" ] |
@ -0,0 +1,402 @@ |
|||
Attribution-NonCommercial-NoDerivatives 4.0 International |
|||
|
|||
======================================================================= |
|||
|
|||
Creative Commons Corporation ("Creative Commons") is not a law firm and |
|||
does not provide legal services or legal advice. Distribution of |
|||
Creative Commons public licenses does not create a lawyer-client or |
|||
other relationship. Creative Commons makes its licenses and related |
|||
information available on an "as-is" basis. Creative Commons gives no |
|||
warranties regarding its licenses, any material licensed under their |
|||
terms and conditions, or any related information. Creative Commons |
|||
disclaims all liability for damages resulting from their use to the |
|||
fullest extent possible. |
|||
|
|||
Using Creative Commons Public Licenses |
|||
|
|||
Creative Commons public licenses provide a standard set of terms and |
|||
conditions that creators and other rights holders may use to share |
|||
original works of authorship and other material subject to copyright |
|||
and certain other rights specified in the public license below. The |
|||
following considerations are for informational purposes only, are not |
|||
exhaustive, and do not form part of our licenses. |
|||
|
|||
Considerations for licensors: Our public licenses are |
|||
intended for use by those authorized to give the public |
|||
permission to use material in ways otherwise restricted by |
|||
copyright and certain other rights. Our licenses are |
|||
irrevocable. Licensors should read and understand the terms |
|||
and conditions of the license they choose before applying it. |
|||
Licensors should also secure all rights necessary before |
|||
applying our licenses so that the public can reuse the |
|||
material as expected. Licensors should clearly mark any |
|||
material not subject to the license. This includes other CC- |
|||
licensed material, or material used under an exception or |
|||
limitation to copyright. More considerations for licensors: |
|||
wiki.creativecommons.org/Considerations_for_licensors |
|||
|
|||
Considerations for the public: By using one of our public |
|||
licenses, a licensor grants the public permission to use the |
|||
licensed material under specified terms and conditions. If |
|||
the licensor's permission is not necessary for any reason--for |
|||
example, because of any applicable exception or limitation to |
|||
copyright--then that use is not regulated by the license. Our |
|||
licenses grant only permissions under copyright and certain |
|||
other rights that a licensor has authority to grant. Use of |
|||
the licensed material may still be restricted for other |
|||
reasons, including because others have copyright or other |
|||
rights in the material. A licensor may make special requests, |
|||
such as asking that all changes be marked or described. |
|||
Although not required by our licenses, you are encouraged to |
|||
respect those requests where reasonable. More_considerations |
|||
for the public: |
|||
wiki.creativecommons.org/Considerations_for_licensees |
|||
|
|||
======================================================================= |
|||
|
|||
Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 |
|||
International Public License |
|||
|
|||
By exercising the Licensed Rights (defined below), You accept and agree |
|||
to be bound by the terms and conditions of this Creative Commons |
|||
Attribution-NonCommercial-NoDerivatives 4.0 International Public |
|||
License ("Public License"). To the extent this Public License may be |
|||
interpreted as a contract, You are granted the Licensed Rights in |
|||
consideration of Your acceptance of these terms and conditions, and the |
|||
Licensor grants You such rights in consideration of benefits the |
|||
Licensor receives from making the Licensed Material available under |
|||
these terms and conditions. |
|||
|
|||
|
|||
Section 1 -- Definitions. |
|||
|
|||
a. Adapted Material means material subject to Copyright and Similar |
|||
Rights that is derived from or based upon the Licensed Material |
|||
and in which the Licensed Material is translated, altered, |
|||
arranged, transformed, or otherwise modified in a manner requiring |
|||
permission under the Copyright and Similar Rights held by the |
|||
Licensor. For purposes of this Public License, where the Licensed |
|||
Material is a musical work, performance, or sound recording, |
|||
Adapted Material is always produced where the Licensed Material is |
|||
synched in timed relation with a moving image. |
|||
|
|||
b. Copyright and Similar Rights means copyright and/or similar rights |
|||
closely related to copyright including, without limitation, |
|||
performance, broadcast, sound recording, and Sui Generis Database |
|||
Rights, without regard to how the rights are labeled or |
|||
categorized. For purposes of this Public License, the rights |
|||
specified in Section 2(b)(1)-(2) are not Copyright and Similar |
|||
Rights. |
|||
|
|||
c. Effective Technological Measures means those measures that, in the |
|||
absence of proper authority, may not be circumvented under laws |
|||
fulfilling obligations under Article 11 of the WIPO Copyright |
|||
Treaty adopted on December 20, 1996, and/or similar international |
|||
agreements. |
|||
|
|||
d. Exceptions and Limitations means fair use, fair dealing, and/or |
|||
any other exception or limitation to Copyright and Similar Rights |
|||
that applies to Your use of the Licensed Material. |
|||
|
|||
e. Licensed Material means the artistic or literary work, database, |
|||
or other material to which the Licensor applied this Public |
|||
License. |
|||
|
|||
f. Licensed Rights means the rights granted to You subject to the |
|||
terms and conditions of this Public License, which are limited to |
|||
all Copyright and Similar Rights that apply to Your use of the |
|||
Licensed Material and that the Licensor has authority to license. |
|||
|
|||
g. Licensor means the individual(s) or entity(ies) granting rights |
|||
under this Public License. |
|||
|
|||
h. NonCommercial means not primarily intended for or directed towards |
|||
commercial advantage or monetary compensation. For purposes of |
|||
this Public License, the exchange of the Licensed Material for |
|||
other material subject to Copyright and Similar Rights by digital |
|||
file-sharing or similar means is NonCommercial provided there is |
|||
no payment of monetary compensation in connection with the |
|||
exchange. |
|||
|
|||
i. Share means to provide material to the public by any means or |
|||
process that requires permission under the Licensed Rights, such |
|||
as reproduction, public display, public performance, distribution, |
|||
dissemination, communication, or importation, and to make material |
|||
available to the public including in ways that members of the |
|||
public may access the material from a place and at a time |
|||
individually chosen by them. |
|||
|
|||
j. Sui Generis Database Rights means rights other than copyright |
|||
resulting from Directive 96/9/EC of the European Parliament and of |
|||
the Council of 11 March 1996 on the legal protection of databases, |
|||
as amended and/or succeeded, as well as other essentially |
|||
equivalent rights anywhere in the world. |
|||
|
|||
k. You means the individual or entity exercising the Licensed Rights |
|||
under this Public License. Your has a corresponding meaning. |
|||
|
|||
|
|||
Section 2 -- Scope. |
|||
|
|||
a. License grant. |
|||
|
|||
1. Subject to the terms and conditions of this Public License, |
|||
the Licensor hereby grants You a worldwide, royalty-free, |
|||
non-sublicensable, non-exclusive, irrevocable license to |
|||
exercise the Licensed Rights in the Licensed Material to: |
|||
|
|||
a. reproduce and Share the Licensed Material, in whole or |
|||
in part, for NonCommercial purposes only; and |
|||
|
|||
b. produce and reproduce, but not Share, Adapted Material |
|||
for NonCommercial purposes only. |
|||
|
|||
2. Exceptions and Limitations. For the avoidance of doubt, where |
|||
Exceptions and Limitations apply to Your use, this Public |
|||
License does not apply, and You do not need to comply with |
|||
its terms and conditions. |
|||
|
|||
3. Term. The term of this Public License is specified in Section |
|||
6(a). |
|||
|
|||
4. Media and formats; technical modifications allowed. The |
|||
Licensor authorizes You to exercise the Licensed Rights in |
|||
all media and formats whether now known or hereafter created, |
|||
and to make technical modifications necessary to do so. The |
|||
Licensor waives and/or agrees not to assert any right or |
|||
authority to forbid You from making technical modifications |
|||
necessary to exercise the Licensed Rights, including |
|||
technical modifications necessary to circumvent Effective |
|||
Technological Measures. For purposes of this Public License, |
|||
simply making modifications authorized by this Section 2(a) |
|||
(4) never produces Adapted Material. |
|||
|
|||
5. Downstream recipients. |
|||
|
|||
a. Offer from the Licensor -- Licensed Material. Every |
|||
recipient of the Licensed Material automatically |
|||
receives an offer from the Licensor to exercise the |
|||
Licensed Rights under the terms and conditions of this |
|||
Public License. |
|||
|
|||
b. No downstream restrictions. You may not offer or impose |
|||
any additional or different terms or conditions on, or |
|||
apply any Effective Technological Measures to, the |
|||
Licensed Material if doing so restricts exercise of the |
|||
Licensed Rights by any recipient of the Licensed |
|||
Material. |
|||
|
|||
6. No endorsement. Nothing in this Public License constitutes or |
|||
may be construed as permission to assert or imply that You |
|||
are, or that Your use of the Licensed Material is, connected |
|||
with, or sponsored, endorsed, or granted official status by, |
|||
the Licensor or others designated to receive attribution as |
|||
provided in Section 3(a)(1)(A)(i). |
|||
|
|||
b. Other rights. |
|||
|
|||
1. Moral rights, such as the right of integrity, are not |
|||
licensed under this Public License, nor are publicity, |
|||
privacy, and/or other similar personality rights; however, to |
|||
the extent possible, the Licensor waives and/or agrees not to |
|||
assert any such rights held by the Licensor to the limited |
|||
extent necessary to allow You to exercise the Licensed |
|||
Rights, but not otherwise. |
|||
|
|||
2. Patent and trademark rights are not licensed under this |
|||
Public License. |
|||
|
|||
3. To the extent possible, the Licensor waives any right to |
|||
collect royalties from You for the exercise of the Licensed |
|||
Rights, whether directly or through a collecting society |
|||
under any voluntary or waivable statutory or compulsory |
|||
licensing scheme. In all other cases the Licensor expressly |
|||
reserves any right to collect such royalties, including when |
|||
the Licensed Material is used other than for NonCommercial |
|||
purposes. |
|||
|
|||
|
|||
Section 3 -- License Conditions. |
|||
|
|||
Your exercise of the Licensed Rights is expressly made subject to the |
|||
following conditions. |
|||
|
|||
a. Attribution. |
|||
|
|||
1. If You Share the Licensed Material, You must: |
|||
|
|||
a. retain the following if it is supplied by the Licensor |
|||
with the Licensed Material: |
|||
|
|||
i. identification of the creator(s) of the Licensed |
|||
Material and any others designated to receive |
|||
attribution, in any reasonable manner requested by |
|||
the Licensor (including by pseudonym if |
|||
designated); |
|||
|
|||
ii. a copyright notice; |
|||
|
|||
iii. a notice that refers to this Public License; |
|||
|
|||
iv. a notice that refers to the disclaimer of |
|||
warranties; |
|||
|
|||
v. a URI or hyperlink to the Licensed Material to the |
|||
extent reasonably practicable; |
|||
|
|||
b. indicate if You modified the Licensed Material and |
|||
retain an indication of any previous modifications; and |
|||
|
|||
c. indicate the Licensed Material is licensed under this |
|||
Public License, and include the text of, or the URI or |
|||
hyperlink to, this Public License. |
|||
|
|||
For the avoidance of doubt, You do not have permission under |
|||
this Public License to Share Adapted Material. |
|||
|
|||
2. You may satisfy the conditions in Section 3(a)(1) in any |
|||
reasonable manner based on the medium, means, and context in |
|||
which You Share the Licensed Material. For example, it may be |
|||
reasonable to satisfy the conditions by providing a URI or |
|||
hyperlink to a resource that includes the required |
|||
information. |
|||
|
|||
3. If requested by the Licensor, You must remove any of the |
|||
information required by Section 3(a)(1)(A) to the extent |
|||
reasonably practicable. |
|||
|
|||
|
|||
Section 4 -- Sui Generis Database Rights. |
|||
|
|||
Where the Licensed Rights include Sui Generis Database Rights that |
|||
apply to Your use of the Licensed Material: |
|||
|
|||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right |
|||
to extract, reuse, reproduce, and Share all or a substantial |
|||
portion of the contents of the database for NonCommercial purposes |
|||
only and provided You do not Share Adapted Material; |
|||
|
|||
b. if You include all or a substantial portion of the database |
|||
contents in a database in which You have Sui Generis Database |
|||
Rights, then the database in which You have Sui Generis Database |
|||
Rights (but not its individual contents) is Adapted Material; and |
|||
|
|||
c. You must comply with the conditions in Section 3(a) if You Share |
|||
all or a substantial portion of the contents of the database. |
|||
|
|||
For the avoidance of doubt, this Section 4 supplements and does not |
|||
replace Your obligations under this Public License where the Licensed |
|||
Rights include other Copyright and Similar Rights. |
|||
|
|||
|
|||
Section 5 -- Disclaimer of Warranties and Limitation of Liability. |
|||
|
|||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE |
|||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS |
|||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF |
|||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, |
|||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, |
|||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR |
|||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, |
|||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT |
|||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT |
|||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. |
|||
|
|||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE |
|||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, |
|||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, |
|||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, |
|||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR |
|||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN |
|||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR |
|||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR |
|||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. |
|||
|
|||
c. The disclaimer of warranties and limitation of liability provided |
|||
above shall be interpreted in a manner that, to the extent |
|||
possible, most closely approximates an absolute disclaimer and |
|||
waiver of all liability. |
|||
|
|||
|
|||
Section 6 -- Term and Termination. |
|||
|
|||
a. This Public License applies for the term of the Copyright and |
|||
Similar Rights licensed here. However, if You fail to comply with |
|||
this Public License, then Your rights under this Public License |
|||
terminate automatically. |
|||
|
|||
b. Where Your right to use the Licensed Material has terminated under |
|||
Section 6(a), it reinstates: |
|||
|
|||
1. automatically as of the date the violation is cured, provided |
|||
it is cured within 30 days of Your discovery of the |
|||
violation; or |
|||
|
|||
2. upon express reinstatement by the Licensor. |
|||
|
|||
For the avoidance of doubt, this Section 6(b) does not affect any |
|||
right the Licensor may have to seek remedies for Your violations |
|||
of this Public License. |
|||
|
|||
c. For the avoidance of doubt, the Licensor may also offer the |
|||
Licensed Material under separate terms or conditions or stop |
|||
distributing the Licensed Material at any time; however, doing so |
|||
will not terminate this Public License. |
|||
|
|||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public |
|||
License. |
|||
|
|||
|
|||
Section 7 -- Other Terms and Conditions. |
|||
|
|||
a. The Licensor shall not be bound by any additional or different |
|||
terms or conditions communicated by You unless expressly agreed. |
|||
|
|||
b. Any arrangements, understandings, or agreements regarding the |
|||
Licensed Material not stated herein are separate from and |
|||
independent of the terms and conditions of this Public License. |
|||
|
|||
|
|||
Section 8 -- Interpretation. |
|||
|
|||
a. For the avoidance of doubt, this Public License does not, and |
|||
shall not be interpreted to, reduce, limit, restrict, or impose |
|||
conditions on any use of the Licensed Material that could lawfully |
|||
be made without permission under this Public License. |
|||
|
|||
b. To the extent possible, if any provision of this Public License is |
|||
deemed unenforceable, it shall be automatically reformed to the |
|||
minimum extent necessary to make it enforceable. If the provision |
|||
cannot be reformed, it shall be severed from this Public License |
|||
without affecting the enforceability of the remaining terms and |
|||
conditions. |
|||
|
|||
c. No term or condition of this Public License will be waived and no |
|||
failure to comply consented to unless expressly agreed to by the |
|||
Licensor. |
|||
|
|||
d. Nothing in this Public License constitutes or may be interpreted |
|||
as a limitation upon, or waiver of, any privileges and immunities |
|||
that apply to the Licensor or You, including from the legal |
|||
processes of any jurisdiction or authority. |
|||
|
|||
======================================================================= |
|||
|
|||
Creative Commons is not a party to its public |
|||
licenses. Notwithstanding, Creative Commons may elect to apply one of |
|||
its public licenses to material it publishes and in those instances |
|||
will be considered the “Licensor.” The text of the Creative Commons |
|||
public licenses is dedicated to the public domain under the CC0 Public |
|||
Domain Dedication. Except for the limited purpose of indicating that |
|||
material is shared under a Creative Commons public license or as |
|||
otherwise permitted by the Creative Commons policies published at |
|||
creativecommons.org/policies, Creative Commons does not authorize the |
|||
use of the trademark "Creative Commons" or any other trademark or logo |
|||
of Creative Commons without its prior written consent including, |
|||
without limitation, in connection with any unauthorized modifications |
|||
to any of its public licenses or any other arrangements, |
|||
understandings, or agreements concerning use of licensed material. For |
|||
the avoidance of doubt, this paragraph does not form part of the |
|||
public licenses. |
|||
|
|||
Creative Commons may be contacted at creativecommons.org. |
@ -0,0 +1 @@ |
|||
Original readme removed. |
@ -0,0 +1,123 @@ |
|||
--- |
|||
name: Dyno |
|||
author: NoobLance#0002 |
|||
version: 4.0.0 |
|||
lib: eris |
|||
poweredBy: Dyno |
|||
|
|||
# prefixes |
|||
prefix: '?' |
|||
sudopref: '$' |
|||
localPrefix: |
|||
adminPrefix: d. |
|||
|
|||
test: true |
|||
state: 0 |
|||
stateName: Default |
|||
logLevel: debug |
|||
isPremium: false |
|||
|
|||
# cluster/sharding |
|||
clusterCount: 1 |
|||
shardingStrategy: shared |
|||
firstShardOverride: 0 |
|||
lastShardOverride: 0 |
|||
shardCountOverride: 1 |
|||
clusterIds: |
|||
shardIds: |
|||
|
|||
# client config |
|||
client: |
|||
id: '' |
|||
secret: '' |
|||
token: '' |
|||
userid: '' |
|||
game: dynobot.net | ?help |
|||
admin: '155037590859284481' |
|||
fetchAllUsers: false |
|||
disableEveryone: false |
|||
maxCachedMessages: 10 |
|||
|
|||
|
|||
snowgate: |
|||
host: '' |
|||
|
|||
site: |
|||
host: http://localhost |
|||
port: 80 |
|||
listen_port: 8000 |
|||
|
|||
mongo: |
|||
dsn: mongodb://localhost/discordbot |
|||
|
|||
redis: |
|||
host: localhost |
|||
port: 6379 |
|||
auth: '' |
|||
|
|||
sentry: |
|||
dsn: '' |
|||
logLevel: error |
|||
|
|||
statsd: |
|||
host: '' |
|||
port: 4280 |
|||
prefix: dyno.dev. |
|||
|
|||
emojis: |
|||
success: '<:dynoSuccess:314691591484866560>' |
|||
error: '<:dynoError:314691684455809024>' |
|||
|
|||
# bot list config |
|||
carbon: |
|||
key: '' |
|||
url: https://www.carbonitex.net/discord/data/botdata.php |
|||
list: https://www.carbonitex.net/discord/api/listedbots.php |
|||
info: https://www.carbonitex.net/discord/api/bot/info?id=155149108183695360 |
|||
dbots: |
|||
key: '' |
|||
url: https://bots.discord.pw/api/bots/155149108183695360/stats |
|||
dbl: |
|||
key: '' |
|||
url: https://discordbots.org/api/bots/155149108183695360/stats |
|||
botspace: |
|||
key: '' |
|||
url: https://botlist.space/spi/bots/ |
|||
cleverbot: |
|||
key: '' |
|||
|
|||
invite: https://discord.gg/9W6EG56 |
|||
avatar: https://cdn.dyno.gg/dyno-v3x1024.png |
|||
|
|||
dynoGuild: '203039963636301824' |
|||
guildLog: '205567372021465088' |
|||
largeGuildLog: '243639503749775360' |
|||
testGuilds: |
|||
- '203039963636301824' |
|||
- '155149443606380545' |
|||
|
|||
shardWebhook: https://canary.discordapp.com/api/webhooks/263596728299683850/fygpIRg8pcD9nLPL2MxUjK8mupD6dnLfzA1eIwocoD_MnFzba1noE0sXY4XY_ZNfkPtt |
|||
cluster: |
|||
webhookUrl: https://canary.discordapp.com/api/webhooks/263596728299683850/fygpIRg8pcD9nLPL2MxUjK8mupD6dnLfzA1eIwocoD_MnFzba1noE0sXY4XY_ZNfkPtt |
|||
|
|||
disableHeartbeat: true |
|||
logCommands: false |
|||
handleRegion: false |
|||
regions: [] |
|||
disableEvents: |
|||
- TYPING_START |
|||
|
|||
enabledCommandGroups: |
|||
disabledCommandGroups: |
|||
|
|||
disableHelp: false |
|||
maxStreamLimit: 4000 |
|||
maxSongLength: 5400 |
|||
maxPlayingTime: 14400000 |
|||
streamLimitThreshold: 86400 |
|||
|
|||
modules: |
|||
- AdminHandler |
|||
- CommandHandler |
|||
- ShardStatus |
|||
- Dyno |
@ -0,0 +1,34 @@ |
|||
--- |
|||
# prefixes |
|||
prefix: '?' |
|||
sudopref: '$' |
|||
localPrefix: '.' |
|||
adminPrefix: t. |
|||
|
|||
env: 'dev' |
|||
test: true |
|||
state: 2 |
|||
stateName: Development |
|||
logLevel: debug |
|||
isPremium: false |
|||
|
|||
# sharding |
|||
clusterCount: 1 |
|||
shardingStrategy: process |
|||
shardCountOverride: 1 |
|||
|
|||
site: |
|||
host: http://localhost |
|||
port: 80 |
|||
listen_port: 8000 |
|||
|
|||
colorapi: |
|||
host: https://color.dyno.gg |
|||
|
|||
disableHelp: true |
|||
|
|||
modules: |
|||
- AdminHandler |
|||
- CommandHandler |
|||
- ShardStatus |
|||
- Dyno |
@ -0,0 +1,131 @@ |
|||
--- |
|||
# prefixes |
|||
prefix: "?" |
|||
sudopref: "$" |
|||
localPrefix: |
|||
adminPrefix: d. |
|||
|
|||
env: 'prod' |
|||
test: false |
|||
state: 3 |
|||
stateName: Prod |
|||
logLevel: info |
|||
|
|||
isCore: true |
|||
isPremium: false |
|||
|
|||
# sharding |
|||
clusterCount: 1 |
|||
shardingStrategy: balanced |
|||
firstShardOverride: |
|||
lastShardOverride: |
|||
shardCountOverride: |
|||
|
|||
# client |
|||
client: |
|||
id: '' |
|||
secret: '' |
|||
token: '' |
|||
userid: '' |
|||
game: dynobot.net | ?help |
|||
admin: '155037590859284481' |
|||
fetchAllUsers: false |
|||
disableEveryone: false |
|||
maxCachedMessages: 10 |
|||
lazyChunking: true |
|||
|
|||
snowgate: |
|||
host: '' |
|||
|
|||
site: |
|||
host: http://localhost |
|||
port: 80 |
|||
listen_port: 8000 |
|||
|
|||
mongo: |
|||
dsn: mongodb://localhost/discordbot |
|||
|
|||
redis: |
|||
host: localhost |
|||
port: 6379 |
|||
auth: '' |
|||
|
|||
sentry: |
|||
# dsn: https://dedbe1a9f79c4b7384dacf0514235665:[email protected]/8 |
|||
logLevel: error |
|||
|
|||
statsd: |
|||
host: '' |
|||
port: 4280 |
|||
prefix: dyno.dev. |
|||
|
|||
emojis: |
|||
success: "<:dynoSuccess:314691591484866560>" |
|||
error: "<:dynoError:314691684455809024>" |
|||
|
|||
carbon: |
|||
key: '' |
|||
url: https://www.carbonitex.net/discord/data/botdata.php |
|||
list: https://www.carbonitex.net/discord/api/listedbots.php |
|||
info: https://www.carbonitex.net/discord/api/bot/info?id=155149108183695360 |
|||
|
|||
dbots: |
|||
key: '' |
|||
url: https://bots.discord.pw/api/bots/155149108183695360/stats |
|||
|
|||
dbl: |
|||
key: '' |
|||
url: https://discordbots.org/api |
|||
|
|||
invite: https://discord.gg/9W6EG56 |
|||
avatar: http://cdn.dyno.gg/dyno-av-v3x1024.png |
|||
|
|||
dynoGuild: '203039963636301824' |
|||
guildLog: '205567372021465088' |
|||
largeGuildLog: '243639503749775360' |
|||
|
|||
# webhooks |
|||
blockWebhook: https://canary.discordapp.com/api/webhooks/543125066133798922/zm5dAE84tmfsAJuh7vP4uW2fGIbhVOU1sBJdr5i-uTojreaEVLTWzTdKjzpddCrj2qWw |
|||
shardWebhook: https://canary.discordapp.com/api/webhooks/511859673171886090/vC_L2tARlJIdIEFOnpJQPFTPPZ1gzA8LUoEaRQnXBCFK1sDXHKHPJ2mWF6XaTNwIrbhz |
|||
cluster: |
|||
webhookUrl: https://canary.discordapp.com/api/webhooks/511859572617773078/HR9nzO1SbIbg6Wdy2TebJU9gngDg-vY_iPqX2L_WjzUkT74HbknKP4qiJN69pluUKG95 |
|||
|
|||
disableHeartbeat: true |
|||
disableHelp: false |
|||
logCommands: false |
|||
handleRegion: false |
|||
disableEvents: |
|||
- TYPING_START |
|||
maxStreamLimit: 4000 |
|||
maxSongLength: 5400 |
|||
maxPlayingTime: 14400000 |
|||
streamLimitThreshold: 86400 |
|||
|
|||
modules: |
|||
- AdminHandler |
|||
- CommandHandler |
|||
- Manager |
|||
- ShardStatus |
|||
- Lavalink |
|||
- Dyno |
|||
- ActionLog |
|||
- Announcements |
|||
- Automod |
|||
- Automessage |
|||
- Autopurge |
|||
- Autoroles |
|||
- Autoresponder |
|||
- CustomCommands |
|||
- AFK |
|||
- Moderation |
|||
- Music |
|||
- Reminders |
|||
- Tags |
|||
- VoiceTextLinking |
|||
- Streams |
|||
- Fun |
|||
- Cleverbot |
|||
- CoordsChannel |
|||
- ERM |
|||
- MessageEmbedder |
|||
- Slowmode |
@ -0,0 +1,37 @@ |
|||
'use strict'; |
|||
|
|||
const gulp = require('gulp'); |
|||
const babel = require('gulp-babel'); |
|||
const eslint = require('gulp-eslint'); |
|||
|
|||
const paths = ['src/**/*.js']; |
|||
|
|||
gulp.task('default', ['build']); |
|||
|
|||
gulp.task('watch', ['build'], () => { |
|||
gulp.watch(paths, ['build']); |
|||
}); |
|||
|
|||
gulp.task('build', ['babel']); |
|||
|
|||
gulp.task('lint', () => { |
|||
gulp.src(paths) |
|||
.pipe(eslint()) |
|||
.pipe(eslint.format()) |
|||
.pipe(eslint.failAfterError()); |
|||
}); |
|||
|
|||
// gulp.task('sass', () =>
|
|||
// gulp.src('./public/css/**/*.scss')
|
|||
// .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError))
|
|||
// .pipe(gulp.dest('./public/css')));
|
|||
|
|||
// gulp.task('sass:watch', () => {
|
|||
// gulp.watch('./public/css/**/*.scss', ['sass']);
|
|||
// });
|
|||
|
|||
gulp.task('babel', () => { |
|||
gulp.src(paths) |
|||
.pipe(babel()) |
|||
.pipe(gulp.dest('build')); |
|||
}); |
@ -0,0 +1,79 @@ |
|||
{ |
|||
"name": "Dyno", |
|||
"version": "4.0.0", |
|||
"homepage": "https://github.com/FlexLabs/Dyno", |
|||
"author": { |
|||
"name": "Brian Tanner", |
|||
"email": "[email protected]" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://github.com/FlexLabs/Dyno.git" |
|||
}, |
|||
"bugs": { |
|||
"url": "https://github.com/FlexLabs/Dyno/issues" |
|||
}, |
|||
"private": true, |
|||
"scripts": { |
|||
"start": "node src/start.js" |
|||
}, |
|||
"main": "src/index.js", |
|||
"typings": "src/index.d.ts", |
|||
"engines": { |
|||
"node": ">=6.0.0" |
|||
}, |
|||
"engineStrict": true, |
|||
"dependencies": { |
|||
"@dyno.gg/customcommands": "^1.1.0", |
|||
"@dyno.gg/datafactory": "^1.4.3", |
|||
"@dyno.gg/dyno-core": "^1.3.0", |
|||
"@dyno.gg/eris": "0.8.18", |
|||
"async-each": "^1.0.1", |
|||
"axios": "^0.17.1", |
|||
"blocked": "^1.2.1", |
|||
"bluebird": "^3.4.1", |
|||
"bufferutil": ">=1.2.1", |
|||
"cli-progress": "^3.1.0", |
|||
"config": "^1.29.4", |
|||
"dot-object": "^1.9.0", |
|||
"envkey": "^1.2.7", |
|||
"eventemitter3": "^2.0.2", |
|||
"express": "^4.16.4", |
|||
"glob": "^7.1.2", |
|||
"glob-promise": "^3.1.0", |
|||
"google": "^2.1.0", |
|||
"hot-shots": "^4.3.1", |
|||
"ioredis": "^3.2.1", |
|||
"ioredis-lock": "^3.4.0", |
|||
"jayson": "^3.0.2", |
|||
"js-yaml": "^3.10.0", |
|||
"matomo-tracker": "^2.2.0", |
|||
"minimatch": "^3.0.4", |
|||
"moment": "^2.13.0", |
|||
"moment-duration-format": "^1.3.0", |
|||
"mongoose_schema-json": "^1.0.3", |
|||
"node-oom-heapdump": "^1.1.4", |
|||
"pidusage": "^1.1.0", |
|||
"prom-client": "^11.3.0", |
|||
"raven": "^1.1.1", |
|||
"snowtransfer": "^0.2.1", |
|||
"uuid": "^3.3.2", |
|||
"winston": "github:briantanner/winston", |
|||
"zlib-sync": "^0.1.5" |
|||
}, |
|||
"devDependencies": { |
|||
"eslint": "^3.5.0", |
|||
"gulp": "^3.9.1", |
|||
"gulp-eslint": "^3.0.1", |
|||
"longjohn": "^0.2.12" |
|||
}, |
|||
"optionalDependencies": { |
|||
"@dyno.gg/automod": "^1.3.5", |
|||
"@dyno.gg/autoroles": "^1.1.5", |
|||
"@dyno.gg/fun": "0.3.3", |
|||
"@dyno.gg/manager": "^1.0.11", |
|||
"@dyno.gg/moderation": "^1.3.8", |
|||
"@dyno.gg/modules": "^1.5.6", |
|||
"@dyno.gg/music": "^4.4.0" |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
{ |
|||
"apps": [ |
|||
{ |
|||
"name": "Dyno", |
|||
"script": "src/start.js", |
|||
"pmx": false, |
|||
"vizion": false, |
|||
"env_production": { "NODE_ENV": "production" } |
|||
} |
|||
] |
|||
} |
@ -0,0 +1,17 @@ |
|||
const dot = require('dot-object'); |
|||
|
|||
const config = { }; |
|||
|
|||
const flatConfig = dot.dot(config); |
|||
|
|||
for (let k of Object.keys(flatConfig)) { |
|||
switch (typeof flatConfig[k]) { |
|||
case 'object': |
|||
case 'string': |
|||
break; |
|||
default: |
|||
flatConfig[k] += `$typeof:${typeof flatConfig[k]}`; |
|||
} |
|||
} |
|||
|
|||
console.log(flatConfig); |
@ -0,0 +1,8 @@ |
|||
#!/bin/bash |
|||
|
|||
servers=("titan" "atlas" "pandora" "hype" "prom" "janus" "sinope" "narvi" "gany" "europa" "elara" "metis") |
|||
|
|||
for s in "${servers[@]}" |
|||
do |
|||
ssh dyno@"$s".dyno.lan $* |
|||
done |
@ -0,0 +1,105 @@ |
|||
const progress = require('cli-progress'); |
|||
const db = require('../src/core/database.js'); |
|||
const config = require('../src/core/config'); |
|||
const redis = require('../src/core/redis'); |
|||
|
|||
const clientId = config.client.id; |
|||
const shardCount = 1152; |
|||
|
|||
const multibar = new progress.MultiBar({ |
|||
format: '{name} |{bar}| {percentage}% | {duration_formatted} | {value}/{total}', |
|||
barCompleteChar: '\u2588', |
|||
barIncompleteChar: '\u2591', |
|||
clearOnComplete: false, |
|||
stopOnComplete: true, |
|||
hideCursor: true, |
|||
}); |
|||
|
|||
let mongoBar; |
|||
let redisBar; |
|||
|
|||
const buffer = []; |
|||
|
|||
async function flushToRedis() { |
|||
const batchSize = 1000; |
|||
const batchCount = Math.ceil(buffer.length / batchSize); |
|||
|
|||
let barProgress = 0; |
|||
redisBar = multibar.create(batchCount, barProgress, { name: 'Buffer -> Redis' }); |
|||
|
|||
let itemsInPipeline = 0; |
|||
let pipeline = redis.pipeline(); |
|||
|
|||
for (let e of buffer) { |
|||
const shardId = ~~((e._id / 4194304) % shardCount); |
|||
pipeline.hset(`guild_activity:${clientId}:${shardCount}:${shardId}`, e._id, e.lastActive); |
|||
|
|||
itemsInPipeline++; |
|||
|
|||
if (itemsInPipeline >= batchSize) { |
|||
await pipeline.exec(); |
|||
pipeline = redis.pipeline(); |
|||
itemsInPipeline = 0; |
|||
barProgress++; |
|||
redisBar.update(barProgress); |
|||
} |
|||
} |
|||
if (itemsInPipeline > 0) { |
|||
await pipeline.exec(); |
|||
} |
|||
barProgress++; |
|||
redisBar.update(barProgress); |
|||
redisBar.stop(); |
|||
} |
|||
|
|||
async function fetchMongoDocs() { |
|||
const coll = await db.collection('servers'); |
|||
const count = await coll.count({ deleted: false }); |
|||
|
|||
let i = 0; |
|||
mongoBar = multibar.create(count, i, { name: 'Mongo -> Buffer' }); |
|||
|
|||
coll.find({ deleted: false }, { projection: { lastActive: 1 } }).forEach((doc) => { |
|||
i++; |
|||
|
|||
if (!doc.lastActive) { |
|||
return mongoBar.update(i); |
|||
} |
|||
|
|||
buffer.push(doc); |
|||
|
|||
mongoBar.update(i); |
|||
}, |
|||
(err) => { |
|||
if (err) { |
|||
console.error(err); |
|||
process.exit(1); |
|||
} |
|||
mongoBar.stop(); |
|||
flushToRedis(); |
|||
}); |
|||
} |
|||
|
|||
setTimeout(fetchMongoDocs, 5000); |
|||
// models.Server.countDocuments({ deleted: false }).then(count => {
|
|||
// let i = 0, p = 0;
|
|||
// mongoBar = multibar.create(count, i, { name: 'Mongo -> Buffer' });
|
|||
|
|||
// models.Server.find({ deleted: false }, { lastActive: 1 })
|
|||
// .cursor()
|
|||
// .on('data', doc => {
|
|||
// i++;
|
|||
|
|||
// if (!doc.lastActive) {
|
|||
// return mongoBar.update(i);
|
|||
// }
|
|||
|
|||
// buffer.push(doc);
|
|||
|
|||
// mongoBar.update(i);
|
|||
// })
|
|||
// .on('end', () => {
|
|||
// mongoBar.stop();
|
|||
// flushToRedis();
|
|||
// });
|
|||
// });
|
@ -0,0 +1,101 @@ |
|||
const { collection, connection, models } = require('../src/core/database'); |
|||
const config = require('../src/core/config'); |
|||
|
|||
async function createClusters(options) { |
|||
try { |
|||
const { clientId, shardCount } = options; |
|||
const globalConfig = await models.Dyno.findOne().lean(); |
|||
let { clusterCount, serverMap } = globalConfig; |
|||
let firstShardId = 0; |
|||
let lastShardId = shardCount - 1; |
|||
|
|||
clusterCount = options.clusterCount || clusterCount; |
|||
|
|||
const shardIds = [...Array(1 + lastShardId - firstShardId).keys()].map(v => firstShardId + v); |
|||
let clusters = chunkArray(shardIds, clusterCount); |
|||
let servers = chunkArray(clusters, serverMap[clientId].length); |
|||
|
|||
clusters = servers.flatMap((s, i) => { |
|||
const server = serverMap[clientId][i]; |
|||
return s.map((c, i) => ({ |
|||
host: { |
|||
name: server.name, |
|||
hostname: server.host || `${server.name}.dyno.lan`, |
|||
state: server.state, |
|||
}, |
|||
clientId, |
|||
clusterCount, |
|||
shardCount, |
|||
firstShardId: c[0], |
|||
lastShardId: c[c.length-1], |
|||
env: options.env || 'dev', |
|||
})); |
|||
}).map((c, i) => ({ id: i, ...c })); |
|||
|
|||
const coll = collection('clusters'); |
|||
const states = serverMap[clientId].map(s => s.state); |
|||
|
|||
await coll.deleteMany({ 'host.state': { $in: states } }); |
|||
await coll.insertMany(clusters); |
|||
|
|||
connection.close(); |
|||
} catch (err) { |
|||
throw err; |
|||
} |
|||
} |
|||
|
|||
function chunkArray(arr, chunkCount) { |
|||
const arrLength = arr.length; |
|||
const tempArray = []; |
|||
let chunk = []; |
|||
|
|||
const chunkSize = Math.floor(arr.length / chunkCount); |
|||
let mod = arr.length % chunkCount; |
|||
let tempChunkSize = chunkSize; |
|||
|
|||
for (let i = 0; i < arrLength; i += tempChunkSize) { |
|||
tempChunkSize = chunkSize; |
|||
if (mod > 0) { |
|||
tempChunkSize = chunkSize + 1; |
|||
mod--; |
|||
} |
|||
chunk = arr.slice(i, i + tempChunkSize); |
|||
tempArray.push(chunk); |
|||
} |
|||
|
|||
return tempArray; |
|||
} |
|||
|
|||
createClusters({ |
|||
clientId: '174603832993513472', |
|||
shardCount: 2, |
|||
clusterCount: 2, |
|||
env: 'dev', |
|||
}); |
|||
|
|||
createClusters({ |
|||
clientId: '161660517914509312', |
|||
shardCount: 1152, |
|||
env: 'prod', |
|||
}); |
|||
|
|||
createClusters({ |
|||
clientId: '168274214858653696', |
|||
shardCount: 16, |
|||
clusterCount: 16, |
|||
env: 'premium', |
|||
}); |
|||
|
|||
createClusters({ |
|||
clientId: '347378090399236096', |
|||
shardCount: 2, |
|||
clusterCount: 2, |
|||
env: 'alpha', |
|||
}); |
|||
|
|||
// createClusters({
|
|||
// clientId: '161660517914509312',
|
|||
// shardCount: 1440,
|
|||
// });
|
|||
|
|||
// clusters.forEach(c => console.log(JSON.stringify(c)));
|
@ -0,0 +1,37 @@ |
|||
'use strict'; |
|||
|
|||
const axios = require('axios'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class SetAvatar extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['setavatar', 'setav']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Set the bot avatar'; |
|||
this.usage = 'avatar [url]'; |
|||
this.permissions = 'admin'; |
|||
this.extraPermissions = [this.config.owner || this.config.admin]; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
try { |
|||
var res = await axios.get(args[0], { |
|||
header: { Accept: 'image/*' }, |
|||
responseType: 'arraybuffer', |
|||
}).then(response => `data:${response.headers['content-type']};base64,${response.data.toString('base64')}`); |
|||
} catch (err) { |
|||
return this.error(message.channel, 'Failed to get a valid image.'); |
|||
} |
|||
|
|||
console.log(res); |
|||
|
|||
return this.client.editSelf({ avatar: res }) |
|||
.then(() => this.success(message.channel, 'Changed avatar.')) |
|||
.catch(() => this.error(message.channel, 'Failed setting avatar.')); |
|||
} |
|||
} |
|||
|
|||
module.exports = SetAvatar; |
@ -0,0 +1,28 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Changelog extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'changelog'; |
|||
this.aliases = ['changelog']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Add an item to the changelog.'; |
|||
this.usage = 'changelog [stuff]'; |
|||
this.overseerEnabled = true; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
return this.models.Changelog.insert({ entry: args.join(' ') }) |
|||
.then(() => this.success(message.channel, `Entry added.`)) |
|||
.catch(err => this.error(message.channel, err)); |
|||
} |
|||
} |
|||
|
|||
module.exports = Changelog; |
@ -0,0 +1,65 @@ |
|||
/* eslint-disable no-unused-vars */ |
|||
'use strict'; |
|||
|
|||
const util = require('util'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class CommandStats extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['commandstats', 'cs']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Get command stats for the past 7 days'; |
|||
this.usage = 'commandstats [command]'; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
let results = await this.models.CommandLog.aggregate([ |
|||
{ $group : { _id: '$command', count: { $sum: 1 } } }, |
|||
]).exec(); |
|||
|
|||
if (!results || !results.length) { |
|||
return this.error(message.channel, 'No results found.'); |
|||
} |
|||
|
|||
const commands = this.dyno.commands.filter(c => c.permissions === 'admin'); |
|||
const len = Math.max(...results.map(r => r._id.length)); |
|||
|
|||
results = results |
|||
.filter(r => !commands.find(c => c.name === r._id || c.aliases.includes(r._id))) |
|||
.sort((a, b) => (a.count < b.count) ? 1 : (a.count > b.count) ? -1 : 0) |
|||
.map(r => { // eslint-disable-line
|
|||
return { name: r._id, count: r.count }; |
|||
}); |
|||
|
|||
const embed = { |
|||
fields: [], |
|||
timestamp: new Date(), |
|||
}; |
|||
|
|||
const start = 25 * (args[0] ? parseInt(args[0]) - 1 : 0); |
|||
|
|||
const res = results.splice(start, 25); |
|||
for (let cmd of res) { |
|||
embed.fields.push({ name: cmd.name, value: cmd.count.toString(), inline: true }); |
|||
} |
|||
|
|||
this.sendMessage(message.channel, { embed }); |
|||
|
|||
// const msgArray = this.utils.splitMessage(results, 1990);
|
|||
|
|||
// for (let m of msgArray) {
|
|||
// this.sendCode(message.channel, m, 'js');
|
|||
// }
|
|||
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
|||
|
|||
module.exports = CommandStats; |
@ -0,0 +1,730 @@ |
|||
'use strict'; |
|||
|
|||
const each = require('async-each'); |
|||
const axios = require('axios'); |
|||
const moment = require('moment'); |
|||
const uuid = require('uuid/v4'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Data extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['data']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Get various stats and data.'; |
|||
this.defaultCommand = 'user'; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.hideFromHelp = true; |
|||
this.expectedArgs = 0; |
|||
|
|||
this.commands = [ |
|||
{ name: 'user', desc: 'Get information about a user.', default: true }, |
|||
{ name: 'premium', desc: 'Gets information about premium guilds of a user'}, |
|||
{ name: 'guilds', desc: 'Get a list of guilds.' }, |
|||
{ name: 'guild', desc: 'Get information about a guild.' }, |
|||
{ name: 'mods', desc: 'Get moderations by user for the past month.' }, |
|||
{ name: 'automod', desc: 'Get automod stats.' }, |
|||
{ name: 'topshared', desc: 'Top list of bots with guild counts and shared guilds' }, |
|||
{ name: 'addmodule', desc: 'NO' }, |
|||
{ name: 'associate', desc: 'Potato' }, |
|||
{ name: 'associates', desc: 'Potato' }, |
|||
{ name: 'shards', desc: 'Shard stats' }, |
|||
{ name: 'ishards', desc: 'Shard stats' }, |
|||
{ name: 'cfg', desc: 'Potato' }, |
|||
{ name: 'listing', desc: 'Get listing information for a server' }, |
|||
]; |
|||
|
|||
this.usage = [ |
|||
'data [user]', |
|||
'data user [user]', |
|||
'data guilds [page]', |
|||
'data automod', |
|||
]; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (!message.member) return false; |
|||
if (message.guild.id !== this.config.dynoGuild) return false; |
|||
|
|||
if (this.isServerAdmin(message.member, message.channel)) return true; |
|||
if (this.isServerMod(message.member, message.channel)) return true; |
|||
|
|||
let allowedRoles = [ |
|||
'225209883828420608', // Accomplices
|
|||
'355054563931324420', // Trusted
|
|||
]; |
|||
|
|||
const roles = message.guild.roles.filter(r => allowedRoles.includes(r.id)); |
|||
if (roles && message.member.roles.find(r => roles.find(role => role.id === r))) return true; |
|||
|
|||
return false; |
|||
} |
|||
|
|||
execute({ message }) { |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
async guilds({ message, args }) { |
|||
try { |
|||
var guilds = await this.models.Server.find({ deleted: false }) |
|||
.sort({ memberCount: -1 }) |
|||
.limit(25) |
|||
.skip(args[0] ? (args[0] - 1) * 25 : 0) |
|||
.lean() |
|||
.exec(); |
|||
} catch (err) { |
|||
return this.error(message.channel, err); |
|||
} |
|||
|
|||
if (!guilds || !guilds.length) { |
|||
return this.sendMessage(message.channel, 'No guilds returned.'); |
|||
} |
|||
|
|||
const embed = { |
|||
title: `Guilds - ${args[0] || 0}`, |
|||
fields: [], |
|||
}; |
|||
|
|||
for (let guild of guilds) { |
|||
embed.fields.push({ |
|||
name: guild.name, |
|||
value: `${guild._id}\t${guild.region}\t${guild.memberCount} members`, |
|||
inline: true, |
|||
}); |
|||
} |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
|
|||
async guild({ message, args }) { |
|||
const clientOptions = this.dyno.clientOptions; |
|||
const shardCount = parseInt(args[1] || clientOptions.shardCount, 10); |
|||
// const firstShardId = parseInt(args[2] || clientOptions.firstShardId, 10);
|
|||
// const lastShardId = parseInt(args[3] || clientOptions.lastShardId, 10);
|
|||
const clusterCount = parseInt(args[4] || clientOptions.clusterCount, 10); |
|||
|
|||
try { |
|||
var guild = await this.models.Server.findOne({ _id: args[0] || message.channel.guild.id }).lean(); |
|||
} catch (err) { |
|||
return this.error(message.channel, err); |
|||
} |
|||
|
|||
if (!guild) { |
|||
return this.error(message.channel, 'No guild found.'); |
|||
} |
|||
|
|||
// const shardIds = [...Array(1 + lastShardId - firstShardId).keys()].map(v => firstShardId + v);
|
|||
// const clusterShardCount = Math.ceil(shardIds.length / clusterCount);
|
|||
// const shardCounts = this.chunkArray(shardIds, clusterShardCount);
|
|||
|
|||
guild.shardId = ~~((args[0] / 4194304) % shardCount); |
|||
// guild.clusterId = shardCounts.findIndex(a => a.includes(guild.shardId));
|
|||
|
|||
if (guild.ownerID) { |
|||
var owner = await this.restClient.getRESTUser(guild.ownerID).catch(() => false); |
|||
} |
|||
|
|||
let premiumUser |
|||
if (guild.premiumUserId) { |
|||
premiumUser = await this.restClient.getRESTUser(guild.premiumUserId).catch(() => false); |
|||
} |
|||
|
|||
const embed = { |
|||
author: { |
|||
name: guild.name, |
|||
icon_url: guild.iconURL, |
|||
}, |
|||
fields: [ |
|||
// { name: 'Cluster', value: guild.clusterId.toString(), inline: true },
|
|||
{ name: 'Shard', value: guild.shardId.toString(), inline: true }, |
|||
{ name: 'Region', value: guild.region || 'Unknown', inline: true }, |
|||
{ name: 'Members', value: guild.memberCount ? guild.memberCount.toString() : '0', inline: true }, |
|||
], |
|||
footer: { text: `ID: ${guild._id}` }, |
|||
timestamp: new Date(), |
|||
}; |
|||
|
|||
embed.fields.push({ name: 'Prefix', value: guild.prefix || '?', inline: true }); |
|||
|
|||
embed.fields.push({ name: 'Mod Only', value: guild.modonly ? 'Yes' : 'No', inline: true }); |
|||
embed.fields.push({ name: 'Premium', value: guild.isPremium ? 'Yes' : 'No', inline: true }); |
|||
|
|||
embed.fields.push({ name: 'Owner ID', value: guild.ownerID || 'Unknown', inline: true }); |
|||
if (owner) { |
|||
embed.fields.push({ name: 'Owner', value: owner ? `${this.utils.fullName(owner)}` : guild.ownerID || 'Unknown', inline: true }); |
|||
} |
|||
|
|||
if (guild.premiumSince) { |
|||
embed.fields.push({ name: 'Premium Since', value: new Date(guild.premiumSince).toISOString().substr(0, 16), inline: true }); |
|||
} |
|||
|
|||
if (premiumUser) { |
|||
embed.fields.push({ name: 'Premium ID', value: `${premiumUser.id}`, inline: true }); |
|||
embed.fields.push({ name: 'Premium User', value: `${this.utils.fullName(premiumUser)}\n<@!${premiumUser.id}>`, inline: true }); |
|||
} |
|||
|
|||
if (guild.beta) { |
|||
embed.fields.push({ name: 'Beta', value: guild.beta ? 'Yes' : 'No', inline: true }); |
|||
} |
|||
|
|||
// START MODULES
|
|||
const modules = this.dyno.modules.filter(m => !m.admin && !m.core && m.list !== false); |
|||
|
|||
if (!modules) { |
|||
return this.error(message.channel, `Couldn't get a list of modules.`); |
|||
} |
|||
|
|||
const enabledModules = modules.filter(m => !guild.modules.hasOwnProperty(m.name) || |
|||
guild.modules[m.name] === true); |
|||
const disabledModules = modules.filter(m => guild.modules.hasOwnProperty(m.name) && |
|||
guild.modules[m.name] === false); |
|||
|
|||
if (enabledModules.length) { |
|||
embed.fields.push({ name: 'Enabled Modules', value: enabledModules.map(m => m.name).join(', '), inline: false }); |
|||
} |
|||
if (disabledModules.length) { |
|||
embed.fields.push({ name: 'Disabled Modules', value: disabledModules.map(m => m.name).join(', '), inline: false }); |
|||
} |
|||
|
|||
embed.fields.push({ name: '\u200b', value: `[Dashboard](https://www.dynobot.net/server/${guild._id})`, inline: true }); |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
|
|||
async listing({ message, args }) { |
|||
const guildId = args[0]; |
|||
|
|||
const coll = await this.db.collection('serverlist_store'); |
|||
const doc = await coll.findOne({ id: guildId }); |
|||
|
|||
if (!doc) { |
|||
return this.error(message.channel, 'No guild found.'); |
|||
} |
|||
|
|||
const embed = { |
|||
title: doc.name, |
|||
thumbnail: { |
|||
url: doc.icon, |
|||
}, |
|||
fields: [ |
|||
{ |
|||
name: 'Description', |
|||
value: doc.description || 'null', |
|||
inline: false, |
|||
}, |
|||
{ |
|||
name: 'Invite URL', |
|||
value: doc.inviteUrl || 'null', |
|||
inline: true, |
|||
}, |
|||
{ |
|||
name: 'Language', |
|||
value: doc.serverLanguage || 'null', |
|||
inline: true, |
|||
}, |
|||
{ |
|||
name: 'Categories', |
|||
value: doc.categoriesFlattened || 'null', |
|||
inline: true, |
|||
}, |
|||
{ |
|||
name: 'Tags', |
|||
value: doc.tagsFlattened || 'null', |
|||
inline: true, |
|||
}, |
|||
{ |
|||
name: 'Listed', |
|||
value: doc.listed || 'false', |
|||
inline: true, |
|||
}, |
|||
{ |
|||
name: 'Blacklisted', |
|||
value: doc.blacklisted || 'false', |
|||
inline: true, |
|||
}, |
|||
], |
|||
footer: { text: `ID: ${doc.id}` }, |
|||
}; |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
|
|||
async user({ message, args }) { |
|||
if (args && args.length) { |
|||
var resolvedUser = this.resolveUser(message.channel.guild, args.join(' ')); |
|||
} |
|||
|
|||
if (!resolvedUser) { |
|||
resolvedUser = await this.dyno.restClient.getRESTUser(args[0]).catch(() => false); |
|||
} |
|||
|
|||
const userId = resolvedUser ? resolvedUser.id : args[0] || message.author.id; |
|||
const user = resolvedUser; |
|||
|
|||
let ownedGuilds, premiumGuilds |
|||
try { |
|||
var guilds = await this.models.Server |
|||
.find({ $or: [ { ownerID: userId }, { premiumUserId: userId } ]}) |
|||
.sort({ memberCount: -1 }) |
|||
.lean() |
|||
.exec(); |
|||
|
|||
ownedGuilds = guilds.filter((i) => i.ownerID === userId); |
|||
premiumGuilds = guilds.filter((i) => i.premiumUserId === userId); |
|||
|
|||
} catch (err) { |
|||
return this.error(`Unable to get guilds.`); |
|||
} |
|||
|
|||
const userEmbed = { |
|||
author: { |
|||
name: `${user.username}#${user.discriminator}`, |
|||
icon_url: resolvedUser.avatarURL, |
|||
}, |
|||
fields: [], |
|||
}; |
|||
|
|||
userEmbed.fields.push({ name: 'ID', value: user.id, inline: true }); |
|||
userEmbed.fields.push({ name: 'Name', value: user.username, inline: true }); |
|||
userEmbed.fields.push({ name: 'Discrim', value: user.discriminator, inline: true }); |
|||
userEmbed.fields.push({ name: 'Premium guilds:', value: premiumGuilds.length, inline: true }); |
|||
|
|||
await this.sendMessage(message.channel, { embed: userEmbed }); |
|||
|
|||
if (!ownedGuilds || !ownedGuilds.length) return Promise.resolve(); |
|||
|
|||
const embed = { |
|||
title: 'Owned Guilds', |
|||
fields: [], |
|||
}; |
|||
|
|||
// START MODULES
|
|||
const modules = this.dyno.modules.filter(m => !m.admin && !m.core && m.list !== false); |
|||
|
|||
if (!modules) { |
|||
return this.error(message.channel, `Couldn't get a list of modules.`); |
|||
} |
|||
|
|||
for (const guild of ownedGuilds) { |
|||
let valArray = [ |
|||
`Region: ${guild.region}`, |
|||
`Members: ${guild.memberCount}`, |
|||
`Prefix: ${guild.prefix || '?'}`, |
|||
]; |
|||
|
|||
if (guild.modonly) { |
|||
valArray.push(`Mod Only: true`); |
|||
} |
|||
if (guild.beta) { |
|||
valArray.push(`Beta: true`); |
|||
} |
|||
if (guild.isPremium) { |
|||
valArray.push(`Premium: true`); |
|||
} |
|||
if (guild.deleted) { |
|||
valArray.push(`Kicked/Deleted: true`); |
|||
} |
|||
|
|||
let disabledModules = modules.filter(m => guild.modules.hasOwnProperty(m.name) && guild.modules[m.name] === false); |
|||
|
|||
if (disabledModules && disabledModules.length) { |
|||
valArray.push(`Disabled Modules: ${disabledModules.map(m => m.name).join(', ')}`); |
|||
} |
|||
|
|||
valArray.push(`[Dashboard](https://www.dynobot.net/server/${guild._id})`); |
|||
|
|||
embed.fields.push({ |
|||
name: `${guild.name} (${guild._id})`, |
|||
value: valArray.join('\n'), |
|||
inline: false, |
|||
}); |
|||
} |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
|
|||
async premium({ message, args }) { |
|||
if (args && args.length) { |
|||
var resolvedUser = this.resolveUser(message.channel.guild, args.join(' ')); |
|||
} |
|||
|
|||
if (!args[0]) { |
|||
resolvedUser = message.author; |
|||
} |
|||
|
|||
if (!resolvedUser) { |
|||
resolvedUser = await this.dyno.restClient.getRESTUser(args[0]).catch(() => false); |
|||
} |
|||
|
|||
const userId = resolvedUser ? resolvedUser.id : args[0] || message.author.id; |
|||
const user = resolvedUser; |
|||
|
|||
let premiumGuilds |
|||
try { |
|||
var guilds = await this.models.Server |
|||
.find({ premiumUserId: userId }) |
|||
.select({ _id: 1, name: 1, premiumUserId: 1 }) |
|||
// .find({ $or: [ { ownerID: userId }, { premiumUserId: userId } ]})
|
|||
.sort({ memberCount: -1 }) |
|||
.lean() |
|||
.exec(); |
|||
|
|||
premiumGuilds = guilds.filter((i) => i.premiumUserId === userId); |
|||
|
|||
} catch (err) { |
|||
return this.error(`Unable to get guilds.`); |
|||
} |
|||
|
|||
const userEmbed = { |
|||
author: { |
|||
name: `${user.username}#${user.discriminator}`, |
|||
icon_url: resolvedUser.avatarURL, |
|||
}, |
|||
fields: [], |
|||
}; |
|||
|
|||
userEmbed.fields.push({ name: 'ID', value: user.id, inline: true }); |
|||
userEmbed.fields.push({ name: 'Name', value: user.username, inline: true }); |
|||
userEmbed.fields.push({ name: 'Discrim', value: user.discriminator, inline: true }); |
|||
userEmbed.fields.push({ name: 'Premium guilds:', value: premiumGuilds.length, inline: true }); |
|||
|
|||
if (premiumGuilds.length) { |
|||
userEmbed.fields.push({ name: '\u200b', value: premiumGuilds.map(g => `${g.name} (${g._id})`).join('\n') }); |
|||
} |
|||
|
|||
return this.sendMessage(message.channel, { embed: userEmbed }); |
|||
} |
|||
|
|||
async automod({ message }) { |
|||
try { |
|||
var counts = await this.redis.hgetall('automod.counts'); |
|||
} catch (err) { |
|||
return this.error(message.channel, err); |
|||
} |
|||
|
|||
const embed = { |
|||
title: 'Automod Stats', |
|||
fields: [ |
|||
{ name: 'All Automods', value: counts.any, inline: true }, |
|||
{ name: 'Spam/Dup Chars', value: counts.spamdup, inline: true }, |
|||
{ name: 'Caps', value: counts.manycaps, inline: true }, |
|||
{ name: 'Bad Words', value: counts.badwords, inline: true }, |
|||
{ name: 'Emojis', value: counts.manyemojis, inline: true }, |
|||
{ name: 'Link Cooldown', value: counts.linkcooldown, inline: true }, |
|||
{ name: 'Any Link', value: counts.anylink, inline: true }, |
|||
{ name: 'Blacklist Link', value: counts.blacklistlink, inline: true }, |
|||
{ name: 'Invite', value: counts.invite, inline: true }, |
|||
{ name: 'Attach/Embed Spam', value: counts.attachments, inline: true }, |
|||
{ name: 'Attach Cooldown', value: counts.attachcooldown, inline: true }, |
|||
{ name: 'Rate Limit', value: counts.ratelimit, inline: true }, |
|||
{ name: 'Chat Clearing', value: counts.spamclear, inline: true }, |
|||
{ name: 'Light Mentions', value: counts.mentionslight, inline: true }, |
|||
{ name: 'Mention Bans', value: counts.mentions, inline: true }, |
|||
{ name: 'Auto Mutes', value: counts.mutes, inline: true }, |
|||
{ name: 'Forced Mutes', value: counts.forcemutes, inline: true }, |
|||
], |
|||
timestamp: new Date(), |
|||
}; |
|||
|
|||
return this.sendMessage(message.channel, { content: 'Note: Automod stats from Dec. 29, 2016', embed }); |
|||
} |
|||
|
|||
async mods({ message, args, guildConfig }) { |
|||
const modlog = guildConfig.moderation ? guildConfig.moderation.channel : null; |
|||
if (!modlog) { |
|||
return this.error(message.channel, 'No log channel set.'); |
|||
} |
|||
|
|||
const startTime = moment().subtract(1, 'months').unix() * 1000; |
|||
let messages; |
|||
|
|||
try { |
|||
const results = await this.client.getMessages(modlog, 1100); |
|||
if (!results) { |
|||
return this.error(message.channel, 'Unable to get results.'); |
|||
} |
|||
|
|||
messages = results.filter(r => r.timestamp >= startTime); |
|||
} catch (err) { |
|||
console.error(err); |
|||
return this.error(message.channel, 'Something went wrong.'); |
|||
} |
|||
|
|||
const groupedMessages = messages.reduce((a, b) => { |
|||
try { |
|||
const embed = b.embeds[0]; |
|||
const mod = embed.fields.find(f => f.name === 'Moderator'); |
|||
if (!mod) return null; |
|||
const modId = mod.value.replace(/[\D]/g, ''); |
|||
a[modId] = a[modId] || 0; |
|||
a[modId]++; |
|||
return a; |
|||
} catch (err) { |
|||
console.error(err); |
|||
} |
|||
}, {}); |
|||
|
|||
// `<@!${k}> ${groupedMessages[k]}`
|
|||
let arr = Object.keys(groupedMessages).map(k => ({ id: k, count: groupedMessages[k] })); |
|||
arr = arr.sort((a, b) => b.count - a.count); |
|||
arr = arr.map(o => `<@!${o.id}> ${o.count}`); |
|||
|
|||
return this.sendMessage(message.channel, { embed: { |
|||
description: arr.join('\n'), |
|||
} }); |
|||
} |
|||
|
|||
invite({ message, args }) { |
|||
if (!args || !args.length) return this.error(message.channel, `No name or ID specified.`); |
|||
this.client.guilds.find(g => g.id === args[0] || g.name === args.join(' ')) |
|||
.createInvite({ max_age: 60 * 30 }) |
|||
.then(invite => this.success(message.channel, `https://discord.gg/${invite.code}`)) |
|||
.catch(() => this.error(message.channel, `Couldn't create invite.`)); |
|||
} |
|||
|
|||
async topshared({ message }) { |
|||
try { |
|||
const dres = await axios.get(`https://bots.discord.pw/api/bots`, { |
|||
headers: { |
|||
Authorization: this.config.dbots.key, |
|||
Accept: 'application/json', |
|||
}, |
|||
}); |
|||
const res = await axios.get(this.config.carbon.list); |
|||
var data = res.data; |
|||
var dbots = dres.data; |
|||
} catch (err) { |
|||
return this.logger.error(err); |
|||
} |
|||
|
|||
if (!data || !data.length) return; |
|||
|
|||
let i = 0; |
|||
|
|||
const list = data.map(bot => { |
|||
bot.botid = bot.botid; |
|||
bot.servercount = parseInt(bot.servercount); |
|||
return bot; |
|||
}) |
|||
.filter(bot => bot.botid > 1000 && bot.servercount >= 25000) |
|||
.sort((a, b) => (a.servercount < b.servercount) ? 1 : (a.servercount > b.servercount) ? -1 : 0); |
|||
// `${++i} ${this.utils.pad(bot.name, 12)} - ${bot.servercount}`
|
|||
|
|||
return new Promise(async (resolve) => { |
|||
let bots = []; |
|||
for (let bot of list) { |
|||
bot.botid = bot.botid.replace('195244341038546948', '195244363339530240'); |
|||
let allShared = await this.ipc.awaitResponse('shared', { user: bot.botid }); |
|||
bot.shared = allShared.reduce((a, b) => { |
|||
a += parseInt(b.result); |
|||
return a; |
|||
}, 0); |
|||
bots.push(bot); |
|||
} |
|||
bots = bots.map(b => { |
|||
++i; |
|||
return `${this.utils.pad('' + i, 2)} ${this.utils.pad(b.name, 12)} ${this.utils.pad('' + b.servercount, 6)} Guilds, ${this.utils.pad('' + b.shared, 5)} Shared`; |
|||
}); |
|||
this.sendCode(message.channel, bots.join('\n')); |
|||
return resolve(); |
|||
}); |
|||
} |
|||
|
|||
addmodule({ message, args }) { |
|||
if (!this.dyno.modules.has(args[0])) return this.error(message.channel, `That module does not exist.`); |
|||
if (this.config.moduleList.includes(args[0])) return this.error(message.channel, `That module is already loaded.`); |
|||
this.config.moduleList.push(args[0]); |
|||
if (this.config.disabledCommandGroups && this.config.disabledCommandGroups.includes(args[0])) { |
|||
let index = this.config.disabledCommandGroups.indexOf(args[0]); |
|||
let commandGroups = this.config.disabledCommandGroups.split(','); |
|||
commandGroups.splice(index, 1); |
|||
this.config.disabledCommandGroups = commandGroups.join(','); |
|||
return this.success(message.channel, `Added module ${args[0]} and removed the disabled command group.`); |
|||
} |
|||
|
|||
return this.success(message.channel, `Added module ${args[0]}.`); |
|||
} |
|||
|
|||
async shards({ message, args }) { |
|||
const instances = ['Titan', 'Atlas', 'Pandora']; |
|||
|
|||
try { |
|||
const instanceStats = await Promise.all([ |
|||
axios.get('http://prod01.dyno.lan:5000/shards'), |
|||
axios.get('http://prod02.dyno.lan:5000/shards'), |
|||
axios.get('http://prod03.dyno.lan:5000/shards'), |
|||
]); |
|||
// const shardStats = await this.dyno.ipc.awaitResponse('shards');
|
|||
|
|||
let response = ''; |
|||
instanceStats.forEach((instance, idx) => { |
|||
response += `${instances[idx]}:\n\n`; |
|||
if (!instance || !instance.data) { |
|||
return; |
|||
} |
|||
for (let result of instance.data) { |
|||
const id = result.id; |
|||
const s = result.result; |
|||
if (!s || typeof s === 'string') { |
|||
response += `ID:${id} Error.\n`; |
|||
} else { |
|||
response += `ID:${id} SHARDS:${s.connectedCount}/${s.shardCount} GUILDS:${s.guildCount} (${s.unavailableCount} unavil) SHARDS:${JSON.stringify(s.shards)} VC:${s.voiceConnections} UPTIME:${s.uptime}\n`; |
|||
} |
|||
} |
|||
response += `\n`; |
|||
}); |
|||
|
|||
let msgArray = []; |
|||
msgArray = msgArray.concat(this.utils.splitMessage(response, 1980)); |
|||
|
|||
for (let m of msgArray) { |
|||
this.sendCode(message.channel, m, 'Haskell'); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
} catch (err) { |
|||
console.log(err); |
|||
} |
|||
} |
|||
|
|||
async ishards({ message, args }) { |
|||
try { |
|||
const shardStats = await this.dyno.ipc.awaitResponse('shards'); |
|||
|
|||
let response = ''; |
|||
shardStats.forEach((result) => { |
|||
const id = result.id; |
|||
const s = result.result; |
|||
if (!s || typeof s === 'string') { |
|||
response += `ID:${id} Error.`; |
|||
} else { |
|||
response += `ID:${id} SHARDS:${s.connectedCount}/${s.shardCount} GUILDS:${s.guildCount} (${s.unavailableCount} unavil) SHARDS:${JSON.stringify(s.shards)} VC:${s.voiceConnections} UPTIME:${s.uptime}\n`; |
|||
} |
|||
}); |
|||
|
|||
let msgArray = []; |
|||
msgArray = msgArray.concat(this.utils.splitMessage(response, 1980)); |
|||
|
|||
for (let m of msgArray) { |
|||
this.sendCode(message.channel, m, 'Haskell'); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
} catch (err) { |
|||
console.log(err); |
|||
} |
|||
} |
|||
|
|||
async associate({ message, args }) { |
|||
let associates = this.dyno.globalConfig.associates || []; |
|||
let o = associates.find(a => a.name.toLowerCase().search(args.join(' ').toLowerCase()) !== -1); |
|||
|
|||
let embed = { |
|||
color: this.utils.hexToInt('#3395d6'), |
|||
title: o.name, |
|||
url: o.links.find(l => l.name === 'Server Invite').value, |
|||
description: o.description, |
|||
image: { url: o.banner }, |
|||
fields: [ |
|||
{ name: 'Links', value: o.links.map(l => `[${l.name}](${l.value})`).join('\n') }, |
|||
], |
|||
footer: { |
|||
text: o.sponsor ? 'Sponsor' : 'Partner', |
|||
}, |
|||
}; |
|||
|
|||
await this.sendMessage(message.channel, { embed }); |
|||
} |
|||
|
|||
async associates({ message, args }) { |
|||
if (!message.member.roles.includes('203040224597508096')) { |
|||
return this.error(message.channel, 'Get off my potato!'); |
|||
} |
|||
|
|||
message.delete(); |
|||
|
|||
let associates = this.dyno.globalConfig.associates || []; |
|||
associates = this.utils.shuffleArray(associates); |
|||
|
|||
for (let o of associates) { |
|||
let embed = { |
|||
color: this.utils.hexToInt('#3395d6'), |
|||
title: o.name, |
|||
url: o.links.find(l => l.name === 'Server Invite').value, |
|||
description: o.description, |
|||
image: { url: o.banner }, |
|||
fields: [ |
|||
{ name: 'Links', value: o.links.map(l => `[${l.name}](${l.value})`).join('\n') }, |
|||
], |
|||
footer: { |
|||
text: o.sponsor ? 'Sponsor' : 'Partner', |
|||
}, |
|||
}; |
|||
|
|||
await this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
async cfg({ message, args }) { |
|||
if (!this.isAdmin(message.author) && !message.member.roles.includes('355054563931324420')) { |
|||
return Promise.reject('Insufficient permissions'); |
|||
} |
|||
|
|||
if (!args || !args.length) { |
|||
return; |
|||
} |
|||
|
|||
const payload = { guildId: args[0], userId: message.member.id }; |
|||
try { |
|||
var uniqueId = uuid(); |
|||
} catch (err) { |
|||
return this.error(message.channel, err); |
|||
} |
|||
|
|||
try { |
|||
await this.redis.setex(`supportcfg:${uniqueId}`, 60, JSON.stringify(payload)); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, 'Something went wrong. Try again later.'); |
|||
} |
|||
|
|||
const url = `https://dyno.gg/support/c/${uniqueId}`; |
|||
|
|||
return this.sendMessage(message.channel, url); |
|||
} |
|||
|
|||
permissionsFor({ message, args }) { |
|||
if (!args || !args.length) return this.error(message.channel, `No name or ID specified.`); |
|||
const guild = this.client.guilds.find(g => g.id === args[0] || g.name === args.join(' ')); |
|||
|
|||
if (!guild) { |
|||
return this.error(message.channel, `Couldn't find that guild.`); |
|||
} |
|||
|
|||
const perms = guild.members.get(this.client.user.id); |
|||
|
|||
const msgArray = this.utils.splitMessage(perms, 1950); |
|||
|
|||
for (let m of msgArray) { |
|||
this.sendCode(message.channel, m, 'js'); |
|||
} |
|||
} |
|||
|
|||
chunkArray(myArray, chunk_size) { |
|||
const arrayLength = myArray.length; |
|||
const tempArray = []; |
|||
let chunk = []; |
|||
|
|||
for (let index = 0; index < arrayLength; index += chunk_size) { |
|||
chunk = myArray.slice(index, index + chunk_size); |
|||
tempArray.push(chunk); |
|||
} |
|||
|
|||
return tempArray; |
|||
} |
|||
} |
|||
|
|||
module.exports = Data; |
@ -0,0 +1,130 @@ |
|||
'use strict'; |
|||
|
|||
const moment = require('moment'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class DisablePremium extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'dispremium'; |
|||
this.aliases = ['dispremium']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Disable premium for a server.'; |
|||
this.usage = 'dispremium [server id] [reason]'; |
|||
this.overseerEnabled = true; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (!message.member) return false; |
|||
if (message.guild.id !== this.config.dynoGuild) return false; |
|||
|
|||
if (this.isServerAdmin(message.member, message.channel)) return true; |
|||
if (this.isServerMod(message.member, message.channel)) return true; |
|||
|
|||
let allowedRoles = [ |
|||
'225209883828420608', // Accomplices
|
|||
]; |
|||
|
|||
const roles = message.guild.roles.filter(r => allowedRoles.includes(r.id)); |
|||
if (roles && message.member.roles.find(r => roles.find(role => role.id === r))) return true; |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
let resolvedUser = await this.resolveUser(message.guild, args[0]); |
|||
if (!resolvedUser) { |
|||
try { |
|||
resolvedUser = await this.dyno.restClient.getRESTUser(args[0]); |
|||
} catch (err) { |
|||
// pass
|
|||
} |
|||
} |
|||
|
|||
if (resolvedUser) { |
|||
try { |
|||
const guilds = await this.models.Server.find({ premiumUserId: resolvedUser.id }, { _id: 1 }); |
|||
if (!guilds || !guilds.length) { |
|||
return this.sendMessage(message.channel, `That user has no premium guilds.`); |
|||
} |
|||
await Promise.all(guilds.map(g => this.disableGuild(message, g._id))); |
|||
return this.success(message.channel, `Disabled ${guilds.length} guilds for ${resolvedUser.username}#${resolvedUser.discriminator}`); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, `Error: ${err.message}`); |
|||
} |
|||
} else { |
|||
return this.disableGuild(message, args[0]); |
|||
} |
|||
} |
|||
|
|||
async disableGuild(message, guildId) { |
|||
const logChannel = this.client.getChannel('231484392365752320'); |
|||
const dataChannel = this.client.getChannel('301131818483318784'); |
|||
|
|||
if (!logChannel || !dataChannel) { |
|||
return this.error(message.channel, 'Unable to find log channel.'); |
|||
} |
|||
|
|||
try { |
|||
await this.dyno.guilds.update(guildId, { $unset: { vip: 1, isPremium: 1, premiumUserId: 1, premiumSince: 1 } }); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, `Error: ${err.message}`); |
|||
} |
|||
|
|||
try { |
|||
var doc = await this.models.Server.findOne({ _id: guildId }).lean().exec(); |
|||
} catch (e) { |
|||
this.logger.error(e); |
|||
return this.error(message.channel, `Error: ${e.message}`); |
|||
} |
|||
|
|||
this.success(logChannel, `[**${this.utils.fullName(message.author)}**] Disabled Premium on **${doc.name} (${doc._id})**`); |
|||
this.success(message.channel, `Disabled Dyno Premium on ${doc.name}`); |
|||
|
|||
message.delete().catch(() => false); |
|||
|
|||
const logDoc = { |
|||
serverID: doc._id, |
|||
serverName: doc.name, |
|||
ownerID: doc.ownerID, |
|||
userID: doc.premiumUserId || 'Unknown', |
|||
timestamp: new Date().getTime(), |
|||
type: 'disable', |
|||
} |
|||
|
|||
await this.dyno.db.collection('premiumactivationlogs').insert(logDoc); |
|||
return Promise.resolve(); |
|||
|
|||
try { |
|||
var messages = await this.client.getMessages(dataChannel.id, 500); |
|||
} catch (err) { |
|||
this.logger.error(e); |
|||
return this.error(message.channel, `Error: ${err.message}`); |
|||
} |
|||
|
|||
if (!messages || !messages.length) { |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
for (let msg of messages) { |
|||
let embed = msg.embeds[0]; |
|||
|
|||
if (embed.fields.find(f => f.name === 'Server ID' && f.value === doc._id)) { |
|||
embed.fields.push({ name: 'Disabled', value: moment().format('llll'), inline: true }); |
|||
// embed.fields.push({ name: 'Reason', value: reason, inline: true });
|
|||
} |
|||
|
|||
return msg.edit({ embed }).catch(err => this.logger.error(err)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = DisablePremium; |
|||
|
@ -0,0 +1,107 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class EnablePremium extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'enpremium'; |
|||
this.aliases = ['enpremium']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Enable premium for a server.'; |
|||
this.usage = 'enpremium [server id] [user]'; |
|||
this.overseerEnabled = true; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 2; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (!message.member) return false; |
|||
if (message.guild.id !== this.config.dynoGuild) return false; |
|||
|
|||
if (this.isServerAdmin(message.member, message.channel)) return true; |
|||
if (this.isServerMod(message.member, message.channel)) return true; |
|||
|
|||
let allowedRoles = [ |
|||
'225209883828420608', // Accomplices
|
|||
]; |
|||
|
|||
const roles = message.guild.roles.filter(r => allowedRoles.includes(r.id)); |
|||
if (roles && message.member.roles.find(r => roles.find(role => role.id === r))) return true; |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
let user = this.resolveUser(message.guild, args.slice(1).join(' ')); |
|||
|
|||
if (!user) { |
|||
if (!isNaN(args[0])) { |
|||
user = await this.restClient.getRESTUser(args[0]); |
|||
} |
|||
if (!user) { |
|||
return this.error(message.channel, 'Unable to find that user.'); |
|||
} |
|||
} |
|||
|
|||
const logChannel = this.client.getChannel('231484392365752320'); |
|||
const dataChannel = this.client.getChannel('301131818483318784'); |
|||
|
|||
if (!logChannel || !dataChannel) { |
|||
return this.error(message.channel, 'Unable to find log channel.'); |
|||
} |
|||
|
|||
return this.dyno.guilds.update(args[0], { $set: { vip: true, isPremium: true, premiumUserId: user.id, premiumSince: new Date().getTime() } }) |
|||
.then(async () => { |
|||
try { |
|||
var doc = await this.models.Server.findOne({ _id: args[0] }).lean().exec(); |
|||
} catch (e) { |
|||
return this.logger.error(e); |
|||
} |
|||
|
|||
this.success(logChannel, `[**${this.utils.fullName(message.author)}**] Enabled Premium on **${doc.name} (${doc._id})** for ${user.mention}`); |
|||
this.success(message.channel, `Enabled Dyno Premium on ${doc.name} for ${this.utils.fullName(user)}.`); |
|||
|
|||
const embed = { |
|||
fields: [ |
|||
{ name: 'Server ID', value: doc._id, inline: true }, |
|||
{ name: 'Server Name', value: doc.name, inline: true }, |
|||
{ name: 'Owner ID', value: doc.ownerID, inline: true }, |
|||
{ name: 'User ID', value: user.id, inline: true }, |
|||
{ name: 'Username', value: this.utils.fullName(user), inline: true }, |
|||
{ name: 'Mention', value: user.mention, inline: true }, |
|||
{ name: 'Member Count', value: `${doc.memberCount || 0}`, inline: true }, |
|||
{ name: 'Region', value: `${doc.region || 'Unknown'}`, inline: true }, |
|||
], |
|||
timestamp: new Date(), |
|||
}; |
|||
|
|||
const logDoc = { |
|||
serverID: doc._id, |
|||
serverName: doc.name, |
|||
ownerID: doc.ownerID, |
|||
userID: user.id, |
|||
username: this.utils.fullName(user), |
|||
memberCount: doc.memberCount || 0, |
|||
region: doc.region || 'Unknown', |
|||
timestamp: new Date().getTime(), |
|||
type: 'enable', |
|||
} |
|||
|
|||
await this.dyno.db.collection('premiumactivationlogs').insert(logDoc); |
|||
|
|||
this.sendMessage(dataChannel, { embed }) |
|||
message.delete().catch(() => false); |
|||
|
|||
}) |
|||
.catch(err => { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, `Error: ${err.message}`); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
module.exports = EnablePremium; |
@ -0,0 +1,74 @@ |
|||
/* eslint-disable no-unused-vars */ |
|||
'use strict'; |
|||
|
|||
const os = require('os'); |
|||
const util = require('util'); |
|||
const moment = require('moment-timezone'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Eval extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'eval'; |
|||
this.aliases = ['eval', 'e']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Evaluate js code from discord'; |
|||
this.usage = 'eval [javascript]'; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (!message.author) return false; |
|||
if (!this.dyno.globalConfig || !this.dyno.globalConfig.developers) return false; |
|||
|
|||
if (this.dyno.globalConfig.developers.includes(message.author.id)) { |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async execute({ message, args, guildConfig }) { |
|||
let msgArray = [], |
|||
msg = message, |
|||
dyno = this.dyno, |
|||
client = this.client, |
|||
config = this.config, |
|||
models = this.models, |
|||
redis = this.redis, |
|||
utils = this.utils, |
|||
result; |
|||
|
|||
try { |
|||
result = eval(args.join(' ')); |
|||
} catch (e) { |
|||
result = e; |
|||
} |
|||
|
|||
if (result && result.then) { |
|||
try { |
|||
result = await result; |
|||
} catch (err) { |
|||
result = err; |
|||
} |
|||
} |
|||
|
|||
if (!result) { |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
msgArray = msgArray.concat(this.utils.splitMessage(result, 1990)); |
|||
|
|||
for (let m of msgArray) { |
|||
this.sendCode(message.channel, m.toString().replace(this.config.client.token, 'potato'), 'js'); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
|||
|
|||
module.exports = Eval; |
@ -0,0 +1,50 @@ |
|||
'use strict'; |
|||
|
|||
const { exec } = require('child_process'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Exec extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'exec'; |
|||
this.aliases = ['exec', 'ex']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Execute a shell command'; |
|||
this.usage = 'exec [command]'; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
exec(command) { |
|||
return new Promise((resolve, reject) => { |
|||
exec(command, (err, stdout, stderr) => { |
|||
if (err) return reject(err); |
|||
return resolve(stdout || stderr); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
let msgArray = [], |
|||
result; |
|||
|
|||
try { |
|||
result = await this.exec(args.join(' ')); |
|||
} catch (err) { |
|||
result = err; |
|||
} |
|||
|
|||
msgArray = msgArray.concat(this.utils.splitMessage(result, 1990)); |
|||
|
|||
for (let m of msgArray) { |
|||
this.sendCode(message.channel, m, 'js'); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
|||
|
|||
module.exports = Exec; |
@ -0,0 +1,50 @@ |
|||
'use strict'; |
|||
|
|||
const { exec } = require('child_process'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Git extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'git'; |
|||
this.aliases = ['git']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Execute a git command'; |
|||
this.usage = 'git [stuff]'; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
exec(command) { |
|||
return new Promise((resolve, reject) => { |
|||
exec(command, (err, stdout, stderr) => { |
|||
if (err) return reject(err); |
|||
return resolve(stdout || stderr); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
let msgArray = [], |
|||
result; |
|||
|
|||
try { |
|||
result = await this.exec(`git ${args.join(' ')}`); |
|||
} catch (err) { |
|||
result = err; |
|||
} |
|||
|
|||
msgArray = msgArray.concat(this.utils.splitMessage(result, 1990)); |
|||
|
|||
for (let m of msgArray) { |
|||
this.sendCode(message.channel, m, 'js'); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
|||
|
|||
module.exports = Git; |
@ -0,0 +1,50 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class GlobalDisable extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['disglobal']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Disable a module or command globally'; |
|||
this.usage = 'disglobal [name]'; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
const name = args.join(' '); |
|||
const module = this.dyno.modules.get(name); |
|||
const command = this.dyno.commands.get(name); |
|||
const globalConfig = this.dyno.globalConfig || {}; |
|||
const options = { new: true, upsert: true }; |
|||
|
|||
if (!module && !command) { |
|||
return this.sendMessage(message.channel, `Couldn't find module or command ${name}`); |
|||
} |
|||
|
|||
if (module) { |
|||
globalConfig.modules = globalConfig.modules || {}; |
|||
globalConfig.modules[name] = false; |
|||
return this.models.Dyno.findOneAndUpdate({}, globalConfig, options).then(doc => { |
|||
this.config.global = doc.toObject(); |
|||
this.success(message.channel, `Disabled module ${name}`); |
|||
}).catch(err => this.logger.error(err)); |
|||
} |
|||
|
|||
if (command) { |
|||
globalConfig.commands = globalConfig.commands || {}; |
|||
globalConfig.commands[name] = false; |
|||
return this.models.Dyno.findOneAndUpdate({}, globalConfig, options).then(doc => { |
|||
this.config.global = doc.toObject(); |
|||
this.success(message.channel, `Disabled command ${name}`); |
|||
}).catch(err => this.logger.error(err)); |
|||
} |
|||
|
|||
return Promise.resulve(); |
|||
} |
|||
} |
|||
|
|||
module.exports = GlobalDisable; |
@ -0,0 +1,50 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class GlobalEnable extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['englobal']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Disable a module or command globally'; |
|||
this.usage = 'englobal [name]'; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
const name = args.join(' '); |
|||
const module = this.dyno.modules.get(name); |
|||
const command = this.dyno.commands.get(name); |
|||
const globalConfig = this.dyno.globalConfig || {}; |
|||
const options = { new: true, upsert: true }; |
|||
|
|||
if (!module || !command) { |
|||
return this.sendMessage(message.channel, `Couldn't find module or command ${name}`); |
|||
} |
|||
|
|||
if (module) { |
|||
globalConfig.modules = globalConfig.modules || {}; |
|||
globalConfig.modules[name] = true; |
|||
return this.models.Dyno.findOneAndUpdate({}, globalConfig, options).then(doc => { |
|||
this.config.global = doc.toObject(); |
|||
this.success(message.channel, `Enabled module ${name}`); |
|||
}).catch(err => this.logger.error(err)); |
|||
} |
|||
|
|||
if (command) { |
|||
globalConfig.commands = globalConfig.commands || {}; |
|||
globalConfig.commands[name] = true; |
|||
return this.models.Dyno.findOneAndUpdate({}, globalConfig, options).then(doc => { |
|||
this.config.global = doc.toObject(); |
|||
this.success(message.channel, `Enabled command ${name}`); |
|||
}).catch(err => this.logger.error(err)); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
|||
|
|||
module.exports = GlobalEnable; |
@ -0,0 +1,167 @@ |
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
const axios = require('axios'); |
|||
const uuid = require('uuid/v4'); |
|||
|
|||
class Guild extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['guild']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Get guild status'; |
|||
this.usage = 'guild [guild id]'; |
|||
this.cooldown = 3000; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (message.guild.id === this.config.dynoGuild) return true; |
|||
|
|||
const allowedRoles = [ |
|||
'225209883828420608', |
|||
'355054563931324420', |
|||
'231095149508296704', |
|||
'203040224597508096', |
|||
]; |
|||
|
|||
if (message.member && allowedRoles.find(r => message.member.roles.includes(r))) { |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async getGuild(guildId) { |
|||
try { |
|||
const options = { |
|||
method: 'POST', |
|||
headers: { Authorization: this.dyno.globalConfig.apiToken }, |
|||
url: `https://premium.dyno.gg/api/guild/${guildId}`, |
|||
}; |
|||
|
|||
const response = await axios(options); |
|||
if (!response.data) { |
|||
return Promise.reject('Unable to retrieve data at this time.'); |
|||
} |
|||
return response.data; |
|||
} catch (err) { |
|||
return Promise.reject(err); |
|||
} |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
const guildId = args[0] || message.guild.id; |
|||
let guild; |
|||
|
|||
try { |
|||
guild = await this.getGuild(guildId); |
|||
} catch (err) { |
|||
return this.error(message.channel, err); |
|||
} |
|||
|
|||
let payload = { guildId, userId: message.member.id }; |
|||
try { |
|||
var uniqueId = uuid(); |
|||
} catch (err) { |
|||
return this.error(message.channel, err); |
|||
} |
|||
|
|||
if (!this.isServerMod(message.member, message.channel)) { |
|||
payload.excludeKeys = ['customcommands', 'autoresponder']; |
|||
} |
|||
|
|||
try { |
|||
await this.redis.setex(`supportcfg:${uniqueId}`, 60, JSON.stringify(payload)); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, 'Something went wrong. Try again later.'); |
|||
} |
|||
|
|||
const url = `https://dyno.gg/support/c/${uniqueId}`; |
|||
|
|||
const shardCount = this.dyno.globalConfig.shardCount; |
|||
const shard = ~~((guildId / 4194304) % shardCount); |
|||
|
|||
const desc = [ |
|||
{ key: 'Server', value: guild.serverName }, |
|||
{ key: 'Cluster', value: guild.cluster }, |
|||
{ key: 'Shard', value: `${shard}/${shardCount}` }, |
|||
{ key: 'Members', value: guild.memberCount.toString() }, |
|||
{ key: 'Region', value: guild.region }, |
|||
{ key: 'Prefix', value: guild.prefix || '?' }, |
|||
{ key: 'Mod Only', value: guild.modonly ? 'Yes' : 'No' }, |
|||
{ key: 'Owner', value: `${guild.owner.username}#${guild.owner.discriminator}\n(${guild.ownerID})` }, |
|||
]; |
|||
|
|||
const color = guild.isPremium ? this.utils.getColor('premium') : this.utils.getColor('blue'); |
|||
|
|||
let status; |
|||
|
|||
if (guild.server.result && guild.server.result.shardStatus) { |
|||
const shardStatus = guild.server.result.shardStatus.find(s => s.id === shard); |
|||
if (shardStatus.status === 'disconnected') { |
|||
status = 'https://cdn.discordapp.com/emojis/313956276893646850.png?v=1'; |
|||
} else if (shardStatus.status === 'ready') { |
|||
status = 'https://cdn.discordapp.com/emojis/313956277808005120.png?v=1'; |
|||
} else { |
|||
status = 'https://cdn.discordapp.com/emojis/313956277220802560.png?v=1'; |
|||
} |
|||
} |
|||
|
|||
const embed = { |
|||
color, |
|||
author: { |
|||
name: guild.name, |
|||
icon_url: guild.iconURL, |
|||
}, |
|||
// description: ,
|
|||
fields: [ |
|||
{ name: 'Server', value: desc.map(o => `**${o.key}:** ${o.value}`).join('\n'), inline: true }, |
|||
], |
|||
footer: { text: `ID: ${guild._id}` }, |
|||
timestamp: new Date(), |
|||
}; |
|||
|
|||
if (status) { |
|||
embed.footer.icon_url = status; |
|||
} |
|||
|
|||
if (guild.premiumUser) { |
|||
const field = [ |
|||
{ key: 'Premium', value: guild.isPremium ? 'Yes' : 'No' }, |
|||
{ key: 'Premium Since', value: new Date(guild.premiumSince).toISOString().substr(0, 16) }, |
|||
{ key: 'Premium User', value: `${guild.premiumUser.username}#${guild.premiumUser.discriminator}\n(${guild.premiumUser.id})` }, |
|||
{ key: 'Premium Installed', value: guild.premiumInstalled ? 'Yes' : 'No' }, |
|||
]; |
|||
embed.fields.push({ name: 'Premium', value: field.map(o => `**${o.key}:** ${o.value}`).join('\n'), inline: true }); |
|||
} |
|||
|
|||
// START MODULES
|
|||
const modules = this.dyno.modules.filter(m => !m.admin && !m.core && m.list !== false); |
|||
|
|||
if (!modules) { |
|||
return this.error(message.channel, `Couldn't get a list of modules.`); |
|||
} |
|||
|
|||
const enabledModules = modules.filter(m => !guild.modules.hasOwnProperty(m.name) || |
|||
guild.modules[m.name] === true); |
|||
const disabledModules = modules.filter(m => guild.modules.hasOwnProperty(m.name) && |
|||
guild.modules[m.name] === false); |
|||
|
|||
if (enabledModules.length) { |
|||
embed.fields.push({ name: 'Enabled Modules', value: enabledModules.map(m => m.name).join(', '), inline: false }); |
|||
} |
|||
if (disabledModules.length) { |
|||
embed.fields.push({ name: 'Disabled Modules', value: disabledModules.map(m => m.name).join(', '), inline: false }); |
|||
} |
|||
|
|||
embed.fields.push({ name: '\u200b', value: `[Dashboard](https://dyno.gg/manage/${guild._id}) **|** [Config](${url})`, inline: true }); |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Guild; |
@ -0,0 +1,40 @@ |
|||
'use strict'; |
|||
|
|||
const axios = require('axios'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class ListingBlacklist extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['listingblacklist']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Blacklists and unlists a server from the server-listing.'; |
|||
this.usage = 'listingblacklist guildid'; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.expectedArgs = 1; |
|||
this.cooldown = 1000; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
if (!this.isAdmin(message.member) && !this.isOverseer(message.member)) { |
|||
return this.error(`You're not authorized to use this command.`); |
|||
} |
|||
|
|||
const guildId = args[0]; |
|||
|
|||
const coll = await this.db.collection('serverlist_store'); |
|||
const doc = await coll.findOne({ id: guildId }); |
|||
|
|||
if (!doc) { |
|||
return this.error(message.channel, 'No guild found.'); |
|||
} |
|||
|
|||
await coll.updateOne({ id: guildId }, { $set: { blacklisted: true, listed: false } }); |
|||
|
|||
return this.success(message.channel, `Succesfully blacklisted & unlisted ${doc.name} - ${doc.id}`); |
|||
} |
|||
} |
|||
|
|||
module.exports = ListingBlacklist; |
@ -0,0 +1,53 @@ |
|||
'use strict'; |
|||
|
|||
const path = require('path'); |
|||
const util = require('util'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class LoadCommand extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['loadcommand', 'loadcmd']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Load a command.'; |
|||
this.usage = 'load [command]'; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
if (!this.dyno) return false; |
|||
|
|||
if (args[0] === 'all') { |
|||
const promises = []; |
|||
for (let cmd of this.dyno.commands.values()) { |
|||
const name = `${cmd.group}/${cmd.constructor.name}`; |
|||
promises.push(this.loadCommand(message, name)); |
|||
} |
|||
|
|||
return Promise.all(promises) |
|||
.then(data => this.sendCode(message.channel, data.map(d => util.inspect(d)), 'js')) |
|||
.catch(err => this.sendCode(message.channel, err, 'js')); |
|||
} |
|||
|
|||
let path = args.length > 1 ? `../modules/${args[0]}/commands/${args[1]}` : args[0]; |
|||
|
|||
return this.loadCommand(message, path) |
|||
.then(data => this.sendCode(message.channel, data.map(d => util.inspect(d)), 'js')) |
|||
.catch(err => this.sendCode(message.channel, err, 'js')); |
|||
} |
|||
|
|||
loadCommand(message, cmd) { |
|||
let filePath = path.join(this.config.paths.commands, cmd); |
|||
filePath = filePath.endsWith('.js') ? filePath : filePath + '.js'; |
|||
|
|||
if (!this.utils.existsSync(filePath)) { |
|||
return this.error(message.channel, `File does not exist: ${filePath}`); |
|||
} |
|||
|
|||
return this.dyno.ipc.awaitResponse('reload', { type: 'commands', name: cmd }); |
|||
} |
|||
} |
|||
|
|||
module.exports = LoadCommand; |
@ -0,0 +1,23 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class LoadController extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['loadc']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Load a controller.'; |
|||
this.usage = 'loadc [controller]'; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ args }) { |
|||
const module = this.modules.get('API'); |
|||
module.loadController(args[0]); |
|||
} |
|||
} |
|||
|
|||
module.exports = LoadController; |
@ -0,0 +1,35 @@ |
|||
'use strict'; |
|||
|
|||
const path = require('path'); |
|||
const util = require('util'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class LoadIPC extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['loadipc']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Load an ipc command.'; |
|||
this.usage = 'loadipc [command]'; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
if (!this.dyno) return false; |
|||
|
|||
let filePath = path.join(this.config.paths.ipc, args[0]); |
|||
filePath = filePath.endsWith('.js') ? filePath : filePath + '.js'; |
|||
|
|||
if (!this.utils.existsSync(filePath)) { |
|||
return this.error(message.channel, `File does not exist: ${filePath}`); |
|||
} |
|||
|
|||
return this.dyno.ipc.awaitResponse('reload', { type: 'ipc', name: args[0] }) |
|||
.then(data => this.sendCode(message.channel, data.map(d => util.inspect(d)), 'js')) |
|||
.catch(err => this.sendCode(message.channel, err, 'js')); |
|||
} |
|||
} |
|||
|
|||
module.exports = LoadIPC; |
@ -0,0 +1,27 @@ |
|||
'use strict'; |
|||
|
|||
const util = require('util'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class LoadModule extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['loadmodule', 'loadmod']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Load a module.'; |
|||
this.usage = 'loadmodule [module]'; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
if (!this.dyno) return false; |
|||
|
|||
return this.dyno.ipc.awaitResponse('reload', { type: 'modules', name: args[0] }) |
|||
.then(data => this.sendCode(message.channel, data.map(d => util.inspect(d)), 'js')) |
|||
.catch(err => this.sendCode(message.channel, err, 'js')); |
|||
} |
|||
} |
|||
|
|||
module.exports = LoadModule; |
@ -0,0 +1,47 @@ |
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
const { Client } = require('../../core/rpc'); |
|||
|
|||
class MoveCluster extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['clmove']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Move a cluster from one server to another.'; |
|||
this.usage = 'clmove'; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 3; |
|||
this.cooldown = 30000; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
if (!this.isAdmin(message.member)) { |
|||
return this.error(`You're not authorized to use this command.`); |
|||
} |
|||
|
|||
try { |
|||
if (!isNaN(args[1])) { |
|||
const clusterId = parseInt(args[1], 10); |
|||
const cluster = await this.db.collection('clusters').findOne({ env: args[0], id: clusterId }); |
|||
|
|||
if (!cluster) { |
|||
return this.error(message.channel, `Unable to find cluster ${args[1]}`); |
|||
} |
|||
|
|||
const host = cluster.host.hostname; |
|||
const client = new Client(host, 5052); |
|||
|
|||
let response = await client.request('moveCluster', { id: cluster.id, name: args[2], token: this.config.restartToken }); |
|||
|
|||
return this.success(message.channel, `Moving cluster ${cluster.id} to ${args[2]}`); |
|||
} else { |
|||
return this.error(message.channel, 'Inavlid cluster id.'); |
|||
} |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, err); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = MoveCluster; |
@ -0,0 +1,39 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Partner extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['partner']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Create a partner invite/id'; |
|||
this.usage = 'partner'; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
async execute({ message }) { |
|||
const channel = message.channel.guild.channels.find(c => c.name === 'welcome'); |
|||
|
|||
if (!channel) { |
|||
return this.error(message.channel, `I can't find the welcome channel.`); |
|||
} |
|||
|
|||
return this.client.createChannelInvite(channel.id, { temporary: false, unique: true, maxAge: 0 }) |
|||
.catch(err => this.error(message.channel, err)) |
|||
.then(invite => { |
|||
const content = [ |
|||
`ID: ${invite.code}`, |
|||
`Invite: https://discord.gg/${invite.code}`, |
|||
`Website: https://www.dynobot.net/?r=${invite.code}`, |
|||
]; |
|||
|
|||
this.sendMessage(message.channel, content); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
module.exports = Partner; |
@ -0,0 +1,68 @@ |
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
|
|||
class RLReset extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['rlreset']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Get various stats and data.'; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.hideFromHelp = true; |
|||
this.expectedArgs = 0; |
|||
this.cooldown = 120000; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (!message.author) return false; |
|||
if (message.guild.id !== this.config.dynoGuild) return false; |
|||
if (!this.dyno.globalConfig || !this.dyno.globalConfig.contributors) return false; |
|||
|
|||
const contribs = this.dyno.globalConfig.contributors.map(c => c.id); |
|||
|
|||
if (!contribs || !contribs.length) return false; |
|||
|
|||
if (contribs.includes(message.author.id)) { |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
if (!args || !args.length) { |
|||
return this.error(message.channel, `Missing server and cluster`); |
|||
} |
|||
|
|||
try { |
|||
const [_env, _cluster, guildId] = args; |
|||
const clusterId = parseInt(_cluster, 10); |
|||
const cluster = await this.db.collection('clusters').findOne({ env: _env, id: clusterId }); |
|||
|
|||
if (!cluster) { |
|||
return this.error(message.channel, `Unable to find cluster ${_cluster} on ${_env}`); |
|||
} |
|||
|
|||
const host = cluster.host.hostname; |
|||
const port = 30000 + clusterId; |
|||
const client = new this.dyno.RPCClient(this.dyno, host, port); |
|||
|
|||
client.request('rlreset', { token: this.config.rpcToken, id: guildId }, (err) => { |
|||
if (err) { |
|||
return this.error(message.channel, `Something went wrong.`); |
|||
} |
|||
|
|||
return this.success(message.channel, 'Success!'); |
|||
}); |
|||
|
|||
return Promise.resolve(); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, `Something went wrong.`); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = RLReset; |
@ -0,0 +1,70 @@ |
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
|
|||
class RemoteDebug extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'rdebug'; |
|||
this.aliases = ['rdebug', 're']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Remotely debug a cluster'; |
|||
this.usage = 're [host] [cluster] [code]'; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (!message.author) return false; |
|||
if (!this.dyno.globalConfig || !this.dyno.globalConfig.developers) return false; |
|||
|
|||
if (this.dyno.globalConfig.developers.includes(message.author.id)) { |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
if (!args || !args.length) { |
|||
return this.error(message.channel, `Missing server/cluster`); |
|||
} |
|||
|
|||
try { |
|||
const [_env, _cluster, ...codeArr] = args; |
|||
const clusterId = parseInt(_cluster, 10); |
|||
const cluster = await this.db.collection('clusters').findOne({ env: _env, id: clusterId }); |
|||
|
|||
if (!cluster) { |
|||
return this.error(message.channel, `Unable to find cluster ${_cluster}`); |
|||
} |
|||
|
|||
const host = cluster.host.hostname; |
|||
const port = 30000 + clusterId; |
|||
const client = new this.dyno.RPCClient(this.dyno, host, port); |
|||
|
|||
client.request('debug', { token: this.config.rpcToken, code: codeArr.join(' ') }, (err, response) => { |
|||
if (err) { |
|||
return this.error(message.channel, `Something went wrong.`); |
|||
} |
|||
|
|||
let msgArray = [], |
|||
result = response.result; |
|||
|
|||
msgArray = msgArray.concat(this.utils.splitMessage(result, 1990)); |
|||
|
|||
for (let m of msgArray) { |
|||
this.sendCode(message.channel, m.toString().replace(this.config.client.token, 'potato'), 'js'); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
}); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, err); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = RemoteDebug; |
@ -0,0 +1,121 @@ |
|||
const axios = require('axios'); |
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
|
|||
class RemoteDiagnose extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['rd']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Remote diagnose a command or module.'; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.hideFromHelp = true; |
|||
this.expectedArgs = 0; |
|||
this.cooldown = 5000; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (!message.author) return false; |
|||
if (message.guild.id !== this.config.dynoGuild) return false; |
|||
if (!this.dyno.globalConfig || !this.dyno.globalConfig.contributors) return false; |
|||
|
|||
const contribs = this.dyno.globalConfig.contributors.map(c => c.id); |
|||
|
|||
if (!contribs || !contribs.length) return false; |
|||
|
|||
if (contribs.includes(message.author.id)) { |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async getStatus() { |
|||
try { |
|||
const response = await axios.get('https://dyno.gg/api/status'); |
|||
if (!response.data) { |
|||
return Promise.reject('Unable to retrieve data at this time.'); |
|||
} |
|||
return response.data; |
|||
} catch (err) { |
|||
return Promise.reject(err); |
|||
} |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
if (!args || !args.length) { |
|||
return this.error(message.channel, `Missing guild id and name`); |
|||
} |
|||
|
|||
let local = false, |
|||
params = args, |
|||
cluster, |
|||
host; |
|||
|
|||
if (args[0].length <= 3) { |
|||
local = true; |
|||
params = args.slice(1); |
|||
} |
|||
|
|||
const [guildId, ...rest] = params; |
|||
const name = rest.join(' '); |
|||
|
|||
if (!local) { |
|||
let shardCount = this.dyno.globalConfig.shardCount || this.dyno.clientOptions.shardCount; |
|||
const shard = ~~((guildId / 4194304) % shardCount); |
|||
const hostMap = { |
|||
titan: `titan.dyno.lan`, |
|||
atlas: `atlas.dyno.lan`, |
|||
pandora: `pandora.dyno.lan`, |
|||
hyperion: `hype.dyno.lan`, |
|||
enceladus: `prom.dyno.lan`, |
|||
janus: `janus.dyno.lan`, |
|||
local: `localhost`, |
|||
}; |
|||
|
|||
let servers; |
|||
|
|||
try { |
|||
servers = await this.getStatus(); |
|||
} catch (err) { |
|||
return this.error(message.channel, err); |
|||
} |
|||
|
|||
if (!servers) { |
|||
return this.error(message.channel, 'Unable to get servers.'); |
|||
} |
|||
|
|||
const serverName = Object.keys(servers).find(serverName => { |
|||
return servers[serverName].find(s => s.result && s.result.shards.includes(shard)); |
|||
}); |
|||
|
|||
if (!serverName) { |
|||
return this.error(`Unable to find shard.`); |
|||
} |
|||
|
|||
const server = servers[serverName].find(s => s.result && s.result.shards.includes(shard)); |
|||
host = hostMap[serverName.toLowerCase()]; |
|||
cluster = server.id; |
|||
} else { |
|||
cluster = args[0].replace(/([A-Z])([\d]+)/, '$2'); |
|||
host = 'localhost'; |
|||
} |
|||
|
|||
const port = 30000 + parseInt(cluster, 10); |
|||
const client = new this.dyno.RPCClient(this.dyno, host, port); |
|||
|
|||
client.request('diagnose', { token: this.config.rpcToken, id: guildId, name }, (err, response) => { |
|||
if (err) { |
|||
return this.error(message.channel, `Something went wrong.`); |
|||
} |
|||
|
|||
return this.sendMessage(message.channel, response.result || response.error); |
|||
}); |
|||
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
|||
|
|||
module.exports = RemoteDiagnose; |
@ -0,0 +1,63 @@ |
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
const { Client } = require('../../core/rpc'); |
|||
|
|||
class Restart extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['restart']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Restart shards.'; |
|||
this.usage = 'restart'; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.expectedArgs = 0; |
|||
this.cooldown = 30000; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
if (!this.isAdmin(message.member) && !this.isOverseer(message.member)) { |
|||
return this.error(`You're not authorized to use this command.`); |
|||
} |
|||
|
|||
const hostMap = { |
|||
dev: 'localhost', |
|||
}; |
|||
|
|||
try { |
|||
if (!isNaN(args[1])) { |
|||
const clusterId = parseInt(args[1], 10); |
|||
const cluster = await this.db.collection('clusters').findOne({ env: args[0], id: clusterId }); |
|||
|
|||
if (!cluster) { |
|||
return this.error(message.channel, `Unable to find cluster ${args[1]}`); |
|||
} |
|||
|
|||
const host = cluster.host.hostname; |
|||
const client = new Client(host, 5052); |
|||
|
|||
client.request('restart', { id: cluster.id, token: this.config.restartToken }); |
|||
return this.success(message.channel, `Restarting cluster ${cluster.id}.`); |
|||
} else { |
|||
const host = hostMap[args[0]] || `${args[0]}.dyno.lan`; |
|||
switch (args[1]) { |
|||
case 'manager': { |
|||
const client = new Client(host, 5050); |
|||
client.request('restartManager', {}); |
|||
return this.success(message.channel, `Restarting cluster manager on ${args[0]}.`); |
|||
} |
|||
case 'all': { |
|||
const client = new Client(host, 5052); |
|||
client.request('restart', { id: 'all', token: this.config.restartToken }); |
|||
return this.success(message.channel, `Restarting all clusters on ${args[0]}.`); |
|||
} |
|||
} |
|||
} |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, err); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Restart; |
@ -0,0 +1,69 @@ |
|||
'use strict'; |
|||
|
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
const moment = require('moment'); |
|||
|
|||
require('moment-duration-format'); |
|||
|
|||
class Sessions extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['sessions']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Get session data'; |
|||
this.usage = 'uptime'; |
|||
this.cooldown = 10000; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.overseerEnabled = true; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
permissionsFn({ message }) { |
|||
if (!message.member) return false; |
|||
if (message.guild.id !== this.config.dynoGuild) return false; |
|||
|
|||
if (this.isServerAdmin(message.member, message.channel)) return true; |
|||
if (this.isServerMod(message.member, message.channel)) return true; |
|||
|
|||
let allowedRoles = [ |
|||
'225209883828420608', // Accomplices
|
|||
'222393180341927936', // Regulars
|
|||
'355054563931324420', // Trusted
|
|||
]; |
|||
|
|||
const roles = message.guild.roles.filter(r => allowedRoles.includes(r.id)); |
|||
if (roles && message.member.roles.find(r => roles.find(role => role.id === r))) return true; |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async execute({ message }) { |
|||
try { |
|||
var data = await this.client.getBotGateway(); |
|||
} catch (err) { |
|||
return this.error(message.channel, err); |
|||
} |
|||
|
|||
let resetAfter = moment.duration(data.session_start_limit.reset_after, 'milliseconds'), |
|||
resetAfterDate = moment().subtract(data.session_start_limit.reset_after, 'milliseconds').format('llll'); |
|||
|
|||
const embed = { |
|||
color: this.utils.getColor('blue'), |
|||
title: 'Session Data', |
|||
fields: [ |
|||
{ name: 'Recommended Shards', value: data.shards.toString(), inline: true }, |
|||
{ name: 'Session Limit', value: data.session_start_limit.total.toString(), inline: true }, |
|||
{ name: 'Session Remaining', value: data.session_start_limit.remaining.toString(), inline: true }, |
|||
{ name: 'Reset After', value: resetAfter.format('d [days], h [hrs], m [min], s [sec]') }, |
|||
{ name: 'Reset After Date', value: resetAfterDate }, |
|||
], |
|||
timestamp: (new Date()).toISOString(), |
|||
}; |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Sessions; |
@ -0,0 +1,33 @@ |
|||
'use strict'; |
|||
|
|||
const { exec } = require('child_process'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Speedtest extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['speedtest', 'speed']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Get the result of a speed test.'; |
|||
this.usage = 'speedtest'; |
|||
this.permissions = 'admin'; |
|||
this.extraPermissions = [this.config.owner || this.config.admin]; |
|||
this.overseerEnabled = true; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
execute({ message }) { |
|||
return this.sendMessage(message.channel, '```Running speed test...```').then(m => { |
|||
exec('/usr/bin/speedtest --simple --share', (err, stdout) => { |
|||
if (err) return m.edit('An error occurred.'); |
|||
return m.edit('```\n' + stdout + '\n```'); |
|||
}); |
|||
}).catch(err => { |
|||
if (this.config.self) return this.logger.error(err); |
|||
return this.error(message.channel, 'Unable to get speedtest.'); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
module.exports = Speedtest; |
@ -0,0 +1,27 @@ |
|||
'use strict'; |
|||
|
|||
const util = require('util'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class UnloadModule extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['unloadmodule', 'unloadmod']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Unload a module.'; |
|||
this.usage = 'unloadmodule [module]'; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
if (!this.dyno) return false; |
|||
|
|||
return this.dyno.ipc.awaitResponse('unload', { type: 'modules', name: args[0] }) |
|||
.then(data => this.sendCode(message.channel, data.map(d => util.inspect(d)), 'js')) |
|||
.catch(err => this.sendCode(message.channel, err, 'js')); |
|||
} |
|||
} |
|||
|
|||
module.exports = UnloadModule; |
@ -0,0 +1,53 @@ |
|||
'use strict'; |
|||
|
|||
const { exec } = require('child_process'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Update extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'update'; |
|||
this.aliases = ['update']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Update the bot'; |
|||
this.usage = 'update (branch)'; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
exec(command) { |
|||
return new Promise((resolve, reject) => { |
|||
exec(command, (err, stdout, stderr) => { |
|||
if (err) return reject(err); |
|||
return resolve(stdout || stderr); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
let msgArray = [], |
|||
result; |
|||
|
|||
const branch = args && args.length ? args[0] : 'develop'; |
|||
this.sendMessage(message.channel, `Pulling the latest from ${branch}...`); |
|||
|
|||
try { |
|||
result = await this.exec(`git pull origin ${branch}; gulp build`); |
|||
} catch (err) { |
|||
result = err; |
|||
} |
|||
|
|||
msgArray = msgArray.concat(this.utils.splitMessage(result, 1990)); |
|||
|
|||
for (let m of msgArray) { |
|||
this.sendCode(message.channel, m, 'js'); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
} |
|||
} |
|||
|
|||
module.exports = Update; |
@ -0,0 +1,94 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class UpdateTeam extends Command { |
|||
|
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.name = 'updateteam'; |
|||
this.aliases = ['updateteam']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Update team data for the website'; |
|||
this.usage = 'updateteam'; |
|||
this.hideFromHelp = true; |
|||
this.permissions = 'admin'; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
parseUser(user, size) { |
|||
return { |
|||
id: user.id, |
|||
name: `${user.username}#${user.discriminator}`, |
|||
avatar: user.dynamicAvatarURL(null, size), |
|||
}; |
|||
} |
|||
|
|||
hasRole(member, roleId) { |
|||
if (!member.roles.includes(roleId)) { |
|||
return false; |
|||
} |
|||
|
|||
const roleIds = [ |
|||
'203040224597508096', |
|||
'250182695693320193', |
|||
'231095149508296704', |
|||
'225209883828420608', |
|||
'355054563931324420' |
|||
]; |
|||
const roleIndex = roleIds.indexOf(roleId); |
|||
const excludeIds = roleIds.filter(id => roleIds.indexOf(id) < roleIndex); |
|||
|
|||
return !excludeIds.filter(id => member.roles.includes(id)).length; |
|||
} |
|||
|
|||
updateCoreMember(members, user) { |
|||
let member = members.find(m => m.id === user.id); |
|||
user = Object.assign(user, member); |
|||
return user; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
const guild = message.channel.guild; |
|||
const roles = { |
|||
contributors: guild.members |
|||
.filter(m => this.hasRole(m, '250182695693320193')) |
|||
.map(m => this.parseUser(m.user, 128)) |
|||
.sort((a, b) => a.id - b.id), |
|||
moderators: guild.members |
|||
.filter(m => this.hasRole(m, '231095149508296704')) |
|||
.map(m => this.parseUser(m.user, 128)) |
|||
.sort((a, b) => a.id - b.id), |
|||
accomplices: guild.members |
|||
.filter(m => this.hasRole(m, '225209883828420608')) |
|||
.map(m => this.parseUser(m.user, 128)) |
|||
.sort((a, b) => a.id - b.id), |
|||
support: guild.members |
|||
.filter(m => this.hasRole(m, '355054563931324420')) |
|||
.map(m => this.parseUser(m.user, 128)) |
|||
.sort((a, b) => a.id - b.id), |
|||
}; |
|||
|
|||
const coreMembers = guild.members |
|||
.filter(m => m.roles.includes('203040224597508096')) |
|||
.map(m => this.parseUser(m.user, 256)); |
|||
|
|||
try { |
|||
const globalConfig = await this.models.Dyno.findOne({}, { team: 1 }).lean().exec(); |
|||
for (let [key, val] of Object.entries(roles)) { |
|||
globalConfig.team[key] = val; |
|||
} |
|||
globalConfig.team.core = globalConfig.team.core.map(user => this.updateCoreMember(coreMembers, user)); |
|||
|
|||
await this.models.Dyno.update({}, { $set: { team: globalConfig.team } }); |
|||
|
|||
return this.success(message.channel, 'Updated team members.'); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return this.error(message.channel, 'An error occurred.'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = UpdateTeam; |
@ -0,0 +1,25 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Username extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['username', 'un']; |
|||
this.group = 'Admin'; |
|||
this.description = 'Change the bot username.'; |
|||
this.usage = 'username [new username]'; |
|||
this.permissions = 'admin'; |
|||
this.extraPermissions = [this.config.owner || this.config.admin]; |
|||
this.expectedArgs = 1; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
return this.client.editSelf({ username: args.join(' ') }) |
|||
.then(() => this.success(message.channel, `Username changed to ${args.join(' ')}`)) |
|||
.catch(() => this.error(message.channel, 'Unable to change username.')); |
|||
} |
|||
} |
|||
|
|||
module.exports = Username; |
@ -0,0 +1,76 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
const moment = require('moment'); |
|||
|
|||
require('moment-duration-format'); |
|||
|
|||
class Info extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['info']; |
|||
this.group = 'Info'; |
|||
this.description = 'Get bot info.'; |
|||
this.usage = 'info'; |
|||
this.cooldown = 60000; |
|||
this.expectedArgs = 0; |
|||
this.noDisable = true; |
|||
this.sendDM = true; |
|||
} |
|||
|
|||
async execute({ message }) { |
|||
const uptime = moment.duration(process.uptime(), 'seconds'); |
|||
const cluster = this.dyno.clientOptions.clusterId.toString(); |
|||
const uptimeText = uptime.format('w [weeks] d [days], h [hrs], m [min], s [sec]'); |
|||
const footer = `${this.config.stateName} | Cluster ${cluster} | Shard ${message.channel.guild.shard.id} | Uptime ${uptimeText}`; |
|||
|
|||
const embed = { |
|||
color: this.utils.hexToInt('#3395d6'), |
|||
author: { |
|||
name: 'Dyno', |
|||
url: 'https://www.dyno.gg', |
|||
icon_url: `${this.config.avatar}?r=${this.config.version}`, |
|||
}, |
|||
fields: [], |
|||
footer: { |
|||
text: footer, |
|||
}, |
|||
}; |
|||
|
|||
embed.fields.push({ name: 'Version', value: this.config.version, inline: true }); |
|||
embed.fields.push({ name: 'Library', value: this.config.lib, inline: true }); |
|||
embed.fields.push({ name: 'Creator', value: this.dyno.globalConfig.author, inline: true }); |
|||
|
|||
try { |
|||
const [res, guildCounts] = await Promise.all([ |
|||
this.redis.hgetall(`dyno:stats:${this.config.state}`), |
|||
this.redis.hgetall(`dyno:guilds:${this.config.client.id}`), |
|||
]); |
|||
|
|||
let guildCount = Object.values(guildCounts).reduce((a, b) => a += parseInt(b), 0); |
|||
|
|||
let shards = []; |
|||
for (const key in res) { |
|||
const shard = JSON.parse(res[key]); |
|||
shards.push(shard); |
|||
} |
|||
|
|||
const userCount = this.utils.sumKeys('users', shards); |
|||
|
|||
embed.fields.push({ name: 'Servers', value: guildCount.toString(), inline: true }); |
|||
embed.fields.push({ name: 'Users', value: userCount.toString(), inline: true }); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
} |
|||
|
|||
embed.fields.push({ name: 'Website', value: '[dyno.gg](https://www.dyno.gg)', inline: true }); |
|||
embed.fields.push({ name: 'Invite', value: '[dyno.gg/invite](https://www.dyno.gg/invite)', inline: true }); |
|||
embed.fields.push({ name: 'Discord', value: '[dyno.gg/discord](https://www.dyno.gg/discord)', inline: true }); |
|||
embed.fields.push({ name: 'Donate', value: '[dyno.gg/donate](https://www.dyno.gg/donate)', inline: true }); |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Info; |
@ -0,0 +1,30 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Ping extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['ping']; |
|||
this.group = 'Info'; |
|||
this.description = 'Ping the bot'; |
|||
this.usage = 'ping'; |
|||
this.hideFromHelp = true; |
|||
this.noDisable = true; |
|||
this.cooldown = 3000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
execute({ message }) { |
|||
let start = Date.now(); |
|||
|
|||
return this.sendMessage(message.channel, 'Pong! ') |
|||
.then(msg => { |
|||
let diff = (Date.now() - start); |
|||
return msg.edit(`Pong! \`${diff}ms\``); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
module.exports = Ping; |
@ -0,0 +1,47 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Premium extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['premium']; |
|||
this.group = 'Info'; |
|||
this.description = 'Dyno premium information. (Responds in DM)'; |
|||
this.usage = 'premium'; |
|||
this.noDisable = true; |
|||
this.expectedArgs = 0; |
|||
this.cooldown = 60000; |
|||
} |
|||
|
|||
execute({ message }) { |
|||
let pref = '`▶`'; |
|||
const embed = { |
|||
color: this.utils.getColor('premium'), |
|||
author: { |
|||
name: 'Dyno Premium', |
|||
icon_url: 'https://cdn.dyno.gg/dyno-premium-64.png', |
|||
}, |
|||
description: [ |
|||
`Premium is an exclusive version of Dyno with premium features, and improved quality / uptime.`, |
|||
`It's also a great way to support Dyno development and hosting!`, |
|||
].join('\n'), |
|||
fields: [ |
|||
{ name: 'Features', value: [ |
|||
`${pref} Hosted on private/dedicated servers for 99.99% uptime.`, |
|||
`${pref} Volume control, playlists, Soundcloud, and more saved queues.`, |
|||
`${pref} Slowmode: Managing chat speed per user or channel.`, |
|||
`${pref} Autopurge: Purge messages at set times.`, |
|||
`${pref} Higher speed/performance and unnoticeable restarts or downtime.`, |
|||
`${pref} Fewer performance-based limits.`, |
|||
].join('\n') }, |
|||
{ name: 'Get Premium', value: `You can upgrade today at https://www.dynobot.net/upgrade` }, |
|||
], |
|||
}; |
|||
|
|||
return this.sendDM(message.author.id, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Premium; |
@ -0,0 +1,107 @@ |
|||
'use strict'; |
|||
|
|||
const os = require('os'); |
|||
const moment = require('moment'); |
|||
const { exec } = require('child_process'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
require('moment-duration-format'); |
|||
|
|||
class Stats extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['stats']; |
|||
this.group = 'Info'; |
|||
this.description = 'Get bot stats.'; |
|||
this.usage = 'stats'; |
|||
this.hideFromHelp = true; |
|||
this.cooldown = 5000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
sumKeys(key, data) { |
|||
return data.reduce((a, b) => a + (b[key] ? parseInt(b[key], 10) : 0), 0); |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
const stateMap = { |
|||
Lance: 0, |
|||
Beta: 1, |
|||
Lunar: 2, |
|||
Carti: 3, |
|||
API: 5, |
|||
Arsen: 6, |
|||
}; |
|||
|
|||
const idMap = Object.keys(stateMap).reduce((obj, key) => { |
|||
obj[stateMap[key]] = key; |
|||
return obj; |
|||
}, {}); |
|||
|
|||
let state = args.length ? (isNaN(args[0]) ? stateMap[args[0]] : args[0]) : this.config.state; |
|||
let stateName = args.length ? (isNaN(args[0]) ? args[0] : idMap[args[0]]) : this.config.stateName; |
|||
|
|||
if (!state || !stateName) { |
|||
state = this.config.state; |
|||
stateName = this.config.stateName; |
|||
} |
|||
|
|||
const [shards, guildCounts, vc] = await Promise.all([ |
|||
this.redis.hgetall(`dyno:cstats:${this.config.client.id}`), |
|||
this.redis.hgetall(`dyno:guilds:${this.config.client.id}`), |
|||
this.redis.hgetall(`dyno:vc`), // eslint-disable-line
|
|||
]).catch(() => false); |
|||
|
|||
const data = {}; |
|||
|
|||
data.shards = []; |
|||
for (const key in shards) { |
|||
const shard = JSON.parse(shards[key]); |
|||
data.shards.push(shard); |
|||
} |
|||
|
|||
data.guilds = Object.values(guildCounts).reduce((a, b) => a += parseInt(b), 0); |
|||
// data.guilds = this.sumKeys('guilds', data.shards);
|
|||
data.users = this.sumKeys('users', data.shards); |
|||
// data.voiceConnections = this.sumKeys('voice', data.shards);
|
|||
data.voice = this.sumKeys('voice', data.shards); |
|||
data.playing = this.sumKeys('playing', data.shards); |
|||
data.events = this.sumKeys('events', data.shards); |
|||
data.allConnections = [...Object.values(vc)].reduce((a, b) => a + parseInt(b), 0); |
|||
|
|||
let streams = this.config.isCore ? data.allConnections : `${data.playing}/${data.voice}`, |
|||
uptime = moment.duration(process.uptime(), 'seconds'), |
|||
footer = `PID ${process.pid} | ${stateName} | Cluster ${this.dyno.clientOptions.clusterId.toString()} | Shard ${message.channel.guild.shard.id}`; |
|||
|
|||
const embed = { |
|||
author: { |
|||
name: 'Dyno', |
|||
icon_url: `${this.config.avatar}`, |
|||
}, |
|||
fields: [ |
|||
{ name: 'Guilds', value: data.guilds.toString(), inline: true }, |
|||
{ name: 'Users', value: data.users.toString(), inline: true }, |
|||
{ name: 'Streams', value: streams.toString(), inline: true }, |
|||
{ name: 'Load Avg', value: os.loadavg().map(n => n.toFixed(3)).join(', '), inline: true }, |
|||
{ name: 'Free Mem', value: `${this.utils.formatBytes(os.freemem())} / ${this.utils.formatBytes(os.totalmem())}`, inline: true }, |
|||
{ name: 'Uptime', value: uptime.format('w [weeks] d [days], h [hrs], m [min], s [sec]'), inline: true }, |
|||
], |
|||
footer: { |
|||
text: footer, |
|||
}, |
|||
timestamp: new Date(), |
|||
}; |
|||
|
|||
if (data.events) { |
|||
let events = Math.round(data.events / 15); |
|||
embed.fields.push({ name: 'Events/sec', value: `${events}/sec`, inline: true }); |
|||
} |
|||
|
|||
embed.fields = embed.fields.filter(f => f.value !== '0'); |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Stats; |
@ -0,0 +1,37 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
const moment = require('moment'); |
|||
|
|||
require('moment-duration-format'); |
|||
|
|||
class Uptime extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['uptime', 'up']; |
|||
this.group = 'Info'; |
|||
this.description = 'Get bot uptime'; |
|||
this.usage = 'uptime'; |
|||
this.cooldown = 3000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
execute({ message }) { |
|||
let uptime = moment.duration(process.uptime(), 'seconds'), |
|||
started = moment().subtract(process.uptime(), 'seconds').format('llll'); |
|||
|
|||
const embed = { |
|||
color: this.utils.getColor('blue'), |
|||
title: 'Uptime', |
|||
description: uptime.format('w [weeks] d [days], h [hrs], m [min], s [sec]'), |
|||
footer: { |
|||
text: `PID ${process.pid} | ${this.config.stateName} | Cluster ${this.dyno.clientOptions.clusterId.toString()} | Shard ${message.channel.guild.shard.id} | Last started on ${started}`, |
|||
}, |
|||
}; |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Uptime; |
@ -0,0 +1,40 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Avatar extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['avatar', 'av']; |
|||
this.group = 'Misc'; |
|||
this.description = `Get a users' avatar.`; |
|||
this.usage = 'avatar [user]'; |
|||
this.expectedArgs = 0; |
|||
this.cooldown = 3000; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
let user = args.length ? this.resolveUser(message.channel.guild, args[0]) : message.author; |
|||
|
|||
if (!user) { |
|||
return this.error(message.channel, `Couldn't find that user.`); |
|||
} |
|||
|
|||
user = user.user || user; |
|||
|
|||
let avatar = user.dynamicAvatarURL(null, 256); |
|||
avatar = avatar.match(/.gif/) ? `${avatar}&f=.gif` : avatar; |
|||
|
|||
return this.sendMessage(message.channel, { embed: { |
|||
author: { |
|||
name: this.utils.fullName(user), |
|||
icon_url: user.dynamicAvatarURL(null, 32).replace(/\?size=.*/, ''), |
|||
}, |
|||
title: 'Avatar', |
|||
image: { url: avatar, width: 256, height: 256 }, |
|||
} }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Avatar; |
@ -0,0 +1,75 @@ |
|||
'use strict'; |
|||
|
|||
const axios = require('axios'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Botlist extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['botlist']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Gets the carbonitex bot list ordered by server counts'; |
|||
this.usage = 'botlist [page]'; |
|||
this.hideFromHelp = true; |
|||
this.cooldown = 5000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
let page = args[0] || 1, |
|||
i = 0; |
|||
|
|||
try { |
|||
const res = await axios.get(this.config.carbon.list); |
|||
var data = res.data; |
|||
} catch (err) { |
|||
return this.logger.error(err); |
|||
} |
|||
|
|||
let list = []; |
|||
if (this.dyno.botlist && (Date.now() - this.dyno.botlist.createdAt) < 300000) { |
|||
list = this.dyno.botlist.data; |
|||
} else { |
|||
list = data.map(bot => { |
|||
bot.botid = parseInt(bot.botid); |
|||
bot.servercount = parseInt(bot.servercount); |
|||
return bot; |
|||
}) |
|||
.filter(bot => bot.botid > 1000) |
|||
.sort((a, b) => (a.servercount < b.servercount) ? 1 : (a.servercount > b.servercount) ? -1 : 0) |
|||
.map(bot => { |
|||
let name = bot.name.includes('spoo.py') ? 'spoo.py' : bot.name; |
|||
let field = { |
|||
name: `${++i}. ${name}`, |
|||
value: `${bot.servercount} Servers`, |
|||
inline: true, |
|||
}; |
|||
return field; |
|||
}); |
|||
|
|||
this.dyno.botlist = { |
|||
createdAt: Date.now(), |
|||
data: list, |
|||
}; |
|||
} |
|||
|
|||
if (!list || !list.length) { |
|||
return this.error(message.channel, `Unable to get results`); |
|||
} |
|||
|
|||
let start = (page - 1) * 10; |
|||
|
|||
list = list.slice(start, start + 10); |
|||
|
|||
return this.sendMessage(message.channel, { embed: { |
|||
color: parseInt(('00000' + (Math.random() * (1 << 24) | 0).toString(16)).slice(-6), 16), |
|||
description: `**Bot List (Page ${page}**)`, |
|||
fields: list, |
|||
footer: { text: 'Last Updated' }, |
|||
timestamp: new Date(this.dyno.botlist.createdAt), |
|||
} }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Botlist; |
@ -0,0 +1,41 @@ |
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Color extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['color', 'colour']; |
|||
this.module = 'Misc'; |
|||
this.description = 'Show a color using hex.'; |
|||
this.usage = ['color #hex', 'color hex']; |
|||
this.example = ['color #ffffff', 'color ffffff']; |
|||
this.cooldown = 3000; |
|||
this.expectedArgs = 1; |
|||
} |
|||
hextoRGB(hex) { |
|||
const r = parseInt(hex.substring(0, 2), 16); |
|||
const g = parseInt(hex.substring(2, 4), 16); |
|||
const b = parseInt(hex.substring(4, 6), 16); |
|||
|
|||
return [r, g, b]; |
|||
} |
|||
execute({ message, args }) { |
|||
const hex = args[0].replace('#', ''); |
|||
const rgb = this.hextoRGB(hex); |
|||
if (rgb.includes(NaN)) return this.error(message.channel, 'Invalid color format!'); |
|||
const colorurl = `${this.config.colorapi.host}/color/${hex}/80x80.png`; |
|||
|
|||
return this.sendMessage(message.channel, { |
|||
embed: { |
|||
color: parseInt(`0x${hex}`), |
|||
fields: [ |
|||
{ name: 'Hex', value: `#${hex}` }, |
|||
{ name: 'RGB', value: `${rgb.join(', ')}` }, |
|||
], |
|||
thumbnail: { url: colorurl }, |
|||
}, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
module.exports = Color; |
@ -0,0 +1,35 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Discrim extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['discrim']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Gets a list of users with a discriminator'; |
|||
this.usage = 'discrim 1234'; |
|||
this.cooldown = 6000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
const discrim = args.length ? args[0] : message.author.discriminator; |
|||
let users = this.client.users.filter(u => u.discriminator === discrim) |
|||
.map(u => this.utils.fullName(u)); |
|||
|
|||
if (!users || !users.length) { |
|||
return this.error(`I couldn't find any results for ${discrim}`); |
|||
} |
|||
|
|||
users = users.slice(0, 10); |
|||
|
|||
return this.sendMessage(message.channel, { embed: { |
|||
color: parseInt(('00000' + (Math.random() * (1 << 24) | 0).toString(16)).slice(-6), 16), |
|||
description: users.join('\n'), |
|||
} }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Discrim; |
@ -0,0 +1,64 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Distance extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['distance']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Get the distance between two sets of coordinates'; |
|||
this.usage = 'distance [coords] [coords]'; |
|||
this.cooldown = 3000; |
|||
this.expectedArgs = 2; |
|||
this.example = [ |
|||
'distance 51.295978,-1.104938 45.407692,2.4415', |
|||
]; |
|||
} |
|||
|
|||
deg2rad(deg) { |
|||
return deg * (Math.PI/180) |
|||
} |
|||
|
|||
getDistanceFromLatLonInKm(lat1,lon1,lat2,lon2) { |
|||
var R = 6371; |
|||
var dLat = this.deg2rad(lat2-lat1); |
|||
var dLon = this.deg2rad(lon2-lon1); |
|||
var a = Math.sin(dLat/2) * Math.sin(dLat/2) + |
|||
Math.cos(this.deg2rad(lat1)) * Math.cos(this.deg2rad(lat2)) * |
|||
Math.sin(dLon/2) * Math.sin(dLon/2); |
|||
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); |
|||
var d = R * c; |
|||
return d; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
args = args.join(' ').replace(/, /g, ',').split(' '); |
|||
|
|||
let coords1 = args[0].split(','), |
|||
coords2 = args[1].split(','); |
|||
|
|||
if (!coords1 || !coords2 || coords1.length !== 2 || coords2.length !== 2) { |
|||
return this.error(message.channel, 'Invalid coordinates, please provide two coordinate pairs. See distance help for more info.'); |
|||
} |
|||
|
|||
let distance = this.getDistanceFromLatLonInKm(coords1[0], coords1[1], coords2[0], coords2[1]); |
|||
if (!distance) { |
|||
return this.error(message.channel, 'Invalid coordinates, please provide two coordinate pairs. See distance help for more info.'); |
|||
} |
|||
|
|||
const embed = { |
|||
color: this.utils.getColor('blue'), |
|||
fields: [ |
|||
{ name: 'Lat/Lng 1', value: coords1.join(', '), inline: true }, |
|||
{ name: 'Lat/Lng 2', value: coords2.join(', '), inline: true }, |
|||
{ name: 'Distance (km)', value: distance.toFixed(2).toString() }, |
|||
], |
|||
}; |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Distance; |
@ -0,0 +1,41 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class DynoAvatar extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['dynoavatar', 'dynoav']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Generates a Dyno-like avatar.'; |
|||
this.usage = 'dynoav'; |
|||
this.cooldown = 10000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
let user = args.length ? this.resolveUser(message.channel.guild, args[0]) : message.author; |
|||
|
|||
if (!user) { |
|||
return this.error(message.channel, `Couldn't find that user.`); |
|||
} |
|||
|
|||
user = user.user || user; |
|||
|
|||
let avatar = user.dynamicAvatarURL(null, 256); |
|||
const dynoAvatar = `${this.config.colorapi.host}/dynoav?url=${avatar}?r=1.1`; |
|||
// avatar = avatar.match(/.gif/) ? `${avatar}&f=.gif` : avatar;
|
|||
|
|||
return this.sendMessage(message.channel, { embed: { |
|||
author: { |
|||
name: this.utils.fullName(user), |
|||
icon_url: user.dynamicAvatarURL(null, 32).replace(/\?size=.*/, ''), |
|||
}, |
|||
title: 'Avatar', |
|||
image: { url: dynoAvatar, width: 256, height: 256 }, |
|||
} }); |
|||
} |
|||
} |
|||
|
|||
module.exports = DynoAvatar; |
@ -0,0 +1,52 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Emojis extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['emotes', 'emojis']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Gets a list of server emojis.'; |
|||
this.usage = 'emotes'; |
|||
this.cooldown = 10000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
let query; |
|||
|
|||
if (args && args.length > 0) { |
|||
query = args.join(' ').toLowerCase(); |
|||
} |
|||
|
|||
let emojis = message.guild.emojis; |
|||
|
|||
if (!emojis.length) { |
|||
return this.sendMessage(message.channel, `There are no emotes in this server.`); |
|||
} |
|||
|
|||
if (query) { |
|||
emojis = emojis.filter(e => e.name.toLowerCase().search(query) > -1); |
|||
} |
|||
|
|||
if (query && (!emojis || !emojis.length)) { |
|||
return this.sendMessage(message.channel, `I couldn't find any emotes.`); |
|||
} |
|||
|
|||
// console.log(emojis.map(e => `<:${e.name}:${e.id}>`).join(' '));
|
|||
const emojiCount = emojis.filter(e => !e.animated).length; |
|||
const animatedCount = emojis.filter(e => e.animated).length; |
|||
|
|||
const embed = { |
|||
color: this.utils.getColor('blue'), |
|||
title: `${emojiCount}${!query ? '/50' : ''} Emotes, ${animatedCount}${!query ? '/50' : ''} Animated`, |
|||
description: emojis.map(e => e.animated ? `<a:${e.name}:${e.id}>` : `<:${e.name}:${e.id}>`).join(' '), |
|||
}; |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = Emojis; |
@ -0,0 +1,94 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class InviteInfo extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['inviteinfo']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Get information about an invite'; |
|||
this.usage = 'inviteinfo [code or invite]'; |
|||
this.cooldown = 6000; |
|||
this.hideFromHelp = true; |
|||
this.expectedArgs = 1; |
|||
|
|||
this._inviteRegex = new RegExp('(discordapp.com/invite|discord.me|discord.gg)(?:/#)?(?:/invite)?/([a-zA-Z0-9\-]+)'); // eslint-disable-line
|
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
const match = args.join(' ').match(this._inviteRegex); |
|||
|
|||
let code = match ? match.pop() : args[0]; |
|||
|
|||
if (!match && !code) { |
|||
return this.error(`Invalid code or link.`); |
|||
} |
|||
|
|||
try { |
|||
var invite = await this.client.getInvite(code, true); |
|||
} catch (err) { |
|||
return this.error(message.channel, `Invalid code or link.`); |
|||
} |
|||
|
|||
if (!invite) { |
|||
return this.error(message.channel, `I can't get that invite.`); |
|||
} |
|||
|
|||
const embed = { |
|||
color: this.utils.getColor('blue'), |
|||
author: { |
|||
name: invite.guild.name, |
|||
icon_url: `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.jpg?size=128`, |
|||
}, |
|||
fields: [], |
|||
footer: { text: `ID: ${invite.guild.id}` }, |
|||
}; |
|||
|
|||
if (invite.inviter) { |
|||
embed.fields.push({ name: 'Inviter', value: this.utils.fullName(invite.inviter), inline: true }); |
|||
} |
|||
|
|||
if (invite.channel) { |
|||
embed.fields.push({ name: 'Channel', value: `#${invite.channel.name}`, inline: true }); |
|||
} |
|||
|
|||
if (invite.memberCount) { |
|||
if (invite.presenceCount) { |
|||
embed.fields.push({ name: 'Members', value: `${invite.presenceCount}/${invite.memberCount}`, inline: true }); |
|||
} else { |
|||
embed.fields.push({ name: 'Members', value: `${invite.memberCount}`, inline: true }); |
|||
} |
|||
} |
|||
|
|||
if (message.guild.id === this.config.dynoGuild) { |
|||
try { |
|||
var inviteGuild = await this.models.Server.findOne({ _id: invite.guild.id }, { deleted: 1, ownerID: 1 }).lean().exec(); |
|||
} catch (err) { |
|||
// pass
|
|||
} |
|||
|
|||
if (inviteGuild) { |
|||
embed.fields.push({ name: 'Dyno', value: inviteGuild.deleted === true ? 'Kicked' : 'In Server', inline: true }); |
|||
|
|||
if (inviteGuild.ownerID) { |
|||
var owner = this.client.users.get(inviteGuild.ownerID); |
|||
if (!owner) { |
|||
owner = await this.restClient.getRESTUser(inviteGuild.ownerID); |
|||
} |
|||
|
|||
if (owner) { |
|||
embed.fields.push({ name: 'Owner', value: this.utils.fullName(owner), inline: true }); |
|||
} |
|||
} |
|||
} else { |
|||
embed.fields.push({ name: 'Dyno', value: 'Never Added', inline: true }); |
|||
} |
|||
} |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = InviteInfo; |
@ -0,0 +1,65 @@ |
|||
'use strict'; |
|||
|
|||
const { Command } = require('@dyno.gg/dyno-core'); |
|||
|
|||
class MemberCount extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['membercount']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Get the server member count.'; |
|||
this.usage = 'membercount'; |
|||
this.cooldown = 10000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
const guild = message.channel.guild; |
|||
|
|||
if (args.length && (args.includes('full') || args.includes('withprune')) && this.isServerMod(message.member, message.channel)) { |
|||
try { |
|||
var pruneCount = await this.client.getPruneCount(guild.id, 30); |
|||
} catch (err) { |
|||
// pass
|
|||
} |
|||
} |
|||
|
|||
if (args.length && (args.includes('full') || args.includes('withbans')) && this.isServerMod(message.member, message.channel)) { |
|||
try { |
|||
let bans = await this.client.getGuildBans(guild.id); |
|||
var banCount = bans.length; |
|||
} catch (err) { |
|||
// pass
|
|||
} |
|||
} |
|||
|
|||
let fields = [ |
|||
{ name: 'Members', value: guild.memberCount.toString(), inline: true }, |
|||
{ name: 'Humans', value: guild.members.filter(m => !m.bot).length.toString(), inline: true }, |
|||
{ name: 'Bots', value: guild.members.filter(m => m.bot).length.toString(), inline: true }, |
|||
] |
|||
|
|||
if (this.config.isPremium) { |
|||
fields.push({ name: 'Online', value: guild.members.filter(m => m.status !== 'offline').length.toString(), inline: true }); |
|||
} |
|||
|
|||
if (pruneCount) { |
|||
fields.push({ name: 'Prune Count', value: pruneCount.toString(), inline: true }); |
|||
} |
|||
|
|||
if (banCount) { |
|||
fields.push({ name: 'Bans', value: banCount.toString(), inline: true }); |
|||
} |
|||
|
|||
const embed = { |
|||
color: this.utils.getColor('blue'), |
|||
fields: fields, |
|||
timestamp: new Date(), |
|||
}; |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = MemberCount; |
@ -0,0 +1,37 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class RandomColor extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['randomcolor', 'randcolor', 'randomcolour']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Generates a random hex color with preview.'; |
|||
this.usage = 'randomcolor'; |
|||
this.cooldown = 3000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
execute({ message }) { |
|||
const int = (Math.random() * (1 << 24) | 0); |
|||
const hex = ('00000' + int.toString(16)).slice(-6); |
|||
const rgb = [(int & 0xff0000) >> 16, (int & 0x00ff00) >> 8, (int & 0x0000ff)]; |
|||
|
|||
const colorurl = `${this.config.colorapi.host}/color/${hex}/80x80.png`; |
|||
|
|||
return this.sendMessage(message.channel, { |
|||
embed: { |
|||
color: int, |
|||
fields: [ |
|||
{ name: 'Hex', value: `#${hex}` }, |
|||
{ name: 'RGB', value: `${rgb.join(', ')}` }, |
|||
], |
|||
thumbnail: { url: colorurl }, |
|||
}, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
module.exports = RandomColor; |
@ -0,0 +1,68 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class ServerInfo extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['serverinfo']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Get server info/stats.'; |
|||
this.usage = 'serverinfo'; |
|||
this.cooldown = 10000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
const guild = (this.isAdmin(message.author) && args && args.length) ? |
|||
this.client.guilds.get(args[0]) : message.channel.guild; |
|||
|
|||
const owner = this.client.users.get(guild.ownerID); |
|||
|
|||
let categories = guild.channels.filter(c => c.type === 4).length; |
|||
let textChannels = guild.channels.filter(c => c.type === 0).length; |
|||
let voiceChannels = guild.channels.filter(c => c.type === 2).length; |
|||
|
|||
const embed = { |
|||
color: (Math.random() * (1 << 24) | 0), |
|||
author: { |
|||
name: guild.name, |
|||
icon_url: guild.iconURL, |
|||
}, |
|||
thumbnail: { |
|||
url: `https://discordapp.com/api/guilds/${guild.id}/icons/${guild.icon}.jpg`, |
|||
}, |
|||
fields: [ |
|||
{ name: 'Owner', value: this.utils.fullName(owner), inline: true }, |
|||
{ name: 'Region', value: guild.region, inline: true }, |
|||
{ name: 'Channel Categories', value: categories ? categories.toString() : '0', inline: true }, |
|||
{ name: 'Text Channels', value: textChannels ? textChannels.toString() : '0', inline: true }, |
|||
{ name: 'Voice Channels', value: voiceChannels ? voiceChannels.toString() : '0', inline: true }, |
|||
{ name: 'Members', value: guild.memberCount.toString(), inline: true }, |
|||
// { name: 'Emojis', value: guild.emojis.length.toString(), inline: true },
|
|||
], |
|||
footer: { |
|||
text: `ID: ${guild.id} | Server Created`, |
|||
}, |
|||
timestamp: new Date(guild.createdAt), |
|||
}; |
|||
|
|||
embed.fields.push({ name: 'Humans', value: guild.members.filter(m => !m.bot).length.toString(), inline: true }); |
|||
embed.fields.push({ name: 'Bots', value: guild.members.filter(m => m.bot).length.toString(), inline: true }); |
|||
|
|||
if (this.config.isPremium) { |
|||
embed.fields.push({ name: 'Online', value: guild.members.filter(m => m.status !== 'offline').length.toString(), inline: true }); |
|||
} |
|||
|
|||
embed.fields.push({ name: 'Roles', value: guild.roles.size.toString(), inline: true }); |
|||
|
|||
if (guild.roles.size < 25) { |
|||
embed.fields.push({ name: 'Role List', value: guild.roles.map(r => r.name).join(', '), inline: false }); |
|||
} |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = ServerInfo; |
@ -0,0 +1,128 @@ |
|||
'use strict'; |
|||
|
|||
const moment = require('moment'); |
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Whois extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['whois', 'userinfo']; |
|||
this.group = 'Misc'; |
|||
this.description = 'Get user information.'; |
|||
this.usage = 'whois [user mention]'; |
|||
this.example = 'whois @NoobLance'; |
|||
this.cooldown = 3000; |
|||
this.expectedArgs = 0; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
let member = args.length ? this.resolveUser(message.channel.guild, args.join(' ')) : message.member; |
|||
|
|||
if (!member) return this.error(message.channel, `Couldn't find user ${args.join(' ')}`); |
|||
|
|||
const perms = { |
|||
administrator: 'Administrator', |
|||
manageGuild: 'Manage Server', |
|||
manageRoles: 'Manage Roles', |
|||
manageChannels: 'Manage Channels', |
|||
manageMessages: 'Manage Messages', |
|||
manageWebhooks: 'Manage Webhooks', |
|||
manageNicknames: 'Manage Nicknames', |
|||
manageEmojis: 'Manage Emojis', |
|||
kickMembers: 'Kick Members', |
|||
banMembers: 'Ban Members', |
|||
mentionEveryone: 'Mention Everyone', |
|||
}; |
|||
|
|||
const contrib = this.dyno.globalConfig.contributors.find(c => c.id === member.id); |
|||
const extra = []; |
|||
let team = []; |
|||
|
|||
const roles = member.roles && member.roles.length ? |
|||
this.utils.sortRoles(member.roles.map(r => { |
|||
r = message.channel.guild.roles.get(r); |
|||
|
|||
if (!r || !r.id) { |
|||
return 'Invalid role.'; |
|||
} |
|||
|
|||
return `<@&${r.id}>`; |
|||
})).join(' ') : 'None'; |
|||
|
|||
const joinPos = [...message.guild.members.values()] |
|||
.sort((a, b) => (a.joinedAt < b.joinedAt) ? -1 : ((a.joinedAt > b.joinedAt) ? 1 : 0)) |
|||
.filter(m => !m.bot) |
|||
.findIndex(m => m.id === member.id) + 1; |
|||
|
|||
const embed = { |
|||
author: { |
|||
name: this.utils.fullName(member), |
|||
icon_url: member.user.avatarURL, |
|||
}, |
|||
thumbnail: { |
|||
url: (contrib && contrib.badge) ? |
|||
`https://cdn.dyno.gg/badges/${contrib.badge}` : |
|||
member.user.avatarURL, |
|||
}, |
|||
description: `\n<@!${member.id}>`, |
|||
fields: [ |
|||
// { name: 'Status', value: member.status, inline: true },
|
|||
{ name: 'Joined', value: moment.unix(member.joinedAt / 1000).format('llll'), inline: true }, |
|||
{ name: 'Join Position', value: joinPos || 'None', inline: true }, |
|||
{ name: 'Registered', value: moment.unix(member.user.createdAt / 1000).format('llll'), inline: true }, |
|||
{ name: `Roles [${member.roles.length}]`, value: roles.length > 1024 ? `Too many roles to show.` : roles, inline: false }, |
|||
], |
|||
footer: { text: `ID: ${member.id}` }, |
|||
timestamp: new Date(), |
|||
}; |
|||
|
|||
if (member.permission) { |
|||
const memberPerms = member.permission.json; |
|||
const infoPerms = []; |
|||
for (let key in memberPerms) { |
|||
if (!perms[key] || memberPerms[key] !== true) continue; |
|||
if (memberPerms[key]) { |
|||
infoPerms.push(perms[key]); |
|||
} |
|||
} |
|||
|
|||
if (infoPerms.length) { |
|||
embed.fields.push({ name: 'Key Permissions', value: infoPerms.join(', '), inline: false }); |
|||
} |
|||
} |
|||
|
|||
if (member.id === this.client.user.id) { |
|||
team.push('A Real Dyno'); |
|||
} |
|||
// if (this.isAdmin(member)) extra.push(`Dyno Creator`);
|
|||
|
|||
if (contrib) { |
|||
team = team.concat(contrib.titles); |
|||
} |
|||
|
|||
if (this.isServerAdmin(member, message.channel)) { |
|||
if (member.id === message.channel.guild.ownerID) { |
|||
extra.push(`Server Owner`); |
|||
} else if (member.permission.has('administrator')) { |
|||
extra.push(`Server Admin`); |
|||
} else { |
|||
extra.push(`Server Manager`); |
|||
} |
|||
} else if (this.isServerMod(member, message.channel)) { |
|||
extra.push(`Server Moderator`); |
|||
} |
|||
|
|||
if (extra.length) { |
|||
embed.fields.push({ name: 'Acknowledgements', value: extra.join(', '), inline: false }); |
|||
} |
|||
|
|||
if (team.length) { |
|||
embed.fields.push({ name: 'Dyno Team', value: `${team.join(', ')}`, inline: false }); |
|||
} |
|||
|
|||
return this.sendMessage(message.channel, { embed }).catch(err => this.logger.error(err)); |
|||
} |
|||
} |
|||
|
|||
module.exports = Whois; |
@ -0,0 +1,60 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class RoleInfo extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['roleinfo']; |
|||
this.group = 'Roles'; |
|||
this.description = 'Get information about a role.'; |
|||
this.usage = 'roleinfo'; |
|||
this.expectedArgs = 1; |
|||
this.cooldown = 6000; |
|||
} |
|||
|
|||
execute({ message, args }) { |
|||
const guild = message.channel.guild; |
|||
const role = this.resolveRole(message.channel.guild, args.join(' ')); |
|||
|
|||
if (!role) { |
|||
return this.error(message.channel, `I can't find that role`); |
|||
} |
|||
|
|||
if (!guild.roles || !guild.roles.size) { |
|||
return this.error(message.channel, 'There are no roles on this server.'); |
|||
} |
|||
|
|||
let members = guild.members.filter(m => m.roles.includes(role.id)); |
|||
|
|||
const color = role.color ? ('00000' + role.color.toString(16)).slice(-6) : null; |
|||
|
|||
const embed = { |
|||
fields: [ |
|||
{ name: 'ID', value: role.id, inline: true }, |
|||
{ name: 'Name', value: role.name, inline: true }, |
|||
{ name: 'Color', value: color ? `#${color}` : 'None', inline: true }, |
|||
{ name: 'Mention', value: `\`<@&${role.id}>\``, inline: true }, |
|||
{ name: 'Members', value: members.length.toString(), inline: true }, |
|||
{ name: 'Hoisted', value: role.hoist ? 'Yes' : 'No', inline: true }, |
|||
{ name: 'Position', value: role.position.toString(), inline: true }, |
|||
{ name: 'Mentionable', value: role.mentionable ? 'Yes' : 'No', inline: true }, |
|||
], |
|||
footer: { |
|||
text: `Role Created`, |
|||
}, |
|||
timestamp: new Date(role.createdAt), |
|||
}; |
|||
|
|||
if (color) { |
|||
const colorurl = `${this.config.colorapi.host}/color/${color}/80x80.png`; |
|||
embed.color = role.color; |
|||
embed.thumbnail = { url: colorurl }; |
|||
} |
|||
|
|||
return this.sendMessage(message.channel, { embed }); |
|||
} |
|||
} |
|||
|
|||
module.exports = RoleInfo; |
@ -0,0 +1,64 @@ |
|||
'use strict'; |
|||
|
|||
const {Command} = require('@dyno.gg/dyno-core'); |
|||
|
|||
class Roles extends Command { |
|||
constructor(...args) { |
|||
super(...args); |
|||
|
|||
this.aliases = ['roles']; |
|||
this.group = 'Roles'; |
|||
this.description = 'Get a list of server roles and member counts.'; |
|||
this.usage = 'roles (optional search)'; |
|||
this.permissions = 'serverMod'; |
|||
this.expectedArgs = 0; |
|||
this.cooldown = 30000; |
|||
} |
|||
|
|||
async execute({ message, args }) { |
|||
try { |
|||
let query; |
|||
|
|||
if (args && args.length > 0) { |
|||
query = args.join(' ').toLowerCase(); |
|||
} |
|||
|
|||
const roles = await this.getRoles(message.channel.guild, query); |
|||
const msgArray = this.utils.splitMessage(roles, 1950); |
|||
|
|||
for (const m of msgArray) { |
|||
this.sendCode(message.channel, m); |
|||
} |
|||
|
|||
return Promise.resolve(); |
|||
} catch (err) { |
|||
return this.error(message.channel, 'Something went wrong.', err); |
|||
} |
|||
} |
|||
|
|||
getRoles(guild, query) { |
|||
if (!guild.roles || !guild.roles.size) { |
|||
return Promise.resolve('There are no roles on this server.'); |
|||
} |
|||
|
|||
let msgArray = [], |
|||
len = Math.max(...guild.roles.map(r => r.name.length)); |
|||
|
|||
let roles = this.utils.sortRoles(guild.roles); |
|||
|
|||
if (query) { |
|||
roles = roles.filter(r => r.name.toLowerCase().search(query) > -1); |
|||
} |
|||
|
|||
for (let role of roles) { |
|||
if (role.name === '@everyone') continue; |
|||
const members = guild.members.filter(m => m.roles.includes(role.id)); |
|||
role.memberCount = members && members.length ? members.length : 0; |
|||
msgArray.push(`${this.utils.pad(role.name, len)} ${role.memberCount} members`); |
|||
} |
|||
|
|||
return Promise.resolve(msgArray.join('\n')); |
|||
} |
|||
} |
|||
|
|||
module.exports = Roles; |
@ -0,0 +1,575 @@ |
|||
'use strict'; |
|||
|
|||
global.Promise = require('bluebird'); |
|||
|
|||
const fs = require('fs'); |
|||
const path = require('path'); |
|||
const axios = require('axios'); |
|||
const Eris = require('@dyno.gg/eris'); |
|||
const { utils } = require('@dyno.gg/dyno-core'); |
|||
const dot = require('dot-object'); |
|||
const each = require('async-each'); |
|||
const StatsD = require('hot-shots'); |
|||
const moment = require('moment'); |
|||
const uuid = require('uuid/v4'); |
|||
const config = require('./config'); |
|||
const logger = require('./logger'); |
|||
const redis = require('./redis'); |
|||
const db = require('./database'); |
|||
const PermissionsManager = require('./managers/PermissionsManager'); |
|||
const CommandCollection = require('./collections/CommandCollection'); |
|||
const ModuleCollection = require('./collections/ModuleCollection'); |
|||
const GuildCollection = require('./collections/GuildCollection'); |
|||
const WebhookManager = require('./managers/WebhookManager'); |
|||
const EventManager = require('./managers/EventManager'); |
|||
const IPCManager = require('./managers/IPCManager'); |
|||
const RPCServer = require('./RPCServer'); |
|||
const RPCClient = require('./RPCClient'); |
|||
const { Client } = require('./rpc'); |
|||
const prom = require('prom-client'); |
|||
|
|||
|
|||
var EventEmitter; |
|||
|
|||
try { |
|||
EventEmitter = require('eventemitter3'); |
|||
} catch (e) { |
|||
EventEmitter = require('events'); |
|||
} |
|||
|
|||
const redisLock = require('ioredis-lock'); |
|||
|
|||
var instance; |
|||
|
|||
const statsdClient = new StatsD({ |
|||
host: config.statsd.host, |
|||
port: config.statsd.port, |
|||
prefix: config.statsd.prefix, |
|||
}); |
|||
|
|||
const premiumWebhook = 'https://canary.discordapp.com/api/webhooks/523575952744120321/xrh6uyOA0MOuMvHDAZLw5qws-jr9cDELU6xOoXZSTZcLlwN7lMHxt6yQD-dqRmJuLnnB'; |
|||
|
|||
/** |
|||
* @class Dyno |
|||
*/ |
|||
class Dyno { |
|||
|
|||
/** |
|||
* Dyno constructor |
|||
*/ |
|||
constructor() { |
|||
this.isReady = false; |
|||
this.startTime = Date.now(); |
|||
this.uuid = uuid(); |
|||
|
|||
instance = this; // eslint-disable-line
|
|||
|
|||
Object.defineProperty(Eris.Message.prototype, 'guild', { |
|||
get: function get() { return this.channel.guild; }, |
|||
}); |
|||
|
|||
process.on('unhandledRejection', this.handleRejection.bind(this)); |
|||
process.on('uncaughtException', this.crashReport.bind(this)); |
|||
|
|||
this.activityInterval = setInterval(this.uncacheGuilds.bind(this), 900000); |
|||
} |
|||
|
|||
static get instance() { |
|||
return instance; |
|||
} |
|||
|
|||
/** |
|||
* Eris client instance |
|||
* @returns {Eris} |
|||
*/ |
|||
get client() { |
|||
return this._client; |
|||
} |
|||
|
|||
/** |
|||
* Eris rest client instance |
|||
* @return {Eris} |
|||
*/ |
|||
get restClient() { |
|||
return this._restClient; |
|||
} |
|||
|
|||
/** |
|||
* Dyno configuration |
|||
* @returns {Object} |
|||
*/ |
|||
get config() { |
|||
return config; |
|||
} |
|||
|
|||
/** |
|||
* Global configuration |
|||
* @return {Object} |
|||
*/ |
|||
get globalConfig() { |
|||
return this._globalConfig; |
|||
} |
|||
|
|||
get logger() { |
|||
return logger; |
|||
} |
|||
|
|||
get db() { |
|||
return db; |
|||
} |
|||
|
|||
get models() { |
|||
return db.models; |
|||
} |
|||
|
|||
get redis() { |
|||
return this._redis; |
|||
} |
|||
|
|||
get statsd() { |
|||
return statsdClient; |
|||
} |
|||
|
|||
get utils() { |
|||
return utils; |
|||
} |
|||
|
|||
get prefix() { |
|||
return (config.prefix != undefined && typeof config.prefix === 'string') ? config.prefix : '?'; |
|||
} |
|||
|
|||
get prom() { |
|||
return prom; |
|||
} |
|||
|
|||
handleError(err) { |
|||
if (!err || (typeof err === 'string' && !err.length)) { |
|||
return logger.error('An undefined exception occurred.'); |
|||
} |
|||
|
|||
try { |
|||
logger.error(err); |
|||
} catch (e) { |
|||
console.error(err); // eslint-disable-line
|
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Unhandled rejection handler |
|||
* @param {Error|*} reason The reason the promise was rejected |
|||
* @param {Promise} p The promise that was rejected |
|||
*/ |
|||
handleRejection(reason, p) { |
|||
try { |
|||
console.error('Unhandled rejection at: Promise ', p, 'reason: ', reason); // eslint-disable-line
|
|||
} catch (err) { |
|||
console.error(reason); // eslint-disable-line
|
|||
} |
|||
} |
|||
|
|||
crashReport(err) { |
|||
const cid = `C${this.clientOptions.clusterId}`; |
|||
const time = (new Date()).toISOString(); |
|||
let report = `Crash Report [${cid}] ${time}:\n\n${err.stack}`; |
|||
|
|||
report += `\n\nClient Options: ${JSON.stringify(this.clientOptions)}`; |
|||
|
|||
for (let module of this.modules.values()) { |
|||
if (module.crashReport) { |
|||
report += `\n\n${module.crashReport()}`; |
|||
} |
|||
} |
|||
|
|||
const file = path.join(__dirname, `crashreport_${cid}_${time}.txt`); |
|||
fs.writeFileSync(file, report); |
|||
|
|||
setTimeout(() => process.exit(), 6000); |
|||
} |
|||
|
|||
range(start, end) { |
|||
return (new Array(end - start + 1)).fill(undefined).map((_, i) => i + start); |
|||
} |
|||
|
|||
/** |
|||
* Setup Dyno and login |
|||
*/ |
|||
async setup(options, rootContext) { |
|||
options = options || {}; |
|||
|
|||
await this.configure(options); |
|||
|
|||
options.restClient = { restMode: true }; |
|||
|
|||
this.options = Object.assign({}, { rootCtx: rootContext }, options); |
|||
this.clientOptions = options; |
|||
|
|||
this.shards = this.range(options.firstShardId, options.lastShardId); |
|||
|
|||
//connect to redis
|
|||
try { |
|||
this._redis = await redis.connect(); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
} |
|||
|
|||
const pipeline = this.redis.pipeline(); |
|||
|
|||
for (let shard of this.shards) { |
|||
pipeline.hgetall(`guild_activity:${config.client.id}:${options.shardCount}:${shard}`); |
|||
} |
|||
|
|||
let results = await pipeline.exec(); |
|||
|
|||
results = results.map(r => { |
|||
let [err, res] = r; |
|||
if (err) { |
|||
return; |
|||
} |
|||
return res; |
|||
}).filter(r => r != null); |
|||
|
|||
this._guildActivity = Object.assign(...results); |
|||
|
|||
// create the discord client
|
|||
const token = this.config.isPremium ? config.client.token : this.globalConfig.prodToken || config.client.token; |
|||
|
|||
this._client = new Eris(token, config.clientOptions); |
|||
this._restClient = new Eris(`Bot ${token}`, options.restClient); |
|||
|
|||
this.client.on('error', err => logger.error(err)); |
|||
this.client.on('warn', err => logger.error(err)); |
|||
this.client.on('debug', msg => { |
|||
if (typeof msg === 'string') { |
|||
msg = msg.replace(config.client.token, 'potato'); |
|||
} |
|||
logger.debug(`[Eris] ${msg}`); |
|||
}); |
|||
|
|||
if (!options.awaitReady) { |
|||
this.client.once('shardReady', () => { |
|||
this.isReady = true; |
|||
this.user = this._client.user; |
|||
this.userid = this._client.user.id; |
|||
}); |
|||
} |
|||
|
|||
this.dispatcher = new EventManager(this); |
|||
this.ipc = new IPCManager(this); |
|||
this.internalEvents = new EventEmitter(); |
|||
|
|||
// Create collections
|
|||
this.commands = new CommandCollection(config, this); |
|||
this.modules = new ModuleCollection(config, this); |
|||
this.guilds = new GuildCollection(config, this); |
|||
|
|||
// Create managers
|
|||
this.webhooks = new WebhookManager(this); |
|||
this.permissions = new PermissionsManager(this); |
|||
|
|||
// Create RPC Server
|
|||
this.rpcServer = new RPCServer(this); |
|||
this.RPCClient = RPCClient; |
|||
this.cmClient = new Client(config.rpcHost || 'localhost', 5052); |
|||
|
|||
// event listeners
|
|||
this.client.once('ready', this.ready.bind(this)); |
|||
this.client.on('error', this.handleError.bind(this)); |
|||
|
|||
// login to discord
|
|||
this.login(); |
|||
|
|||
this.readyTimeout = setTimeout(() => { |
|||
try { |
|||
this.ipc.send('ready'); |
|||
} catch (err) { |
|||
logger.error(`IPC Error Caught:`, err); |
|||
} |
|||
}, 90000); |
|||
} |
|||
|
|||
async configure(options) { |
|||
await this.loadConfig().catch(() => null); |
|||
this.watchGlobal(); |
|||
|
|||
const clientConfig = { |
|||
disableEvents: { |
|||
TYPING_START: true, |
|||
}, |
|||
disableEveryone: config.client.disableEveryone, |
|||
getAllUsers: config.client.fetchAllUsers || false, |
|||
firstShardID: options.firstShardId || options.clusterId || options.shardId || 0, |
|||
lastShardID: options.lastShardId || options.clusterId || options.shardId || 0, |
|||
maxShards: options.shardCount || 1, |
|||
messageLimit: parseInt(config.client.maxCachedMessages) || 10, |
|||
guildCreateTimeout: 2000, |
|||
largeThreshold: 50, |
|||
defaultImageFormat: 'png', |
|||
preIdentify: this.preIdentify.bind(this), |
|||
intents: config.client.intents || undefined, |
|||
}; |
|||
|
|||
if (!this.config.isPremium && !config.test) { |
|||
// if ((options.clusterId % 2) > 0) {
|
|||
// clientConfig.compress = true;
|
|||
// }
|
|||
|
|||
if (!this.globalConfig.disableGuildActivity) { |
|||
clientConfig.disableEvents.PRESENCE_UPDATE = true; |
|||
clientConfig.createGuild = this.createGuild.bind(this); |
|||
} |
|||
} |
|||
|
|||
if (config.disableEvents) { |
|||
for (let event of config.disableEvents) { |
|||
clientConfig.disableEvents[event] = true; |
|||
} |
|||
} |
|||
|
|||
config.clientOptions = clientConfig; |
|||
|
|||
await this.loadConfig().catch(() => null); |
|||
await this.watchGlobal(); |
|||
|
|||
return clientConfig; |
|||
} |
|||
|
|||
async watchGlobal() { |
|||
await this.updateGlobal(); |
|||
|
|||
this._globalConfigInterval = setInterval(() => this.updateGlobal(), 2 * 60 * 1000); |
|||
} |
|||
|
|||
async loadConfig() { |
|||
try { |
|||
if (this.models.Config != undefined) { |
|||
const dbConfig = await this.models.Config.findOne({ clientId: config.client.id }).lean(); |
|||
if (dbConfig) { |
|||
config = Object.assign(config, dbConfig); |
|||
} |
|||
} |
|||
const globalConfig = await this.models.Dyno.findOne().lean(); |
|||
this._globalConfig = config.global = globalConfig; |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
} |
|||
} |
|||
|
|||
async updateGlobal() { |
|||
try { |
|||
const globalConfig = await this.models.Dyno.findOne().lean(); |
|||
if (globalConfig) { |
|||
this._globalConfig = config.global = globalConfig; |
|||
} |
|||
} catch (err) { |
|||
logger.error(err, 'globalConfigRefresh'); |
|||
} |
|||
} |
|||
|
|||
updateStatus(status) { |
|||
this.playingStatus = status; |
|||
this.client.editStatus('online', { name: this.playingStatus, type: 0 }); |
|||
} |
|||
|
|||
/** |
|||
* Login to Discord |
|||
* @returns {*} |
|||
*/ |
|||
login() { |
|||
// connect to discord
|
|||
this.client.connect(); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
preIdentify(id) { |
|||
let bucket, key, timeout; |
|||
|
|||
if (config.isPremium && !config.test) { |
|||
timeout = 5250; |
|||
key = `shard:identify:${config.client.id}`; |
|||
} else { |
|||
bucket = id % 16; |
|||
timeout = 7500; |
|||
key = `shard:identify:${config.client.id}:${bucket}`; |
|||
} |
|||
|
|||
const lock = redisLock.createLock(this.redis, { |
|||
timeout: timeout, |
|||
retries: Number.MAX_SAFE_INTEGER, |
|||
delay: 50, |
|||
}); |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
lock.acquire(key).then(() => { |
|||
if (id) { |
|||
logger.debug(`Acquired lock on ${id}`); |
|||
} |
|||
|
|||
return resolve(); |
|||
|
|||
// this.client.getBotGateway().then(data => {
|
|||
// const sessionLimit = data.session_start_limit;
|
|||
// if (sessionLimit.remaining <= 5) {
|
|||
// this.alertSessionLimit();
|
|||
// return reject(`Session limit dangerously low.`);
|
|||
// }
|
|||
|
|||
// return resolve();
|
|||
// });
|
|||
}); |
|||
}); |
|||
} |
|||
|
|||
createGuild(_guild) { |
|||
let lastActive = this._guildActivity[_guild.id]; |
|||
|
|||
if (lastActive) { |
|||
lastActive = parseInt(lastActive, 10); |
|||
_guild.lastActive = lastActive; |
|||
let diff = (Date.now() - lastActive); |
|||
let min = (60 * 60 * 24 * 1 * 1000); // 1 days
|
|||
|
|||
delete this._guildActivity[_guild.id]; |
|||
|
|||
if (diff > min) { |
|||
_guild.inactive = true; |
|||
return this.client.guilds.add(_guild, this.client, true); |
|||
} |
|||
} |
|||
|
|||
let guild = this.client.guilds.add(_guild, this.client, true); |
|||
|
|||
if (config.clientOptions.getAllUsers && guild.members.size < guild.memberCount) { |
|||
guild.fetchAllMembers(); |
|||
} |
|||
|
|||
return guild; |
|||
} |
|||
|
|||
uncacheGuilds() { |
|||
const guilds = this.client.guilds.filter(g => !g.inactive); |
|||
for (let guild of guilds) { |
|||
const diff = (Date.now() - guild.lastActive); |
|||
const min = (60 * 60 * 24 * 1 * 1000); // 1 days
|
|||
|
|||
if (diff > min) { |
|||
let _guild = { |
|||
id: guild.id, |
|||
unavailable: guild.unavailable, |
|||
member_count: guild.memberCount, |
|||
lastActive: guild.lastActive, |
|||
inactive: true, |
|||
}; |
|||
this.client.guilds.add(_guild, this.client, true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
alertSessionLimit() { |
|||
const lock = redisLock.createLock(redis, { |
|||
timeout: 60000, |
|||
retries: 0, |
|||
delay: 250, |
|||
}); |
|||
|
|||
lock.acquire(`alerts:session:${config.client.id}`).then(() => { |
|||
this.restClient.executeWebhook('482709011356057631', 'oLrn3NnEg2cL-7cM6mFrvgdTPoCMx-unh8k2YwlEIkcZnXeOy54-QKZpOMUkPZ527x7X', { |
|||
embeds: [{ |
|||
color: 15607824, |
|||
title: `Danger`, |
|||
description: `**Dyno is dangerously close to token reset. Some shards may be offline. Let someone know.**`, |
|||
timestamp: (new Date()).toISOString(), |
|||
}], |
|||
}).catch(() => null); |
|||
}).catch(() => null); |
|||
} |
|||
|
|||
/** |
|||
* Ready event handler |
|||
*/ |
|||
ready() { |
|||
logger.info(`[Dyno] ${this.config.name} ready with ${this.client.guilds.size} guilds.`); |
|||
|
|||
// register discord event listeners
|
|||
this.dispatcher.bindListeners(); |
|||
|
|||
clearTimeout(this.readyTimeout); |
|||
try { |
|||
this.ipc.send('ready'); |
|||
} catch (err) { |
|||
logger.error(`IPC Error Caught:`, err); |
|||
} |
|||
|
|||
this.user = this._client.user; |
|||
this.userid = this._client.user.id; |
|||
|
|||
this.isReady = true; |
|||
|
|||
if (this.globalConfig.playingStatus) { |
|||
this.playingStatus = this.globalConfig.playingStatus[this.config.client.id] || |
|||
this.globalConfig.playingStatus.default || |
|||
this.config.client.game; |
|||
this.client.editStatus('online', { name: this.playingStatus, type: 0 }); |
|||
} else if (this.config.client.game) { |
|||
this.client.editStatus('online', { name: this.config.client.game, type: 0 }); |
|||
} |
|||
|
|||
if (this.config.isPremium) { |
|||
this.leaveInterval = setInterval(this.leaveGuilds.bind(this), 300000); |
|||
this.leaveGuilds(); |
|||
} |
|||
} |
|||
|
|||
async leaveGuilds() { |
|||
try { |
|||
var docs = await this.models.Server.find({ deleted: false, isPremium: true }, { _id: 1, isPremium: 1, premiumInstalled: 1 }).lean().exec(); |
|||
} catch (err) { |
|||
return logger.error(err); |
|||
} |
|||
|
|||
each([...this.client.guilds.values()], guild => { |
|||
let guildConfig = docs.find(d => d._id === guild.id); |
|||
if (!guildConfig || !guildConfig.isPremium) { |
|||
this.verifyAndLeave(guild.id); |
|||
// this.guilds.update(guild.id, { $set: { premiumInstalled: false } }).catch(err => false);
|
|||
// this.client.leaveGuild(guild.id);
|
|||
} |
|||
}); |
|||
} |
|||
|
|||
async verifyAndLeave(guildId) { |
|||
try { |
|||
const doc = await this.models.Server.findOne({ _id: guildId }).lean(); |
|||
if (!doc) { |
|||
this.postWebhook(premiumWebhook, { embeds: [{ title: 'Premium Verification Failed', description: `No guild config was returned: ${guildId}`, color: 16729871 }] }); |
|||
return logger.error(`Premium verification failed: No guild config was returned. ${guildId}`); |
|||
} |
|||
|
|||
if (doc.isPremium) { |
|||
this.postWebhook(premiumWebhook, { embeds: [{ title: 'Premium Verification Failed', description: `Guild is premium but was scheduled to leave: ${guildId}`, color: 16729871 }] }); |
|||
return logger.error(`Premium verification failed: Guild is premium, but was flagged for deletion. ${guildId}`); |
|||
} |
|||
|
|||
this.postWebhook(premiumWebhook, { embeds: [{ title: 'Premium Verification Passed', description: `Leaving Guild ${guildId}` }], color: 2347360 }); |
|||
this.guilds.update(guildId, { $set: { premiumInstalled: false } }).catch(err => false); |
|||
this.client.leaveGuild(guildId); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
} |
|||
} |
|||
|
|||
postWebhook(webhook, payload) { |
|||
return new Promise((resolve, reject) => |
|||
axios.post(webhook, { |
|||
headers: { |
|||
Accept: 'application/json', |
|||
'Content-Type': 'application/json', |
|||
}, |
|||
...payload, |
|||
}) |
|||
.then(resolve) |
|||
.catch(reject)); |
|||
} |
|||
} |
|||
|
|||
module.exports = Dyno; |
@ -0,0 +1,15 @@ |
|||
const jayson = require('jayson'); |
|||
|
|||
class RPCClient { |
|||
constructor(dyno, host, port) { |
|||
this.dyno = dyno; |
|||
this.client = jayson.client.http({ |
|||
host, |
|||
port, |
|||
}); |
|||
|
|||
return this.client; |
|||
} |
|||
} |
|||
|
|||
module.exports = RPCClient; |
@ -0,0 +1,125 @@ |
|||
/* eslint-disable no-unused-vars */ |
|||
const { Server } = require('./rpc'); |
|||
const util = require('util'); |
|||
const Diagnostics = require('./utils/Diagnostics'); |
|||
|
|||
class RPCServer extends Server { |
|||
constructor(dyno) { |
|||
super(); |
|||
|
|||
this.dyno = dyno; |
|||
this.id = dyno.clientOptions.clusterId; |
|||
this.logger = dyno.logger; |
|||
|
|||
const host = dyno.config.rpcHost || 'localhost'; |
|||
const port = 30000 + parseInt(this.id, 10); |
|||
|
|||
let methods = { |
|||
rlreset: this.rlreset.bind(this), |
|||
debug: this.debug.bind(this), |
|||
diagnose: this.diagnose.bind(this), |
|||
}; |
|||
|
|||
// wrap methods for auth
|
|||
for (let [key, fn] of Object.entries(methods)) { |
|||
methods[key] = this.auth.bind(this, fn); |
|||
} |
|||
|
|||
this.diagnostics = new Diagnostics(dyno); |
|||
|
|||
this.init(host, port, methods); |
|||
} |
|||
|
|||
auth(handler, payload, cb) { |
|||
if (payload) { |
|||
if (!payload.token || payload.token !== (this.dyno.config.rpcToken)) { |
|||
return cb(`Invalid token.`); |
|||
} |
|||
|
|||
this.logger.debug('[RPC] Auth Passed.'); |
|||
return handler(payload, cb); |
|||
} |
|||
} |
|||
|
|||
rlreset(payload, cb) { |
|||
try { |
|||
const guild = this.dyno.client.guilds.get(payload.id); |
|||
|
|||
this.logger.debug(`[RPC] Clearing guild level rate limits.`); |
|||
|
|||
Object.keys(this.dyno.client.requestHandler.ratelimits).filter(k => k.includes(guild.id)).forEach(k => { |
|||
this.dyno.client.requestHandler.ratelimits[k]._queue = []; |
|||
this.dyno.client.requestHandler.ratelimits[k].remaining = 1; |
|||
this.dyno.client.requestHandler.ratelimits[k].check(true); |
|||
delete this.dyno.client.requestHandler.ratelimits[k]; |
|||
}); |
|||
|
|||
this.logger.debug(`[RPC] Clearing channel level rate limits.`); |
|||
for (let channel of guild.channels.values()) { |
|||
Object.keys(this.dyno.client.requestHandler.ratelimits).filter(k => k.includes(channel.id)).forEach(k => { |
|||
this.dyno.client.requestHandler.ratelimits[k]._queue = []; |
|||
this.dyno.client.requestHandler.ratelimits[k].remaining = 1; |
|||
this.dyno.client.requestHandler.ratelimits[k].check(true); |
|||
delete this.dyno.client.requestHandler.ratelimits[k]; |
|||
}); |
|||
} |
|||
|
|||
return cb(null, 'Success!'); |
|||
} catch (err) { |
|||
return cb(err); |
|||
} |
|||
} |
|||
|
|||
async debug(payload, cb) { |
|||
let dyno = this.dyno, |
|||
client = dyno.client, |
|||
config = dyno.config, |
|||
models = dyno.models, |
|||
redis = dyno.redis, |
|||
utils = dyno.utils, |
|||
result; |
|||
|
|||
try { |
|||
result = eval(payload.code); |
|||
} catch (e) { |
|||
result = e; |
|||
} |
|||
|
|||
try { |
|||
if (result && result.then) { |
|||
try { |
|||
result = await result; |
|||
} catch (err) { |
|||
result = err; |
|||
} |
|||
} |
|||
|
|||
if (!result) { |
|||
return cb(); |
|||
} |
|||
|
|||
result = result.toString(); |
|||
|
|||
return cb(null, result); |
|||
} catch (err) { |
|||
dyno.logger.error(err); |
|||
return cb(err); |
|||
} |
|||
} |
|||
|
|||
async diagnose(payload, cb) { |
|||
if (!payload.id || !payload.name) { |
|||
return cb('Invalid arguments.'); |
|||
} |
|||
|
|||
try { |
|||
const result = await this.diagnostics.diagnose(payload.id, payload.name); |
|||
return cb(null, result); |
|||
} catch (err) { |
|||
this.dyno.logger.error(err); |
|||
return cb(err); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = RPCServer; |
@ -0,0 +1,145 @@ |
|||
'use strict'; |
|||
|
|||
const cluster = require('cluster'); |
|||
|
|||
var EventEmitter; |
|||
|
|||
try { |
|||
EventEmitter = require('eventemitter3'); |
|||
} catch (e) { |
|||
EventEmitter = require('events'); |
|||
} |
|||
|
|||
/** |
|||
* @class Cluster |
|||
* @extends {EventEmitter} |
|||
*/ |
|||
class Cluster extends EventEmitter { |
|||
/** |
|||
* Representation of a cluster |
|||
* |
|||
* @param {Number} id Cluster ID |
|||
* @prop {Number} [options.shardCount] Shard count |
|||
* @prop {Number} [options.firstShardId] Optional first shard ID |
|||
* @prop {Number} [options.lastShardId] Optional last shard ID |
|||
* @prop {Number} [options.clusterCount] Optional cluster count |
|||
* |
|||
* @prop {Number} id Cluster ID |
|||
* @prop {Object} worker The worker |
|||
* @prop {Object} process The worker process |
|||
* @prop {Number} pid The worker process ID |
|||
*/ |
|||
constructor(manager, options) { |
|||
super(); |
|||
|
|||
this.id = options.id; |
|||
this.options = options; |
|||
this.shardCount = options.shardCount; |
|||
this.firstShardId = options.firstShardId; |
|||
this.lastShardId = options.lastShardId; |
|||
this.clusterCount = options.clusterCount; |
|||
|
|||
this.worker = this.createWorker(); |
|||
this.process = this.worker.process; |
|||
this.pid = this.process.pid; |
|||
} |
|||
|
|||
/** |
|||
* Create a cluster worker |
|||
* @return {Object} The worker process reference |
|||
*/ |
|||
createWorker(awaitReady = false) { |
|||
const worker = cluster.fork( |
|||
Object.assign({ |
|||
awaitReady: awaitReady, |
|||
clusterId: this.id, |
|||
}, this.options) |
|||
); |
|||
|
|||
this._pid = worker.process.pid; |
|||
|
|||
process.nextTick(() => { |
|||
this._readyListener = this.ready.bind(this); |
|||
this._shardReadyListener = this.shardReady.bind(this); |
|||
|
|||
worker.on('message', this._readyListener); |
|||
worker.on('message', this._shardReadyListener); |
|||
}); |
|||
|
|||
return worker; |
|||
} |
|||
|
|||
/** |
|||
* Restart a cluster worker |
|||
*/ |
|||
restartWorker(awaitReady = false) { |
|||
const worker = this.createWorker(awaitReady); |
|||
const oldWorker = this.worker; |
|||
this._pid = worker.process.pid; |
|||
|
|||
return new Promise(resolve => { |
|||
this.on('ready', () => { |
|||
if (this.worker) { |
|||
oldWorker.kill('SIGTERM'); |
|||
} |
|||
|
|||
process.nextTick(() => { |
|||
this.worker = worker; |
|||
this.process = worker.process; |
|||
this.pid = worker.pid; |
|||
// this.worker.removeListener('ready', this._readyListener);
|
|||
this.worker.removeListener('shardReady', this._shardReadyListener); |
|||
|
|||
return resolve(); |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Listen for cluster ready event |
|||
* @param {String|Object} message Message received from the worker |
|||
*/ |
|||
ready(message) { |
|||
if (!message || !message.op) return; |
|||
if (message.op === 'ready') { |
|||
this.emit('ready'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Listen for shard ready event |
|||
* @param {String|Object} message Message received from the worker |
|||
*/ |
|||
shardReady(message) { |
|||
if (!message || !message.op) return; |
|||
if (message.op === 'shardReady') { |
|||
this.emit('shardReady', message.d); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Send a command to the shard and await a response |
|||
* @param {Object} message The message to send |
|||
* @returns {Promise} |
|||
*/ |
|||
awaitResponse(message) { |
|||
return new Promise((resolve) => { |
|||
const awaitListener = (msg) => { |
|||
if (!['resp', 'error'].includes(msg.op)) return; |
|||
this.worker.removeListener('message', awaitListener); |
|||
return resolve({ id: this.id, result: msg.d }); |
|||
}; |
|||
|
|||
this.worker.on('message', awaitListener); |
|||
this.worker.send(message); |
|||
|
|||
setTimeout(() => { |
|||
this.worker.removeListener('message', awaitListener); |
|||
return resolve({ id: this.id, error: 'IPC request timed out.' }); |
|||
}, 2000); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
module.exports = Cluster; |
@ -0,0 +1,194 @@ |
|||
'use strict'; |
|||
|
|||
const cluster = require('cluster'); |
|||
const logger = require('../logger'); |
|||
|
|||
/** |
|||
* @class Events |
|||
*/ |
|||
class Events { |
|||
/** |
|||
* Events manager |
|||
* @param {Manager} manager Cluster Manager instance |
|||
*/ |
|||
constructor(manager) { |
|||
this.manager = manager; |
|||
this.logger = manager.logger; |
|||
this.clusters = manager.clusters; |
|||
|
|||
this.readyListener = this.onReady.bind(this); |
|||
this.messageListener = this.onMessage.bind(this); |
|||
this.exitHandler = this.manager.handleExit.bind(this.manager); |
|||
} |
|||
|
|||
/** |
|||
* Remove event listeners on module unload |
|||
*/ |
|||
unload() { |
|||
cluster.removeListener('exit', this.exitHandler); |
|||
cluster.removeListener('online', this.readyListener); |
|||
cluster.removeListener('message', this.messageListener); |
|||
} |
|||
|
|||
/** |
|||
* Create event listeners when modul is loaded |
|||
*/ |
|||
register() { |
|||
cluster.on('exit', this.exitHandler); |
|||
cluster.on('online', this.readyListener); |
|||
cluster.on('message', this.messageListener); |
|||
} |
|||
|
|||
/** |
|||
* Fired when a worker goes online |
|||
* @param {Object} worker Worker process |
|||
*/ |
|||
onReady(worker) { |
|||
const cluster = this.manager.getCluster(worker); |
|||
const meta = cluster.firstShardId ? `Shards ${cluster.firstShardId}-${cluster.lastShardId}` : `Shard ${cluster.id}`; |
|||
logger.info(`[Events] Cluster ${cluster.id} online | ${meta}`); |
|||
} |
|||
|
|||
/** |
|||
* Fired when the cluster receives a message |
|||
* @param {Object} worker Worker process |
|||
* @param {Object} message The message object |
|||
* @returns {void} |
|||
*/ |
|||
onMessage(worker, message) { |
|||
if (!message.op) return; |
|||
|
|||
// ignore responses
|
|||
if (message.op === 'resp') return; |
|||
|
|||
if (this[message.op]) { |
|||
this[message.op](message); |
|||
} else if (message.op !== 'ready') { |
|||
this.awaitResponse(worker, message); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Send a command to and await a response from the cluster |
|||
* @param {Object} worker Worker process |
|||
* @param {Object} message The message to send |
|||
* @returns {void} |
|||
*/ |
|||
awaitResponse(worker, message) { |
|||
const promises = []; |
|||
|
|||
for (const cluster of this.clusters.values()) { |
|||
if (!cluster.worker || !cluster.worker.isConnected()) continue; |
|||
promises.push(cluster.awaitResponse(message)); |
|||
} |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
Promise.all(promises).then(results => { |
|||
if (worker != null && worker.send) { |
|||
try { |
|||
worker.send({ op: 'resp', d: results }); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
} |
|||
} |
|||
return resolve(results); |
|||
}).catch(err => { |
|||
if (worker != null && worker.send) { |
|||
try { |
|||
worker.send({ op: 'error', d: err }); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
} |
|||
} |
|||
return reject(err); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Send a command to a cluster |
|||
* @param {Number} clusterId Cluster ID |
|||
* @param {String|Object} message Message to send |
|||
* @return {Boolean} |
|||
*/ |
|||
send(clusterId, message) { |
|||
const cluster = this.clusters.get(clusterId); |
|||
if (!cluster) { |
|||
logger.warn(`[Events] Cluster ${clusterId} not found attempting to send.`); |
|||
return; |
|||
} |
|||
if (!cluster.worker) { |
|||
logger.warn(`[Events] Cluster ${clusterId} worker not connected.`); |
|||
return; |
|||
} |
|||
|
|||
cluster.worker.send(message); |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Broadcast a message to all clusters |
|||
* @param {Object} message The message to send |
|||
*/ |
|||
broadcast(message) { |
|||
if (message.op === 'broadcast') { |
|||
message = message.d; |
|||
} |
|||
|
|||
for (const cluster of this.clusters.values()) { |
|||
if (!cluster.worker || !cluster.worker.isConnected()) { |
|||
logger.warn(`[Events] Cluster ${cluster.id} worker not connected.`); |
|||
continue; |
|||
} |
|||
cluster.worker.send(message); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Restart a cluster or clusters sequentially |
|||
* @param {Object} message The message received |
|||
* @returns {*} |
|||
*/ |
|||
async restart(message) { |
|||
if (message.d !== undefined && message.d !== null && !isNaN(message.d)) { |
|||
const cluster = this.clusters.get(parseInt(message.d)); |
|||
if (!cluster) return; |
|||
|
|||
return cluster.restartWorker(true); |
|||
} else { |
|||
for (const cluster of this.clusters.values()) { |
|||
cluster.restartWorker(true); |
|||
await this.manager.awaitReady(cluster); |
|||
} |
|||
} |
|||
} |
|||
|
|||
blocked(message) { |
|||
this.logger.blocked.push(message.d); |
|||
} |
|||
|
|||
shardDisconnect(message) { |
|||
let msg = `[Events] Shard ${message.d.id} disconnected.`; |
|||
if (message.d.err) { |
|||
msg += ` ${message.d.err}`; |
|||
} |
|||
this.logger.shardStatus.push(msg); |
|||
} |
|||
|
|||
shardReady(message) { |
|||
let msg = `[Events] Shard ${message.d} ready.`; |
|||
this.logger.shardStatus.push(msg); |
|||
} |
|||
|
|||
shardResume(message) { |
|||
let msg = `[Events] Shard ${message.d} resumed.`; |
|||
this.logger.shardStatus.push(msg); |
|||
} |
|||
|
|||
shardIdentify(message) { |
|||
let msg = `[Events] Shard ${message.d} identified.`; |
|||
this.logger.shardStatus.push(msg); |
|||
} |
|||
} |
|||
|
|||
module.exports = Events; |
@ -0,0 +1,129 @@ |
|||
'use strict'; |
|||
|
|||
const axios = require('axios'); |
|||
const config = require('../config'); |
|||
const logger = require('../logger'); |
|||
|
|||
/** |
|||
* @class Logger |
|||
*/ |
|||
class Logger { |
|||
constructor(manager) { |
|||
this._postBlockedInterval = null; |
|||
this._postStatusInterval = null; |
|||
|
|||
this.blocked = []; |
|||
this.shardStatus = []; |
|||
|
|||
this.logger = manager.logger; |
|||
} |
|||
|
|||
register() { |
|||
this._postBlockedInterval = setInterval(() => { |
|||
if (!this.blocked || !this.blocked.length) return; |
|||
|
|||
this.log('Event Loops Blocked', null, { |
|||
webhookUrl: config.shardWebhook, |
|||
username: 'Shard Manager', |
|||
text: this.blocked.join('\n'), |
|||
suppress: true, |
|||
}); |
|||
|
|||
this.blocked = []; |
|||
}, 5000); |
|||
|
|||
this._postStatusInterval = setInterval(() => { |
|||
if (!this.shardStatus || !this.shardStatus.length) return; |
|||
|
|||
this.log('Shard Status Updates', null, { |
|||
webhookUrl: config.shardWebhook, |
|||
username: 'Shard Manager', |
|||
text: this.shardStatus.join('\n'), |
|||
suppress: true, |
|||
}); |
|||
|
|||
this.shardStatus = []; |
|||
}, 14000); |
|||
} |
|||
|
|||
unload() { |
|||
if (this._postBlockedInterval) { |
|||
clearInterval(this._postBlockedInterval); |
|||
} |
|||
|
|||
if (this._postStatusInterval) { |
|||
clearInterval(this._postStatusInterval); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Log cluster status to console and discord |
|||
* @param {String} text Text to log |
|||
* @param {Array} [fields] Array of field objects for embed |
|||
* @param {Object} [options] An options object |
|||
*/ |
|||
log(title, fields, options) { |
|||
if (!options || !options.suppress) { |
|||
logger.info(title); |
|||
} |
|||
|
|||
// if (config.state === 2) return;
|
|||
if (!config.cluster) return; |
|||
|
|||
options = options || {}; |
|||
|
|||
const webhookUrl = options.webhookUrl || config.cluster.webhookUrl, |
|||
username = options.username || 'Cluster Manager'; |
|||
|
|||
const payload = { |
|||
username: username, |
|||
avatar_url: `${config.avatar}`, |
|||
embeds: [], |
|||
tts: false, |
|||
}; |
|||
|
|||
const embed = { |
|||
title: title, |
|||
timestamp: new Date(), |
|||
footer: { |
|||
text: config.stateName, |
|||
}, |
|||
}; |
|||
|
|||
if (options.text) { |
|||
embed.description = options.text; |
|||
} |
|||
|
|||
if (fields) embed.fields = fields; |
|||
|
|||
payload.embeds.push(embed); |
|||
|
|||
this.postWebhook(webhookUrl, payload) |
|||
.catch(err => logger.error(err)); // eslint-disable-line
|
|||
} |
|||
|
|||
info(...args) { |
|||
logger.info(...args); |
|||
} |
|||
|
|||
/** |
|||
* Post to a discord webhook |
|||
* @param {String} webhook The webhook to post to |
|||
* @param {Object} payload The json payload to send |
|||
* @return {Promise} |
|||
*/ |
|||
postWebhook(webhook, payload) { |
|||
return new Promise((resolve, reject) => |
|||
axios.post(webhook, { |
|||
headers: { |
|||
Accept: 'application/json', |
|||
'Content-Type': 'application/json', |
|||
}, |
|||
...payload |
|||
}) |
|||
.then(resolve) |
|||
.catch(reject)); |
|||
} |
|||
} |
|||
|
|||
module.exports = Logger; |
@ -0,0 +1,185 @@ |
|||
'use strict'; |
|||
|
|||
const os = require('os'); |
|||
const Cluster = require('./Cluster'); |
|||
const Events = require('./Events'); |
|||
const Logger = require('./Logger'); |
|||
const Sharding = require('./Sharding'); |
|||
const Server = require('./Server'); |
|||
const config = require('../config'); |
|||
const { Collection } = require('@dyno.gg/dyno-core'); |
|||
|
|||
/** |
|||
* @class Manager |
|||
*/ |
|||
class Manager { |
|||
/** |
|||
* Create the cluster manager |
|||
* @param {String} strategy Sharding strategy |
|||
*/ |
|||
constructor(strategy) { |
|||
this.clusters = new Collection(); |
|||
this.queue = []; |
|||
|
|||
this.shardCount = config.shardCountOverride || os.cpus().length; |
|||
|
|||
process.on('uncaughtException', this.handleException.bind(this)); |
|||
process.on('unhandledRejection', this.handleRejection.bind(this)); |
|||
|
|||
this.logger = new Logger(this); |
|||
this.events = new Events(this); |
|||
this.sharding = new Sharding(this); |
|||
this.server = new Server(this); |
|||
|
|||
strategy = strategy || config.shardingStrategy; |
|||
|
|||
this.logger.register(); |
|||
this.logger.info(`[Manager] Sharding strategy ${strategy}`); |
|||
|
|||
if (strategy && this.sharding[strategy]) { |
|||
this.sharding[strategy](); |
|||
} else { |
|||
this.sharding.createShardsProcess(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Unhandled rejection handler |
|||
* @param {Error|*} reason The reason the promise was rejected |
|||
* @param {Promise} p The promise that was rejected |
|||
*/ |
|||
handleRejection(reason, p) { |
|||
try { |
|||
console.error('Unhandled rejection at: Promise ', p, 'reason: ', reason); // eslint-disable-line
|
|||
} catch (err) { |
|||
console.error(reason); // eslint-disable-line
|
|||
} |
|||
} |
|||
|
|||
handleException(err) { |
|||
if (!err || (typeof err === 'string' && !err.length)) { |
|||
return logger.error('An undefined exception occurred.'); |
|||
} |
|||
|
|||
try { |
|||
logger.error(err); |
|||
} catch (e) { |
|||
console.error(err); // eslint-disable-line
|
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Reload a module |
|||
* @param {String} module Module name |
|||
*/ |
|||
reloadModule(module) { |
|||
const modulekey = module.toLowerCase(); |
|||
const activeModule = this[modulekey]; |
|||
if (!activeModule) return; |
|||
|
|||
if (activeModule.unload) { |
|||
activeModule.unload(); |
|||
} |
|||
|
|||
this[modulekey] = requireReload(require)(`./${module}`); |
|||
|
|||
if (this[modulekey].register) { |
|||
this[modulekey].register(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Create a cluster |
|||
* @param {Number} id Shard ID |
|||
*/ |
|||
createCluster(options) { |
|||
const cluster = new Cluster(this, options); |
|||
this.clusters.set(parseInt(cluster.id), cluster); |
|||
|
|||
return cluster; |
|||
// return this.awaitReady(cluster);
|
|||
} |
|||
|
|||
/** |
|||
* Queue a cluster for restart |
|||
* @param {Cluster} cluster The cluster to queue |
|||
*/ |
|||
queueCluster(cluster) { |
|||
this.queue.push(cluster); |
|||
|
|||
if (this.queue.length === 1) { |
|||
this.processQueue(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Process the restart queue |
|||
*/ |
|||
processQueue() { |
|||
const cluster = this.queue[0]; |
|||
|
|||
process.nextTick(() => { |
|||
this.logger.log(`Cluster ${cluster.id} restarting...`); |
|||
}); |
|||
|
|||
cluster.restartWorker().then(() => { |
|||
this.queue.shift(); |
|||
this.logger.log(`Cluster ${cluster.id} ready.`); |
|||
if (this.queue.length > 0) { |
|||
this.processQueue(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Await the ready event from a cluster |
|||
* @param {Shard} cluster The cluster to wait |
|||
* @return {Promise} |
|||
*/ |
|||
awaitReady(cluster) { |
|||
return new Promise(resolve => |
|||
cluster.on('ready', resolve)); |
|||
} |
|||
|
|||
/** |
|||
* Get a cluster by worker |
|||
* @param {Object} worker Worker process |
|||
* @returns {Shard} A cluster matching the worker pid |
|||
*/ |
|||
getCluster(worker) { |
|||
return this.clusters.find(s => s.pid === worker.process.pid || s._pid === worker.process.pid); |
|||
} |
|||
|
|||
/** |
|||
* Handle a cluster dying |
|||
* @param {Object} worker Worker process |
|||
*/ |
|||
handleExit(worker, code, signal) { |
|||
const cluster = this.getCluster(worker); |
|||
|
|||
if (signal && signal === 'SIGTERM') return; |
|||
if (!cluster) return; |
|||
|
|||
const meta = cluster.firstShardId !== null ? `${cluster.firstShardId}-${cluster.lastShardId}` : cluster.id.toString(); |
|||
|
|||
this.logger.log(`Cluster ${cluster.id} died with code ${signal || code}, restarting...`, [ |
|||
{ name: 'Shards', value: meta }, |
|||
]); |
|||
|
|||
// process.nextTick(() => {
|
|||
// this.logger.log(`Cluster ${cluster.id} restarting...`);
|
|||
// });
|
|||
|
|||
cluster.restartWorker().then(() => { |
|||
this.queue.shift(); |
|||
this.logger.log(`Cluster ${cluster.id} ready.`); |
|||
if (this.queue.length > 0) { |
|||
this.processQueue(); |
|||
} |
|||
}); |
|||
|
|||
// this.queueCluster(cluster);
|
|||
} |
|||
} |
|||
|
|||
module.exports = Manager; |
@ -0,0 +1,164 @@ |
|||
'use strict'; |
|||
|
|||
const http = require('http'); |
|||
const config = require('../config'); |
|||
const { models } = require('../database'); |
|||
const logger = require('../logger'); |
|||
|
|||
/** |
|||
* @class Server |
|||
*/ |
|||
class Server { |
|||
/** |
|||
* HTTP Server |
|||
* @param {Manager} manager Cluster Manager instance |
|||
*/ |
|||
constructor(manager) { |
|||
this.manager = manager; |
|||
this.events = manager.events; |
|||
|
|||
this.sockets = {}; |
|||
this.nextSocketId = 0; |
|||
|
|||
this.server = http.createServer(this.handleRequest.bind(this)) |
|||
.listen(5000); |
|||
|
|||
this.server.on('connection', socket => { |
|||
let socketId = this.nextSocketId++; |
|||
this.sockets[socketId] = socket; |
|||
socket.on('close', () => { |
|||
delete this.sockets[socketId]; |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
unload() { |
|||
this.server.close(); |
|||
for (var socketId in this.sockets) { |
|||
this.sockets[socketId].destroy(); |
|||
} |
|||
} |
|||
|
|||
handleRequest(req, res) { |
|||
const parts = req.url.split('/'); |
|||
const handler = parts[1]; |
|||
|
|||
let getKey = `get${ucfirst(handler)}`, |
|||
postKey = `post${ucfirst(handler)}`, |
|||
key; |
|||
|
|||
if (req.method === 'GET' && this[getKey]) { |
|||
key = getKey; |
|||
} else if (req.method === 'POST' && this[postKey]) { |
|||
key = postKey; |
|||
} else { |
|||
return this.end(res, 404, 'Not Found'); |
|||
} |
|||
|
|||
let body = ''; |
|||
req.on('data', data => { |
|||
body += data; |
|||
}); |
|||
|
|||
req.on('end', () => { |
|||
const payload = { |
|||
req, res, body, |
|||
path: parts.slice(2), |
|||
}; |
|||
|
|||
this[key](payload); |
|||
}); |
|||
} |
|||
|
|||
end(res, status, body) { |
|||
if (typeof body === 'object' || Array.isArray(body)) { |
|||
body = JSON.stringify(body); |
|||
} |
|||
|
|||
res.writeHead(status); |
|||
res.write(body); |
|||
return res.end(); |
|||
} |
|||
|
|||
getPing({ res }) { |
|||
return this.end(res, 200, 'Pong!'); |
|||
} |
|||
|
|||
getShards({ res }) { |
|||
return this.events.awaitResponse(null, { op: 'shards' }) |
|||
.then(data => this.end(res, 200, data)) |
|||
.catch(err => this.end(res, 500, err)); |
|||
} |
|||
|
|||
async postRestart({ res, body }) { |
|||
if (!body) return this.end(res, 500, 'Invalid request 0'); |
|||
|
|||
try { |
|||
body = JSON.parse(body); |
|||
} catch (err) { |
|||
return this.end(res, 500, err); |
|||
} |
|||
|
|||
if (!body.token || body.id == undefined) return this.end(res, 500, 'Invalid request 1'); |
|||
|
|||
const restartToken = config.restartToken; |
|||
if (body.token !== restartToken) return this.end(res, 403, 'Forbidden'); |
|||
|
|||
if (body.id === 'all') { |
|||
for (const cluster of this.manager.clusters.values()) { |
|||
cluster.restartWorker(true); |
|||
await this.manager.awaitReady(cluster); |
|||
} |
|||
|
|||
return this.end(res, 200, 'OK'); |
|||
} |
|||
|
|||
const cluster = this.manager.clusters.get(parseInt(body.id)); |
|||
if (!cluster) return this.end(res, 404, 'Cluster not found'); |
|||
cluster.restartWorker(true); |
|||
return this.end(res, 200, 'OK'); |
|||
} |
|||
|
|||
postPing({ res }) { |
|||
return this.events.awaitResponse(null, { op: 'ping' }) |
|||
.then(data => this.end(res, 200, data)) |
|||
.catch(err => this.end(res, 500, err)); |
|||
} |
|||
|
|||
postGuildUpdate({ res, body }) { |
|||
if (!body) return this.end(res, 500, 'Invalid request'); |
|||
|
|||
this.events.broadcast({ op: 'guildUpdate', d: body }); |
|||
|
|||
return this.end(res, 200, 'OK'); |
|||
} |
|||
|
|||
postStats({ res, body }) { |
|||
if (!body) return this.end(res, 500, 'Invalid request'); |
|||
|
|||
this.events.send({ op: 'postStats', d: body }); |
|||
|
|||
return this.end(res, 200, 'OK'); |
|||
} |
|||
|
|||
postReload({ res, body }) { |
|||
if (!body) return this.end(res, 500, 'Invalid request'); |
|||
|
|||
try { |
|||
body = JSON.parse(body); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
return this.end(res, 500, 'Internal error'); |
|||
} |
|||
|
|||
if (!body.c) return this.end(res, 500, 'Invalid request'); |
|||
this.manager.reloadModule(body.c); |
|||
return this.end(res, 200, 'OK'); |
|||
} |
|||
} |
|||
|
|||
function ucfirst(str) { |
|||
return str.charAt(0).toUpperCase() + str.slice(1); |
|||
} |
|||
|
|||
module.exports = Server; |
@ -0,0 +1,226 @@ |
|||
'use strict'; |
|||
|
|||
const os = require('os'); |
|||
const Eris = require('@dyno.gg/eris'); |
|||
const config = require('../config'); |
|||
const logger = require('../logger'); |
|||
|
|||
/** |
|||
* @class Sharding |
|||
*/ |
|||
class Sharding { |
|||
/** |
|||
* Sharding manager |
|||
* @param {Manager} manager Cluster Manager instance |
|||
*/ |
|||
constructor(manager) { |
|||
this.manager = manager; |
|||
this.logger = manager.logger; |
|||
this.shardCount = os.cpus().length; |
|||
} |
|||
|
|||
/** |
|||
* Alias for process strategy |
|||
*/ |
|||
createShardsProcess() { |
|||
return this.process(); |
|||
} |
|||
|
|||
/** |
|||
* Alias for shared strategy |
|||
*/ |
|||
createShardsShared() { |
|||
return this.shared(); |
|||
} |
|||
|
|||
/** |
|||
* Alias for balanced strategy |
|||
*/ |
|||
createShardsBalancedCores() { |
|||
return this.balanced(); |
|||
} |
|||
|
|||
/** |
|||
* Alias for semibalanced strategy |
|||
*/ |
|||
createShardsSemiBalanced() { |
|||
return this.semibalanced(); |
|||
} |
|||
|
|||
/** |
|||
* Create clusters sequentially |
|||
*/ |
|||
async process() { |
|||
const shardCount = config.shardCountOverride || await this.getShardCount(); |
|||
|
|||
const shardIds = config.shardIds || []; |
|||
|
|||
this.shardCount = shardCount; |
|||
this.manager.events.register(); |
|||
|
|||
this.logger.log(`[Sharding] Starting with ${shardIds.length || shardCount} shards.`); |
|||
|
|||
for (let i = 0; i < shardCount; i++) { |
|||
if (shardIds.length && !shardIds.includes(i.toString())) continue; |
|||
|
|||
this.manager.createCluster({ |
|||
id: i, |
|||
shardCount, |
|||
}); |
|||
await new Promise(res => setTimeout(res, 6500)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Create a shared state instance |
|||
*/ |
|||
async shared() { |
|||
const shardCount = config.shardCountOverride || await this.getShardCount(); |
|||
|
|||
this.shardCount = shardCount; |
|||
this.manager.events.register(); |
|||
this.logger.log(`[Sharding] Starting with ${shardCount} shards.`); |
|||
|
|||
this.manager.createCluster({ |
|||
id: 0, |
|||
clusterCount: 1, |
|||
shardCount: shardCount, |
|||
firstShardId: 0, |
|||
lastShardId: shardCount - 1, |
|||
}); |
|||
} |
|||
|
|||
chunkArray(arr, chunkCount) { |
|||
const arrLength = arr.length; |
|||
const tempArray = []; |
|||
let chunk = []; |
|||
|
|||
const chunkSize = Math.floor(arr.length / chunkCount); |
|||
let mod = arr.length % chunkCount; |
|||
let tempChunkSize = chunkSize; |
|||
|
|||
for (let i = 0; i < arrLength; i += tempChunkSize) { |
|||
tempChunkSize = chunkSize; |
|||
if (mod > 0) { |
|||
tempChunkSize = chunkSize + 1; |
|||
mod--; |
|||
} |
|||
chunk = arr.slice(i, i + tempChunkSize); |
|||
tempArray.push(chunk); |
|||
} |
|||
|
|||
return tempArray; |
|||
} |
|||
|
|||
/** |
|||
* Create shards balanced across all cores |
|||
* @param {Boolean|undefined} semi If false, round up to a multiple of core count |
|||
*/ |
|||
async balanced(semi) { |
|||
const shardCount = config.shardCountOverride || await this.getShardCount(semi); |
|||
const len = config.clusterCount || os.cpus().length; |
|||
|
|||
let firstShardId = config.firstShardOverride || 0, |
|||
lastShardId = config.lastShardOverride || (shardCount - 1); |
|||
|
|||
const localShardCount = config.shardCountOverride ? (lastShardId + 1) - firstShardId : shardCount; |
|||
|
|||
const shardIds = [...Array(1 + lastShardId - firstShardId).keys()].map(v => firstShardId + v); |
|||
// const clusterShardCount = Math.ceil(shardIds.length / len);
|
|||
const shardCounts = this.chunkArray(shardIds, len); |
|||
|
|||
this.shardCount = shardCount; |
|||
|
|||
this.manager.events.register(); |
|||
this.logger.log(`[Sharding] Starting with ${localShardCount} shards in ${len} clusters.`); |
|||
|
|||
const clusterIds = config.clusterIds || []; |
|||
|
|||
for (let i in shardCounts) { |
|||
const count = shardCounts[i].length; |
|||
lastShardId = (firstShardId + count) - 1; |
|||
|
|||
if (clusterIds.length && !clusterIds.includes(i.toString())) { |
|||
firstShardId += count; |
|||
continue; |
|||
} |
|||
|
|||
await this.manager.createCluster({ |
|||
id: i.toString(), |
|||
clusterCount: len.toString(), |
|||
shardCount: shardCount.toString(), |
|||
firstShardId: firstShardId.toString(), |
|||
lastShardId: lastShardId.toString(), |
|||
}); |
|||
|
|||
firstShardId += count; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Create shards semi-balanced across all ores |
|||
*/ |
|||
async semibalanced() { |
|||
return this.balanced(true); |
|||
} |
|||
|
|||
/** |
|||
* Get estimated guild count |
|||
*/ |
|||
async getEstimatedGuilds() { |
|||
const client = new Eris(config.client.token); |
|||
|
|||
try { |
|||
var data = await client.getBotGateway(); |
|||
} catch (err) { |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
if (!data || !data.shards) return Promise.resolve(); |
|||
|
|||
logger.info(`[Sharding] Discord suggested ${data.shards} shards.`); |
|||
return Promise.resolve(parseInt(data.shards) * 1000); |
|||
} |
|||
|
|||
/** |
|||
* Fetch guild count with fallbacks in the event of an error |
|||
* @return {Number} Guild count |
|||
*/ |
|||
async fetchGuildCount() { |
|||
let res, guildCount; |
|||
guildCount = await this.getEstimatedGuilds(); |
|||
return guildCount; |
|||
} |
|||
|
|||
/** |
|||
* Get shard count to start |
|||
* @param {Boolean} balanced Whether or not to round up |
|||
* @return {Number} Shard count |
|||
*/ |
|||
async getShardCount(balanced) { |
|||
try { |
|||
var guildCount = await this.fetchGuildCount(); |
|||
} catch (err) { |
|||
throw new Error(err); |
|||
} |
|||
|
|||
if (!guildCount || isNaN(guildCount)) { |
|||
throw new Error('Unable to get guild count.'); |
|||
} |
|||
|
|||
guildCount = parseInt(guildCount); |
|||
|
|||
logger.debug(`${guildCount} Guilds`); |
|||
|
|||
if (guildCount < 2500) { |
|||
guildCount = 2500; |
|||
} |
|||
|
|||
let n = balanced ? os.cpus().length : 2; |
|||
|
|||
const shardCalc = Math.round((Math.ceil(guildCount / 2500) * 2500) / 1400); |
|||
return Math.max(this.shardCount, n * Math.ceil(shardCalc / n)); |
|||
} |
|||
} |
|||
|
|||
module.exports = Sharding; |
@ -0,0 +1,161 @@ |
|||
const config = require('../config'); |
|||
const db = require('../database'); |
|||
const { Client } = require('../rpc'); |
|||
|
|||
class Commands { |
|||
constructor(manager) { |
|||
this.manager = manager; |
|||
this.logger = manager.logger; |
|||
this.pmClient = manager.pmClient; |
|||
this.restClient = manager.restClient; |
|||
|
|||
return { |
|||
blocked: this.blocked.bind(this), |
|||
processExit: this.processExit.bind(this), |
|||
processReady: this.processReady.bind(this), |
|||
createCluster: this.createCluster.bind(this), |
|||
moveCluster: this.moveCluster.bind(this), |
|||
shardDisconnect: this.shardDisconnect.bind(this), |
|||
shardReady: this.shardReady.bind(this), |
|||
shardResume: this.shardResume.bind(this), |
|||
restart: this.restart.bind(this), |
|||
} |
|||
} |
|||
|
|||
async processReady({ process }, cb) { |
|||
if (process.port) { |
|||
process.client = new Client(config.rpcHost || 'localhost', process.port); |
|||
} |
|||
this.manager.processes.set(process.id, process); |
|||
|
|||
const cluster = process.options; |
|||
|
|||
if (cluster && cluster.id) { |
|||
this.logger.log(`[${process.pid}] Cluster ${cluster.id} ready`); |
|||
} |
|||
|
|||
return cb(null); |
|||
} |
|||
|
|||
processExit({ process, code, signal }, cb) { |
|||
const cluster = process.options; |
|||
|
|||
if (cluster && cluster.id) { |
|||
const meta = cluster.firstShardId !== null ? `${cluster.firstShardId}-${cluster.lastShardId}` : cluster.id.toString(); |
|||
|
|||
this.logger.log(`Cluster ${cluster.id} died with code ${signal || code}`, [ |
|||
{ name: 'Shards', value: meta }, |
|||
]); |
|||
} |
|||
|
|||
return cb(null); |
|||
} |
|||
|
|||
async restart({ id, token }, cb) { |
|||
if (!token || id == undefined) { |
|||
return cb('Invalid request'); |
|||
} |
|||
|
|||
const restartToken = config.restartToken; |
|||
if (token !== restartToken) { |
|||
return cb('Invalid token'); |
|||
} |
|||
|
|||
try { |
|||
if (id === 'all') { |
|||
for (let proc of this.manager.processes.values()) { |
|||
await this.manager.restartProcess(proc); |
|||
} |
|||
|
|||
return cb(null); |
|||
} |
|||
|
|||
const proc = this.manager.processes.find(p => p.options.id === parseInt(id, 10)); |
|||
if (!proc) { |
|||
return cb(`Unable to find cluster ${id}`); |
|||
} |
|||
|
|||
this.logger.log(`[${proc.pid}] Cluster ${id} restarting`); |
|||
await this.manager.restartProcess(proc); |
|||
return cb(null); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return cb(err); |
|||
} |
|||
} |
|||
|
|||
async createCluster({ id }, cb) { |
|||
try { |
|||
let cluster = this.manager.clusters.get(id); |
|||
if (!cluster) { |
|||
const coll = db.collection('clusters'); |
|||
cluster = await coll.findOne({ 'host.state': config.state, id }); |
|||
this.manager.clusters.set(id, cluster); |
|||
} |
|||
await this.manager.createProcess(cluster); |
|||
return cb(null); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return cb(err); |
|||
} |
|||
} |
|||
|
|||
async moveCluster({ id, name, token }, cb) { |
|||
if (!token || id == undefined) { |
|||
return cb('Invalid request'); |
|||
} |
|||
|
|||
const restartToken = config.restartToken; |
|||
if (token !== restartToken) { |
|||
return cb('Invalid token'); |
|||
} |
|||
|
|||
try { |
|||
const cluster = this.manager.clusters.get(id); |
|||
const host = await db.collection('hosts').findOne({ name }); |
|||
if (!host) { |
|||
return cb('Inavalid host.'); |
|||
} |
|||
|
|||
await db.collection('clusters').updateOne({ 'host.state': config.state, id }, { $set: { host: host } }); |
|||
|
|||
const client = new Client(host.hostname, 5052); |
|||
|
|||
await client.request('createCluster', { id }); |
|||
await this.manager.deleteProcess(cluster); |
|||
|
|||
return cb(null); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return cb(err); |
|||
} |
|||
} |
|||
|
|||
blocked({ text }, cb) { |
|||
this.logger.blocked.push(text); |
|||
return cb(null); |
|||
} |
|||
|
|||
shardDisconnect({ id, cluster, err }, cb) { |
|||
let msg = `[C${cluster}] Shard ${id} disconnected`; |
|||
if (err) { |
|||
msg += ` ${err}`; |
|||
} |
|||
this.logger.shardStatus.push(msg); |
|||
return cb(null); |
|||
} |
|||
|
|||
shardReady({ id, cluster }, cb) { |
|||
let msg = `[C${cluster}] Shard ${id} ready`; |
|||
this.logger.shardStatus.push(msg); |
|||
return cb(null); |
|||
} |
|||
|
|||
shardResume({ id, cluster }, cb) { |
|||
let msg = `[C${cluster}] Shard ${id} resumed`; |
|||
this.logger.shardStatus.push(msg); |
|||
return cb(null); |
|||
} |
|||
} |
|||
|
|||
module.exports = Commands; |
@ -0,0 +1,121 @@ |
|||
const config = require('../config'); |
|||
const logger = require('../logger'); |
|||
const { utils } = require('@dyno.gg/dyno-core'); |
|||
|
|||
/** |
|||
* @class Logger |
|||
*/ |
|||
class Logger { |
|||
constructor(manager) { |
|||
this._postBlockedInterval = null; |
|||
this._postStatusInterval = null; |
|||
|
|||
this.blocked = []; |
|||
this.shardStatus = []; |
|||
|
|||
this.manager = manager; |
|||
this.client = manager.restClient; |
|||
|
|||
this.register(); |
|||
} |
|||
|
|||
register() { |
|||
this._postBlockedInterval = setInterval(() => { |
|||
if (!this.blocked || !this.blocked.length) return; |
|||
|
|||
this.log('Event Loops Blocked', null, { |
|||
webhookUrl: config.shardWebhook, |
|||
username: 'Shard Manager', |
|||
text: this.blocked.join('\n'), |
|||
suppress: true, |
|||
}); |
|||
|
|||
this.blocked = []; |
|||
}, 6000); |
|||
|
|||
this._postStatusInterval = setInterval(() => { |
|||
if (!this.shardStatus || !this.shardStatus.length) return; |
|||
|
|||
let msgArray = []; |
|||
msgArray = msgArray.concat(utils.splitMessage(this.shardStatus, 1900)); |
|||
|
|||
for (let msg of msgArray) { |
|||
this.log('Shard Status Updates', null, { |
|||
webhookUrl: config.shardWebhook, |
|||
username: 'Shard Manager', |
|||
text: msg, |
|||
suppress: true, |
|||
}); |
|||
} |
|||
|
|||
this.shardStatus = []; |
|||
}, 5500); |
|||
} |
|||
|
|||
/** |
|||
* Log cluster status to console and discord |
|||
* @param {String} text Text to log |
|||
* @param {Array} [fields] Array of field objects for embed |
|||
* @param {Object} [options] An options object |
|||
*/ |
|||
log(title, fields, options) { |
|||
if (!options || !options.suppress) { |
|||
logger.info(title); |
|||
} |
|||
|
|||
// if (config.state === 2) return;
|
|||
if (!config.cluster) return; |
|||
|
|||
options = options || {}; |
|||
|
|||
const webhookUrl = options.webhookUrl || config.cluster.webhookUrl; |
|||
const username = options.username || 'Cluster Manager'; |
|||
|
|||
const payload = { |
|||
username: username, |
|||
avatar_url: `${config.avatar}`, |
|||
embeds: [], |
|||
tts: false, |
|||
}; |
|||
|
|||
const embed = { |
|||
title: title, |
|||
timestamp: new Date(), |
|||
footer: { |
|||
text: config.stateName, |
|||
}, |
|||
}; |
|||
|
|||
if (options.text) { |
|||
embed.description = options.text; |
|||
} |
|||
|
|||
if (fields) embed.fields = fields; |
|||
|
|||
payload.embeds.push(embed); |
|||
|
|||
this.postWebhook(webhookUrl, payload) |
|||
.catch(err => logger.error(err)); // eslint-disable-line
|
|||
} |
|||
|
|||
info(...args) { |
|||
logger.info(...args); |
|||
} |
|||
|
|||
error(...args) { |
|||
logger.error(...args); |
|||
} |
|||
|
|||
/** |
|||
* Post to a discord webhook |
|||
* @param {String} webhook The webhook to post to |
|||
* @param {Object} payload The json payload to send |
|||
* @return {Promise} |
|||
*/ |
|||
postWebhook(webhook, payload) { |
|||
const [id, token] = webhook.split('/').slice(-2); |
|||
return this.manager.restClient.executeWebhook(id, token, payload); |
|||
} |
|||
} |
|||
|
|||
module.exports = Logger; |
@ -0,0 +1,184 @@ |
|||
const Eris = require('@dyno.gg/eris'); |
|||
const Logger = require('./Logger'); |
|||
const Commands = require('./Commands'); |
|||
const config = require('../config'); |
|||
const db = require('../database'); |
|||
const { Client, Server } = require('../rpc'); |
|||
const { Collection } = require('@dyno.gg/dyno-core'); |
|||
const { models } = db; |
|||
|
|||
/** |
|||
* @class Manager |
|||
*/ |
|||
class Manager { |
|||
/** |
|||
* Create the cluster manager |
|||
* @param {String} strategy Sharding strategy |
|||
*/ |
|||
constructor() { |
|||
this.processes = new Collection(); |
|||
this.clusters = new Collection(); |
|||
|
|||
process.on('uncaughtException', this.handleException.bind(this)); |
|||
process.on('unhandledRejection', this.handleRejection.bind(this)); |
|||
|
|||
this.globalConfig = null; |
|||
this.restClient = null; |
|||
|
|||
this.logger = new Logger(this); |
|||
this.methods = new Commands(this); |
|||
|
|||
this.pmClient = new Client(config.rpcHost || 'localhost', 5050); |
|||
|
|||
this.server = new Server(); |
|||
this.server.init(config.rpcHost || 'localhost', 5052, this.methods); |
|||
|
|||
db.connection.once('open', () => |
|||
this.connect().catch(err => { |
|||
throw err; |
|||
})); |
|||
} |
|||
|
|||
/** |
|||
* Unhandled rejection handler |
|||
* @param {Error|*} reason The reason the promise was rejected |
|||
* @param {Promise} p The promise that was rejected |
|||
*/ |
|||
handleRejection(reason, p) { |
|||
try { |
|||
console.error('Unhandled rejection at: Promise ', p, 'reason: ', reason); // eslint-disable-line
|
|||
} catch (err) { |
|||
console.error(reason); // eslint-disable-line
|
|||
} |
|||
} |
|||
|
|||
handleException(err) { |
|||
if (!err || (typeof err === 'string' && !err.length)) { |
|||
return console.error('An undefined exception occurred.'); // eslint-disable-line
|
|||
} |
|||
|
|||
console.error(err); // eslint-disable-line
|
|||
} |
|||
|
|||
async connect() { |
|||
try { |
|||
const coll = await db.collection('clusters'); |
|||
const [globalConfig, _clusters] = await Promise.all([ |
|||
models.Dyno.findOne().lean(), |
|||
coll.find({ 'host.state': config.state }).toArray(), |
|||
]); |
|||
|
|||
this.globalConfig = globalConfig; |
|||
|
|||
const token = config.isPremium ? config.client.token : this.globalConfig.prodToken || config.client.token; |
|||
this.restClient = new Eris(`Bot ${token}`, { restMode: true }); |
|||
|
|||
for (let c of _clusters) { |
|||
this.clusters.set(c.id, c); |
|||
} |
|||
|
|||
process.send('ready'); |
|||
let response = await this.pmClient.request('list', {}); |
|||
let processes = response && response.result ? response.result : []; |
|||
|
|||
if (!processes.length) { |
|||
this.logger.log(`[${process.pid}] Cluster manager online, starting ${this.clusters.size} clusters`); |
|||
return this.startup(); |
|||
} else { |
|||
this.logger.log(`[${process.pid}] Cluster manager online, resuming with ${this.clusters.size} clusters`); |
|||
} |
|||
|
|||
for (let proc of processes) { |
|||
if (this.processes.has(proc.id)) { continue; } |
|||
proc.client = new Client(config.rpcHost || 'localhost', proc.port); |
|||
this.processes.set(proc.id, proc); |
|||
} |
|||
} catch (err) { |
|||
return Promise.reject(err); |
|||
} |
|||
} |
|||
|
|||
async startup() { |
|||
for (let cluster of this.clusters.values()) { |
|||
await this.createProcess(cluster); |
|||
await this.wait(config.clusterStartDelay || 1500); |
|||
} |
|||
} |
|||
|
|||
async createProcess(cluster) { |
|||
try { |
|||
const response = await this.pmClient.request('create', { cluster }); |
|||
if (!response || !response.result) { |
|||
let error = response.error || response; |
|||
return Promise.reject(error); |
|||
} |
|||
let proc = response.result; |
|||
if (proc.port) { |
|||
proc.client = new Client(config.rpcHost || 'localhost', proc.port); |
|||
} |
|||
this.processes.set(proc.id, proc); |
|||
|
|||
const options = proc.options; |
|||
|
|||
if (options && options.hasOwnProperty('id')) { |
|||
this.logger.log(`[${proc.pid}] Cluster ${options.id} online`); |
|||
} |
|||
|
|||
return true; |
|||
} catch (err) { |
|||
return Promise.reject(err); |
|||
} |
|||
} |
|||
|
|||
async deleteProcess(cluster) { |
|||
try { |
|||
const proc = this.processes.find(p => p.options && p.options.id === cluster.id); |
|||
const response = await this.pmClient.request('delete', { id: proc.id }); |
|||
if (!response || !response.result) { |
|||
let error = response.error || response; |
|||
return Promise.reject(error); |
|||
} |
|||
this.clusters.delete(cluster.id); |
|||
if (proc) { |
|||
this.processes.delete(proc.id); |
|||
} |
|||
return true; |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return Promise.reject(err); |
|||
} |
|||
} |
|||
|
|||
async restartProcess(proc) { |
|||
try { |
|||
const response = await this.pmClient.request('restart', { id: proc.id }); |
|||
if (!response || !response.result) { |
|||
let error = response.error || response; |
|||
return Promise.reject(error); |
|||
} |
|||
|
|||
proc = response.result; |
|||
if (proc.port && !proc.client) { |
|||
proc.client = new Client(config.rpcHost || 'localhost', proc.port); |
|||
} |
|||
this.processes.set(proc.id, proc); |
|||
|
|||
const cluster = proc.options; |
|||
|
|||
if (cluster && cluster.hasOwnProperty('id')) { |
|||
this.logger.log(`[${proc.pid}] Cluster ${cluster.id} online`); |
|||
} |
|||
|
|||
return true; |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
return Promise.reject(err); |
|||
} |
|||
} |
|||
|
|||
wait(ms) { |
|||
return new Promise(resolve => setTimeout(resolve, ms)); |
|||
} |
|||
} |
|||
|
|||
module.exports = Manager; |
@ -0,0 +1,111 @@ |
|||
'use strict'; |
|||
|
|||
const each = require('async-each'); |
|||
const glob = require('glob-promise'); |
|||
const minimatch = require('minimatch'); |
|||
const { EventCollection, utils } = require('@dyno.gg/dyno-core'); |
|||
const { models } = require('../database'); |
|||
const logger = require('../logger'); |
|||
|
|||
/** |
|||
* @class CommandCollection |
|||
* @extends EventCollection |
|||
*/ |
|||
class CommandCollection extends EventCollection { |
|||
/** |
|||
* A collection of commands |
|||
* @param {Object} config The Dyno configuration object |
|||
* @param {Dyno} dyno The Dyno instance |
|||
*/ |
|||
constructor(config, dyno) { |
|||
super(); |
|||
|
|||
this.dyno = dyno; |
|||
this._client = dyno.client; |
|||
this._config = config; |
|||
|
|||
this.loadCommands(); |
|||
} |
|||
|
|||
/** |
|||
* Load commands |
|||
*/ |
|||
async loadCommands() { |
|||
try { |
|||
var [files, moduleFiles] = await Promise.all([ |
|||
glob('**/*.js', { |
|||
cwd: this._config.paths.commands, |
|||
root: this._config.paths.commands, |
|||
absolute: true, |
|||
}), |
|||
glob('**/*.js', { |
|||
cwd: this._config.paths.modules, |
|||
root: this._config.paths.modules, |
|||
absolute: true, |
|||
}), |
|||
]); |
|||
|
|||
moduleFiles = moduleFiles.filter(minimatch.filter('**/commands/*.js')); |
|||
files = files.concat(moduleFiles); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
} |
|||
|
|||
utils.asyncForEach(files, file => { |
|||
if (!file.endsWith('.js')) return; |
|||
let load = () => { |
|||
var command = require(file); |
|||
this.register(command); |
|||
}; |
|||
load(); |
|||
// utils.time(load, file);
|
|||
return; |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Register command |
|||
* @param {Function} Command A Command class to register |
|||
*/ |
|||
register(Command) { |
|||
if (Object.getPrototypeOf(Command).name !== 'Command') { |
|||
return logger.debug('[CommandCollection] Skipping unknown command'); |
|||
} |
|||
|
|||
// create the command
|
|||
let command = new Command(this.dyno); |
|||
|
|||
// ensure command defines all required properties/methods
|
|||
command.name = command.aliases[0]; |
|||
|
|||
logger.debug(`[CommandCollection] Registering command ${command.name}`); |
|||
|
|||
models.Command.update({ name: command.name, _state: this._config.state }, command.toJSON(), { upsert: true }) |
|||
.catch(err => logger.error(err)); |
|||
|
|||
if (command.aliases && command.aliases.length) { |
|||
for (let alias of command.aliases) { |
|||
this.set(alias, command); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Unregister command |
|||
* @param {String} name Name of the command to unregister |
|||
*/ |
|||
unregister(name) { |
|||
logger.info(`Unregistering command: ${name}`); |
|||
|
|||
const command = this.get(name); |
|||
if (!command) return; |
|||
|
|||
if (!command.aliases && !command.aliases.length) return; |
|||
for (let alias of command.aliases) { |
|||
logger.info(`Removing alias ${alias}`); |
|||
this.delete(alias); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = CommandCollection; |
@ -0,0 +1,424 @@ |
|||
'use strict'; |
|||
|
|||
const dot = require('dot-object'); |
|||
const each = require('async-each'); |
|||
const { Collection, utils } = require('@dyno.gg/dyno-core'); |
|||
const { models } = require('../../core/database'); |
|||
const redis = require('../../core/redis'); |
|||
const logger = require('../logger'); |
|||
|
|||
const premiumWebhook = 'https://canary.discordapp.com/api/webhooks/523575952744120321/xrh6uyOA0MOuMvHDAZLw5qws-jr9cDELU6xOoXZSTZcLlwN7lMHxt6yQD-dqRmJuLnnB'; |
|||
|
|||
/** |
|||
* @class GuildCollection |
|||
* @extends Collection |
|||
*/ |
|||
class GuildCollection extends Collection { |
|||
/** |
|||
* A collection of guild configurations |
|||
* @param {Object} config The Dyno configuration object |
|||
* @param {Dyno} dyno The Dyno instance |
|||
*/ |
|||
constructor(config, dyno) { |
|||
super(); |
|||
|
|||
this.dyno = dyno; |
|||
this.client = dyno.client; |
|||
this.config = config; |
|||
this._registering = new Set(); |
|||
this._activeThreshold = 3600 * 1000; // 24 hrs
|
|||
|
|||
dyno.dispatcher.registerListener('guildCreate', this.guildCreated.bind(this)); |
|||
dyno.dispatcher.registerListener('guildDelete', this.guildDeleted.bind(this)); |
|||
|
|||
this.createWatch(); |
|||
|
|||
setInterval(this.uncacheData.bind(this), 150000); |
|||
} |
|||
|
|||
get globalConfig() { |
|||
return this.dyno.globalConfig; |
|||
} |
|||
|
|||
async createWatch() { |
|||
// We need an exclusive connection for publish / subscribe
|
|||
this.subRedis = await redis.connect(); |
|||
|
|||
await this.subRedis.subscribe('guildConfig'); |
|||
|
|||
this.subRedis.on('message', (channel, message) => { |
|||
if (channel === 'guildConfig') { |
|||
this.guildUpdate(message); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
guildUpdate(id) { |
|||
if (!this.client.guilds.has(id) || !this.has(id)) { |
|||
return; |
|||
} |
|||
|
|||
this.fetch(id).catch(err => logger.error(err)); |
|||
} |
|||
|
|||
/** |
|||
* Uncache guild configs |
|||
*/ |
|||
uncacheData() { |
|||
each([...this.values()], guild => { |
|||
if ((Date.now() - guild.cachedAt) > 900) { |
|||
this.delete(guild._id); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Get or fetch a guild, no async/await for performance reasons |
|||
* @param {String} id Guild ID |
|||
* @returns {Promise} |
|||
*/ |
|||
getOrFetch(id) { |
|||
const doc = this.get(id); |
|||
if (doc) { |
|||
doc.cachedAt = Date.now(); |
|||
return Promise.resolve(doc); |
|||
} |
|||
|
|||
return this.fetch(id).then(doc => { |
|||
if (!doc) { |
|||
return this.registerGuild(this.client.guilds.get(id)); |
|||
} |
|||
|
|||
doc.cachedAt = Date.now(); |
|||
this.set(doc._id, doc); |
|||
|
|||
return doc; |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Fetch a guild from the database |
|||
* @param {String} id Guild ID |
|||
* @returns {Promise} |
|||
*/ |
|||
fetch(id) { |
|||
let updateKeys = ['name', 'region', 'iconURL', 'ownerID', 'memberCount']; |
|||
return new Promise((resolve, reject) => { |
|||
models.Server.findAndPopulate(id) |
|||
.then(doc => { |
|||
if (!doc) { |
|||
return resolve(); |
|||
} |
|||
|
|||
doc = doc.toObject(); |
|||
let update = false; |
|||
|
|||
if (this.client.guilds.has(id)) { |
|||
const guild = this.client.guilds.get(id); |
|||
|
|||
if (!doc.longId) { |
|||
update = update || {}; |
|||
update.longId = guild.id; |
|||
} |
|||
|
|||
for (let key of updateKeys) { |
|||
if (guild[key] && doc[key] !== guild[key]) { |
|||
update = update || {}; |
|||
update[key] = guild[key]; |
|||
doc[key] = guild[key]; |
|||
} |
|||
} |
|||
|
|||
if (doc.deleted === true) { |
|||
update = update || {}; |
|||
update.deleted = false; |
|||
} |
|||
|
|||
if (!doc.clientID || doc.clientID !== this.config.client.id) { |
|||
if ((this.config.isPremium && doc.isPremium) || (!this.config.isPremium && !doc.isPremium)) { |
|||
update = update || {}; |
|||
update.clientID = this.config.client.id; |
|||
} |
|||
} |
|||
|
|||
if (!doc.lastActive || (Date.now() - doc.lastActive) > this._activeThreshold) { |
|||
update = update || {}; |
|||
update.lastActive = Date.now(); |
|||
this.setActive(guild, update.lastActive); |
|||
} |
|||
|
|||
if (update) { |
|||
this.update(id, { $set: update }).catch(err => logger.error(err)); |
|||
} |
|||
} |
|||
|
|||
this.set(doc._id, doc); |
|||
return resolve(doc); |
|||
}) |
|||
.catch(err => reject(err)); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Fired when a web update is received |
|||
* @param {String} id Guild ID |
|||
*/ |
|||
// guildUpdate(id) {
|
|||
// const guild = this.client.guilds.get(id);
|
|||
// if (!guild) return;
|
|||
|
|||
// logger.debug(`Web update for guild: ${id}`);
|
|||
|
|||
// this.fetch(id).catch(err => logger.error(err));
|
|||
// }
|
|||
|
|||
/** |
|||
* Wrapper to update guild config |
|||
* @param {String} id Guild ID |
|||
* @param {Object} update Mongoose update query |
|||
* @param {...*} args Any additional arguments to pass to the model |
|||
* @returns {Promise} |
|||
*/ |
|||
update(id, update, ...args) { |
|||
if (update.$set) { |
|||
const serverlistColl = this.dyno.db.collection('serverlist_store'); |
|||
let serverlistUpdate = false; |
|||
if (update.$set.iconURL) { |
|||
serverlistUpdate = serverlistUpdate || {}; |
|||
serverlistUpdate.iconURL = update.$set.iconURL; |
|||
} |
|||
|
|||
if (update.$set.deleted === true) { |
|||
serverlistUpdate = serverlistUpdate || {}; |
|||
serverlistUpdate.markedForDeletionAt = Date.now(); |
|||
} |
|||
|
|||
if (update.$set.name) { |
|||
serverlistUpdate = serverlistUpdate || {}; |
|||
serverlistUpdate.name = update.$set.name; |
|||
} |
|||
|
|||
if (update.$set.memberCount) { |
|||
serverlistUpdate = serverlistUpdate || {}; |
|||
serverlistUpdate.memberCount = update.$set.memberCount; |
|||
} |
|||
|
|||
if (serverlistUpdate) { |
|||
serverlistColl.update({ id }, { $set: serverlistUpdate }); |
|||
} |
|||
|
|||
if (update.$set.deleted === false) { |
|||
serverlistColl.update({ id }, { $unset: { markedForDeletionAt: 1 } }); |
|||
} |
|||
} |
|||
|
|||
try { |
|||
const result = models.Server.update({ _id: id }, update, ...args); |
|||
this.dyno.redis.publish('guildConfig', id); |
|||
return result; |
|||
} catch (err) { |
|||
logger.error(err); |
|||
} |
|||
} |
|||
|
|||
// getGlobal() {
|
|||
// if (this._globalConfig) return Promise.resolve(this._globalConfig);
|
|||
// return Dyno.findOne().lean().exec();
|
|||
// }
|
|||
|
|||
/** |
|||
* Guild created event listener |
|||
* @param {Guild} guild Guild object |
|||
*/ |
|||
async guildCreated(guild) { |
|||
// if (this.config.handleRegion && !utils.regionEnabled(guild, this.config) && guild.id !== this.config.dynoGuild) {
|
|||
// return this.client.uncacheGuild(guild.id);
|
|||
// }
|
|||
|
|||
logger.info(`Connected to server: ${guild.id} with ${guild.channels.size} channels and ${guild.members.size} members | ${guild.name}`); |
|||
|
|||
try { |
|||
var doc = await models.Server.findOne({ _id: guild.id }).lean().exec(); |
|||
if (!doc) { |
|||
return this.registerGuild(guild, true); |
|||
} |
|||
|
|||
if (this.config.isPremium && !doc.isPremium) { |
|||
this.postWebhook(premiumWebhook, { embeds: [{ title: 'Non-premium Guild Create', description: `Leaving Guild ${guild.id}`, color: 16729871 }] }); |
|||
return this.client.leaveGuild(guild.id); |
|||
} |
|||
|
|||
await this.update(guild.id, { $set: { deleted: false } }, { multi: true }); |
|||
this.set(doc._id, doc); |
|||
} catch (err) { |
|||
return logger.error(err); |
|||
} |
|||
|
|||
if (this.config.isPremium && !doc.premiumInstalled) { |
|||
doc.premiumInstalled = true; |
|||
this.set(doc._id, doc); |
|||
this.update(doc._id, { $set: { premiumInstalled: true } }).catch(err => logger.error(err)); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Guild deleted event listener |
|||
* @param {Guild} guild Guild object |
|||
*/ |
|||
async guildDeleted(guild) { |
|||
if (guild.unavailable) return; |
|||
|
|||
if (this.config.isPremium) { |
|||
var guildConfig = await this.getOrFetch(guild.id); |
|||
if (!guildConfig || !guildConfig.isPremium) return; |
|||
if (guildConfig.isPremium && guildConfig.premiumInstalled) { |
|||
return this.update(guild.id, { $set: { premiumInstalled: false } }).catch(() => false); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
this.update(guild.id, { $set: { deleted: true, deletedAt: new Date() } }) |
|||
.catch(err => logger.error(err)); |
|||
} |
|||
|
|||
/** |
|||
* Register server in the database |
|||
* @param {Guild} guild Guild object |
|||
*/ |
|||
registerGuild(guild, newGuild) { |
|||
if (!guild || !guild.id) { |
|||
return; |
|||
} |
|||
|
|||
if (this._registering.has(guild.id)) { |
|||
return; |
|||
} |
|||
|
|||
this._registering.add(guild.id); |
|||
|
|||
let doc = { |
|||
_id: guild.id, |
|||
longId: guild.id, |
|||
clientID: this.config.clientID, |
|||
name: guild.name, |
|||
iconURL: guild.iconURL, |
|||
ownerID: guild.ownerID, |
|||
memberCount: guild.memberCount, |
|||
region: guild.region || null, |
|||
modules: {}, |
|||
commands: {}, |
|||
lastActive: Date.now(), |
|||
deleted: false, |
|||
}; |
|||
|
|||
logger.info(`Registering guild: ${guild.id} ${guild.name}`); |
|||
|
|||
if (newGuild && !this.config.isPremium) { |
|||
this.dmOwner(guild); |
|||
} |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
// add modules
|
|||
for (let mod of this.dyno.modules.values()) { |
|||
// ignore core modules or modules that shouldn't be listed
|
|||
if (mod.core && (mod.hasOwnProperty('list') && mod.list === false)) continue; |
|||
doc.modules[mod.module] = mod.enabled; |
|||
} |
|||
|
|||
for (let cmd of this.dyno.commands.values()) { |
|||
if (cmd.permissions === 'admin') continue; |
|||
|
|||
// ignore commands that belong to a module
|
|||
if (this.dyno.modules.find(o => o.module === cmd.group) && doc.modules[cmd.group] === false) { |
|||
doc.commands[cmd.name] = false; |
|||
continue; |
|||
} |
|||
doc.commands[cmd.name] = (cmd.enabled || !cmd.disabled); |
|||
} |
|||
|
|||
this.update(doc._id, doc, { upsert: true }) |
|||
.then(() => { |
|||
doc.cachedAt = Date.now(); |
|||
|
|||
this.set(guild.id, doc); |
|||
return resolve(doc); |
|||
}) |
|||
.catch(err => { |
|||
logger.error(err); |
|||
return reject(err); |
|||
}) |
|||
.then(() => this._registering.delete(guild.id)); |
|||
}); |
|||
} |
|||
|
|||
setActive(guild, time) { |
|||
guild.lastActive = time; |
|||
this.dyno.redis.hset(`guild_activity:${this.config.client.id}:${this.config.clientOptions.maxShards}:${guild.shard.id}`, guild.id, time) |
|||
.catch(() => null); |
|||
} |
|||
|
|||
/** |
|||
* Attempt to send a DM to guild owner |
|||
* @param {Guild} guild Guild object |
|||
* @param {String} content Message to send |
|||
* @returns {Promise} |
|||
*/ |
|||
async sendDM(guild, content) { |
|||
try { |
|||
var channel = await this.client.getDMChannel(guild.ownerID); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
return Promise.reject(err); |
|||
} |
|||
|
|||
if (!channel) { |
|||
return Promise.reject('Channel is undefined or null.'); |
|||
} |
|||
|
|||
this.client.createMessage(channel, content).catch(() => false); |
|||
} |
|||
|
|||
/** |
|||
* DM Guild owner |
|||
* @param {Guild} guild Guild |
|||
*/ |
|||
dmOwner(guild) { |
|||
if (this.config.test || this.config.beta) return; |
|||
if (this.config.handleRegion && !utils.regionEnabled(guild, this.config)) return; |
|||
|
|||
let msgArray = []; |
|||
|
|||
msgArray.push(`Thanks for adding me to your server. Just a few things to note.`); |
|||
msgArray.push('**1.** The default prefix is **`?`**.'); |
|||
msgArray.push('**2.** Setup the bot at **https://www.dynobot.net**'); |
|||
msgArray.push('**3.** Commands do not work in DM.'); |
|||
msgArray.push(`**4.** Join the Dyno discord server for questions, suggestions, or updates. **https://www.dynobot.net/discord**`); |
|||
|
|||
const content = msgArray.join('\n'); |
|||
|
|||
this.sendDM(guild, content) |
|||
.then(() => logger.debug('Successful DM to owner')) |
|||
.catch(() => { |
|||
if (guild.memberCount > 70) return; |
|||
this.client.createMessage(guild.defaultChannel, msgArray.join('\n')); |
|||
}); |
|||
} |
|||
|
|||
postWebhook(webhook, payload) { |
|||
return new Promise((resolve, reject) => |
|||
axios.post(webhook, { |
|||
headers: { |
|||
Accept: 'application/json', |
|||
'Content-Type': 'application/json', |
|||
}, |
|||
...payload, |
|||
}) |
|||
.then(resolve) |
|||
.catch(reject)); |
|||
} |
|||
} |
|||
|
|||
module.exports = GuildCollection; |
@ -0,0 +1,203 @@ |
|||
'use strict'; |
|||
|
|||
const each = require('async-each'); |
|||
const glob = require('glob-promise'); |
|||
const minimatch = require('minimatch'); |
|||
const jsonSchema = require('mongoose_schema-json'); |
|||
const { Collection, utils } = require('@dyno.gg/dyno-core'); |
|||
const { models } = require('../database'); |
|||
const logger = require('../logger'); |
|||
|
|||
/** |
|||
* @class ModuleCollection |
|||
* @extends Collection |
|||
*/ |
|||
class ModuleCollection extends Collection { |
|||
/** |
|||
* A collection of modules |
|||
* @param {Object} config The Dyno configuration object |
|||
* @param {Dyno} dyno The Dyno instance |
|||
*/ |
|||
constructor(config, dyno) { |
|||
super(); |
|||
|
|||
this.dyno = dyno; |
|||
this._client = dyno.client; |
|||
this._config = config; |
|||
this._listenerCount = 0; |
|||
|
|||
this.moduleList = this._config.moduleList || []; |
|||
|
|||
this.loadModules(); |
|||
} |
|||
|
|||
unload() { |
|||
for (let module of this.values()) { |
|||
module._unload(); |
|||
this.delete(module.name); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Load commands |
|||
*/ |
|||
async loadModules() { |
|||
try { |
|||
var files = await glob('**/*.js', { |
|||
cwd: this._config.paths.modules, |
|||
root: this._config.paths.modules, |
|||
absolute: true, |
|||
}); |
|||
files = files.filter(f => !minimatch(f, '**/commands/*')); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
} |
|||
|
|||
let modules = []; |
|||
|
|||
each(files, (file, next) => { |
|||
if (file.endsWith('.map')) return next(); |
|||
|
|||
const module = require(file); |
|||
|
|||
if (module.hasModules) { |
|||
modules = modules.concat(Object.values(module.modules)); |
|||
return next(); |
|||
} |
|||
|
|||
modules.push(require(file)); |
|||
return next(); |
|||
}, err => { |
|||
if (err) { |
|||
logger.error(err); |
|||
} |
|||
|
|||
utils.asyncForEach(modules, (module, next) => { |
|||
this.register(module); |
|||
return; |
|||
}, (e) => { |
|||
if (e) { |
|||
logger.error(e); |
|||
} |
|||
logger.info(`[ModuleCollection] Registered ${this.size} modules.`); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Register module |
|||
* @param {Function} Module the module class |
|||
*/ |
|||
register(Module) { |
|||
if (Object.getPrototypeOf(Module).name !== 'Module') { |
|||
return logger.debug(`[ModuleCollection] Skipping unknown module`); |
|||
} |
|||
|
|||
let module = new Module(this.dyno), |
|||
activeModule = this.get(module.name), |
|||
globalConfig = this.dyno.globalConfig; |
|||
|
|||
if (activeModule) { |
|||
logger.debug(`[ModuleCollection] Unloading module ${module.name}`); |
|||
activeModule._unload(); |
|||
this.delete(module.name); |
|||
} |
|||
|
|||
logger.debug(`[ModuleCollection] Registering module ${module.name}`); |
|||
|
|||
if (module.commands) { |
|||
const commands = Array.isArray(module.commands) ? module.commands : Object.values(module.commands); |
|||
each(commands, command => this.dyno.commands.register(command)); |
|||
} |
|||
|
|||
if (module.moduleModels) { |
|||
this.registerModels(module.moduleModels); |
|||
} |
|||
|
|||
// ensure the module defines all required properties/methods
|
|||
module.ensureInterface(); |
|||
|
|||
if (!activeModule) { |
|||
const moduleCopy = module.toJSON(); |
|||
|
|||
if (module.settings) { |
|||
moduleCopy.settings = jsonSchema.schema2json(module.settings); |
|||
|
|||
models.Server.schema.add({ |
|||
[module.name.toLowerCase()]: module.settings, |
|||
}); |
|||
} |
|||
|
|||
moduleCopy._state = this._config.state; |
|||
|
|||
models.Module.findOneAndUpdate({ name: module.name, _state: this._config.state }, moduleCopy, { upsert: true, overwrite: true }) |
|||
.catch(err => logger.error(err)); |
|||
} |
|||
|
|||
this.set(module.name, module); |
|||
|
|||
if (this.moduleList.length && !this.moduleList.includes(Module.name)) { |
|||
return; |
|||
} |
|||
|
|||
if (globalConfig && globalConfig.modules.hasOwnProperty(module.name) && |
|||
globalConfig.modules[module.name] === false) return; |
|||
|
|||
each(this.dyno.dispatcher.events, (event, next) => { |
|||
if (!module[event]) return next(); |
|||
module.registerListener(event, module[event]); |
|||
this._listenerCount++; |
|||
next(); |
|||
}, err => { |
|||
if (err) logger.error(err); |
|||
this.get(module.name)._start(this._client); |
|||
}); |
|||
} |
|||
|
|||
registerModels(moduleModels) { |
|||
if(!moduleModels || !moduleModels.length || moduleModels.length === 0) { |
|||
return; |
|||
} |
|||
|
|||
each(moduleModels, (model, next) => { |
|||
if(typeof model !== 'object' || !model.name || (!model.skeleton && !model.schema)) { |
|||
next(); |
|||
return; |
|||
} |
|||
|
|||
logger.debug(`[ModuleCollection] Registering model: ${model.name}`); |
|||
|
|||
const schema = new this.dyno.db.Schema(model.skeleton || model.schema, model.options); |
|||
|
|||
this.dyno.db.registerModel({ name: model.name, schema }); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Enable or disable a module |
|||
* @param {String} id Guild id |
|||
* @param {String} name Module name |
|||
* @param {String|Boolean} enabled Enabled or disabled |
|||
* @returns {Promise} |
|||
*/ |
|||
async toggle(id, name, enabled) { |
|||
let guildConfig = await this.dyno.guilds.getOrFetch(id), |
|||
guild = this._client.guilds.get(id), |
|||
module = this.get(name), |
|||
key = `modules.${name}`; |
|||
|
|||
enabled = enabled === 'true'; |
|||
|
|||
if (!guild || !guildConfig) |
|||
return Promise.reject(`Couldn't get guild or config for module ${name}.`); |
|||
|
|||
guildConfig.modules[name] = enabled; |
|||
|
|||
if (enabled && module && module.enable) module.enable(guild); |
|||
if (!enabled && module && module.disable) module.disable(guild); |
|||
|
|||
return this.dyno.guilds.update(guildConfig._id, { $set: { [key]: enabled } }); |
|||
} |
|||
} |
|||
|
|||
module.exports = ModuleCollection; |
@ -0,0 +1,108 @@ |
|||
'use strict'; |
|||
|
|||
const path = require('path'); |
|||
const pkg = require('../../package.json'); |
|||
const dot = require('dot-object'); |
|||
|
|||
const basePath = path.resolve(path.join(__dirname, '..')); |
|||
|
|||
const envkeyLoader = require('envkey/loader'); |
|||
|
|||
let config = envkeyLoader.fetch(); |
|||
|
|||
for (let k of Object.keys(config)) { |
|||
const index = config[k].indexOf('$typeof:'); |
|||
if (index > 0) { |
|||
const value = config[k].substr(0, index); |
|||
const type = config[k].substr(index).replace('$typeof:', ''); |
|||
|
|||
switch (type) { |
|||
case 'int': |
|||
case 'number': |
|||
config[k] = Number.parseInt(value); |
|||
break; |
|||
case 'bool': |
|||
case 'boolean': |
|||
config[k] = (value === 'true'); |
|||
break; |
|||
case 'json': |
|||
config[k] = JSON.parse(value); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
config = dot.object(config); |
|||
|
|||
config.paths = { |
|||
base: basePath, |
|||
commands: path.join(basePath, 'commands'), |
|||
controllers: path.join(basePath, 'controllers'), |
|||
ipc: path.join(basePath, 'ipc'), |
|||
events: path.join(basePath, 'events'), |
|||
modules: path.join(basePath, 'modules'), |
|||
}; |
|||
|
|||
config.pkg = pkg; |
|||
|
|||
config.permissions = { |
|||
createInstantInvite: 1, |
|||
kickMembers: 2, |
|||
banMembers: 4, |
|||
administrator: 8, |
|||
manageChannels: 16, |
|||
manageGuild: 32, |
|||
addReactions: 64, |
|||
readMessages: 1024, |
|||
sendMessages: 2048, |
|||
sendTTSMessages: 4096, |
|||
manageMessages: 8192, |
|||
embedLinks: 16384, |
|||
attachFiles: 32768, |
|||
readMessageHistory: 65536, |
|||
mentionEveryone: 131072, |
|||
externalEmojis: 262144, |
|||
voiceConnect: 1048576, |
|||
voiceSpeak: 2097152, |
|||
voiceMuteMembers: 4194304, |
|||
voiceDeafenMembers: 8388608, |
|||
voiceMoveMembers: 16777216, |
|||
voiceUseVAD: 33554432, |
|||
changeNickname: 67108864, |
|||
manageNicknames: 134217728, |
|||
manageRoles: 268435456, |
|||
manageWebhooks: 536870912, |
|||
manageEmojis: 1073741824, |
|||
}; |
|||
|
|||
config.permissionsMap = { |
|||
createInstantInvite: 'Create Instant Invite', |
|||
kickMembers: 'Kick Members', |
|||
banMembers: 'Ban Members', |
|||
administrator: 'Administrator', |
|||
manageChannels: 'Manage Channels', |
|||
manageGuild: 'Manage Server', |
|||
addReactions: 'Add Reactions', |
|||
readMessages: 'Read Messages', |
|||
sendMessages: 'Send Messages', |
|||
sendTTSMessages: 'Send TTS Messages', |
|||
manageMessages: 'Manage Messages', |
|||
embedLinks: 'Embed Links', |
|||
attachFiles: 'Attach Files', |
|||
readMessageHistory: 'Read Message History', |
|||
mentionEveryone: 'Mention Everyone', |
|||
externalEmojis: 'External Emojis', |
|||
voiceConnect: 'Connect', |
|||
voiceSpeak: 'Speak', |
|||
voiceMuteMembers: 'Mute Members', |
|||
voiceDeafenMembers: 'Deafen Members', |
|||
voiceMoveMembers: 'Move Members', |
|||
voiceUseVAD: 'Use Voice Activity', |
|||
changeNickname: 'Change Nickname', |
|||
manageNicknames: 'Manage Nicknames', |
|||
manageRoles: 'Manage Roles', |
|||
manageWebhooks: 'Manage Webhooks', |
|||
manageEmojis: 'Manage Emojis', |
|||
}; |
|||
|
|||
module.exports = config; |
@ -0,0 +1,24 @@ |
|||
'use strict'; |
|||
|
|||
const DataFactory = require('@dyno.gg/datafactory'); |
|||
const config = require('./config'); |
|||
|
|||
const dbString = config.mongo.dsn; |
|||
|
|||
if (!dbString) { |
|||
throw new Error('Missing environment variable CLIENT_MONGO_URL.'); |
|||
} |
|||
|
|||
const db = new DataFactory({ |
|||
dbString, |
|||
disableReplica: config.mongo.disableReplica || false, |
|||
logger: { |
|||
level: config.logLevel || 'error', |
|||
sentry: { |
|||
level: config.sentry.logLevel, |
|||
dsn: config.sentry.dsn, |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
module.exports = db; |
@ -0,0 +1,98 @@ |
|||
'use strict'; |
|||
|
|||
const util = require('util'); |
|||
const moment = require('moment'); |
|||
const winston = require('winston'); |
|||
const config = require('./config'); |
|||
const Sentry = require('./transports/winston-sentry'); |
|||
|
|||
/** |
|||
* @class Logger |
|||
*/ |
|||
class Logger { |
|||
/** |
|||
* @prop {Array} transports |
|||
* @prop {Boolean} exitOnError |
|||
*/ |
|||
constructor() { |
|||
this.transports = [ |
|||
new (winston.transports.Console)({ |
|||
colorize: true, |
|||
level: config.logLevel || 'info', |
|||
debugStdout: true, |
|||
// handleExceptions: true,
|
|||
// humanReadableUnhandledException: true,
|
|||
timestamp: () => new Date(), |
|||
formatter: this._formatter.bind(this), |
|||
}), |
|||
]; |
|||
|
|||
if (config.sentry.dsn) { |
|||
this.transports.push(new Sentry({ |
|||
// patchGlobal: true,
|
|||
level: config.sentry.logLevel || 'error', |
|||
dsn: config.sentry.dsn, |
|||
logger: config.stateName, |
|||
})); |
|||
} |
|||
|
|||
this.exitOnError = false; |
|||
|
|||
return new (winston.Logger)(this); |
|||
} |
|||
|
|||
/** |
|||
* Custom formatter for console |
|||
* @param {Object} options Formatter options |
|||
* @returns {String} |
|||
* @private |
|||
*/ |
|||
_formatter(options) { |
|||
let ts = util.format('[%s]', moment(options.timestamp()).format('HH:mm:ss')), |
|||
level = winston.config.colorize(options.level); |
|||
|
|||
if (process.env.hasOwnProperty('clusterId')) { |
|||
ts = `[C${process.env.clusterId}] ${ts}`; |
|||
} |
|||
|
|||
if (!options.message.length && options.meta instanceof Error) { |
|||
options.message = options.meta + options.meta.stack; |
|||
} |
|||
|
|||
if (options.meta && options.meta.guild && typeof options.meta.guild !== 'string') { |
|||
if (options.meta.guild.shard) { |
|||
options.meta.shard = options.meta.guild.shard.id; |
|||
} |
|||
options.meta.guild = options.meta.guild.id; |
|||
} |
|||
|
|||
switch (options.level) { |
|||
case 'debug': |
|||
ts += ' ⚙ '; |
|||
break; |
|||
case 'info': |
|||
ts += ' 🆗 '; |
|||
break; |
|||
case 'error': |
|||
ts += ' 🔥 '; |
|||
break; |
|||
case 'warn': |
|||
ts += ' ☣ '; |
|||
break; |
|||
case 'silly': |
|||
ts += ' 💩 '; |
|||
break; |
|||
} |
|||
|
|||
let message = ts + ' ' + level + ': ' + (undefined !== options.message ? options.message : '') + |
|||
(options.meta && Object.keys(options.meta).length ? '\n\t' + util.inspect(options.meta) : ''); |
|||
|
|||
if (options.colorize === 'all') { |
|||
return winston.config.colorize(options.level, message); |
|||
} |
|||
|
|||
return message; |
|||
} |
|||
} |
|||
|
|||
module.exports = new Logger(); |
@ -0,0 +1,278 @@ |
|||
'use strict'; |
|||
|
|||
const { utils } = require('@dyno.gg/dyno-core'); |
|||
const each = require('async-each'); |
|||
const logger = require('../logger'); |
|||
|
|||
class EventManager { |
|||
constructor(dyno) { |
|||
this.dyno = dyno; |
|||
this._client = dyno.client; |
|||
this._config = dyno.config; |
|||
|
|||
this._handlers = new Map(); |
|||
this._listeners = {}; |
|||
this._boundListeners = new Map(); |
|||
this.chunkedGuilds = new Map(); |
|||
this.disabledGuilds = new Map(); |
|||
|
|||
this.events = [ |
|||
'channelCreate', |
|||
'channelDelete', |
|||
'guildBanAdd', |
|||
'guildBanRemove', |
|||
'guildCreate', |
|||
'guildDelete', |
|||
'guildMemberAdd', |
|||
'guildMemberRemove', |
|||
'guildMemberUpdate', |
|||
'guildRoleCreate', |
|||
'guildRoleDelete', |
|||
'guildRoleUpdate', |
|||
'messageCreate', |
|||
'messageDelete', |
|||
'messageDeleteBulk', |
|||
'messageUpdate', |
|||
'userUpdate', |
|||
'voiceChannelJoin', |
|||
'voiceChannelLeave', |
|||
'voiceChannelSwitch', |
|||
'messageReactionAdd', |
|||
'messageReactionRemove', |
|||
'messageReactionRemoveAll', |
|||
]; |
|||
|
|||
this.registerHandlers(); |
|||
} |
|||
|
|||
get client() { |
|||
return this._client; |
|||
} |
|||
|
|||
get config() { |
|||
return this._config; |
|||
} |
|||
|
|||
/** |
|||
* Register the root event handlers from the events directory |
|||
*/ |
|||
registerHandlers() { |
|||
utils.readdirRecursive(this._config.paths.events).then(files => { |
|||
each(files, (file, next) => { |
|||
if (file.endsWith('.map')) return next(); |
|||
const handler = require(file); |
|||
if (!handler || !handler.name) return logger.error('Invalid handler.'); |
|||
this._handlers.set(handler.name, handler); |
|||
logger.debug(`[EventManager] Registering ${handler.name} handler`); |
|||
return next(); |
|||
}, err => { |
|||
if (err) logger.error(err); |
|||
logger.info(`[EventManager] Registered ${this.events.size} events.`); |
|||
}); |
|||
}).catch(err => logger.error(err)); |
|||
} |
|||
|
|||
/** |
|||
* Bind event listeners and store a reference |
|||
* to the bound listener so they can be unregistered. |
|||
*/ |
|||
bindListeners() { |
|||
let listenerCount = 0; |
|||
for (let event in this._listeners) { |
|||
// Bind the listener so it can be removed
|
|||
this._boundListeners[event] = this.createListener.bind(this, event); |
|||
// Register the listener
|
|||
this.client.on(event, this._boundListeners[event]); |
|||
listenerCount++; |
|||
} |
|||
logger.info(`[EventManager] Bound ${listenerCount} listeners.`); |
|||
} |
|||
|
|||
/** |
|||
* Register event listener |
|||
* @param {String} event Event name |
|||
* @param {Function} listener Event listener |
|||
* @param {String} [module] module name |
|||
*/ |
|||
registerListener(event, listener, module) { |
|||
// Register but don't bind listeners before the client is ready
|
|||
if (!this._listeners[event] || !this._listeners[event].find(l => l.listener === listener)) { |
|||
this._listeners[event] = this._listeners[event] || []; |
|||
this._listeners[event].push({ module: module || null, listener: listener }); |
|||
return; |
|||
} |
|||
|
|||
// Remove the listener from listeners if it exists, and re-add it
|
|||
let index = this._listeners[event].findIndex(l => l.listener === listener); |
|||
if (index > -1) this._listeners[event].splice(index, 1); |
|||
|
|||
this._listeners[event].push({ module: module, listener: listener }); |
|||
|
|||
this.client.removeListener(event, this._boundListeners[event]); |
|||
|
|||
// Bind the listener so it can be removed
|
|||
this._boundListeners[event] = this.createListener.bind(this, event); |
|||
|
|||
// Register the bound listener
|
|||
this.client.on(event, this._boundListeners[event]); |
|||
} |
|||
|
|||
/** |
|||
* Deregister event listener |
|||
* @param {String} event Event name |
|||
* @param {Function} listener Event listener |
|||
*/ |
|||
unregisterListener(event, listener) { |
|||
let index = this._listeners[event].findIndex(l => l.listener === listener); |
|||
if (index > -1) this._listeners[event].splice(index, 1); |
|||
} |
|||
|
|||
awaitChunkState(guild, cb) { |
|||
if (guild.members && guild.members.size >= (guild.memberCount * 0.9)) { |
|||
this.chunkedGuilds.set(guild.id, 2); |
|||
return cb(); |
|||
} else { |
|||
setTimeout(() => this.awaitChunkState(guild, cb), 100); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Create an event listener |
|||
* @param {String} event Event name |
|||
* @param {...*} args Event arguments |
|||
*/ |
|||
createListener(event, ...args) { |
|||
if (!this._listeners[event]) return; |
|||
|
|||
const handler = this._handlers.get(event); |
|||
|
|||
// Check if a root handler exists before calling module listeners
|
|||
if (handler) { |
|||
return handler(this, ...args).then(async (e) => { |
|||
if (!e || !e.guildConfig) { |
|||
return; |
|||
// return logger.warn(`${event} no event or guild config`);
|
|||
} |
|||
|
|||
if (e.guild.id && this.disabledGuilds.has(e.guild.id)) { |
|||
return; |
|||
} |
|||
|
|||
// Check if guild is enabled for the app state
|
|||
if (e.guild.id !== this._config.dynoGuild && event !== 'messageCreate') { |
|||
if (!this.guildEnabled(e.guildConfig, e.guild.id)) return; |
|||
} |
|||
|
|||
let chunkState = this.chunkedGuilds.get(e.guild.id); |
|||
|
|||
if (this.config.lazyChunking) { |
|||
if (!chunkState) { |
|||
chunkState = 1; |
|||
this.chunkedGuilds.set(e.guild.id, 1); |
|||
e.guild.fetchAllMembers(); |
|||
} |
|||
if (chunkState === 2) { |
|||
if (e.guild.members.size < (e.guild.memberCount * 0.9)) { |
|||
chunkState = 1; |
|||
this.chunkedGuilds.set(e.guild.id, 1); |
|||
e.guild.fetchAllMembers(); |
|||
} |
|||
} |
|||
if (chunkState !== 2) { |
|||
await new Promise(resolve => this.awaitChunkState(e.guild, resolve)); |
|||
} |
|||
} |
|||
|
|||
each(this._listeners[event], o => { |
|||
if (o.module && e.guild && e.guildConfig) { |
|||
if (!this.moduleEnabled(e.guild, e.guildConfig, o.module)) return; |
|||
} |
|||
|
|||
o.listener(e); |
|||
}); |
|||
}).catch(err => err ? logger.error(err) : false); |
|||
} |
|||
|
|||
// No root handler exists, execute the module listeners
|
|||
each(this._listeners[event], o => o.listener(...args)); |
|||
} |
|||
|
|||
/** |
|||
* Check if an event handler should continue or not based on app state and guild config |
|||
* @param {Object} guildConfig Guild configuration |
|||
* @param {String} guildId Guild ID |
|||
* @returns {Boolean} |
|||
*/ |
|||
guildEnabled(guildConfig, guildId) { |
|||
const guild = this._client.guilds.get(guildId); |
|||
|
|||
// handle events based on region, ignore in dev
|
|||
if (!guild) return false; |
|||
if (this._config.handleRegion && !utils.regionEnabled(guild, this._config)) return false; |
|||
|
|||
if (this.disabledGuilds.has(guildId)) { |
|||
return false; |
|||
} |
|||
|
|||
if (this._config.test) { |
|||
if (this._config.testGuilds.includes(guildId) || guildConfig.test) return true; |
|||
return false; |
|||
} |
|||
|
|||
// premium checks
|
|||
if (!this._config.isPremium && guildConfig.isPremium && guildConfig.premiumInstalled) { |
|||
return false; |
|||
} |
|||
if (this._config.isPremium && (!guildConfig.isPremium || !guildConfig.premiumInstalled)) { |
|||
return false; |
|||
} |
|||
|
|||
if (!this._config.isPremium && guildConfig.clientID && guildConfig.clientID !== this._config.client.id) { |
|||
return false; |
|||
} |
|||
|
|||
// Shared state, receive all events
|
|||
if (this._config.shared) return true; |
|||
|
|||
if (guildConfig.beta) { |
|||
// Guild is using beta, but app state is not test/beta
|
|||
if (!this._config.test && !this._config.beta) { |
|||
return false; |
|||
} |
|||
} else if (this._config.beta) { |
|||
// App state is beta, but guild is not.
|
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Check if a module is enabled or should execute code |
|||
* @param {Guild} guild Guild object |
|||
* @param {Object} guildConfig Guild configuration |
|||
* @param {String} module Module name |
|||
* @return {Boolean} |
|||
*/ |
|||
moduleEnabled(guild, guildConfig, module) { |
|||
// Ignore events before the client is ready or guild is cached
|
|||
if (!this.dyno.isReady || !guild || !guildConfig) return false; |
|||
if (!guildConfig.modules) return false; |
|||
|
|||
const name = module.module || module.name; |
|||
|
|||
// check if globally disabled
|
|||
const globalConfig = this.dyno.globalConfig; |
|||
if (globalConfig && globalConfig.modules.hasOwnProperty(name) && |
|||
globalConfig.modules[name] === false) return false; |
|||
|
|||
// check if module is disabled
|
|||
if (guildConfig.modules.hasOwnProperty(name) && guildConfig.modules[name] === false) { |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
|
|||
module.exports = EventManager; |
@ -0,0 +1,146 @@ |
|||
'use strict'; |
|||
|
|||
const { utils } = require('@dyno.gg/dyno-core'); |
|||
const EventEmitter = require('eventemitter3'); |
|||
const each = require('async-each'); |
|||
const logger = require('../logger'); |
|||
|
|||
/** |
|||
* @class IPCManager |
|||
* @extends EventEmitter |
|||
*/ |
|||
class IPCManager extends EventEmitter { |
|||
/** |
|||
* Manages the IPC communications with the shard manager |
|||
* @param {Dyno} dyno The Dyno instance |
|||
* |
|||
* @prop {Number} id Shard ID |
|||
* @prop {Number} pid Process ID |
|||
* @prop {Map} commands Collection of IPC commands |
|||
*/ |
|||
constructor(dyno) { |
|||
super(); |
|||
|
|||
const config = this._config = dyno.config; |
|||
|
|||
this.dyno = dyno; |
|||
this.client = dyno.client; |
|||
|
|||
this.id = dyno.clientOptions.clusterId || dyno.clientOptions.shardId || 0; |
|||
this.pid = process.pid; |
|||
this.commands = new Map(); |
|||
|
|||
process.on('message', this.onMessage.bind(this)); |
|||
|
|||
utils.readdirRecursive(this._config.paths.ipc).then(files => { |
|||
each(files, (file, next) => { |
|||
if (file.endsWith('.map')) return next(); |
|||
this.register(require(file)); |
|||
return next(); |
|||
}, err => { |
|||
if (err) logger.error(err); |
|||
logger.info(`[IPCManager] Registered ${this.commands.size} IPC commands.`); |
|||
}); |
|||
}).catch(err => logger.error(err)); |
|||
} |
|||
|
|||
/** |
|||
* Send a command or event to the shard manager |
|||
* @param {String} event Event to send |
|||
* @param {Mixed} data The data to send |
|||
*/ |
|||
send(event, data) { |
|||
if (!process.send) return; |
|||
try { |
|||
process.send({ |
|||
op: event, |
|||
d: data || null, |
|||
}); |
|||
} catch (err) { |
|||
logger.error(`IPC Error Caught:`, err); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Fired when the shard receives a message |
|||
* @param {Object} message The message object |
|||
* @returns {*} |
|||
*/ |
|||
onMessage(message) { |
|||
// op for internal dyno messages, type for prom-client cluster messages
|
|||
if (!message.op && !message.type) { |
|||
return logger.warn('Received IPC message with no op or type.'); |
|||
} |
|||
|
|||
if (['resp', 'broadcast'].includes(message.op)) return; |
|||
|
|||
if (this[message.op]) { |
|||
try { |
|||
return this[message.op](message); |
|||
} catch (err) { |
|||
return this.logger.error(err); |
|||
} |
|||
} |
|||
|
|||
const command = this.commands.get(message.op); |
|||
|
|||
if (!command) return; |
|||
|
|||
try { |
|||
return command(this.dyno, this._config, message); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
} |
|||
|
|||
this.emit(message.op, message.d); |
|||
} |
|||
|
|||
/** |
|||
* Send a command and await a response from the shard manager |
|||
* @param {String} op Op to send |
|||
* @param {Object} d The data to send |
|||
* @returns {Promise} |
|||
*/ |
|||
awaitResponse(op, d) { |
|||
if (!process.send) return; |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const awaitListener = (msg) => { |
|||
if (!['resp', 'error'].includes(msg.op)) return; |
|||
|
|||
process.removeListener('message', awaitListener); |
|||
|
|||
if (msg.op === 'resp') return resolve(msg.d); |
|||
if (msg.op === 'error') return reject(msg.d); |
|||
}; |
|||
|
|||
const payload = { op: op }; |
|||
if (d) payload.d = d; |
|||
|
|||
process.on('message', awaitListener); |
|||
try { |
|||
process.send(payload); |
|||
} catch (err) { |
|||
logger.error(`IPC Error Caught:`, err); |
|||
} |
|||
|
|||
setTimeout(() => { |
|||
process.removeListener('message', awaitListener); |
|||
reject('IPC Timed out.'); |
|||
}, 5000); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Register an IPC command |
|||
* @param {Function} command The command to execute |
|||
* @returns {*|void} |
|||
*/ |
|||
register(command) { |
|||
if (!command || !command.name) return logger.error('[IPCManager] Invalid command.'); |
|||
logger.debug(`[IPCManager] Registering ipc command ${command.name}`); |
|||
this.commands.set(command.name, command); |
|||
} |
|||
} |
|||
|
|||
module.exports = IPCManager; |
@ -0,0 +1,348 @@ |
|||
'use strict'; |
|||
|
|||
const dot = require('dot-prop'); |
|||
const each = require('async-each'); |
|||
const glob = require('glob-promise'); |
|||
const path = require('path'); |
|||
const { Collection } = require('@dyno.gg/dyno-core'); |
|||
|
|||
class LangManager extends Collection { |
|||
constructor(localePath) { |
|||
super(); |
|||
|
|||
this.localePath = localePath; |
|||
} |
|||
|
|||
async loadLocales() { |
|||
try { |
|||
var files = await glob('**/*.json', { |
|||
cwd: path.resolve(this.localePath), |
|||
root: path.resolve(this.localePath), |
|||
absolute: true, |
|||
}); |
|||
} catch (err) { |
|||
console.error(err); |
|||
return Promise.reject(err); |
|||
} |
|||
|
|||
each(files, file => { |
|||
const locale = path.dirname(file).split(path.sep).pop(); |
|||
this.set(locale, new I18n(locale, file)); |
|||
}); |
|||
} |
|||
|
|||
t(locale, ...args) { |
|||
locale = locale || 'en'; |
|||
const fallbackLang = this.get('en'); |
|||
if (!this.has(locale)) { |
|||
locale = 'en'; |
|||
} |
|||
return this.get(locale).__(fallbackLang, ...args); |
|||
} |
|||
} |
|||
|
|||
class I18n { |
|||
constructor(lang, filePath) { |
|||
this._lang = lang; |
|||
this._filePath = filePath; |
|||
this._locale = require(filePath); |
|||
} |
|||
reload() { |
|||
delete require.cache[this._filePath]; |
|||
this._locale = require(this._filePath); |
|||
} |
|||
// Get the rule for pluralization
|
|||
// http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
|
|||
get_rule(count, language) { |
|||
switch (language){ |
|||
// nplurals=2; plural=(n > 1);
|
|||
case 'ach': |
|||
case 'ak': |
|||
case 'am': |
|||
case 'arn': |
|||
case 'br': |
|||
case 'fil': |
|||
case 'fr': |
|||
case 'gun': |
|||
case 'ln': |
|||
case 'mfe': |
|||
case 'mg': |
|||
case 'mi': |
|||
case 'oc': |
|||
case 'pt_BR': |
|||
case 'tg': |
|||
case 'ti': |
|||
case 'tr': |
|||
case 'uz': |
|||
case 'wa': |
|||
return (count > 1) ? 1 : 0; |
|||
break; |
|||
|
|||
// nplurals=2; plural=(n != 1);
|
|||
case 'af': |
|||
case 'an': |
|||
case 'anp': |
|||
case 'as': |
|||
case 'ast': |
|||
case 'az': |
|||
case 'bg': |
|||
case 'bn': |
|||
case 'brx': |
|||
case 'ca': |
|||
case 'da': |
|||
case 'doi': |
|||
case 'de': |
|||
case 'el': |
|||
case 'en': |
|||
case 'eo': |
|||
case 'es': |
|||
case 'es_AR': |
|||
case 'et': |
|||
case 'eu': |
|||
case 'ff': |
|||
case 'fi': |
|||
case 'fo': |
|||
case 'fur': |
|||
case 'fy': |
|||
case 'gl': |
|||
case 'gu': |
|||
case 'ha': |
|||
case 'he': |
|||
case 'hi': |
|||
case 'hne': |
|||
case 'hu': |
|||
case 'hy': |
|||
case 'ia': |
|||
case 'it': |
|||
case 'kl': |
|||
case 'kn': |
|||
case 'ku': |
|||
case 'lb': |
|||
case 'mai': |
|||
case 'ml': |
|||
case 'mn': |
|||
case 'mni': |
|||
case 'mr': |
|||
case 'nah': |
|||
case 'nap': |
|||
case 'nb': |
|||
case 'ne': |
|||
case 'nl': |
|||
case 'nn': |
|||
case 'no': |
|||
case 'nso': |
|||
case 'or': |
|||
case 'pa': |
|||
case 'pap': |
|||
case 'pms': |
|||
case 'ps': |
|||
case 'pt': |
|||
case 'rm': |
|||
case 'rw': |
|||
case 'sat': |
|||
case 'sco': |
|||
case 'sd': |
|||
case 'se': |
|||
case 'si': |
|||
case 'so': |
|||
case 'son': |
|||
case 'sq': |
|||
case 'sv': |
|||
case 'sw': |
|||
case 'ta': |
|||
case 'te': |
|||
case 'tk': |
|||
case 'ur': |
|||
case 'yo': |
|||
return (count != 1) ? 1 : 0; |
|||
|
|||
// nplurals=1; plural=0;
|
|||
case 'ay': |
|||
case 'bo': |
|||
case 'cgg': |
|||
case 'dz': |
|||
case 'fa': |
|||
case 'id': |
|||
case 'ja': |
|||
case 'jbo': |
|||
case 'ka': |
|||
case 'kk': |
|||
case 'km': |
|||
case 'ko': |
|||
case 'ky': |
|||
case 'lo': |
|||
case 'ms': |
|||
case 'my': |
|||
case 'sah': |
|||
case 'su': |
|||
case 'th': |
|||
case 'tt': |
|||
case 'ug': |
|||
case 'vi': |
|||
case 'wo': |
|||
case 'zh': |
|||
case 'jv': |
|||
return 0; |
|||
|
|||
// nplurals=2; plural=(n%10!=1 || n%100==11);
|
|||
case 'is': |
|||
return (count % 10!=1 || count % 100==11) ? 1 : 0; |
|||
|
|||
// nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3;
|
|||
case 'kw': |
|||
return (count==1) ? 0 : (count==2) ? 1 : (count == 3) ? 2 : 3; |
|||
|
|||
// nplurals=3; plural=(n % 10==1 && n % 100!=11 ? 0 : n % 10>=2 && n % 10 <= 4 && (n % 100 < 10 || n % 100>=20) ? 1 : 2);
|
|||
case 'uk': |
|||
case 'sr': |
|||
case 'ru': |
|||
case 'hr': |
|||
case 'bs': |
|||
case 'be': |
|||
return count % 10 === 1 && count % 100 !== 11 ? 0 : count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20) ? 1 : 2; |
|||
|
|||
// nplurals=3; plural=(n === 0 ? 0 : n === 1 ? 1 : 2);
|
|||
case 'mnk': |
|||
return count === 0 ? 0 : count === 1 ? 1 : 2; |
|||
|
|||
// nplurals=3; plural=(n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2;
|
|||
case 'sk': |
|||
return (count === 1) ? 0 : (count >= 2 && count <= 4) ? 1 : 2; |
|||
|
|||
// nplurals=3; plural=(n === 1 ? 0 : (n === 0 || (n % 100 > 0 && n % 100 < 20)) ? 1 : 2);
|
|||
case 'ro': |
|||
return count === 1 ? 0 : (count === 0 || (count % 100 > 0 && count % 100 < 20)) ? 1 : 2; |
|||
|
|||
// nplurals=6; plural=(n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5);
|
|||
case 'ar': |
|||
return count === 0 ? 0 : count === 1 ? 1 : count === 2 ? 2 : count % 100 >= 3 && count % 100 <= 10 ? 3 : count % 100 >= 11 ? 4 : 5; |
|||
|
|||
// nplurals=3; plural=(n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2;
|
|||
case 'cs': |
|||
return count === 1 ? 0 : (count >= 2 && count <= 4) ? 1 : 2; |
|||
|
|||
// countplurals=3; plural=(n === 1) ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;
|
|||
case 'csb': |
|||
return (count === 1) ? 0 : count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20) ? 1 : 2; |
|||
|
|||
// nplurals=4; plural=(n === 1) ? 0 : (n === 2) ? 1 : (n !== 8 && n !== 11) ? 2 : 3;
|
|||
case 'cy': |
|||
return (count === 1) ? 0 : (count === 2) ? 1 : (count !== 8 && count !== 11) ? 2 : 3; |
|||
|
|||
// nplurals=5; plural=n === 1 ? 0 : n === 2 ? 1 : (n>2 && n<7) ? 2 :(n>6 && n<11) ? 3 : 4;
|
|||
case 'ga': |
|||
return count === 1 ? 0 : count === 2 ? 1 : (count>2 && count<7) ? 2 :(count>6 && count<11) ? 3 : 4; |
|||
|
|||
// nplurals=4; plural=(n === 1 || n === 11) ? 0 : (n === 2 || n === 12) ? 1 : (n > 2 && n < 20) ? 2 : 3;
|
|||
case 'gd': |
|||
return (count === 1 || count === 11) ? 0 : (count === 2 || count === 12) ? 1 : (count > 2 && count < 20) ? 2 : 3; |
|||
|
|||
// nplurals=3; plural=(n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
|
|||
case 'it': |
|||
return count % 10 === 1 && count % 100 !== 11 ? 0 : count % 10 >= 2 && (count % 100 < 10 || count % 100 >= 20) ? 1 : 2; |
|||
|
|||
// nplurals=3; plural=(n % 10 === 1 && n % 100 !== 11 ? 0 : n !== 0 ? 1 : 2);
|
|||
case 'lv': |
|||
return count % 10 === 1 && count % 100 !== 11 ? 0 : count !== 0 ? 1 : 2; |
|||
|
|||
// nplurals=2; plural= n === 1 || n % 10 === 1 ? 0 : 1;
|
|||
case 'mk': { |
|||
return count === 1 || count % 10 === 1 ? 0 : 1; |
|||
} |
|||
|
|||
// nplurals=4; plural=(n === 1 ? 0 : n === 0 || ( n % 100 > 1 && n % 100 < 11) ? 1 : (n % 100 > 10 && n % 100 < 20 ) ? 2 : 3);
|
|||
case 'mt': |
|||
return count === 1 ? 0 : count === 0 || (count % 100 > 1 && n % 100 < 11) ? 1 : (count % 100 > 10 && count % 100 < 20 ) ? 2 : 3; |
|||
|
|||
// nplurals=3; plural=(n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
|
|||
case 'pl': |
|||
return count === 1 ? 0 : count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20) ? 1 : 2; |
|||
|
|||
// nplurals=4; plural=(n % 100 === 1 ? 1 : n % 100 === 2 ? 2 : n % 100 === 3 || n % 100 === 4 ? 3 : 0);
|
|||
case 'sl': |
|||
return count % 100 === 1 ? 1 : count % 100 === 2 ? 2 : count % 100 === 3 || count % 100 === 4 ? 3 : 0; |
|||
|
|||
default: |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
__(fallbackLang, string, values) { |
|||
//return translation of the original sting if did not find the translation
|
|||
// let translation = string;
|
|||
let translation = dot.get(this._locale, string, null); |
|||
|
|||
if (fallbackLang && !translation) { |
|||
translation = fallbackLang.__(null, string, values); |
|||
} |
|||
|
|||
// get the corresponding translation from the file
|
|||
// if (typeof this._locale[string] != 'undefined' && typeof this._locale[string][this._lang] != 'undefined') {
|
|||
// translation = this._locale[string][this._lang];
|
|||
// } else if (fallbackLang) {
|
|||
// translation = fallbackLang.__(null, string, values);
|
|||
// }
|
|||
|
|||
// If the string have place to render values withen
|
|||
if ((/{{.+?}}/g).test(translation)) { |
|||
// get all the parts needed to be replaced
|
|||
var matches = translation.match(/{{.+?}}/g); |
|||
// loop on each match
|
|||
for (const index in matches) { |
|||
// get the match {{example}}
|
|||
const match = matches[index]; |
|||
// get the word in the match example
|
|||
let match_word = (match.replace('}}', '')).replace('{{', ''); |
|||
let match_search; |
|||
|
|||
// translate the word if was passed in the values var
|
|||
if (values && values[match_word] != undefined) { |
|||
translation = translation.replace(match, values[match_word]); |
|||
continue; // move to the next word in the loop
|
|||
} else { |
|||
// match_search = dot.get(this._locale, match_word);
|
|||
// if (match_search != undefined) {
|
|||
if (typeof this._locale[match_word] != 'undefined') { |
|||
// If the translation is there in the file then translate it directly
|
|||
translation = translation.replace(match, match_search); |
|||
continue;//move to the next word in the loop
|
|||
} |
|||
} |
|||
|
|||
// if the matched word have a count
|
|||
if ((/\|\|.+/g).test(match_word)) { |
|||
const temp_array = match_word.split('||'); |
|||
// update the matched word
|
|||
match_word = temp_array[0]; |
|||
// get the variable of the count for the word
|
|||
const item_count_variable = temp_array[1]; |
|||
|
|||
// get the value form values passed to this function
|
|||
// TODO through error if not found in values
|
|||
const item_count = values[item_count_variable]; |
|||
|
|||
// will get the rule or for pluralization based on the lang
|
|||
const rule = this.get_rule(item_count, this._lang); |
|||
// match_search = dot.get(this._locale, match_word);
|
|||
|
|||
if (typeof this._locale[match_word] == 'object') { |
|||
// if (typeof match_search == 'object') {
|
|||
translation = translation.replace(match, match_search[rule]); |
|||
} else { |
|||
translation = translation.replace(match, match_search); |
|||
} |
|||
} else { |
|||
if (typeof values == 'object') { |
|||
translation = translation.replace(match, values[match_word]); |
|||
} else { |
|||
translation = translation.replace(match, match_search); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return translation; |
|||
} |
|||
} |
|||
|
|||
module.exports = LangManager; |
@ -0,0 +1,37 @@ |
|||
'use strict'; |
|||
|
|||
const { Collection, Pager } = require('@dyno.gg/dyno-core'); |
|||
|
|||
/** |
|||
* @class PagerManager |
|||
* @extends Collection |
|||
*/ |
|||
class PagerManager extends Collection { |
|||
/** |
|||
* PagerManager constructor |
|||
* @param {Dyno} dyno Dyno core instance |
|||
*/ |
|||
constructor(dyno) { |
|||
super(); |
|||
this.dyno = dyno; |
|||
} |
|||
|
|||
/** |
|||
* Create a pager |
|||
* @param {Object} options Pager options |
|||
* @param {String|GuildChannel} options.channel The channel this pager will be created in |
|||
* @param {User|Member} options.user The user this pager is created for |
|||
* @param {Object} options.embed The embed object to be sent without fields |
|||
* @param {Object[]} options.fields All embed fields that will be paged |
|||
* @param {Number} [options.pageLimit=10] The number of items per page, max 25, default 10 |
|||
*/ |
|||
create(options) { |
|||
if (!options || !options.channel || !options.user) return; |
|||
let id = `${options.channel.id}.${options.user.id}`; |
|||
let pager = new Pager(this, id, options); |
|||
this.set(id, pager); |
|||
return pager; |
|||
} |
|||
} |
|||
|
|||
module.exports = PagerManager; |
@ -0,0 +1,105 @@ |
|||
'use strict'; |
|||
|
|||
const { utils } = require('@dyno.gg/dyno-core'); |
|||
|
|||
class PermissionsManager { |
|||
constructor(dyno) { |
|||
this._config = dyno.config; |
|||
this.dyno = dyno; |
|||
} |
|||
|
|||
/** |
|||
* Check if user is bot admin |
|||
* @param {User|Member} user User object |
|||
* @returns {Boolean} |
|||
*/ |
|||
isAdmin(user) { |
|||
if (!user || !user.id) return false; |
|||
if (this.dyno.globalConfig.developers && this.dyno.globalConfig.developers.includes(user.id)) { |
|||
return true; |
|||
} |
|||
return (user.id === this._config.client.admin); |
|||
} |
|||
|
|||
isOverseer(user) { |
|||
if (!user || !user.id) return false; |
|||
return (this.dyno.globalConfig.overseers && this.dyno.globalConfig.overseers.includes(user.id)); |
|||
} |
|||
|
|||
/** |
|||
* Check if user is server admin |
|||
* @param {Member} member Member object |
|||
* @param {GuildChannel} channel Channel object |
|||
* @returns {Boolean} |
|||
*/ |
|||
isServerAdmin(member, channel) { |
|||
// ignore DM
|
|||
if (!member || channel.type !== 0) return false; |
|||
// let permissions = member.permissionsFor(channel);
|
|||
return (member.id === channel.guild.ownerID || (member.permission && |
|||
(member.permission.has('administrator') || member.permission.has('manageGuild')))); |
|||
} |
|||
|
|||
/** |
|||
* Check if user is server mod |
|||
* @param {Member} member Guild member object |
|||
* @param {GuildChannel} channel Channel object |
|||
* @returns {Boolean} |
|||
*/ |
|||
isServerMod(member, channel) { |
|||
// ignore DM
|
|||
if (!member || channel.type !== 0) return false; |
|||
|
|||
const guildConfig = this.dyno.guilds.get(channel.guild.id); |
|||
|
|||
if (this.isAdmin(member) || this.isServerAdmin(member, channel)) return true; |
|||
|
|||
// server config may not have loaded yet
|
|||
if (!guildConfig) return false; |
|||
|
|||
// check mod roles
|
|||
if (guildConfig.modRoles && member.roles && member.roles.find(r => guildConfig.modRoles.includes(r))) { |
|||
return true; |
|||
} |
|||
|
|||
// sanity check
|
|||
if (!guildConfig.mods) return false; |
|||
|
|||
return guildConfig.mods.includes(member.id); |
|||
} |
|||
|
|||
canOverride(channel, member, command) { |
|||
if (!member || !channel) return null; |
|||
|
|||
const guildConfig = this.dyno.guilds.get(channel.guild.id); |
|||
|
|||
if (!guildConfig.permissions || !guildConfig.permissions.length) return null; |
|||
|
|||
const channelPerms = guildConfig.channelPermissions; |
|||
const rolePerms = guildConfig.rolePermissions; |
|||
|
|||
let canOverride = null; |
|||
|
|||
if (channelPerms && channelPerms[channel.id] && channelPerms[channel.id].commands.hasOwnProperty(command)) { |
|||
canOverride = channelPerms[channel.id].commands[command]; |
|||
} |
|||
|
|||
if (!rolePerms) return canOverride; |
|||
|
|||
const roles = utils.sortRoles(channel.guild.roles); |
|||
|
|||
for (let role of roles) { |
|||
if (!rolePerms[role.id]) continue; |
|||
if (member.roles.indexOf(role.id) === -1) continue; |
|||
|
|||
if (rolePerms[role.id].commands.hasOwnProperty(command)) { |
|||
canOverride = rolePerms[role.id].commands[command]; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return canOverride; |
|||
} |
|||
} |
|||
|
|||
module.exports = PermissionsManager; |
@ -0,0 +1,147 @@ |
|||
'use strict'; |
|||
|
|||
const { utils } = require('@dyno.gg/dyno-core'); |
|||
const Hemera = require('nats-hemera'); |
|||
const HemeraJoi = require('hemera-joi'); |
|||
const Nats = require('nats'); |
|||
const EventEmitter = require('eventemitter3'); |
|||
const each = require('async-each'); |
|||
const logger = require('../logger'); |
|||
|
|||
/** |
|||
* @class RPCManager |
|||
* @extends EventEmitter |
|||
*/ |
|||
class RPCManager extends EventEmitter { |
|||
/** |
|||
* Manages RPC communications |
|||
* @param {Dyno} dyno The Dyno instance |
|||
*/ |
|||
constructor(dyno) { |
|||
super(); |
|||
|
|||
this.dyno = dyno; |
|||
this.config = dyno.config; |
|||
this.client = dyno.client; |
|||
this.clusterConfig = dyno.clientOptions; |
|||
|
|||
this.id = dyno.options.clusterId || dyno.options.shardId || 0; |
|||
this.pid = process.pid; |
|||
|
|||
this.commands = new Map(); |
|||
|
|||
this.nats = Nats.connect({ url: 'nats://ares.dyno.gg:4222' }); |
|||
this.hemera = new Hemera(this.nats, { logLevel: 'info' }); |
|||
this.hemera.use(HemeraJoi); |
|||
this.hemera.ready(this.onReady.bind(this)); |
|||
} |
|||
|
|||
onReady() { |
|||
logger.info(`[RPCManager] ready.`); |
|||
utils.readdirRecursive(this.config.paths.ipc).then(files => { |
|||
each(files, (file, next) => { |
|||
if (file.endsWith('.map')) return next(); |
|||
this.register(require(file)); |
|||
return next(); |
|||
}, err => { |
|||
if (err) logger.error(err); |
|||
logger.info(`[RPCManager] Registered ${this.commands.size} RPC commands.`); |
|||
}); |
|||
}).catch(err => logger.error(err)); |
|||
} |
|||
|
|||
/** |
|||
* Send a command or event to the shard manager |
|||
* @param {String} event Event to send |
|||
* @param {Mixed} data The data to send |
|||
*/ |
|||
send(event, data) { |
|||
if (!process.send) return; |
|||
process.send({ |
|||
op: event, |
|||
d: data || null, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Fired when the shard receives a message |
|||
* @param {Object} message The message object |
|||
* @returns {*} |
|||
*/ |
|||
onMessage(message) { |
|||
if (!message.op) { |
|||
return logger.warn('Received RPC message with no op.'); |
|||
} |
|||
|
|||
if (['resp', 'broadcast'].includes(message.op)) return; |
|||
|
|||
if (this[message.op]) { |
|||
try { |
|||
return this[message.op](message); |
|||
} catch (err) { |
|||
return this.logger.error(err); |
|||
} |
|||
} |
|||
|
|||
const command = this.commands.get(message.op); |
|||
|
|||
if (!command) return; |
|||
|
|||
try { |
|||
return command(this.dyno, this.config, message); |
|||
} catch (err) { |
|||
this.logger.error(err); |
|||
} |
|||
|
|||
this.emit(message.op, message.d); |
|||
} |
|||
|
|||
/** |
|||
* Send a command and await a response from the shard manager |
|||
* @param {String} op Op to send |
|||
* @param {Object} d The data to send |
|||
* @returns {Promise} |
|||
*/ |
|||
awaitResponse(op, d) { |
|||
if (!process.send) return; |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const awaitListener = (msg) => { |
|||
if (!['resp', 'error'].includes(msg.op)) return; |
|||
|
|||
process.removeListener('message', awaitListener); |
|||
|
|||
if (msg.op === 'resp') return resolve(msg.d); |
|||
if (msg.op === 'error') return reject(msg.d); |
|||
}; |
|||
|
|||
const payload = { op: op }; |
|||
if (d) payload.d = d; |
|||
|
|||
process.on('message', awaitListener); |
|||
process.send(payload); |
|||
|
|||
setTimeout(() => { |
|||
process.removeListener('message', awaitListener); |
|||
reject('RPC Timed out.'); |
|||
}, 5000); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Register an RPC command |
|||
* @param {Function} command The command to execute |
|||
* @returns {*|void} |
|||
*/ |
|||
register(command) { |
|||
if (!command || !command.name) { return; } |
|||
|
|||
logger.debug(`[RPCManager] Registering rpc command ${command.name}`); |
|||
const cmd = command(this); |
|||
cmd.pattern.topic = `dyno.bot`; |
|||
// cmd.pattern.cmd = `${cmd.pattern.cmd}.${this.clusterConfig.clusterId}`;
|
|||
this.hemera.add(cmd.pattern, cmd.handler.bind(this)); |
|||
} |
|||
} |
|||
|
|||
module.exports = RPCManager; |
@ -0,0 +1,93 @@ |
|||
'use strict'; |
|||
|
|||
const axios = require('axios'); |
|||
|
|||
/** |
|||
* @class WebhookManager |
|||
*/ |
|||
class WebhookManager { |
|||
/** |
|||
* Manage webhook operations |
|||
* @param {Object} config The Dyno configuration |
|||
* @param {Dyno} dyno The Dyno instance |
|||
*/ |
|||
constructor(dyno) { |
|||
this.dyno = dyno; |
|||
this.config = dyno.config; |
|||
this.client = dyno.client; |
|||
|
|||
this.avatarUrl = `${this.config.avatar}?r=${this.config.version}`; |
|||
|
|||
this.default = { |
|||
username: 'Dyno', |
|||
avatarURL: this.avatarUrl, |
|||
tts: false, |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Get or create a channel webhook |
|||
* @param {Channel} channel Eris channel object |
|||
* @returns {Promise} |
|||
*/ |
|||
async getOrCreate(channel) { |
|||
let id = (typeof channel === 'string') ? channel : channel.id || null; |
|||
if (!id) return Promise.reject(`Invalid channel or id.`); |
|||
|
|||
try { |
|||
const webhooks = await this.client.getChannelWebhooks(channel.id); |
|||
|
|||
let webhook = webhooks.find(hook => hook.name === 'Dyno'); |
|||
if (webhook) { |
|||
return Promise.resolve(webhook); |
|||
} |
|||
|
|||
const res = await axios.get(this.avatarUrl, { |
|||
headers: { Accept: 'image/*' }, |
|||
responseType: 'arraybuffer', |
|||
}).then(response => `data:${response.headers['content-type']};base64,${response.data.toString('base64')}`); |
|||
|
|||
webhook = await this.client.createChannelWebhook(channel.id, { |
|||
name: 'Dyno', |
|||
avatar: res, |
|||
}); |
|||
|
|||
return Promise.resolve(webhook); |
|||
} catch (err) { |
|||
return Promise.reject(err); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Execute a webhook |
|||
* @param {Channel} channel Eris channel object |
|||
* @param {Object} options Webhook options to send |
|||
* @returns {Promise} |
|||
*/ |
|||
async execute(channel, options, webhook) { |
|||
let avatarUrl = `https://cdn.discordapp.com/avatars/${this.dyno.user.id}/${this.dyno.user.avatar}.jpg`; |
|||
options.avatarURL = options.avatarURL || avatarUrl; |
|||
const content = Object.assign({}, this.default, options || {}); |
|||
|
|||
if (webhook) { |
|||
if (options.slack) { |
|||
delete options.slack; |
|||
return this.client.executeSlackWebhook(webhook.id, webhook.token, content); |
|||
} |
|||
return this.client.executeWebhook(webhook.id, webhook.token, content); |
|||
} |
|||
|
|||
try { |
|||
const webhook = await this.getOrCreate(channel); |
|||
if (options.slack) { |
|||
delete options.slack; |
|||
return this.client.executeSlackWebhook(webhook.id, webhook.token, content); |
|||
} |
|||
return this.client.executeWebhook(webhook.id, webhook.token, content); |
|||
} catch (err) { |
|||
return Promise.reject(err); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = WebhookManager; |
@ -0,0 +1,497 @@ |
|||
var MatomoTracker = require('matomo-tracker'); |
|||
var logger = require('./logger'); |
|||
|
|||
let matomoUsers, matomoGuilds, matomoMusicGuilds, matomoMusicUsers, actionLogBuffer, autoresponderBuffer, automodBuffer, commandBuffer, musicBuffer; |
|||
|
|||
const regionToCountryCodeMap = { |
|||
'brazil': 'br', |
|||
'vip-us-west': 'us', |
|||
'vip-us-east': 'us', |
|||
'us-west': 'us', |
|||
'us-central': 'us', |
|||
'us-east': 'us', |
|||
'us-south': 'us', |
|||
'japan': 'jp', |
|||
'singapore': 'sg', |
|||
'hongkong': 'hk', |
|||
'vip-amsterdam': 'nl', |
|||
'amsterdam': 'nl', |
|||
'southafrica': 'za', |
|||
'london': 'gb', |
|||
'sydney': 'au', |
|||
'frankfurt': 'DE', |
|||
'russia': 'ru', |
|||
'eu-central': 'pl', |
|||
'eu-west': 'pt', |
|||
}; |
|||
|
|||
function initMatomo(dyno) { |
|||
matomoUsers = new MatomoTracker(4, 'http://10.12.0.69/piwik.php'); |
|||
matomoGuilds = new MatomoTracker(5, 'http://10.12.0.69/piwik.php'); |
|||
matomoMusicGuilds = new MatomoTracker(8, 'http://10.12.0.69/piwik.php'); |
|||
matomoMusicUsers = new MatomoTracker(9, 'http://10.12.0.69/piwik.php'); |
|||
|
|||
actionLogBuffer = { guildBuffer: [] }; |
|||
autoresponderBuffer = { guildBuffer: [] }; |
|||
automodBuffer = { guildBuffer: [] }; |
|||
musicBuffer = { guildBuffer: [], userBuffer: [] }; |
|||
commandBuffer = { guildBuffer: [], userBuffer: [] }; |
|||
|
|||
setInterval(() => { |
|||
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; } |
|||
if (!dyno.isReady) { return; } |
|||
|
|||
try { |
|||
const guildArr = []; |
|||
guildArr.push(...actionLogBuffer.guildBuffer); |
|||
actionLogBuffer.guildBuffer = []; |
|||
|
|||
guildArr.push(...autoresponderBuffer.guildBuffer); |
|||
autoresponderBuffer.guildBuffer = []; |
|||
|
|||
guildArr.push(...automodBuffer.guildBuffer); |
|||
automodBuffer.guildBuffer = []; |
|||
|
|||
guildArr.push(...commandBuffer.guildBuffer); |
|||
commandBuffer.guildBuffer = []; |
|||
|
|||
const userArr = []; |
|||
|
|||
userArr.push(...commandBuffer.userBuffer); |
|||
commandBuffer.userBuffer = []; |
|||
|
|||
let start = new Date().getTime(); |
|||
|
|||
if (guildArr.length > 0) { |
|||
matomoGuilds.trackBulk(guildArr, () => { |
|||
const end = new Date().getTime(); |
|||
logger.debug(`Flushed Matomo guild buffer. Took ${Math.abs(start - end)}ms for ${guildArr.length} events`); |
|||
}); |
|||
} |
|||
|
|||
start = new Date().getTime(); |
|||
|
|||
if (userArr.length > 0) { |
|||
matomoUsers.trackBulk(userArr, () => { |
|||
const end = new Date().getTime(); |
|||
logger.debug(`Flushed Matomo user buffer. Took ${Math.abs(start - end)}ms for ${userArr.length} events`); |
|||
}); |
|||
} |
|||
|
|||
const musicGuildArr = []; |
|||
musicGuildArr.push(...musicBuffer.guildBuffer); |
|||
musicBuffer.guildBuffer = []; |
|||
|
|||
|
|||
const musicUserArr = []; |
|||
musicUserArr.push(...musicBuffer.userBuffer); |
|||
musicBuffer.userBuffer = []; |
|||
|
|||
start = new Date().getTime(); |
|||
|
|||
if (musicGuildArr.length > 0) { |
|||
matomoMusicGuilds.trackBulk(musicGuildArr, () => { |
|||
const end = new Date().getTime(); |
|||
logger.debug(`Flushed Matomo music guild buffer. Took ${Math.abs(start - end)}ms for ${musicGuildArr.length} events`); |
|||
}); |
|||
} |
|||
|
|||
start = new Date().getTime(); |
|||
|
|||
if (musicUserArr.length > 0) { |
|||
matomoMusicUsers.trackBulk(musicUserArr, () => { |
|||
const end = new Date().getTime(); |
|||
logger.debug(`Flushed Matomo music user buffer. Took ${Math.abs(start - end)}ms for ${musicUserArr.length} events`); |
|||
}); |
|||
} |
|||
} catch (err) { |
|||
logger.error(err); |
|||
} |
|||
}, 10000); |
|||
|
|||
setInterval(() => { |
|||
if (!dyno.isReady) { return; } |
|||
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; } |
|||
|
|||
dyno.players.forEach((p) => { |
|||
if (!p.voiceChannel || !p.voiceChannel.voiceMembers || !p.playing) { return; } |
|||
const guild = p.voiceChannel.guild; |
|||
|
|||
const country = regionToCountryCodeMap[guild.region] || 'aq'; |
|||
musicBuffer.guildBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/session`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'Refresh', |
|||
uid: guild.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
}), |
|||
}); |
|||
|
|||
p.voiceChannel.voiceMembers.forEach((m) => { |
|||
musicBuffer.userBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/user/session`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'Refresh', |
|||
uid: m.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['User ID', m.id], |
|||
}), |
|||
}); |
|||
}); |
|||
}); |
|||
}, 14 * 1000 * 60); |
|||
|
|||
matomoUsers.on('error', (err) => { |
|||
logger.error(err); |
|||
}); |
|||
|
|||
matomoGuilds.on('error', (err) => { |
|||
logger.error(err); |
|||
}); |
|||
|
|||
matomoMusicGuilds.on('error', (err) => { |
|||
logger.error(err); |
|||
}); |
|||
|
|||
matomoMusicUsers.on('error', (err) => { |
|||
logger.error(err); |
|||
}); |
|||
|
|||
dyno.internalEvents.on('music', ({ type, guild, channel, user, search, trackInfo }) => { |
|||
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; } |
|||
let guildEvent; |
|||
let userEvent; |
|||
let player; |
|||
|
|||
const country = regionToCountryCodeMap[guild.region] || 'aq'; |
|||
switch (type) { |
|||
case 'start': |
|||
guildEvent = { |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/session/start`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'Start', |
|||
uid: guild.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Server', dyno.config.stateName], |
|||
}), |
|||
}; |
|||
|
|||
player = dyno.players.get(guild.id); |
|||
if (!player || !player.playing || !player.voiceChannel || !player.voiceChannel.voiceMembers) { |
|||
player.voiceChannel.voiceMembers.forEach((m) => { |
|||
musicBuffer.userBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/user/session/join`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'Start', |
|||
uid: m.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['User ID', m.id], |
|||
3: ['Server', dyno.config.stateName], |
|||
}), |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
break; |
|||
case 'end': |
|||
guildEvent = { |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/session/end`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'End', |
|||
uid: guild.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Server', dyno.config.stateName], |
|||
}), |
|||
}; |
|||
|
|||
player = dyno.players.get(guild.id); |
|||
if (!player || !player.voiceChannel || !player.voiceChannel.voiceMembers) { |
|||
break; |
|||
} |
|||
|
|||
player.voiceChannel.voiceMembers.forEach((m) => { |
|||
musicBuffer.userBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/user/session/leave`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'End', |
|||
uid: m.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['User ID', m.id], |
|||
3: ['Server', dyno.config.stateName], |
|||
}), |
|||
}); |
|||
}); |
|||
break; |
|||
case 'join': |
|||
userEvent = { |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/user/session/join`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'Start', |
|||
uid: user.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['User ID', user.id], |
|||
3: ['Server', dyno.config.stateName], |
|||
}), |
|||
}; |
|||
break; |
|||
case 'leave': |
|||
userEvent = { |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/user/session/leave`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'End', |
|||
uid: user.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['User ID', user.id], |
|||
3: ['Server', dyno.config.stateName], |
|||
}), |
|||
}; |
|||
break; |
|||
case 'changeSong': |
|||
guildEvent = { |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/session/changeSong`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'ChangeSong', |
|||
uid: guild.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Server', dyno.config.stateName], |
|||
}), |
|||
}; |
|||
break; |
|||
case 'playSong': |
|||
guildEvent = { |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/session/playSong`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'PlaySong', |
|||
uid: guild.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Server', dyno.config.stateName], |
|||
}), |
|||
}; |
|||
break; |
|||
case 'search': |
|||
guildEvent = { |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/session/search`, |
|||
ua: 'Node.js', |
|||
search: search, |
|||
search_count: 1, |
|||
uid: guild.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Params', search], |
|||
3: ['Server', dyno.config.stateName], |
|||
}), |
|||
}; |
|||
break; |
|||
case 'skip': |
|||
guildEvent = { |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/music/session/skip`, |
|||
action_name: 'Music', |
|||
ua: 'Node.js', |
|||
e_c: 'Music', |
|||
e_a: 'Session', |
|||
e_n: 'Skip', |
|||
uid: guild.id, |
|||
country, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Server', dyno.config.stateName], |
|||
}), |
|||
}; |
|||
break; |
|||
} |
|||
|
|||
if (guildEvent) { |
|||
musicBuffer.guildBuffer.push(guildEvent); |
|||
} |
|||
|
|||
if (userEvent) { |
|||
musicBuffer.userBuffer.push(userEvent); |
|||
} |
|||
}); |
|||
|
|||
dyno.internalEvents.on('actionlog', ({ type, guild }) => { |
|||
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; } |
|||
if (!matomoUsers || !matomoGuilds) { |
|||
return; |
|||
} |
|||
|
|||
if (type === 'commands') { |
|||
return; |
|||
} |
|||
|
|||
actionLogBuffer.guildBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/actionlog/${type}`, |
|||
action_name: 'ActionLog', |
|||
ua: 'Node.js', |
|||
e_c: 'AutomatedAction', |
|||
e_a: 'ActionLog', |
|||
e_n: type, |
|||
uid: guild.id, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Server', dyno.config.stateName], |
|||
}), |
|||
}); |
|||
}); |
|||
|
|||
dyno.internalEvents.on('autoresponder', ({ type, guild }) => { |
|||
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; } |
|||
if (!matomoUsers || !matomoGuilds) { |
|||
return; |
|||
} |
|||
|
|||
autoresponderBuffer.guildBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/autoresponder/${type}`, |
|||
action_name: 'AutoResponder', |
|||
ua: 'Node.js', |
|||
e_c: 'AutomatedAction', |
|||
e_a: 'AutoResponder', |
|||
e_n: type, |
|||
uid: guild.id, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Server', dyno.config.stateName], |
|||
}), |
|||
}); |
|||
}); |
|||
|
|||
dyno.internalEvents.on('automod', ({ type, guild }) => { |
|||
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; } |
|||
if (!matomoUsers || !matomoGuilds) { |
|||
return; |
|||
} |
|||
|
|||
automodBuffer.guildBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/modules/automod/${type}`, |
|||
action_name: 'AutoMod', |
|||
ua: 'Node.js', |
|||
e_c: 'AutomatedAction', |
|||
e_a: 'AutoMod', |
|||
e_n: type, |
|||
uid: guild.id, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Guild ID', guild.id], |
|||
2: ['Server', dyno.config.stateName], |
|||
}), |
|||
}); |
|||
}); |
|||
|
|||
dyno.commands.on('command', ({ command, message, guildConfig, args, time, isServerAdmin, isServerMod }) => { |
|||
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; } |
|||
if (!matomoUsers || !matomoGuilds) { |
|||
return; |
|||
} |
|||
|
|||
const user = message.author; |
|||
const channel = message.channel; |
|||
const guild = channel.guild; |
|||
commandBuffer.userBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/commands/${command.name}/${user.id}`, |
|||
action_name: 'CommandUsed', |
|||
ua: 'Node.js', |
|||
e_c: 'Command', |
|||
e_a: command.module || command.group, |
|||
e_n: command.name, |
|||
gt_ms: time, |
|||
uid: user.id, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Command Name', command.name], |
|||
2: ['Arguments', args.join(' ')], |
|||
3: ['Guild ID', guild.id], |
|||
4: ['Server', dyno.config.stateName], |
|||
5: ['User ID', user.id], |
|||
}), |
|||
dimension2: (isServerAdmin || isServerMod) ? 'true' : 'false', |
|||
}); |
|||
|
|||
commandBuffer.userBuffer.push({ |
|||
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5', |
|||
url: `/commands/${command.name}/${user.id}`, |
|||
action_name: 'CommandUsed', |
|||
ua: 'Node.js', |
|||
e_c: 'Command', |
|||
e_a: command.module || command.group, |
|||
e_n: command.name, |
|||
gt_ms: time, |
|||
uid: guild.id, |
|||
_cvar: JSON.stringify({ |
|||
1: ['Command Name', command.name], |
|||
2: ['Arguments', args.join(' ')], |
|||
3: ['Guild ID', guild.id], |
|||
4: ['Server', dyno.config.stateName], |
|||
5: ['User ID', user.id], |
|||
}), |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
module.exports = { |
|||
initMatomo, |
|||
}; |
@ -0,0 +1,182 @@ |
|||
const express = require('express'); |
|||
const cluster = require('cluster'); |
|||
const prom = require('prom-client'); |
|||
const config = require('./config'); |
|||
const logger = require('./logger'); |
|||
const server = express(); |
|||
const Registry = prom.Registry; |
|||
const AggregatorRegistry = prom.AggregatorRegistry; |
|||
|
|||
const aggregatorRegistry = new AggregatorRegistry(); |
|||
const cmRegister = new Registry(); |
|||
|
|||
if (cluster.isMaster) { |
|||
server.get('/metrics', (req, res) => { |
|||
aggregatorRegistry.clusterMetrics((err, metrics) => { |
|||
if (err) logger.error(err); |
|||
res.set('Content-Type', aggregatorRegistry.contentType); |
|||
res.send(metrics); |
|||
}); |
|||
}); |
|||
|
|||
server.get('/cm_metrics', (req, res) => { |
|||
res.set('Content-Type', cmRegister.contentType); |
|||
res.end(cmRegister.metrics()); |
|||
}); |
|||
|
|||
cmRegister.setDefaultLabels({ server: config.stateName.toLowerCase() }); |
|||
prom.collectDefaultMetrics({ register: cmRegister, prefix: 'dyno_cm_' }); |
|||
server.listen(3001); |
|||
} else { |
|||
const defaultLabels = { clusterId: process.env.clusterId, server: config.stateName.toLowerCase() }; |
|||
prom.register.setDefaultLabels(defaultLabels); |
|||
prom.collectDefaultMetrics({ prefix: 'dyno_app_' }); |
|||
|
|||
const messagesCounter = new prom.Counter({ |
|||
name: 'dyno_app_messages_sent', |
|||
help: 'Counts messages sent (type = dm|normal|webhook)', |
|||
labelNames: ['type'], |
|||
}); |
|||
const helpSentCounter = new prom.Counter({ |
|||
name: 'dyno_app_help_sent', |
|||
help: 'Counts helps sent', |
|||
}); |
|||
const helpFailedCounter = new prom.Counter({ |
|||
name: 'dyno_app_help_failed', |
|||
help: 'Counts helps failed', |
|||
}); |
|||
const guildsCarbon = new prom.Gauge({ |
|||
name: 'dyno_app_guilds_carbon', |
|||
help: 'Guild count for Dyno', |
|||
}); |
|||
const guildEvents = new prom.Counter({ |
|||
name: 'dyno_app_guild_events', |
|||
help: 'Guild events counter (type = create, delete, etc)', |
|||
labelNames: ['type'], |
|||
}); |
|||
const guildCounts = new prom.Gauge({ |
|||
name: 'dyno_app_guild_count', |
|||
help: 'Guild count based on cluster id', |
|||
}); |
|||
const userCounts = new prom.Gauge({ |
|||
name: 'dyno_app_user_count', |
|||
help: 'User count based on cluster id', |
|||
}); |
|||
const gatewayEvents = new prom.Gauge({ |
|||
name: 'dyno_app_gateway_events', |
|||
help: 'GW Event counter (type = event type)', |
|||
labelNames: ['type'], |
|||
}); |
|||
const messageEvents = new prom.Counter({ |
|||
name: 'dyno_app_message_events', |
|||
help: 'Message events counter (type = create, delete, etc)', |
|||
labelNames: ['type'], |
|||
}); |
|||
const discordShard = new prom.Counter({ |
|||
name: 'dyno_app_discord_shard', |
|||
help: 'Discord shard status (type = connect, disconnect, resume, etc)', |
|||
labelNames: ['type'], |
|||
}); |
|||
const commandSuccess = new prom.Counter({ |
|||
name: 'dyno_app_command_success', |
|||
help: 'Command success counter (group = cmd group, name = cmd name)', |
|||
labelNames: ['group', 'name'], |
|||
}); |
|||
const commandError = new prom.Counter({ |
|||
name: 'dyno_app_command_error', |
|||
help: 'Command error counter (group = cmd group, name = cmd name)', |
|||
labelNames: ['group', 'name'], |
|||
}); |
|||
const commandTimings = new prom.Histogram({ |
|||
name: 'dyno_app_command_time', |
|||
help: 'Command timing histogram (group = cmd group, name = cmd name)', |
|||
labelNames: ['group', 'name'], |
|||
buckets: [100, 200, 300, 500, 800, 1000, 5000], |
|||
}); |
|||
const purgeSuccessCounter = new prom.Counter({ |
|||
name: 'dyno_app_purge_success', |
|||
help: 'Counts successful purges', |
|||
}); |
|||
const purgeFailedCounter = new prom.Counter({ |
|||
name: 'dyno_app_purge_failed', |
|||
help: 'Counts failed purges', |
|||
}); |
|||
const eventLoopBlockCounter = new prom.Counter({ |
|||
name: 'dyno_app_node_blocked', |
|||
help: 'Counts node event loop blocks', |
|||
}); |
|||
const musicPlaylists = new prom.Counter({ |
|||
name: 'dyno_app_music_playlists', |
|||
help: 'Counts music playlists', |
|||
}); |
|||
const musicAdds = new prom.Counter({ |
|||
name: 'dyno_app_music_adds', |
|||
help: 'Counts music adds', |
|||
}); |
|||
const voiceTotals = new prom.Gauge({ |
|||
name: 'dyno_app_voice_total', |
|||
help: 'Voice totals gauge', |
|||
labelNames: ['state'], |
|||
}); |
|||
const voicePlaying = new prom.Gauge({ |
|||
name: 'dyno_app_voice_playing', |
|||
help: 'Voice playing gauge', |
|||
labelNames: ['state'], |
|||
}); |
|||
|
|||
// Music module metrics
|
|||
const musicModuleMetrics = [ |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_total_user_listen_time', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_total_playing_time', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_song_ends', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_partial_song_ends', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_unique_session_joins', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_disconnects', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_joins', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_leaves', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_plays', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_search', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Counter({ |
|||
name: 'dyno_app_music_skips', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Summary({ |
|||
name: 'dyno_app_music_user_session_summary', |
|||
help: 'Music module metrics', |
|||
}), |
|||
new prom.Summary({ |
|||
name: 'dyno_app_music_session_summary', |
|||
help: 'Music module metrics', |
|||
}), |
|||
]; |
|||
} |
@ -0,0 +1,146 @@ |
|||
const cluster = require('cluster'); |
|||
const config = require('../config'); |
|||
const logger = require('../logger'); |
|||
const { Collection } = require('@dyno.gg/dyno-core'); |
|||
const { Client, Server, LogServer } = require('../rpc'); |
|||
const Process = require('./Process'); |
|||
|
|||
/** |
|||
* @class Manager |
|||
*/ |
|||
class Manager { |
|||
constructor() { |
|||
this.processes = new Collection(); |
|||
|
|||
process.on('uncaughtException', this.handleException.bind(this)); |
|||
process.on('unhandledRejection', this.handleRejection.bind(this)); |
|||
|
|||
cluster.on('exit', this.handleExit.bind(this)); |
|||
|
|||
cluster.setupMaster({ |
|||
silent: true, |
|||
}); |
|||
|
|||
this.logServer = new LogServer(); |
|||
this.logServer.init(5025); |
|||
|
|||
this.clusterManager = this.createManager(); |
|||
|
|||
let methods = { |
|||
create: this.create.bind(this), |
|||
delete: this.delete.bind(this), |
|||
list: this.list.bind(this), |
|||
restart: this.restart.bind(this), |
|||
restartManager: this.restartManager.bind(this), |
|||
}; |
|||
|
|||
this.client = new Client(config.rpcHost || 'localhost', 5052); |
|||
|
|||
this.server = new Server(); |
|||
this.server.init(config.rpcHost || 'localhost', 5050, methods); |
|||
} |
|||
|
|||
handleRejection(reason, p) { |
|||
try { |
|||
console.error('Unhandled rejection at: Promise ', p, 'reason: ', reason); // eslint-disable-line
|
|||
} catch (err) { |
|||
console.error(reason); // eslint-disable-line
|
|||
} |
|||
} |
|||
|
|||
handleException(err) { |
|||
if (!err || (typeof err === 'string' && !err.length)) { |
|||
return console.error('An undefined exception occurred.'); // eslint-disable-line
|
|||
} |
|||
|
|||
console.error(err); // eslint-disable-line
|
|||
} |
|||
|
|||
createManager() { |
|||
const proc = new Process(this, { |
|||
manager: true, |
|||
}); |
|||
this.logServer.hook(proc); |
|||
|
|||
return proc; |
|||
} |
|||
|
|||
createProcess(options) { |
|||
const process = new Process(this, options); |
|||
this.processes.set(process.id, process); |
|||
this.logServer.hook(process); |
|||
|
|||
return process; |
|||
} |
|||
|
|||
getProcess(worker) { |
|||
return this.processes.find(s => s.pid === worker.process.pid || s._pid === worker.process.pid); |
|||
} |
|||
|
|||
handleExit(worker, code, signal) { |
|||
const process = this.getProcess(worker); |
|||
|
|||
if (signal && signal === 'SIGTERM') return; |
|||
if (!process) return; |
|||
|
|||
this.client.request('processExit', { process, code, signal }); |
|||
|
|||
// Restart worker
|
|||
process.restartWorker().then(() => { |
|||
this.client.request('processReady', { process }); |
|||
}); |
|||
} |
|||
|
|||
create(payload, cb) { |
|||
const options = payload && (payload.cluster || {}); |
|||
const process = this.createProcess(options); |
|||
return cb(null, process); |
|||
} |
|||
|
|||
delete(payload, cb) { |
|||
if (!payload.id) { |
|||
return cb('Missing ID'); |
|||
} |
|||
const process = this.processes.get(payload.id); |
|||
if (!process) { |
|||
return cb(`Process ${payload.id} not found.`); |
|||
} |
|||
try { |
|||
process.worker.kill('SIGTERM'); |
|||
this.processes.delete(payload.id); |
|||
return cb(null, 'OK'); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
return cb(err); |
|||
} |
|||
} |
|||
|
|||
list(payload, cb) { |
|||
return cb(null, [...this.processes.values()]); |
|||
} |
|||
|
|||
async restart(payload, cb) { |
|||
if (!payload.id) { |
|||
return cb('Missing ID'); |
|||
} |
|||
|
|||
const process = this.processes.get(payload.id); |
|||
if (!process) { |
|||
return cb(`Process ${payload.id} not found.`); |
|||
} |
|||
try { |
|||
const proc = await process.restartWorker(true); |
|||
return cb(null, proc); |
|||
} catch (err) { |
|||
logger.error(err); |
|||
return cb(err); |
|||
} |
|||
} |
|||
|
|||
restartManager(payload, cb) { |
|||
this.clusterManager.restartWorker(); |
|||
return cb(null, 'OK'); |
|||
} |
|||
} |
|||
|
|||
module.exports = Manager; |
@ -0,0 +1,99 @@ |
|||
const cluster = require('cluster'); |
|||
const uuid = require('uuid/v4'); |
|||
|
|||
var EventEmitter; |
|||
|
|||
try { |
|||
EventEmitter = require('eventemitter3'); |
|||
} catch (e) { |
|||
EventEmitter = require('events'); |
|||
} |
|||
|
|||
/** |
|||
* @class Process |
|||
* @extends {EventEmitter} |
|||
*/ |
|||
class Process extends EventEmitter { |
|||
/** |
|||
* Representation of a process |
|||
* |
|||
* @prop {Number} id Process ID |
|||
* @prop {Object} worker The worker |
|||
* @prop {Object} process The worker process |
|||
* @prop {Number} pid The worker process ID |
|||
* @prop {Number} port The process RPC port |
|||
*/ |
|||
constructor(manager, options = {}) { |
|||
super(); |
|||
|
|||
this.id = uuid(); |
|||
this.pid = null; |
|||
this.port = null; |
|||
this.options = options || {}; |
|||
this.createdAt = Date.now(); |
|||
|
|||
if (options.manager) { |
|||
this.manager = true; |
|||
this.port = 5052; |
|||
} |
|||
|
|||
if (options.cluster && options.cluster.id) { |
|||
this.port = 30000 + parseInt(options.cluster.id, 10); |
|||
} |
|||
|
|||
this.worker = this.createWorker(options.awaitReady); |
|||
this.process = this.worker.process; |
|||
} |
|||
|
|||
createWorker(awaitReady = false) { |
|||
const worker = cluster.fork( |
|||
Object.assign({ |
|||
awaitReady: awaitReady, |
|||
uuid: this.id, |
|||
}, this.options) |
|||
); |
|||
|
|||
this.pid = worker.process.pid; |
|||
this.createdAt = Date.now(); |
|||
|
|||
process.nextTick(() => { |
|||
this._readyListener = this.ready.bind(this); |
|||
worker.on('message', this._readyListener); |
|||
}); |
|||
|
|||
return worker; |
|||
} |
|||
|
|||
restartWorker(awaitReady = false) { |
|||
const worker = this.createWorker(awaitReady); |
|||
const oldWorker = this.worker; |
|||
const createdAt = Date.now(); |
|||
this._pid = worker.process.pid; |
|||
|
|||
return new Promise(resolve => { |
|||
this.on('ready', () => { |
|||
if (this.worker) { |
|||
oldWorker.kill('SIGTERM'); |
|||
} |
|||
|
|||
process.nextTick(() => { |
|||
this.worker = worker; |
|||
this.process = worker.process; |
|||
this.pid = worker.process.pid; |
|||
this.createdAt = createdAt; |
|||
return resolve(this); |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
ready(message) { |
|||
if (!message) return; |
|||
if (message === 'ready' || message.op === 'ready') { |
|||
this.worker.removeListener('ready', this._readyListener); |
|||
this.emit('ready'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Process; |
@ -0,0 +1,36 @@ |
|||
'use strict'; |
|||
|
|||
const Redis = require('ioredis'); |
|||
const logger = require('./logger'); |
|||
|
|||
async function connect() { |
|||
return new Promise((resolve, reject) => { |
|||
const client = new Redis({ |
|||
name: 'master', |
|||
sentinels: [ |
|||
{ host: '10.12.0.55', port: 26379 }, |
|||
{ host: '10.12.0.56', port: 26379 }, |
|||
{ host: '10.12.0.57', port: 26379 }, |
|||
{ host: '10.12.0.58', port: 26379 }, |
|||
], |
|||
}); |
|||
|
|||
const rejectFunc = (err) => { |
|||
reject(err); |
|||
}; |
|||
|
|||
client.on('ready', () => { |
|||
logger.info('Connected to redis.'); |
|||
client.removeListener('error', rejectFunc); |
|||
resolve(client); |
|||
}); |
|||
|
|||
client.once('error', rejectFunc); |
|||
|
|||
client.on('error', err => { |
|||
logger.error(err); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
module.exports = { connect }; |
@ -0,0 +1,14 @@ |
|||
const jayson = require('jayson/promise'); |
|||
|
|||
class Client { |
|||
constructor(host, port) { |
|||
this.client = jayson.client.http({ |
|||
host, |
|||
port, |
|||
}); |
|||
|
|||
return this.client; |
|||
} |
|||
} |
|||
|
|||
module.exports = Client; |
@ -0,0 +1,44 @@ |
|||
/* eslint-disable no-unused-vars */ |
|||
const WebSocket = require('ws'); |
|||
const jayson = require('jayson'); |
|||
|
|||
class LogServer { |
|||
init(port) { |
|||
this.wss = new WebSocket.Server({ port }); |
|||
} |
|||
|
|||
hook(proc) { |
|||
proc.process.stdout.pipe(process.stdout); |
|||
proc.process.stdout.on('data', (data) => { |
|||
this._broadcastLog(data, proc, 'stdout'); |
|||
}); |
|||
|
|||
proc.process.stderr.pipe(process.stderr); |
|||
proc.process.stderr.on('data', (data) => { |
|||
this._broadcastLog(data, proc, 'stderr'); |
|||
}); |
|||
} |
|||
|
|||
_broadcastLog(data, proc, stream) { |
|||
const msg = data.toString(); |
|||
const payload = { |
|||
pid: proc.pid, |
|||
createdAt: proc.createdAt, |
|||
cm: !!proc.manager, |
|||
msg, |
|||
stream, |
|||
}; |
|||
|
|||
if (proc.options && proc.options.id !== undefined) { |
|||
payload.cid = proc.options.id; |
|||
} |
|||
|
|||
this.wss.clients.forEach((client) => { |
|||
if (client.readyState === WebSocket.OPEN) { |
|||
client.send(JSON.stringify(payload)); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
module.exports = LogServer; |
@ -0,0 +1,13 @@ |
|||
/* eslint-disable no-unused-vars */ |
|||
const jayson = require('jayson'); |
|||
|
|||
class Server { |
|||
init(host, port, methods) { |
|||
this.host = host; |
|||
this.port = port; |
|||
this.server = jayson.server(methods); |
|||
this.server.http().listen(this.port, this.host); |
|||
} |
|||
} |
|||
|
|||
module.exports = Server; |
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue