Browse Source

Upload

master
aididan20 4 years ago
commit
c1db61fecd
  1. 4
      .dockerignore
  2. 9
      .eslintignore
  3. 108
      .eslintrc.json
  4. 50
      .gitignore
  5. 3
      .gitmodules
  6. 1
      .npmrc
  7. 23
      Dockerfile
  8. 402
      LICENSE.md
  9. 1
      README.md
  10. 123
      config/default.yaml
  11. 34
      config/development.yaml
  12. 131
      config/production.yaml
  13. 37
      gulpfile.js
  14. 79
      package.json
  15. 11
      pm2/dyno.bot.json
  16. 17
      scripts/convertConfig.js
  17. 8
      scripts/execAll.sh
  18. 105
      scripts/loadActivity.js
  19. 101
      scripts/reshard.js
  20. 37
      src/commands/Admin/Avatar.js
  21. 28
      src/commands/Admin/Changelog.js
  22. 65
      src/commands/Admin/CommandStats.js
  23. 730
      src/commands/Admin/Data.js
  24. 130
      src/commands/Admin/DisablePremium.js
  25. 107
      src/commands/Admin/EnablePremium.js
  26. 74
      src/commands/Admin/Eval.js
  27. 50
      src/commands/Admin/Exec.js
  28. 50
      src/commands/Admin/Git.js
  29. 50
      src/commands/Admin/GlobalDisable.js
  30. 50
      src/commands/Admin/GlobalEnable.js
  31. 167
      src/commands/Admin/Guild.js
  32. 40
      src/commands/Admin/ListingBlacklist.js
  33. 53
      src/commands/Admin/LoadCommand.js
  34. 23
      src/commands/Admin/LoadController.js
  35. 35
      src/commands/Admin/LoadIPC.js
  36. 27
      src/commands/Admin/LoadModule.js
  37. 47
      src/commands/Admin/MoveCluster.js
  38. 39
      src/commands/Admin/Partner.js
  39. 68
      src/commands/Admin/RLReset.js
  40. 70
      src/commands/Admin/RemoteDebug.js
  41. 121
      src/commands/Admin/RemoteDiagnose.js
  42. 63
      src/commands/Admin/Restart.js
  43. 69
      src/commands/Admin/Sessions.js
  44. 33
      src/commands/Admin/Speedtest.js
  45. 27
      src/commands/Admin/UnloadModule.js
  46. 53
      src/commands/Admin/Update.js
  47. 94
      src/commands/Admin/UpdateTeam.js
  48. 25
      src/commands/Admin/Username.js
  49. 76
      src/commands/Info/Info.js
  50. 30
      src/commands/Info/Ping.js
  51. 47
      src/commands/Info/Premium.js
  52. 107
      src/commands/Info/Stats.js
  53. 37
      src/commands/Info/Uptime.js
  54. 40
      src/commands/Misc/Avatar.js
  55. 75
      src/commands/Misc/Botlist.js
  56. 41
      src/commands/Misc/Color.js
  57. 35
      src/commands/Misc/Discrim.js
  58. 64
      src/commands/Misc/Distance.js
  59. 41
      src/commands/Misc/DynoAvatar.js
  60. 52
      src/commands/Misc/Emotes.js
  61. 94
      src/commands/Misc/InviteInfo.js
  62. 65
      src/commands/Misc/MemberCount.js
  63. 37
      src/commands/Misc/RandomColor.js
  64. 68
      src/commands/Misc/ServerInfo.js
  65. 128
      src/commands/Misc/Whois.js
  66. 60
      src/commands/Roles/RoleInfo.js
  67. 64
      src/commands/Roles/Roles.js
  68. 575
      src/core/Dyno.js
  69. 15
      src/core/RPCClient.js
  70. 125
      src/core/RPCServer.js
  71. 145
      src/core/cluster/Cluster.js
  72. 194
      src/core/cluster/Events.js
  73. 129
      src/core/cluster/Logger.js
  74. 185
      src/core/cluster/Manager.js
  75. 164
      src/core/cluster/Server.js
  76. 226
      src/core/cluster/Sharding.js
  77. 161
      src/core/clusterManager/Commands.js
  78. 121
      src/core/clusterManager/Logger.js
  79. 184
      src/core/clusterManager/Manager.js
  80. 111
      src/core/collections/CommandCollection.js
  81. 424
      src/core/collections/GuildCollection.js
  82. 203
      src/core/collections/ModuleCollection.js
  83. 108
      src/core/config.js
  84. 24
      src/core/database.js
  85. 98
      src/core/logger.js
  86. 278
      src/core/managers/EventManager.js
  87. 146
      src/core/managers/IPCManager.js
  88. 348
      src/core/managers/LangManager.js
  89. 37
      src/core/managers/PagerManager.js
  90. 105
      src/core/managers/PermissionsManager.js
  91. 147
      src/core/managers/RPCManager.js
  92. 93
      src/core/managers/WebhookManager.js
  93. 497
      src/core/matomo.js
  94. 182
      src/core/metrics.js
  95. 146
      src/core/processManager/Manager.js
  96. 99
      src/core/processManager/Process.js
  97. 36
      src/core/redis.js
  98. 14
      src/core/rpc/Client.js
  99. 44
      src/core/rpc/LogServer.js
  100. 13
      src/core/rpc/Server.js

4
.dockerignore

@ -0,0 +1,4 @@
.env
node_modules
npm-debug.logs
config/local*

9
.eslintignore

@ -0,0 +1,9 @@
node_modules/*
test/*
build/*
public/*
private/*
views/*
*.md
*.json
commands/Admin/Eval.js

108
.eslintrc.json

@ -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"
}
}

50
.gitignore

@ -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.*

3
.gitmodules

@ -0,0 +1,3 @@
[submodule "translations"]
path = translations
url = [email protected]:FlexLabs/dyno-translations.git

1
.npmrc

@ -0,0 +1 @@

23
Dockerfile

@ -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" ]

402
LICENSE.md

@ -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.

1
README.md

@ -0,0 +1 @@
Original readme removed.

123
config/default.yaml

@ -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

34
config/development.yaml

@ -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

131
config/production.yaml

@ -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

37
gulpfile.js

@ -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'));
});

79
package.json

@ -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"
}
}

11
pm2/dyno.bot.json

@ -0,0 +1,11 @@
{
"apps": [
{
"name": "Dyno",
"script": "src/start.js",
"pmx": false,
"vizion": false,
"env_production": { "NODE_ENV": "production" }
}
]
}

17
scripts/convertConfig.js

@ -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);

8
scripts/execAll.sh

@ -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

105
scripts/loadActivity.js

@ -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();
// });
// });

101
scripts/reshard.js

@ -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)));

37
src/commands/Admin/Avatar.js

@ -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;

28
src/commands/Admin/Changelog.js

@ -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;

65
src/commands/Admin/CommandStats.js

@ -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;

730
src/commands/Admin/Data.js

@ -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;

130
src/commands/Admin/DisablePremium.js

@ -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;

107
src/commands/Admin/EnablePremium.js

@ -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;

74
src/commands/Admin/Eval.js

@ -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;

50
src/commands/Admin/Exec.js

@ -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;

50
src/commands/Admin/Git.js

@ -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;

50
src/commands/Admin/GlobalDisable.js

@ -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;

50
src/commands/Admin/GlobalEnable.js

@ -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;

167
src/commands/Admin/Guild.js

@ -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;

40
src/commands/Admin/ListingBlacklist.js

@ -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;

53
src/commands/Admin/LoadCommand.js

@ -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;

23
src/commands/Admin/LoadController.js

@ -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;

35
src/commands/Admin/LoadIPC.js

@ -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;

27
src/commands/Admin/LoadModule.js

@ -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;

47
src/commands/Admin/MoveCluster.js

@ -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;

39
src/commands/Admin/Partner.js

@ -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;

68
src/commands/Admin/RLReset.js

@ -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;

70
src/commands/Admin/RemoteDebug.js

@ -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;

121
src/commands/Admin/RemoteDiagnose.js

@ -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;

63
src/commands/Admin/Restart.js

@ -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;

69
src/commands/Admin/Sessions.js

@ -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;

33
src/commands/Admin/Speedtest.js

@ -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;

27
src/commands/Admin/UnloadModule.js

@ -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;

53
src/commands/Admin/Update.js

@ -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;

94
src/commands/Admin/UpdateTeam.js

@ -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;

25
src/commands/Admin/Username.js

@ -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;

76
src/commands/Info/Info.js

@ -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;

30
src/commands/Info/Ping.js

@ -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;

47
src/commands/Info/Premium.js

@ -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;

107
src/commands/Info/Stats.js

@ -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;

37
src/commands/Info/Uptime.js

@ -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;

40
src/commands/Misc/Avatar.js

@ -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;

75
src/commands/Misc/Botlist.js

@ -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;

41
src/commands/Misc/Color.js

@ -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;

35
src/commands/Misc/Discrim.js

@ -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;

64
src/commands/Misc/Distance.js

@ -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;

41
src/commands/Misc/DynoAvatar.js

@ -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;

52
src/commands/Misc/Emotes.js

@ -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;

94
src/commands/Misc/InviteInfo.js

@ -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;

65
src/commands/Misc/MemberCount.js

@ -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;

37
src/commands/Misc/RandomColor.js

@ -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;

68
src/commands/Misc/ServerInfo.js

@ -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;

128
src/commands/Misc/Whois.js

@ -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;

60
src/commands/Roles/RoleInfo.js

@ -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;

64
src/commands/Roles/Roles.js

@ -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;

575
src/core/Dyno.js

@ -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;

15
src/core/RPCClient.js

@ -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;

125
src/core/RPCServer.js

@ -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;

145
src/core/cluster/Cluster.js

@ -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;

194
src/core/cluster/Events.js

@ -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;

129
src/core/cluster/Logger.js

@ -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;

185
src/core/cluster/Manager.js

@ -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;

164
src/core/cluster/Server.js

@ -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;

226
src/core/cluster/Sharding.js

@ -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;

161
src/core/clusterManager/Commands.js

@ -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;

121
src/core/clusterManager/Logger.js

@ -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;

184
src/core/clusterManager/Manager.js

@ -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;

111
src/core/collections/CommandCollection.js

@ -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;

424
src/core/collections/GuildCollection.js

@ -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;

203
src/core/collections/ModuleCollection.js

@ -0,0 +1,203 @@
'use strict';
const each = require('async-each');
const glob = require('glob-promise');
const minimatch = require('minimatch');
const jsonSchema = require('mongoose_schema-json');
const { Collection, utils } = require('@dyno.gg/dyno-core');
const { models } = require('../database');
const logger = require('../logger');
/**
* @class ModuleCollection
* @extends Collection
*/
class ModuleCollection extends Collection {
/**
* A collection of modules
* @param {Object} config The Dyno configuration object
* @param {Dyno} dyno The Dyno instance
*/
constructor(config, dyno) {
super();
this.dyno = dyno;
this._client = dyno.client;
this._config = config;
this._listenerCount = 0;
this.moduleList = this._config.moduleList || [];
this.loadModules();
}
unload() {
for (let module of this.values()) {
module._unload();
this.delete(module.name);
}
}
/**
* Load commands
*/
async loadModules() {
try {
var files = await glob('**/*.js', {
cwd: this._config.paths.modules,
root: this._config.paths.modules,
absolute: true,
});
files = files.filter(f => !minimatch(f, '**/commands/*'));
} catch (err) {
logger.error(err);
}
let modules = [];
each(files, (file, next) => {
if (file.endsWith('.map')) return next();
const module = require(file);
if (module.hasModules) {
modules = modules.concat(Object.values(module.modules));
return next();
}
modules.push(require(file));
return next();
}, err => {
if (err) {
logger.error(err);
}
utils.asyncForEach(modules, (module, next) => {
this.register(module);
return;
}, (e) => {
if (e) {
logger.error(e);
}
logger.info(`[ModuleCollection] Registered ${this.size} modules.`);
});
});
}
/**
* Register module
* @param {Function} Module the module class
*/
register(Module) {
if (Object.getPrototypeOf(Module).name !== 'Module') {
return logger.debug(`[ModuleCollection] Skipping unknown module`);
}
let module = new Module(this.dyno),
activeModule = this.get(module.name),
globalConfig = this.dyno.globalConfig;
if (activeModule) {
logger.debug(`[ModuleCollection] Unloading module ${module.name}`);
activeModule._unload();
this.delete(module.name);
}
logger.debug(`[ModuleCollection] Registering module ${module.name}`);
if (module.commands) {
const commands = Array.isArray(module.commands) ? module.commands : Object.values(module.commands);
each(commands, command => this.dyno.commands.register(command));
}
if (module.moduleModels) {
this.registerModels(module.moduleModels);
}
// ensure the module defines all required properties/methods
module.ensureInterface();
if (!activeModule) {
const moduleCopy = module.toJSON();
if (module.settings) {
moduleCopy.settings = jsonSchema.schema2json(module.settings);
models.Server.schema.add({
[module.name.toLowerCase()]: module.settings,
});
}
moduleCopy._state = this._config.state;
models.Module.findOneAndUpdate({ name: module.name, _state: this._config.state }, moduleCopy, { upsert: true, overwrite: true })
.catch(err => logger.error(err));
}
this.set(module.name, module);
if (this.moduleList.length && !this.moduleList.includes(Module.name)) {
return;
}
if (globalConfig && globalConfig.modules.hasOwnProperty(module.name) &&
globalConfig.modules[module.name] === false) return;
each(this.dyno.dispatcher.events, (event, next) => {
if (!module[event]) return next();
module.registerListener(event, module[event]);
this._listenerCount++;
next();
}, err => {
if (err) logger.error(err);
this.get(module.name)._start(this._client);
});
}
registerModels(moduleModels) {
if(!moduleModels || !moduleModels.length || moduleModels.length === 0) {
return;
}
each(moduleModels, (model, next) => {
if(typeof model !== 'object' || !model.name || (!model.skeleton && !model.schema)) {
next();
return;
}
logger.debug(`[ModuleCollection] Registering model: ${model.name}`);
const schema = new this.dyno.db.Schema(model.skeleton || model.schema, model.options);
this.dyno.db.registerModel({ name: model.name, schema });
});
}
/**
* Enable or disable a module
* @param {String} id Guild id
* @param {String} name Module name
* @param {String|Boolean} enabled Enabled or disabled
* @returns {Promise}
*/
async toggle(id, name, enabled) {
let guildConfig = await this.dyno.guilds.getOrFetch(id),
guild = this._client.guilds.get(id),
module = this.get(name),
key = `modules.${name}`;
enabled = enabled === 'true';
if (!guild || !guildConfig)
return Promise.reject(`Couldn't get guild or config for module ${name}.`);
guildConfig.modules[name] = enabled;
if (enabled && module && module.enable) module.enable(guild);
if (!enabled && module && module.disable) module.disable(guild);
return this.dyno.guilds.update(guildConfig._id, { $set: { [key]: enabled } });
}
}
module.exports = ModuleCollection;

108
src/core/config.js

@ -0,0 +1,108 @@
'use strict';
const path = require('path');
const pkg = require('../../package.json');
const dot = require('dot-object');
const basePath = path.resolve(path.join(__dirname, '..'));
const envkeyLoader = require('envkey/loader');
let config = envkeyLoader.fetch();
for (let k of Object.keys(config)) {
const index = config[k].indexOf('$typeof:');
if (index > 0) {
const value = config[k].substr(0, index);
const type = config[k].substr(index).replace('$typeof:', '');
switch (type) {
case 'int':
case 'number':
config[k] = Number.parseInt(value);
break;
case 'bool':
case 'boolean':
config[k] = (value === 'true');
break;
case 'json':
config[k] = JSON.parse(value);
break;
}
}
}
config = dot.object(config);
config.paths = {
base: basePath,
commands: path.join(basePath, 'commands'),
controllers: path.join(basePath, 'controllers'),
ipc: path.join(basePath, 'ipc'),
events: path.join(basePath, 'events'),
modules: path.join(basePath, 'modules'),
};
config.pkg = pkg;
config.permissions = {
createInstantInvite: 1,
kickMembers: 2,
banMembers: 4,
administrator: 8,
manageChannels: 16,
manageGuild: 32,
addReactions: 64,
readMessages: 1024,
sendMessages: 2048,
sendTTSMessages: 4096,
manageMessages: 8192,
embedLinks: 16384,
attachFiles: 32768,
readMessageHistory: 65536,
mentionEveryone: 131072,
externalEmojis: 262144,
voiceConnect: 1048576,
voiceSpeak: 2097152,
voiceMuteMembers: 4194304,
voiceDeafenMembers: 8388608,
voiceMoveMembers: 16777216,
voiceUseVAD: 33554432,
changeNickname: 67108864,
manageNicknames: 134217728,
manageRoles: 268435456,
manageWebhooks: 536870912,
manageEmojis: 1073741824,
};
config.permissionsMap = {
createInstantInvite: 'Create Instant Invite',
kickMembers: 'Kick Members',
banMembers: 'Ban Members',
administrator: 'Administrator',
manageChannels: 'Manage Channels',
manageGuild: 'Manage Server',
addReactions: 'Add Reactions',
readMessages: 'Read Messages',
sendMessages: 'Send Messages',
sendTTSMessages: 'Send TTS Messages',
manageMessages: 'Manage Messages',
embedLinks: 'Embed Links',
attachFiles: 'Attach Files',
readMessageHistory: 'Read Message History',
mentionEveryone: 'Mention Everyone',
externalEmojis: 'External Emojis',
voiceConnect: 'Connect',
voiceSpeak: 'Speak',
voiceMuteMembers: 'Mute Members',
voiceDeafenMembers: 'Deafen Members',
voiceMoveMembers: 'Move Members',
voiceUseVAD: 'Use Voice Activity',
changeNickname: 'Change Nickname',
manageNicknames: 'Manage Nicknames',
manageRoles: 'Manage Roles',
manageWebhooks: 'Manage Webhooks',
manageEmojis: 'Manage Emojis',
};
module.exports = config;

24
src/core/database.js

@ -0,0 +1,24 @@
'use strict';
const DataFactory = require('@dyno.gg/datafactory');
const config = require('./config');
const dbString = config.mongo.dsn;
if (!dbString) {
throw new Error('Missing environment variable CLIENT_MONGO_URL.');
}
const db = new DataFactory({
dbString,
disableReplica: config.mongo.disableReplica || false,
logger: {
level: config.logLevel || 'error',
sentry: {
level: config.sentry.logLevel,
dsn: config.sentry.dsn,
},
},
});
module.exports = db;

98
src/core/logger.js

@ -0,0 +1,98 @@
'use strict';
const util = require('util');
const moment = require('moment');
const winston = require('winston');
const config = require('./config');
const Sentry = require('./transports/winston-sentry');
/**
* @class Logger
*/
class Logger {
/**
* @prop {Array} transports
* @prop {Boolean} exitOnError
*/
constructor() {
this.transports = [
new (winston.transports.Console)({
colorize: true,
level: config.logLevel || 'info',
debugStdout: true,
// handleExceptions: true,
// humanReadableUnhandledException: true,
timestamp: () => new Date(),
formatter: this._formatter.bind(this),
}),
];
if (config.sentry.dsn) {
this.transports.push(new Sentry({
// patchGlobal: true,
level: config.sentry.logLevel || 'error',
dsn: config.sentry.dsn,
logger: config.stateName,
}));
}
this.exitOnError = false;
return new (winston.Logger)(this);
}
/**
* Custom formatter for console
* @param {Object} options Formatter options
* @returns {String}
* @private
*/
_formatter(options) {
let ts = util.format('[%s]', moment(options.timestamp()).format('HH:mm:ss')),
level = winston.config.colorize(options.level);
if (process.env.hasOwnProperty('clusterId')) {
ts = `[C${process.env.clusterId}] ${ts}`;
}
if (!options.message.length && options.meta instanceof Error) {
options.message = options.meta + options.meta.stack;
}
if (options.meta && options.meta.guild && typeof options.meta.guild !== 'string') {
if (options.meta.guild.shard) {
options.meta.shard = options.meta.guild.shard.id;
}
options.meta.guild = options.meta.guild.id;
}
switch (options.level) {
case 'debug':
ts += ' ⚙ ';
break;
case 'info':
ts += ' 🆗 ';
break;
case 'error':
ts += ' 🔥 ';
break;
case 'warn':
ts += ' ☣ ';
break;
case 'silly':
ts += ' 💩 ';
break;
}
let message = ts + ' ' + level + ': ' + (undefined !== options.message ? options.message : '') +
(options.meta && Object.keys(options.meta).length ? '\n\t' + util.inspect(options.meta) : '');
if (options.colorize === 'all') {
return winston.config.colorize(options.level, message);
}
return message;
}
}
module.exports = new Logger();

278
src/core/managers/EventManager.js

@ -0,0 +1,278 @@
'use strict';
const { utils } = require('@dyno.gg/dyno-core');
const each = require('async-each');
const logger = require('../logger');
class EventManager {
constructor(dyno) {
this.dyno = dyno;
this._client = dyno.client;
this._config = dyno.config;
this._handlers = new Map();
this._listeners = {};
this._boundListeners = new Map();
this.chunkedGuilds = new Map();
this.disabledGuilds = new Map();
this.events = [
'channelCreate',
'channelDelete',
'guildBanAdd',
'guildBanRemove',
'guildCreate',
'guildDelete',
'guildMemberAdd',
'guildMemberRemove',
'guildMemberUpdate',
'guildRoleCreate',
'guildRoleDelete',
'guildRoleUpdate',
'messageCreate',
'messageDelete',
'messageDeleteBulk',
'messageUpdate',
'userUpdate',
'voiceChannelJoin',
'voiceChannelLeave',
'voiceChannelSwitch',
'messageReactionAdd',
'messageReactionRemove',
'messageReactionRemoveAll',
];
this.registerHandlers();
}
get client() {
return this._client;
}
get config() {
return this._config;
}
/**
* Register the root event handlers from the events directory
*/
registerHandlers() {
utils.readdirRecursive(this._config.paths.events).then(files => {
each(files, (file, next) => {
if (file.endsWith('.map')) return next();
const handler = require(file);
if (!handler || !handler.name) return logger.error('Invalid handler.');
this._handlers.set(handler.name, handler);
logger.debug(`[EventManager] Registering ${handler.name} handler`);
return next();
}, err => {
if (err) logger.error(err);
logger.info(`[EventManager] Registered ${this.events.size} events.`);
});
}).catch(err => logger.error(err));
}
/**
* Bind event listeners and store a reference
* to the bound listener so they can be unregistered.
*/
bindListeners() {
let listenerCount = 0;
for (let event in this._listeners) {
// Bind the listener so it can be removed
this._boundListeners[event] = this.createListener.bind(this, event);
// Register the listener
this.client.on(event, this._boundListeners[event]);
listenerCount++;
}
logger.info(`[EventManager] Bound ${listenerCount} listeners.`);
}
/**
* Register event listener
* @param {String} event Event name
* @param {Function} listener Event listener
* @param {String} [module] module name
*/
registerListener(event, listener, module) {
// Register but don't bind listeners before the client is ready
if (!this._listeners[event] || !this._listeners[event].find(l => l.listener === listener)) {
this._listeners[event] = this._listeners[event] || [];
this._listeners[event].push({ module: module || null, listener: listener });
return;
}
// Remove the listener from listeners if it exists, and re-add it
let index = this._listeners[event].findIndex(l => l.listener === listener);
if (index > -1) this._listeners[event].splice(index, 1);
this._listeners[event].push({ module: module, listener: listener });
this.client.removeListener(event, this._boundListeners[event]);
// Bind the listener so it can be removed
this._boundListeners[event] = this.createListener.bind(this, event);
// Register the bound listener
this.client.on(event, this._boundListeners[event]);
}
/**
* Deregister event listener
* @param {String} event Event name
* @param {Function} listener Event listener
*/
unregisterListener(event, listener) {
let index = this._listeners[event].findIndex(l => l.listener === listener);
if (index > -1) this._listeners[event].splice(index, 1);
}
awaitChunkState(guild, cb) {
if (guild.members && guild.members.size >= (guild.memberCount * 0.9)) {
this.chunkedGuilds.set(guild.id, 2);
return cb();
} else {
setTimeout(() => this.awaitChunkState(guild, cb), 100);
}
}
/**
* Create an event listener
* @param {String} event Event name
* @param {...*} args Event arguments
*/
createListener(event, ...args) {
if (!this._listeners[event]) return;
const handler = this._handlers.get(event);
// Check if a root handler exists before calling module listeners
if (handler) {
return handler(this, ...args).then(async (e) => {
if (!e || !e.guildConfig) {
return;
// return logger.warn(`${event} no event or guild config`);
}
if (e.guild.id && this.disabledGuilds.has(e.guild.id)) {
return;
}
// Check if guild is enabled for the app state
if (e.guild.id !== this._config.dynoGuild && event !== 'messageCreate') {
if (!this.guildEnabled(e.guildConfig, e.guild.id)) return;
}
let chunkState = this.chunkedGuilds.get(e.guild.id);
if (this.config.lazyChunking) {
if (!chunkState) {
chunkState = 1;
this.chunkedGuilds.set(e.guild.id, 1);
e.guild.fetchAllMembers();
}
if (chunkState === 2) {
if (e.guild.members.size < (e.guild.memberCount * 0.9)) {
chunkState = 1;
this.chunkedGuilds.set(e.guild.id, 1);
e.guild.fetchAllMembers();
}
}
if (chunkState !== 2) {
await new Promise(resolve => this.awaitChunkState(e.guild, resolve));
}
}
each(this._listeners[event], o => {
if (o.module && e.guild && e.guildConfig) {
if (!this.moduleEnabled(e.guild, e.guildConfig, o.module)) return;
}
o.listener(e);
});
}).catch(err => err ? logger.error(err) : false);
}
// No root handler exists, execute the module listeners
each(this._listeners[event], o => o.listener(...args));
}
/**
* Check if an event handler should continue or not based on app state and guild config
* @param {Object} guildConfig Guild configuration
* @param {String} guildId Guild ID
* @returns {Boolean}
*/
guildEnabled(guildConfig, guildId) {
const guild = this._client.guilds.get(guildId);
// handle events based on region, ignore in dev
if (!guild) return false;
if (this._config.handleRegion && !utils.regionEnabled(guild, this._config)) return false;
if (this.disabledGuilds.has(guildId)) {
return false;
}
if (this._config.test) {
if (this._config.testGuilds.includes(guildId) || guildConfig.test) return true;
return false;
}
// premium checks
if (!this._config.isPremium && guildConfig.isPremium && guildConfig.premiumInstalled) {
return false;
}
if (this._config.isPremium && (!guildConfig.isPremium || !guildConfig.premiumInstalled)) {
return false;
}
if (!this._config.isPremium && guildConfig.clientID && guildConfig.clientID !== this._config.client.id) {
return false;
}
// Shared state, receive all events
if (this._config.shared) return true;
if (guildConfig.beta) {
// Guild is using beta, but app state is not test/beta
if (!this._config.test && !this._config.beta) {
return false;
}
} else if (this._config.beta) {
// App state is beta, but guild is not.
return false;
}
return true;
}
/**
* Check if a module is enabled or should execute code
* @param {Guild} guild Guild object
* @param {Object} guildConfig Guild configuration
* @param {String} module Module name
* @return {Boolean}
*/
moduleEnabled(guild, guildConfig, module) {
// Ignore events before the client is ready or guild is cached
if (!this.dyno.isReady || !guild || !guildConfig) return false;
if (!guildConfig.modules) return false;
const name = module.module || module.name;
// check if globally disabled
const globalConfig = this.dyno.globalConfig;
if (globalConfig && globalConfig.modules.hasOwnProperty(name) &&
globalConfig.modules[name] === false) return false;
// check if module is disabled
if (guildConfig.modules.hasOwnProperty(name) && guildConfig.modules[name] === false) {
return false;
}
return true;
}
}
module.exports = EventManager;

146
src/core/managers/IPCManager.js

@ -0,0 +1,146 @@
'use strict';
const { utils } = require('@dyno.gg/dyno-core');
const EventEmitter = require('eventemitter3');
const each = require('async-each');
const logger = require('../logger');
/**
* @class IPCManager
* @extends EventEmitter
*/
class IPCManager extends EventEmitter {
/**
* Manages the IPC communications with the shard manager
* @param {Dyno} dyno The Dyno instance
*
* @prop {Number} id Shard ID
* @prop {Number} pid Process ID
* @prop {Map} commands Collection of IPC commands
*/
constructor(dyno) {
super();
const config = this._config = dyno.config;
this.dyno = dyno;
this.client = dyno.client;
this.id = dyno.clientOptions.clusterId || dyno.clientOptions.shardId || 0;
this.pid = process.pid;
this.commands = new Map();
process.on('message', this.onMessage.bind(this));
utils.readdirRecursive(this._config.paths.ipc).then(files => {
each(files, (file, next) => {
if (file.endsWith('.map')) return next();
this.register(require(file));
return next();
}, err => {
if (err) logger.error(err);
logger.info(`[IPCManager] Registered ${this.commands.size} IPC commands.`);
});
}).catch(err => logger.error(err));
}
/**
* Send a command or event to the shard manager
* @param {String} event Event to send
* @param {Mixed} data The data to send
*/
send(event, data) {
if (!process.send) return;
try {
process.send({
op: event,
d: data || null,
});
} catch (err) {
logger.error(`IPC Error Caught:`, err);
}
}
/**
* Fired when the shard receives a message
* @param {Object} message The message object
* @returns {*}
*/
onMessage(message) {
// op for internal dyno messages, type for prom-client cluster messages
if (!message.op && !message.type) {
return logger.warn('Received IPC message with no op or type.');
}
if (['resp', 'broadcast'].includes(message.op)) return;
if (this[message.op]) {
try {
return this[message.op](message);
} catch (err) {
return this.logger.error(err);
}
}
const command = this.commands.get(message.op);
if (!command) return;
try {
return command(this.dyno, this._config, message);
} catch (err) {
this.logger.error(err);
}
this.emit(message.op, message.d);
}
/**
* Send a command and await a response from the shard manager
* @param {String} op Op to send
* @param {Object} d The data to send
* @returns {Promise}
*/
awaitResponse(op, d) {
if (!process.send) return;
return new Promise((resolve, reject) => {
const awaitListener = (msg) => {
if (!['resp', 'error'].includes(msg.op)) return;
process.removeListener('message', awaitListener);
if (msg.op === 'resp') return resolve(msg.d);
if (msg.op === 'error') return reject(msg.d);
};
const payload = { op: op };
if (d) payload.d = d;
process.on('message', awaitListener);
try {
process.send(payload);
} catch (err) {
logger.error(`IPC Error Caught:`, err);
}
setTimeout(() => {
process.removeListener('message', awaitListener);
reject('IPC Timed out.');
}, 5000);
});
}
/**
* Register an IPC command
* @param {Function} command The command to execute
* @returns {*|void}
*/
register(command) {
if (!command || !command.name) return logger.error('[IPCManager] Invalid command.');
logger.debug(`[IPCManager] Registering ipc command ${command.name}`);
this.commands.set(command.name, command);
}
}
module.exports = IPCManager;

348
src/core/managers/LangManager.js

@ -0,0 +1,348 @@
'use strict';
const dot = require('dot-prop');
const each = require('async-each');
const glob = require('glob-promise');
const path = require('path');
const { Collection } = require('@dyno.gg/dyno-core');
class LangManager extends Collection {
constructor(localePath) {
super();
this.localePath = localePath;
}
async loadLocales() {
try {
var files = await glob('**/*.json', {
cwd: path.resolve(this.localePath),
root: path.resolve(this.localePath),
absolute: true,
});
} catch (err) {
console.error(err);
return Promise.reject(err);
}
each(files, file => {
const locale = path.dirname(file).split(path.sep).pop();
this.set(locale, new I18n(locale, file));
});
}
t(locale, ...args) {
locale = locale || 'en';
const fallbackLang = this.get('en');
if (!this.has(locale)) {
locale = 'en';
}
return this.get(locale).__(fallbackLang, ...args);
}
}
class I18n {
constructor(lang, filePath) {
this._lang = lang;
this._filePath = filePath;
this._locale = require(filePath);
}
reload() {
delete require.cache[this._filePath];
this._locale = require(this._filePath);
}
// Get the rule for pluralization
// http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
get_rule(count, language) {
switch (language){
// nplurals=2; plural=(n > 1);
case 'ach':
case 'ak':
case 'am':
case 'arn':
case 'br':
case 'fil':
case 'fr':
case 'gun':
case 'ln':
case 'mfe':
case 'mg':
case 'mi':
case 'oc':
case 'pt_BR':
case 'tg':
case 'ti':
case 'tr':
case 'uz':
case 'wa':
return (count > 1) ? 1 : 0;
break;
// nplurals=2; plural=(n != 1);
case 'af':
case 'an':
case 'anp':
case 'as':
case 'ast':
case 'az':
case 'bg':
case 'bn':
case 'brx':
case 'ca':
case 'da':
case 'doi':
case 'de':
case 'el':
case 'en':
case 'eo':
case 'es':
case 'es_AR':
case 'et':
case 'eu':
case 'ff':
case 'fi':
case 'fo':
case 'fur':
case 'fy':
case 'gl':
case 'gu':
case 'ha':
case 'he':
case 'hi':
case 'hne':
case 'hu':
case 'hy':
case 'ia':
case 'it':
case 'kl':
case 'kn':
case 'ku':
case 'lb':
case 'mai':
case 'ml':
case 'mn':
case 'mni':
case 'mr':
case 'nah':
case 'nap':
case 'nb':
case 'ne':
case 'nl':
case 'nn':
case 'no':
case 'nso':
case 'or':
case 'pa':
case 'pap':
case 'pms':
case 'ps':
case 'pt':
case 'rm':
case 'rw':
case 'sat':
case 'sco':
case 'sd':
case 'se':
case 'si':
case 'so':
case 'son':
case 'sq':
case 'sv':
case 'sw':
case 'ta':
case 'te':
case 'tk':
case 'ur':
case 'yo':
return (count != 1) ? 1 : 0;
// nplurals=1; plural=0;
case 'ay':
case 'bo':
case 'cgg':
case 'dz':
case 'fa':
case 'id':
case 'ja':
case 'jbo':
case 'ka':
case 'kk':
case 'km':
case 'ko':
case 'ky':
case 'lo':
case 'ms':
case 'my':
case 'sah':
case 'su':
case 'th':
case 'tt':
case 'ug':
case 'vi':
case 'wo':
case 'zh':
case 'jv':
return 0;
// nplurals=2; plural=(n%10!=1 || n%100==11);
case 'is':
return (count % 10!=1 || count % 100==11) ? 1 : 0;
// nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3;
case 'kw':
return (count==1) ? 0 : (count==2) ? 1 : (count == 3) ? 2 : 3;
// nplurals=3; plural=(n % 10==1 && n % 100!=11 ? 0 : n % 10>=2 && n % 10 <= 4 && (n % 100 < 10 || n % 100>=20) ? 1 : 2);
case 'uk':
case 'sr':
case 'ru':
case 'hr':
case 'bs':
case 'be':
return count % 10 === 1 && count % 100 !== 11 ? 0 : count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20) ? 1 : 2;
// nplurals=3; plural=(n === 0 ? 0 : n === 1 ? 1 : 2);
case 'mnk':
return count === 0 ? 0 : count === 1 ? 1 : 2;
// nplurals=3; plural=(n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2;
case 'sk':
return (count === 1) ? 0 : (count >= 2 && count <= 4) ? 1 : 2;
// nplurals=3; plural=(n === 1 ? 0 : (n === 0 || (n % 100 > 0 && n % 100 < 20)) ? 1 : 2);
case 'ro':
return count === 1 ? 0 : (count === 0 || (count % 100 > 0 && count % 100 < 20)) ? 1 : 2;
// nplurals=6; plural=(n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5);
case 'ar':
return count === 0 ? 0 : count === 1 ? 1 : count === 2 ? 2 : count % 100 >= 3 && count % 100 <= 10 ? 3 : count % 100 >= 11 ? 4 : 5;
// nplurals=3; plural=(n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2;
case 'cs':
return count === 1 ? 0 : (count >= 2 && count <= 4) ? 1 : 2;
// countplurals=3; plural=(n === 1) ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;
case 'csb':
return (count === 1) ? 0 : count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20) ? 1 : 2;
// nplurals=4; plural=(n === 1) ? 0 : (n === 2) ? 1 : (n !== 8 && n !== 11) ? 2 : 3;
case 'cy':
return (count === 1) ? 0 : (count === 2) ? 1 : (count !== 8 && count !== 11) ? 2 : 3;
// nplurals=5; plural=n === 1 ? 0 : n === 2 ? 1 : (n>2 && n<7) ? 2 :(n>6 && n<11) ? 3 : 4;
case 'ga':
return count === 1 ? 0 : count === 2 ? 1 : (count>2 && count<7) ? 2 :(count>6 && count<11) ? 3 : 4;
// nplurals=4; plural=(n === 1 || n === 11) ? 0 : (n === 2 || n === 12) ? 1 : (n > 2 && n < 20) ? 2 : 3;
case 'gd':
return (count === 1 || count === 11) ? 0 : (count === 2 || count === 12) ? 1 : (count > 2 && count < 20) ? 2 : 3;
// nplurals=3; plural=(n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
case 'it':
return count % 10 === 1 && count % 100 !== 11 ? 0 : count % 10 >= 2 && (count % 100 < 10 || count % 100 >= 20) ? 1 : 2;
// nplurals=3; plural=(n % 10 === 1 && n % 100 !== 11 ? 0 : n !== 0 ? 1 : 2);
case 'lv':
return count % 10 === 1 && count % 100 !== 11 ? 0 : count !== 0 ? 1 : 2;
// nplurals=2; plural= n === 1 || n % 10 === 1 ? 0 : 1;
case 'mk': {
return count === 1 || count % 10 === 1 ? 0 : 1;
}
// nplurals=4; plural=(n === 1 ? 0 : n === 0 || ( n % 100 > 1 && n % 100 < 11) ? 1 : (n % 100 > 10 && n % 100 < 20 ) ? 2 : 3);
case 'mt':
return count === 1 ? 0 : count === 0 || (count % 100 > 1 && n % 100 < 11) ? 1 : (count % 100 > 10 && count % 100 < 20 ) ? 2 : 3;
// nplurals=3; plural=(n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
case 'pl':
return count === 1 ? 0 : count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20) ? 1 : 2;
// nplurals=4; plural=(n % 100 === 1 ? 1 : n % 100 === 2 ? 2 : n % 100 === 3 || n % 100 === 4 ? 3 : 0);
case 'sl':
return count % 100 === 1 ? 1 : count % 100 === 2 ? 2 : count % 100 === 3 || count % 100 === 4 ? 3 : 0;
default:
return 0;
}
}
__(fallbackLang, string, values) {
//return translation of the original sting if did not find the translation
// let translation = string;
let translation = dot.get(this._locale, string, null);
if (fallbackLang && !translation) {
translation = fallbackLang.__(null, string, values);
}
// get the corresponding translation from the file
// if (typeof this._locale[string] != 'undefined' && typeof this._locale[string][this._lang] != 'undefined') {
// translation = this._locale[string][this._lang];
// } else if (fallbackLang) {
// translation = fallbackLang.__(null, string, values);
// }
// If the string have place to render values withen
if ((/{{.+?}}/g).test(translation)) {
// get all the parts needed to be replaced
var matches = translation.match(/{{.+?}}/g);
// loop on each match
for (const index in matches) {
// get the match {{example}}
const match = matches[index];
// get the word in the match example
let match_word = (match.replace('}}', '')).replace('{{', '');
let match_search;
// translate the word if was passed in the values var
if (values && values[match_word] != undefined) {
translation = translation.replace(match, values[match_word]);
continue; // move to the next word in the loop
} else {
// match_search = dot.get(this._locale, match_word);
// if (match_search != undefined) {
if (typeof this._locale[match_word] != 'undefined') {
// If the translation is there in the file then translate it directly
translation = translation.replace(match, match_search);
continue;//move to the next word in the loop
}
}
// if the matched word have a count
if ((/\|\|.+/g).test(match_word)) {
const temp_array = match_word.split('||');
// update the matched word
match_word = temp_array[0];
// get the variable of the count for the word
const item_count_variable = temp_array[1];
// get the value form values passed to this function
// TODO through error if not found in values
const item_count = values[item_count_variable];
// will get the rule or for pluralization based on the lang
const rule = this.get_rule(item_count, this._lang);
// match_search = dot.get(this._locale, match_word);
if (typeof this._locale[match_word] == 'object') {
// if (typeof match_search == 'object') {
translation = translation.replace(match, match_search[rule]);
} else {
translation = translation.replace(match, match_search);
}
} else {
if (typeof values == 'object') {
translation = translation.replace(match, values[match_word]);
} else {
translation = translation.replace(match, match_search);
}
}
}
}
return translation;
}
}
module.exports = LangManager;

37
src/core/managers/PagerManager.js

@ -0,0 +1,37 @@
'use strict';
const { Collection, Pager } = require('@dyno.gg/dyno-core');
/**
* @class PagerManager
* @extends Collection
*/
class PagerManager extends Collection {
/**
* PagerManager constructor
* @param {Dyno} dyno Dyno core instance
*/
constructor(dyno) {
super();
this.dyno = dyno;
}
/**
* Create a pager
* @param {Object} options Pager options
* @param {String|GuildChannel} options.channel The channel this pager will be created in
* @param {User|Member} options.user The user this pager is created for
* @param {Object} options.embed The embed object to be sent without fields
* @param {Object[]} options.fields All embed fields that will be paged
* @param {Number} [options.pageLimit=10] The number of items per page, max 25, default 10
*/
create(options) {
if (!options || !options.channel || !options.user) return;
let id = `${options.channel.id}.${options.user.id}`;
let pager = new Pager(this, id, options);
this.set(id, pager);
return pager;
}
}
module.exports = PagerManager;

105
src/core/managers/PermissionsManager.js

@ -0,0 +1,105 @@
'use strict';
const { utils } = require('@dyno.gg/dyno-core');
class PermissionsManager {
constructor(dyno) {
this._config = dyno.config;
this.dyno = dyno;
}
/**
* Check if user is bot admin
* @param {User|Member} user User object
* @returns {Boolean}
*/
isAdmin(user) {
if (!user || !user.id) return false;
if (this.dyno.globalConfig.developers && this.dyno.globalConfig.developers.includes(user.id)) {
return true;
}
return (user.id === this._config.client.admin);
}
isOverseer(user) {
if (!user || !user.id) return false;
return (this.dyno.globalConfig.overseers && this.dyno.globalConfig.overseers.includes(user.id));
}
/**
* Check if user is server admin
* @param {Member} member Member object
* @param {GuildChannel} channel Channel object
* @returns {Boolean}
*/
isServerAdmin(member, channel) {
// ignore DM
if (!member || channel.type !== 0) return false;
// let permissions = member.permissionsFor(channel);
return (member.id === channel.guild.ownerID || (member.permission &&
(member.permission.has('administrator') || member.permission.has('manageGuild'))));
}
/**
* Check if user is server mod
* @param {Member} member Guild member object
* @param {GuildChannel} channel Channel object
* @returns {Boolean}
*/
isServerMod(member, channel) {
// ignore DM
if (!member || channel.type !== 0) return false;
const guildConfig = this.dyno.guilds.get(channel.guild.id);
if (this.isAdmin(member) || this.isServerAdmin(member, channel)) return true;
// server config may not have loaded yet
if (!guildConfig) return false;
// check mod roles
if (guildConfig.modRoles && member.roles && member.roles.find(r => guildConfig.modRoles.includes(r))) {
return true;
}
// sanity check
if (!guildConfig.mods) return false;
return guildConfig.mods.includes(member.id);
}
canOverride(channel, member, command) {
if (!member || !channel) return null;
const guildConfig = this.dyno.guilds.get(channel.guild.id);
if (!guildConfig.permissions || !guildConfig.permissions.length) return null;
const channelPerms = guildConfig.channelPermissions;
const rolePerms = guildConfig.rolePermissions;
let canOverride = null;
if (channelPerms && channelPerms[channel.id] && channelPerms[channel.id].commands.hasOwnProperty(command)) {
canOverride = channelPerms[channel.id].commands[command];
}
if (!rolePerms) return canOverride;
const roles = utils.sortRoles(channel.guild.roles);
for (let role of roles) {
if (!rolePerms[role.id]) continue;
if (member.roles.indexOf(role.id) === -1) continue;
if (rolePerms[role.id].commands.hasOwnProperty(command)) {
canOverride = rolePerms[role.id].commands[command];
break;
}
}
return canOverride;
}
}
module.exports = PermissionsManager;

147
src/core/managers/RPCManager.js

@ -0,0 +1,147 @@
'use strict';
const { utils } = require('@dyno.gg/dyno-core');
const Hemera = require('nats-hemera');
const HemeraJoi = require('hemera-joi');
const Nats = require('nats');
const EventEmitter = require('eventemitter3');
const each = require('async-each');
const logger = require('../logger');
/**
* @class RPCManager
* @extends EventEmitter
*/
class RPCManager extends EventEmitter {
/**
* Manages RPC communications
* @param {Dyno} dyno The Dyno instance
*/
constructor(dyno) {
super();
this.dyno = dyno;
this.config = dyno.config;
this.client = dyno.client;
this.clusterConfig = dyno.clientOptions;
this.id = dyno.options.clusterId || dyno.options.shardId || 0;
this.pid = process.pid;
this.commands = new Map();
this.nats = Nats.connect({ url: 'nats://ares.dyno.gg:4222' });
this.hemera = new Hemera(this.nats, { logLevel: 'info' });
this.hemera.use(HemeraJoi);
this.hemera.ready(this.onReady.bind(this));
}
onReady() {
logger.info(`[RPCManager] ready.`);
utils.readdirRecursive(this.config.paths.ipc).then(files => {
each(files, (file, next) => {
if (file.endsWith('.map')) return next();
this.register(require(file));
return next();
}, err => {
if (err) logger.error(err);
logger.info(`[RPCManager] Registered ${this.commands.size} RPC commands.`);
});
}).catch(err => logger.error(err));
}
/**
* Send a command or event to the shard manager
* @param {String} event Event to send
* @param {Mixed} data The data to send
*/
send(event, data) {
if (!process.send) return;
process.send({
op: event,
d: data || null,
});
}
/**
* Fired when the shard receives a message
* @param {Object} message The message object
* @returns {*}
*/
onMessage(message) {
if (!message.op) {
return logger.warn('Received RPC message with no op.');
}
if (['resp', 'broadcast'].includes(message.op)) return;
if (this[message.op]) {
try {
return this[message.op](message);
} catch (err) {
return this.logger.error(err);
}
}
const command = this.commands.get(message.op);
if (!command) return;
try {
return command(this.dyno, this.config, message);
} catch (err) {
this.logger.error(err);
}
this.emit(message.op, message.d);
}
/**
* Send a command and await a response from the shard manager
* @param {String} op Op to send
* @param {Object} d The data to send
* @returns {Promise}
*/
awaitResponse(op, d) {
if (!process.send) return;
return new Promise((resolve, reject) => {
const awaitListener = (msg) => {
if (!['resp', 'error'].includes(msg.op)) return;
process.removeListener('message', awaitListener);
if (msg.op === 'resp') return resolve(msg.d);
if (msg.op === 'error') return reject(msg.d);
};
const payload = { op: op };
if (d) payload.d = d;
process.on('message', awaitListener);
process.send(payload);
setTimeout(() => {
process.removeListener('message', awaitListener);
reject('RPC Timed out.');
}, 5000);
});
}
/**
* Register an RPC command
* @param {Function} command The command to execute
* @returns {*|void}
*/
register(command) {
if (!command || !command.name) { return; }
logger.debug(`[RPCManager] Registering rpc command ${command.name}`);
const cmd = command(this);
cmd.pattern.topic = `dyno.bot`;
// cmd.pattern.cmd = `${cmd.pattern.cmd}.${this.clusterConfig.clusterId}`;
this.hemera.add(cmd.pattern, cmd.handler.bind(this));
}
}
module.exports = RPCManager;

93
src/core/managers/WebhookManager.js

@ -0,0 +1,93 @@
'use strict';
const axios = require('axios');
/**
* @class WebhookManager
*/
class WebhookManager {
/**
* Manage webhook operations
* @param {Object} config The Dyno configuration
* @param {Dyno} dyno The Dyno instance
*/
constructor(dyno) {
this.dyno = dyno;
this.config = dyno.config;
this.client = dyno.client;
this.avatarUrl = `${this.config.avatar}?r=${this.config.version}`;
this.default = {
username: 'Dyno',
avatarURL: this.avatarUrl,
tts: false,
};
}
/**
* Get or create a channel webhook
* @param {Channel} channel Eris channel object
* @returns {Promise}
*/
async getOrCreate(channel) {
let id = (typeof channel === 'string') ? channel : channel.id || null;
if (!id) return Promise.reject(`Invalid channel or id.`);
try {
const webhooks = await this.client.getChannelWebhooks(channel.id);
let webhook = webhooks.find(hook => hook.name === 'Dyno');
if (webhook) {
return Promise.resolve(webhook);
}
const res = await axios.get(this.avatarUrl, {
headers: { Accept: 'image/*' },
responseType: 'arraybuffer',
}).then(response => `data:${response.headers['content-type']};base64,${response.data.toString('base64')}`);
webhook = await this.client.createChannelWebhook(channel.id, {
name: 'Dyno',
avatar: res,
});
return Promise.resolve(webhook);
} catch (err) {
return Promise.reject(err);
}
}
/**
* Execute a webhook
* @param {Channel} channel Eris channel object
* @param {Object} options Webhook options to send
* @returns {Promise}
*/
async execute(channel, options, webhook) {
let avatarUrl = `https://cdn.discordapp.com/avatars/${this.dyno.user.id}/${this.dyno.user.avatar}.jpg`;
options.avatarURL = options.avatarURL || avatarUrl;
const content = Object.assign({}, this.default, options || {});
if (webhook) {
if (options.slack) {
delete options.slack;
return this.client.executeSlackWebhook(webhook.id, webhook.token, content);
}
return this.client.executeWebhook(webhook.id, webhook.token, content);
}
try {
const webhook = await this.getOrCreate(channel);
if (options.slack) {
delete options.slack;
return this.client.executeSlackWebhook(webhook.id, webhook.token, content);
}
return this.client.executeWebhook(webhook.id, webhook.token, content);
} catch (err) {
return Promise.reject(err);
}
}
}
module.exports = WebhookManager;

497
src/core/matomo.js

@ -0,0 +1,497 @@
var MatomoTracker = require('matomo-tracker');
var logger = require('./logger');
let matomoUsers, matomoGuilds, matomoMusicGuilds, matomoMusicUsers, actionLogBuffer, autoresponderBuffer, automodBuffer, commandBuffer, musicBuffer;
const regionToCountryCodeMap = {
'brazil': 'br',
'vip-us-west': 'us',
'vip-us-east': 'us',
'us-west': 'us',
'us-central': 'us',
'us-east': 'us',
'us-south': 'us',
'japan': 'jp',
'singapore': 'sg',
'hongkong': 'hk',
'vip-amsterdam': 'nl',
'amsterdam': 'nl',
'southafrica': 'za',
'london': 'gb',
'sydney': 'au',
'frankfurt': 'DE',
'russia': 'ru',
'eu-central': 'pl',
'eu-west': 'pt',
};
function initMatomo(dyno) {
matomoUsers = new MatomoTracker(4, 'http://10.12.0.69/piwik.php');
matomoGuilds = new MatomoTracker(5, 'http://10.12.0.69/piwik.php');
matomoMusicGuilds = new MatomoTracker(8, 'http://10.12.0.69/piwik.php');
matomoMusicUsers = new MatomoTracker(9, 'http://10.12.0.69/piwik.php');
actionLogBuffer = { guildBuffer: [] };
autoresponderBuffer = { guildBuffer: [] };
automodBuffer = { guildBuffer: [] };
musicBuffer = { guildBuffer: [], userBuffer: [] };
commandBuffer = { guildBuffer: [], userBuffer: [] };
setInterval(() => {
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; }
if (!dyno.isReady) { return; }
try {
const guildArr = [];
guildArr.push(...actionLogBuffer.guildBuffer);
actionLogBuffer.guildBuffer = [];
guildArr.push(...autoresponderBuffer.guildBuffer);
autoresponderBuffer.guildBuffer = [];
guildArr.push(...automodBuffer.guildBuffer);
automodBuffer.guildBuffer = [];
guildArr.push(...commandBuffer.guildBuffer);
commandBuffer.guildBuffer = [];
const userArr = [];
userArr.push(...commandBuffer.userBuffer);
commandBuffer.userBuffer = [];
let start = new Date().getTime();
if (guildArr.length > 0) {
matomoGuilds.trackBulk(guildArr, () => {
const end = new Date().getTime();
logger.debug(`Flushed Matomo guild buffer. Took ${Math.abs(start - end)}ms for ${guildArr.length} events`);
});
}
start = new Date().getTime();
if (userArr.length > 0) {
matomoUsers.trackBulk(userArr, () => {
const end = new Date().getTime();
logger.debug(`Flushed Matomo user buffer. Took ${Math.abs(start - end)}ms for ${userArr.length} events`);
});
}
const musicGuildArr = [];
musicGuildArr.push(...musicBuffer.guildBuffer);
musicBuffer.guildBuffer = [];
const musicUserArr = [];
musicUserArr.push(...musicBuffer.userBuffer);
musicBuffer.userBuffer = [];
start = new Date().getTime();
if (musicGuildArr.length > 0) {
matomoMusicGuilds.trackBulk(musicGuildArr, () => {
const end = new Date().getTime();
logger.debug(`Flushed Matomo music guild buffer. Took ${Math.abs(start - end)}ms for ${musicGuildArr.length} events`);
});
}
start = new Date().getTime();
if (musicUserArr.length > 0) {
matomoMusicUsers.trackBulk(musicUserArr, () => {
const end = new Date().getTime();
logger.debug(`Flushed Matomo music user buffer. Took ${Math.abs(start - end)}ms for ${musicUserArr.length} events`);
});
}
} catch (err) {
logger.error(err);
}
}, 10000);
setInterval(() => {
if (!dyno.isReady) { return; }
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; }
dyno.players.forEach((p) => {
if (!p.voiceChannel || !p.voiceChannel.voiceMembers || !p.playing) { return; }
const guild = p.voiceChannel.guild;
const country = regionToCountryCodeMap[guild.region] || 'aq';
musicBuffer.guildBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/session`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'Refresh',
uid: guild.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
}),
});
p.voiceChannel.voiceMembers.forEach((m) => {
musicBuffer.userBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/user/session`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'Refresh',
uid: m.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['User ID', m.id],
}),
});
});
});
}, 14 * 1000 * 60);
matomoUsers.on('error', (err) => {
logger.error(err);
});
matomoGuilds.on('error', (err) => {
logger.error(err);
});
matomoMusicGuilds.on('error', (err) => {
logger.error(err);
});
matomoMusicUsers.on('error', (err) => {
logger.error(err);
});
dyno.internalEvents.on('music', ({ type, guild, channel, user, search, trackInfo }) => {
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; }
let guildEvent;
let userEvent;
let player;
const country = regionToCountryCodeMap[guild.region] || 'aq';
switch (type) {
case 'start':
guildEvent = {
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/session/start`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'Start',
uid: guild.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Server', dyno.config.stateName],
}),
};
player = dyno.players.get(guild.id);
if (!player || !player.playing || !player.voiceChannel || !player.voiceChannel.voiceMembers) {
player.voiceChannel.voiceMembers.forEach((m) => {
musicBuffer.userBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/user/session/join`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'Start',
uid: m.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['User ID', m.id],
3: ['Server', dyno.config.stateName],
}),
});
});
}
break;
case 'end':
guildEvent = {
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/session/end`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'End',
uid: guild.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Server', dyno.config.stateName],
}),
};
player = dyno.players.get(guild.id);
if (!player || !player.voiceChannel || !player.voiceChannel.voiceMembers) {
break;
}
player.voiceChannel.voiceMembers.forEach((m) => {
musicBuffer.userBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/user/session/leave`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'End',
uid: m.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['User ID', m.id],
3: ['Server', dyno.config.stateName],
}),
});
});
break;
case 'join':
userEvent = {
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/user/session/join`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'Start',
uid: user.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['User ID', user.id],
3: ['Server', dyno.config.stateName],
}),
};
break;
case 'leave':
userEvent = {
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/user/session/leave`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'End',
uid: user.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['User ID', user.id],
3: ['Server', dyno.config.stateName],
}),
};
break;
case 'changeSong':
guildEvent = {
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/session/changeSong`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'ChangeSong',
uid: guild.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Server', dyno.config.stateName],
}),
};
break;
case 'playSong':
guildEvent = {
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/session/playSong`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'PlaySong',
uid: guild.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Server', dyno.config.stateName],
}),
};
break;
case 'search':
guildEvent = {
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/session/search`,
ua: 'Node.js',
search: search,
search_count: 1,
uid: guild.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Params', search],
3: ['Server', dyno.config.stateName],
}),
};
break;
case 'skip':
guildEvent = {
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/music/session/skip`,
action_name: 'Music',
ua: 'Node.js',
e_c: 'Music',
e_a: 'Session',
e_n: 'Skip',
uid: guild.id,
country,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Server', dyno.config.stateName],
}),
};
break;
}
if (guildEvent) {
musicBuffer.guildBuffer.push(guildEvent);
}
if (userEvent) {
musicBuffer.userBuffer.push(userEvent);
}
});
dyno.internalEvents.on('actionlog', ({ type, guild }) => {
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; }
if (!matomoUsers || !matomoGuilds) {
return;
}
if (type === 'commands') {
return;
}
actionLogBuffer.guildBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/actionlog/${type}`,
action_name: 'ActionLog',
ua: 'Node.js',
e_c: 'AutomatedAction',
e_a: 'ActionLog',
e_n: type,
uid: guild.id,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Server', dyno.config.stateName],
}),
});
});
dyno.internalEvents.on('autoresponder', ({ type, guild }) => {
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; }
if (!matomoUsers || !matomoGuilds) {
return;
}
autoresponderBuffer.guildBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/autoresponder/${type}`,
action_name: 'AutoResponder',
ua: 'Node.js',
e_c: 'AutomatedAction',
e_a: 'AutoResponder',
e_n: type,
uid: guild.id,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Server', dyno.config.stateName],
}),
});
});
dyno.internalEvents.on('automod', ({ type, guild }) => {
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; }
if (!matomoUsers || !matomoGuilds) {
return;
}
automodBuffer.guildBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/modules/automod/${type}`,
action_name: 'AutoMod',
ua: 'Node.js',
e_c: 'AutomatedAction',
e_a: 'AutoMod',
e_n: type,
uid: guild.id,
_cvar: JSON.stringify({
1: ['Guild ID', guild.id],
2: ['Server', dyno.config.stateName],
}),
});
});
dyno.commands.on('command', ({ command, message, guildConfig, args, time, isServerAdmin, isServerMod }) => {
if (dyno.globalConfig && dyno.globalConfig.enableMatomo && dyno.globalConfig.enableMatomo[dyno.config.stateName.toLowerCase()]) { return; }
if (!matomoUsers || !matomoGuilds) {
return;
}
const user = message.author;
const channel = message.channel;
const guild = channel.guild;
commandBuffer.userBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/commands/${command.name}/${user.id}`,
action_name: 'CommandUsed',
ua: 'Node.js',
e_c: 'Command',
e_a: command.module || command.group,
e_n: command.name,
gt_ms: time,
uid: user.id,
_cvar: JSON.stringify({
1: ['Command Name', command.name],
2: ['Arguments', args.join(' ')],
3: ['Guild ID', guild.id],
4: ['Server', dyno.config.stateName],
5: ['User ID', user.id],
}),
dimension2: (isServerAdmin || isServerMod) ? 'true' : 'false',
});
commandBuffer.userBuffer.push({
token_auth: dyno.globalConfig.matomoTokenAuth || 'ee9e3342c6a8f02ea0e1278060dd3db5',
url: `/commands/${command.name}/${user.id}`,
action_name: 'CommandUsed',
ua: 'Node.js',
e_c: 'Command',
e_a: command.module || command.group,
e_n: command.name,
gt_ms: time,
uid: guild.id,
_cvar: JSON.stringify({
1: ['Command Name', command.name],
2: ['Arguments', args.join(' ')],
3: ['Guild ID', guild.id],
4: ['Server', dyno.config.stateName],
5: ['User ID', user.id],
}),
});
});
}
module.exports = {
initMatomo,
};

182
src/core/metrics.js

@ -0,0 +1,182 @@
const express = require('express');
const cluster = require('cluster');
const prom = require('prom-client');
const config = require('./config');
const logger = require('./logger');
const server = express();
const Registry = prom.Registry;
const AggregatorRegistry = prom.AggregatorRegistry;
const aggregatorRegistry = new AggregatorRegistry();
const cmRegister = new Registry();
if (cluster.isMaster) {
server.get('/metrics', (req, res) => {
aggregatorRegistry.clusterMetrics((err, metrics) => {
if (err) logger.error(err);
res.set('Content-Type', aggregatorRegistry.contentType);
res.send(metrics);
});
});
server.get('/cm_metrics', (req, res) => {
res.set('Content-Type', cmRegister.contentType);
res.end(cmRegister.metrics());
});
cmRegister.setDefaultLabels({ server: config.stateName.toLowerCase() });
prom.collectDefaultMetrics({ register: cmRegister, prefix: 'dyno_cm_' });
server.listen(3001);
} else {
const defaultLabels = { clusterId: process.env.clusterId, server: config.stateName.toLowerCase() };
prom.register.setDefaultLabels(defaultLabels);
prom.collectDefaultMetrics({ prefix: 'dyno_app_' });
const messagesCounter = new prom.Counter({
name: 'dyno_app_messages_sent',
help: 'Counts messages sent (type = dm|normal|webhook)',
labelNames: ['type'],
});
const helpSentCounter = new prom.Counter({
name: 'dyno_app_help_sent',
help: 'Counts helps sent',
});
const helpFailedCounter = new prom.Counter({
name: 'dyno_app_help_failed',
help: 'Counts helps failed',
});
const guildsCarbon = new prom.Gauge({
name: 'dyno_app_guilds_carbon',
help: 'Guild count for Dyno',
});
const guildEvents = new prom.Counter({
name: 'dyno_app_guild_events',
help: 'Guild events counter (type = create, delete, etc)',
labelNames: ['type'],
});
const guildCounts = new prom.Gauge({
name: 'dyno_app_guild_count',
help: 'Guild count based on cluster id',
});
const userCounts = new prom.Gauge({
name: 'dyno_app_user_count',
help: 'User count based on cluster id',
});
const gatewayEvents = new prom.Gauge({
name: 'dyno_app_gateway_events',
help: 'GW Event counter (type = event type)',
labelNames: ['type'],
});
const messageEvents = new prom.Counter({
name: 'dyno_app_message_events',
help: 'Message events counter (type = create, delete, etc)',
labelNames: ['type'],
});
const discordShard = new prom.Counter({
name: 'dyno_app_discord_shard',
help: 'Discord shard status (type = connect, disconnect, resume, etc)',
labelNames: ['type'],
});
const commandSuccess = new prom.Counter({
name: 'dyno_app_command_success',
help: 'Command success counter (group = cmd group, name = cmd name)',
labelNames: ['group', 'name'],
});
const commandError = new prom.Counter({
name: 'dyno_app_command_error',
help: 'Command error counter (group = cmd group, name = cmd name)',
labelNames: ['group', 'name'],
});
const commandTimings = new prom.Histogram({
name: 'dyno_app_command_time',
help: 'Command timing histogram (group = cmd group, name = cmd name)',
labelNames: ['group', 'name'],
buckets: [100, 200, 300, 500, 800, 1000, 5000],
});
const purgeSuccessCounter = new prom.Counter({
name: 'dyno_app_purge_success',
help: 'Counts successful purges',
});
const purgeFailedCounter = new prom.Counter({
name: 'dyno_app_purge_failed',
help: 'Counts failed purges',
});
const eventLoopBlockCounter = new prom.Counter({
name: 'dyno_app_node_blocked',
help: 'Counts node event loop blocks',
});
const musicPlaylists = new prom.Counter({
name: 'dyno_app_music_playlists',
help: 'Counts music playlists',
});
const musicAdds = new prom.Counter({
name: 'dyno_app_music_adds',
help: 'Counts music adds',
});
const voiceTotals = new prom.Gauge({
name: 'dyno_app_voice_total',
help: 'Voice totals gauge',
labelNames: ['state'],
});
const voicePlaying = new prom.Gauge({
name: 'dyno_app_voice_playing',
help: 'Voice playing gauge',
labelNames: ['state'],
});
// Music module metrics
const musicModuleMetrics = [
new prom.Counter({
name: 'dyno_app_music_total_user_listen_time',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_total_playing_time',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_song_ends',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_partial_song_ends',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_unique_session_joins',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_disconnects',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_joins',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_leaves',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_plays',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_search',
help: 'Music module metrics',
}),
new prom.Counter({
name: 'dyno_app_music_skips',
help: 'Music module metrics',
}),
new prom.Summary({
name: 'dyno_app_music_user_session_summary',
help: 'Music module metrics',
}),
new prom.Summary({
name: 'dyno_app_music_session_summary',
help: 'Music module metrics',
}),
];
}

146
src/core/processManager/Manager.js

@ -0,0 +1,146 @@
const cluster = require('cluster');
const config = require('../config');
const logger = require('../logger');
const { Collection } = require('@dyno.gg/dyno-core');
const { Client, Server, LogServer } = require('../rpc');
const Process = require('./Process');
/**
* @class Manager
*/
class Manager {
constructor() {
this.processes = new Collection();
process.on('uncaughtException', this.handleException.bind(this));
process.on('unhandledRejection', this.handleRejection.bind(this));
cluster.on('exit', this.handleExit.bind(this));
cluster.setupMaster({
silent: true,
});
this.logServer = new LogServer();
this.logServer.init(5025);
this.clusterManager = this.createManager();
let methods = {
create: this.create.bind(this),
delete: this.delete.bind(this),
list: this.list.bind(this),
restart: this.restart.bind(this),
restartManager: this.restartManager.bind(this),
};
this.client = new Client(config.rpcHost || 'localhost', 5052);
this.server = new Server();
this.server.init(config.rpcHost || 'localhost', 5050, methods);
}
handleRejection(reason, p) {
try {
console.error('Unhandled rejection at: Promise ', p, 'reason: ', reason); // eslint-disable-line
} catch (err) {
console.error(reason); // eslint-disable-line
}
}
handleException(err) {
if (!err || (typeof err === 'string' && !err.length)) {
return console.error('An undefined exception occurred.'); // eslint-disable-line
}
console.error(err); // eslint-disable-line
}
createManager() {
const proc = new Process(this, {
manager: true,
});
this.logServer.hook(proc);
return proc;
}
createProcess(options) {
const process = new Process(this, options);
this.processes.set(process.id, process);
this.logServer.hook(process);
return process;
}
getProcess(worker) {
return this.processes.find(s => s.pid === worker.process.pid || s._pid === worker.process.pid);
}
handleExit(worker, code, signal) {
const process = this.getProcess(worker);
if (signal && signal === 'SIGTERM') return;
if (!process) return;
this.client.request('processExit', { process, code, signal });
// Restart worker
process.restartWorker().then(() => {
this.client.request('processReady', { process });
});
}
create(payload, cb) {
const options = payload && (payload.cluster || {});
const process = this.createProcess(options);
return cb(null, process);
}
delete(payload, cb) {
if (!payload.id) {
return cb('Missing ID');
}
const process = this.processes.get(payload.id);
if (!process) {
return cb(`Process ${payload.id} not found.`);
}
try {
process.worker.kill('SIGTERM');
this.processes.delete(payload.id);
return cb(null, 'OK');
} catch (err) {
logger.error(err);
return cb(err);
}
}
list(payload, cb) {
return cb(null, [...this.processes.values()]);
}
async restart(payload, cb) {
if (!payload.id) {
return cb('Missing ID');
}
const process = this.processes.get(payload.id);
if (!process) {
return cb(`Process ${payload.id} not found.`);
}
try {
const proc = await process.restartWorker(true);
return cb(null, proc);
} catch (err) {
logger.error(err);
return cb(err);
}
}
restartManager(payload, cb) {
this.clusterManager.restartWorker();
return cb(null, 'OK');
}
}
module.exports = Manager;

99
src/core/processManager/Process.js

@ -0,0 +1,99 @@
const cluster = require('cluster');
const uuid = require('uuid/v4');
var EventEmitter;
try {
EventEmitter = require('eventemitter3');
} catch (e) {
EventEmitter = require('events');
}
/**
* @class Process
* @extends {EventEmitter}
*/
class Process extends EventEmitter {
/**
* Representation of a process
*
* @prop {Number} id Process ID
* @prop {Object} worker The worker
* @prop {Object} process The worker process
* @prop {Number} pid The worker process ID
* @prop {Number} port The process RPC port
*/
constructor(manager, options = {}) {
super();
this.id = uuid();
this.pid = null;
this.port = null;
this.options = options || {};
this.createdAt = Date.now();
if (options.manager) {
this.manager = true;
this.port = 5052;
}
if (options.cluster && options.cluster.id) {
this.port = 30000 + parseInt(options.cluster.id, 10);
}
this.worker = this.createWorker(options.awaitReady);
this.process = this.worker.process;
}
createWorker(awaitReady = false) {
const worker = cluster.fork(
Object.assign({
awaitReady: awaitReady,
uuid: this.id,
}, this.options)
);
this.pid = worker.process.pid;
this.createdAt = Date.now();
process.nextTick(() => {
this._readyListener = this.ready.bind(this);
worker.on('message', this._readyListener);
});
return worker;
}
restartWorker(awaitReady = false) {
const worker = this.createWorker(awaitReady);
const oldWorker = this.worker;
const createdAt = Date.now();
this._pid = worker.process.pid;
return new Promise(resolve => {
this.on('ready', () => {
if (this.worker) {
oldWorker.kill('SIGTERM');
}
process.nextTick(() => {
this.worker = worker;
this.process = worker.process;
this.pid = worker.process.pid;
this.createdAt = createdAt;
return resolve(this);
});
});
});
}
ready(message) {
if (!message) return;
if (message === 'ready' || message.op === 'ready') {
this.worker.removeListener('ready', this._readyListener);
this.emit('ready');
}
}
}
module.exports = Process;

36
src/core/redis.js

@ -0,0 +1,36 @@
'use strict';
const Redis = require('ioredis');
const logger = require('./logger');
async function connect() {
return new Promise((resolve, reject) => {
const client = new Redis({
name: 'master',
sentinels: [
{ host: '10.12.0.55', port: 26379 },
{ host: '10.12.0.56', port: 26379 },
{ host: '10.12.0.57', port: 26379 },
{ host: '10.12.0.58', port: 26379 },
],
});
const rejectFunc = (err) => {
reject(err);
};
client.on('ready', () => {
logger.info('Connected to redis.');
client.removeListener('error', rejectFunc);
resolve(client);
});
client.once('error', rejectFunc);
client.on('error', err => {
logger.error(err);
});
});
}
module.exports = { connect };

14
src/core/rpc/Client.js

@ -0,0 +1,14 @@
const jayson = require('jayson/promise');
class Client {
constructor(host, port) {
this.client = jayson.client.http({
host,
port,
});
return this.client;
}
}
module.exports = Client;

44
src/core/rpc/LogServer.js

@ -0,0 +1,44 @@
/* eslint-disable no-unused-vars */
const WebSocket = require('ws');
const jayson = require('jayson');
class LogServer {
init(port) {
this.wss = new WebSocket.Server({ port });
}
hook(proc) {
proc.process.stdout.pipe(process.stdout);
proc.process.stdout.on('data', (data) => {
this._broadcastLog(data, proc, 'stdout');
});
proc.process.stderr.pipe(process.stderr);
proc.process.stderr.on('data', (data) => {
this._broadcastLog(data, proc, 'stderr');
});
}
_broadcastLog(data, proc, stream) {
const msg = data.toString();
const payload = {
pid: proc.pid,
createdAt: proc.createdAt,
cm: !!proc.manager,
msg,
stream,
};
if (proc.options && proc.options.id !== undefined) {
payload.cid = proc.options.id;
}
this.wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(payload));
}
});
}
}
module.exports = LogServer;

13
src/core/rpc/Server.js

@ -0,0 +1,13 @@
/* eslint-disable no-unused-vars */
const jayson = require('jayson');
class Server {
init(host, port, methods) {
this.host = host;
this.port = port;
this.server = jayson.server(methods);
this.server.http().listen(this.port, this.host);
}
}
module.exports = Server;

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

Loading…
Cancel
Save