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