mirror of https://github.com/aididan20/dyno
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) { |
|||