mirror of
https://github.com/discordeno/discordeno.git
synced 2026-09-17 08:47:22 +00:00
BREAKING: node migration + major improvements and style changes (#2783)
* Setup turborepo (#2610) * chore: BREAKING move to monorepo structure * chore: setup turborepo * setup more eslint and tsconfig (#2612) * fix packages setting lcoation * add dist to ignore * style * fix: .vscode set prettier and remove deno configs (#2611) * fix: rewrite to process.env * chore: add node types * style: run eslint --fix * fix: tests import ext * chore: fix formatter * chore: add build script * chore: remove site from turborepo * chore: move to seperate packages * chore: seperate util * chore: rename to index * fix: utils * chore: bump typescript * fix: in process of fixing rest * fix: logger * style * chore: fix turbo script * fix: types * fix: types * fix: shard * fix: gateway * fixing: bot * fix: at least it can run now * chore: fix turbo script * chore: move plugins * chore: add type and utils export * chore: working bot type and cache plugin * Add git hooks (#2618) * CI: Setup ci (#2669) * ci: fix test * ci: fix typo * fix: turbo script * fixes: yarn and linter errors in embeds pkg * chore: fix yarn gitignore * Node migration - devcontainer (#2672) * ✨ - feat: devcontainer -> node * 👷♂ - ci: devcontainer - add tabnine, prettier * fix: ignore .env and debug.ts * fix: couple of linter errors * fix: linter error * fix: gateway linter errors * style: update style * style: fix bot style * fix: type * ci: move all old workflow * chore: close #2619 * chore: close #2671 * test: add mocha * chore: add typescript plugin * test: add mocha * test: add test to utils * test/ci: update ci and coverage * chore: change script naming * ci: update include test * test: add coverage * ci: fix cache * ci: fix ci and codecov * Discordeno Documentation (#2673) * Add git hooks * Add documentation generatation * Change Documentation Engine * Add documentation * Remove autogenerated docs * combine lint staged action into one * style: fix Co-authored-by: H01001000 <heiheiho000@gmail.com> * chore: new package client * test: add rest test * ci: enable codecov * fix: type * test: add test to all packages * ci: add release to gh per commit * fix: ci syntax * fix: package version * fix: publish script * fix: remove private from gateway * ci: add filter for changes * fix: ci syntex * ci: try fix path filter * ci: try fix path filter * test: add test * ci: fix string and boolean * fix: package and ci * chore: fix turbo type cache * ci: also publish to npm * ci: change to public * fix: not publish to npm * fix: dependencies * chore: fix fmt script * fix: better rest typecheck Closes #2621 * fix: run yarn install * feat: add transformers to rest * feat: add helpers to rest * test: move bot utils test * reverse change to release.yml * chore: add clean build * refactor: discordeno * chore: add import type * chore: remove bot * fix: change deps from bot to dd * chore: update yarn lock * test: temp remove test from logger * refactor: remove transformers in helpers/channel * type: close #2622 * ci(fix): explicitly define coverage file * refactor: remove transformers in helpers/emoji * type: fix discord guild type * feat: DiscordEditAutomoderationRule type * fix: remove unused type * feat: DiscordCreateGuildEmoji DiscordModifyGuildEmoji & DiscordModifyCHannel types * feat: DiscordCreateChannel DiscordBuDeleteMessages DiscordCreateMessage DiscordEditMessage * feat: DiscordCreateScheduledEvent EditScheduledEvent DiscordCreateInvite * fix: types for guild stuff for rest * feat: thread discord types for rest * feat: channel rest types * feat: member rest types * feat: more discord rest types * fix: type errors * fix: docs bot param name should be rest * type: fix type error * ci(fix): codecov * chore(client) :add export transformer * fix: verifySignature * test: fix types * test: add test prevent #2683 #2678 * fix: export transformer twice * ci: reuse cache * test: add test:unit-noTextCoverage * feat: add transform and constant package * fix: half fix #2683 #2688 * fix: #2688 fix all transformer.spec.ts * fix: transformer name * ci: update style * fix: dependencies * fix: naming * fix: yarn lock * fix: remove validations from helpers. Closes #2700 * fix: rest routes as a constants pkg * fix: esm import with .js * test: add exception case * feat: adding transformer * chore: change script name * chore: add path fixing to coverage file * fix: camelize tuple bug * fix: reverse transformers * fix: transformers folder to camel * fix: transformers as an object * fix: linter error * ci: run test with deno close #2701 * fix: test depends on build * fix: deno import node:crypto * fix: rest improvements * fix: channel type * fix: export user * chore: move to unit dir * test: moving bench * fix: remove transformers from rest package * fix: move toggle transformers to bot pkg * fix: move out gw helpers to gw package * test: add rest e2e test * fix: syntex * ci: add discord token * fix: ci not passing secret * test: add role test * ci: fix secret inherit * fix: role helpers transformer * test: increase timeout to 10s * fix: member helpers transformer * test: add member test * fix: name and type * fix: guild ban * test: fix test * fix: test add await * test: skip some test * test: increase timeout * chore: add transformer to import map * fix: test * test: add why-is-node-still-running * test: add debug hook * ci: add timeout incase any async running * test: try fix * test: turn on rest debug * fix: if undefined * test: reduce timeout * fix: queue not running after some request * fix: increase remaining after request without ratelimit header * fix: partial webhook * fix: fetch hooks not working if debug defined later * fix: nickname null to undefined * test: finish adding webhook test * refactor(test): move rest to utils * fix: add await * fix: sticker * feat: add message embed component transformer * fix: test not done * fix: arg type to bigString * fix: sendMessage * fix: add allowedMentions, interactionResponse * test: add emoji e2e test * test: add guild e2e test * refactor(test): remove extra rest call * fix: create emoji BINARY_TYPE_INVALID_DATA_URI * fix: create guild rate limit * fix: automode rule helpers and test * fix: test run in only * test: add some queue bucket test * test: remove empty test * refactor(test): use new guild * test: use new guild for other test * fix: guild not defind * reactor(test): remove duplicated creat channel * test: increase timeout to 30s * test: add thread test * fix: more transformers * fix: gateway helpers use transformers * fix: types belong in types pkg * fix: helpers use transformers * BREAKING: v19 rewrite to node + major improvements (#2703) * fix: move all to old folder * fix: cleanup types * fix: more cleanup * fix: more base cleanup * fix: token dotenv * fix: add base transformer * fix: partial error handling * fix: handle 429 rate limit * test(rest): fix unit test * fix(script): transform extension * test: fix error and buffer * feat: camelizer util * fix: cleanup * fix: rate limit queues and headers processing * fix: rest exports * fix: no more transformers * fix: queue header null bug * fix: add gateway package base * fix: lint error * fix: add prettier file * fix: prettier is default fmtr * fix: fmt shard file * fix: fmt * fix: types issue * fix: remove unused consts * fix: all import issues * fix: import error * fix: import ending with .js * fix: remove transformers package * Fix eslint (#2710) * fix: typing of button component label to be optional (#2708) * feat: add guild_connections to role tags and toggles (#2706) Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> * Add lint checks and autofixing workflow (#2702) * Add lint checks and autofixing workflow * Update lint.yml * Fix: use yarn instead npm * fix: add ts to eslint_extensions * fix: update dir * fix: lint.yml format Co-authored-by: Jonathan Ho <heiheiho000@gmail.com> * fix: unused deps * ci: fix e2e test not running * chore: run yarn install * test: this should run test * feat: getCHannel * feat: createEMji helper * feat: collection class in util package * fix: gateway bugs * why on earth is this needed change * fix: cleanup docs on collection * Emoji rest methods (node-migration-clean) (#2713) * feat: all emoji rest methods * Fix code style issues with ESLint Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * fix: stuff * fix: bot types * fix: remove logs * fix: camelize all gateway payloads * fix: remove todos * fix: start deris * fix: lint/ts errors except shard file * thats 1 way to fix type errors * yes (#2714) Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> * feat: rest channel helpers * Update release.yml * Update release.yml * ci: add build filter * ci: fail fast false * fix: complete webhook related helpers * fix: follow announcement helper * add forum helper * add stage helpers * add thread related helpers * alphabetize * cleanup webhook routes * automod helpers * scheduled events * integrations helpers * invite stuff incomplete * dm channel and avatar url * chore: move ts-node into package * fix: importing esm * fix: tris message helpers * test(rest): add simplifyUrl test * test(rest): add checkRateLimits test * test(rest): add processRateLimitedPaths test * test(rest): fix missing beforeEach * test(utils): enable old test * interaction helpers * perf(utils): optimize snake to camel case conversion (#2717) * perf(utils): optimize snake to camel case conversion * fmt * wont change much but still faster * actually this was a stupid idea * shh * Fix(client): Fix typings. (#2716) Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> * test: fix rest and utils test (#2723) * test(utils): fix cant import collection * test(rest): await expect * fix(utils): deno compactability * test(utils): typing * fix(utils): add return type * Add rest helpers for templates (#2727) * Add rest helpers for templates * Fix code style issues with ESLint Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * fix: remove frozenAt * fix: cleanup templates * ci: release to npm (#2725) * ci: release to npm * Update release.yml * 🐛 - fix: types - slashcommands - add nsfw prop (#2731) * feat(rest helpers): Add template and member helpers. (#2728) * feat(rest helpers): Add template and member helpers. * format code * feat(rest): add template routes * fix(rest): routes and runMethod * fix(rest): try to fix most of type Co-authored-by: H01001000 <heiheiho000@gmail.com> Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> * ci: fix release (#2732) * fix: rest type errors * fix: v19 begin * fix: yarn lock * chore: fix deps and script (#2733) * fix: bug camelizer deleting letters * fix: falsy token check * fix: add frozenat check for queue * fix: max stack trace error do to infinite loop * fix: type error * test(rest): fix missing import (#2734) Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> * fix: file paths for imports * fix: lastShardId should default to 0 * fix: use isomorphic ws * test(rest): fix "TypeError: [Function] is not a thenable" (#2736) * test(utils): add utils tests (#2737) * test(utils): add urltobase64 test * test(utils): add token test * test(utils): fix missing import buffer * test(utils): add casting test * test(utils): add casting test * test(utils): fix use correct function * chore: make eslint error if missing .js extension (#2735) * chore: eslint error if missing .js extension * chore * lint: fix missing .js error Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> * fix: readme runtime list (#2739) * Bot pkg (#2740) * fix: bot pkg test * Fix code style issues with ESLint * Update Guild.ts * Update Guild.ts * Fix code style issues with ESLint Co-authored-by: Lint Action <lint-action@samuelmeuli.com> Co-authored-by: Jonathan Ho <heiheiho000@gmail.com> * Update tsdoc.json (#2741) Updates tsdoc.json to reflect the current packages. * Interaction types - remove member,channel,role from value type (#2743) https://discord.com/channels/785384884197392384/1067265182776176690/1068189883073572924 * Add missing types (#2742) * Revert "fix: use isomorphic ws" (#2744) This reverts commitad306b0d0a. * fix: interaction requests that sent without full url * fix: lint issues * fix: remote gateway test file * fix: interaction response bug with body being invalid (#2746) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * fix: interaction followup type (#2747) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests * fix: perma fix for type error in ci * fix: followupmessage option type --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * fix: e2e exit bug (#2748) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests * fix: perma fix for type error in ci * fix: followupmessage option type * fix: e2e tests exit bug * fix: color console logger --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * fix: rest sending attachments files (#2749) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests * fix: perma fix for type error in ci * fix: followupmessage option type * fix: e2e tests exit bug * fix: color console logger * fix: image file sending * Fix code style issues with ESLint --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * fix: guild and role methods (#2751) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests * fix: perma fix for type error in ci * fix: followupmessage option type * fix: e2e tests exit bug * fix: color console logger * fix: image file sending * Fix code style issues with ESLint * guild and role methods * Fix code style issues with ESLint --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * ci: add bot package (#2752) * ci: add bot package * ci: fix test * e2e test stuff (#2754) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests * fix: perma fix for type error in ci * fix: followupmessage option type * fix: e2e tests exit bug * fix: color console logger * fix: image file sending * Fix code style issues with ESLint * guild and role methods * Fix code style issues with ESLint * fix: dont send heartbeat if socket is not open * fix: remove logs * fox: remove more logs * fix some bugs in role tests * Switch to after hook style * hoti's speed snaker * auto convert objects for discord * Fix code style issues with ESLint * fix: remove dup imports * fix: i hate linters * speeder --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * fix: delete guilds test (#2758) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests * fix: perma fix for type error in ci * fix: followupmessage option type * fix: e2e tests exit bug * fix: color console logger * fix: image file sending * Fix code style issues with ESLint * guild and role methods * Fix code style issues with ESLint * fix: dont send heartbeat if socket is not open * fix: remove logs * fox: remove more logs * fix some bugs in role tests * Switch to after hook style * hoti's speed snaker * auto convert objects for discord * Fix code style issues with ESLint * fix: remove dup imports * fix: i hate linters * speeder * fix: tests delete guilds * Fix code style issues with ESLint --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * chore: fix deps (#2757) * ci/test: fix bot pkg e2e test (#2759) * chore: fix script and update import map (#2761) * chore: fix version script * chore: update import map * chore: fix ws import map * chore: fix deno import map * test: fix * fix: more rest e2e tests (#2763) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests * fix: perma fix for type error in ci * fix: followupmessage option type * fix: e2e tests exit bug * fix: color console logger * fix: image file sending * Fix code style issues with ESLint * guild and role methods * Fix code style issues with ESLint * fix: dont send heartbeat if socket is not open * fix: remove logs * fox: remove more logs * fix some bugs in role tests * Switch to after hook style * hoti's speed snaker * auto convert objects for discord * Fix code style issues with ESLint * fix: remove dup imports * fix: i hate linters * speeder * fix: tests delete guilds * Fix code style issues with ESLint * fix: easier to provide custom intents in bot * fix: shutdown bot after test * fix: add getGuild * fix: multiple guild delete attempts * fix: add emoji e2e tests * fix: remaining old e2e rest tests * Fix code style issues with ESLint --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * test: add gateway integration test (#2756) * test: add gateway integration test * test(gateway): fix connection test * test(gateway): add heartbeat test * ci: add integration test * fix: add uWebSockets.js * ci: add timeout * test(utils): remove old test * Revert "test(utils): remove old test" This reverts commit04fb6dd4b5. * test(gateway): fix uws server * test(gateway): fix type * chore: update codecov flag * test(gateway): remove dev code --------- Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> * Update release.yml (#2768) * fix: bot logger (#2769) * fix: readme runtime list * Fix code style issues with ESLint * node 18 * fix: websocket import type * fix: body for interaction requests * fix: perma fix for type error in ci * fix: followupmessage option type * fix: e2e tests exit bug * fix: color console logger * fix: image file sending * Fix code style issues with ESLint * guild and role methods * Fix code style issues with ESLint * fix: dont send heartbeat if socket is not open * fix: remove logs * fox: remove more logs * fix some bugs in role tests * Switch to after hook style * hoti's speed snaker * auto convert objects for discord * Fix code style issues with ESLint * fix: remove dup imports * fix: i hate linters * speeder * fix: tests delete guilds * Fix code style issues with ESLint * fix: easier to provide custom intents in bot * fix: shutdown bot after test * fix: add getGuild * fix: multiple guild delete attempts * fix: add emoji e2e tests * fix: remaining old e2e rest tests * Fix code style issues with ESLint * fix: add bot.logger * fix: make logger name capital --------- Co-authored-by: Lint Action <lint-action@samuelmeuli.com> * chore: update readme (#2772) * chore: add coverage per pkg * chore: add npm version * chore: add test status * chore: fix ci * chore: fix ci * ci: fix ci needs * chore: add only push event * style: remove import logger * 📚 - docs: fix README package links (#2773) * test(all): add test importing index (#2774) * test(all): add test importing index * chore: remove old benchmark dir * chore: disable coverage status fail * test(client): add import test with try catch * test(rest): fix narrow import scope * test(utils): add test (#2764) * test(utils): remove old test * test(utils): add color test * test(utils): fix import mocha * test(utils): fix test type error * test(utils): remove dev code * fix(utils): bucket not export all function * test(utils): add some test for bucket * fix(utils): close #2775 * test(utils): add test for permissions.ts * test(utils): fix missing mocha import * fix(utils): better fix for #2775 * feat: addReaction & addReactions * feat: connectToVoice * fix: linters issues * fix: remove aliases and add createGuildFromTemplate * feat: deleteMessages * fix: reaction related helpers * mfa level * voie states editing * image urls * fix: typos * get message typeguards * fix: more helpers * fix: remaining helpers * fix: add logs to gateway manager * fix: rest resolve sends status and body * fix: lots of errors * fix: client errors * fix: remove old pkg * snaker * fix: broken util import for image url * fix: cleanup shard and circular deps * fix: remove ThreadChannel from GuildChannel * fix: generate interaction usage * fix: more bugs * fix: use node:events to import * fix(rest): add interface RestRequestRejection (#2782) * fix: remove invalid todo * fix: timeout bug --------- Co-authored-by: Skillz <skillz@discord.gg/ddeno> Co-authored-by: Jonathan Ho <heiheiho000@gmail.com> Co-authored-by: deepsarda <92147339+deepsarda@users.noreply.github.com> Co-authored-by: Awesome Stickz <awesome@stickz.dev> Co-authored-by: Lint Action <lint-action@samuelmeuli.com> Co-authored-by: Lars_und_so <46791248+Larsundso@users.noreply.github.com> Co-authored-by: ITOH <to@itoh.at> Co-authored-by: Yaikava <83710104+Yaikava@users.noreply.github.com> Co-authored-by: Andreas Fink <mail@afink.dev> --------- Co-authored-by: Jonathan Ho <48591478+H01001000@users.noreply.github.com> Co-authored-by: H01001000 <heiheiho000@gmail.com> Co-authored-by: deepsarda <92147339+deepsarda@users.noreply.github.com> Co-authored-by: Yaikava <83710104+Yaikava@users.noreply.github.com> Co-authored-by: Skillz <skillz@discord.gg/ddeno> Co-authored-by: Awesome Stickz <awesome@stickz.dev> Co-authored-by: Lint Action <lint-action@samuelmeuli.com> Co-authored-by: Lars_und_so <46791248+Larsundso@users.noreply.github.com> Co-authored-by: ITOH <to@itoh.at> Co-authored-by: Andreas Fink <mail@afink.dev>
This commit is contained in:
co-authored by
H01001000
Lint Action
Skillz
deepsarda
Awesome Stickz
Lars_und_so
ITOH
Yaikava
Andreas Fink
Jonathan Ho
parent
1a717ddc7d
commit
2714e1e7d1
@@ -0,0 +1,13 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:0-buster
|
||||
|
||||
ENV DENO_INSTALL=/deno
|
||||
RUN mkdir -p /deno \
|
||||
&& curl -fsSL https://deno.land/x/install/install.sh | sh \
|
||||
&& chown -R vscode /deno
|
||||
|
||||
ENV PATH=${DENO_INSTALL}/bin:${PATH} \
|
||||
DENO_DIR=${DENO_INSTALL}/.cache/deno
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
@@ -0,0 +1,51 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.162.0/containers/deno
|
||||
{
|
||||
"name": "Deno",
|
||||
"dockerFile": "Dockerfile",
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"deno.enable": true,
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"editor.minimap.enabled": false,
|
||||
"editor.wordWrap": "on",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll": true
|
||||
},
|
||||
"editor.fontSize": 16,
|
||||
"workbench.colorTheme": "Material Theme Darker",
|
||||
"workbench.iconTheme": "eq-material-theme-icons-darker",
|
||||
"breadcrumbs.enabled": true,
|
||||
"editor.renderWhitespace": "all",
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.formatOnSave": true,
|
||||
"files.autoSave": "afterDelay",
|
||||
"editor.fontFamily": "Fira Code, Menlo, Monaco, 'Courier New', monospace",
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"deno.inlayHints.enumMemberValues.enabled": true,
|
||||
"deno.inlayHints.functionLikeReturnTypes.enabled": true,
|
||||
"deno.inlayHints.parameterNames.enabled": "all",
|
||||
"deno.inlayHints.parameterNames.suppressWhenArgumentMatchesName": false,
|
||||
"deno.inlayHints.parameterTypes.enabled": true,
|
||||
"deno.inlayHints.propertyDeclarationTypes.enabled": true,
|
||||
"deno.inlayHints.variableTypes.enabled": true,
|
||||
"deno.inlayHints.variableTypes.suppressWhenTypeMatchesName": false
|
||||
},
|
||||
// Add the Ids of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"denoland.vscode-deno",
|
||||
"pkief.material-icon-theme",
|
||||
"coenraads.bracket-pair-colorizer",
|
||||
"equinusocio.vsc-material-theme",
|
||||
"tabnine.tabnine-vscode"
|
||||
],
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
// Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker.
|
||||
// "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.env
|
||||
fileloader.ts
|
||||
|
||||
config.json
|
||||
|
||||
# Folder made by Kwik db to watch guild command versions
|
||||
db/
|
||||
node_modules
|
||||
package-lock.json
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll": true
|
||||
},
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"deno.suggest.imports.hosts": {
|
||||
"https://deno.land": true
|
||||
},
|
||||
"discord.enabled": true,
|
||||
"cSpell.words": [
|
||||
"denoland",
|
||||
"Discordeno",
|
||||
"Kwik",
|
||||
"Loglevels",
|
||||
"msgpack",
|
||||
"Slowmode",
|
||||
"upsert"
|
||||
],
|
||||
"deno.unstable": true,
|
||||
"deno.inlayHints.enumMemberValues.enabled": true,
|
||||
"deno.inlayHints.functionLikeReturnTypes.enabled": true,
|
||||
"deno.inlayHints.parameterNames.enabled": "all",
|
||||
"deno.inlayHints.parameterNames.suppressWhenArgumentMatchesName": false,
|
||||
"deno.inlayHints.parameterTypes.enabled": true,
|
||||
"deno.inlayHints.propertyDeclarationTypes.enabled": true,
|
||||
"deno.inlayHints.variableTypes.enabled": true,
|
||||
"deno.inlayHints.variableTypes.suppressWhenTypeMatchesName": false
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Discordeno Bot Template
|
||||
|
||||

|
||||
|
||||
This repo is meant as a template which you can use to create a Discord bot very easily using the
|
||||
[Discordeno library](https://github.com/discordeno/discordeno).
|
||||
|
||||
[Website/Guide](https://discordeno.mod.land/)
|
||||
|
||||
[Discord Server](https://discord.com/invite/5vBgXk3UcZ)
|
||||
@@ -0,0 +1,2 @@
|
||||
BOT_TOKEN=''
|
||||
DEV_GUILD_ID=''
|
||||
@@ -0,0 +1,18 @@
|
||||
# Beginner Bot Template
|
||||
|
||||
This template is designed for the beginner developer to start coding discord bots.
|
||||
|
||||
Make sure to install the latest version when you use it.
|
||||
|
||||
## Setup
|
||||
|
||||
- [Click here](https://github.com/discordeno/template/generate) to make your own copy.
|
||||
- Delete all the template folders except the beginner folder.
|
||||
- Move all files from this folder to the root of the project.
|
||||
- You may encounter an issue with README.md file but force move the files to the root of the project.
|
||||
- Rename the .env.example file to .env OR create a new .env file and copy the example file code to this new file.
|
||||
- Fill out the .env file
|
||||
|
||||
## Run Bot
|
||||
|
||||
- deno run -A mod.ts
|
||||
@@ -0,0 +1,37 @@
|
||||
import { configs } from './configs.ts.js';
|
||||
import type {
|
||||
BotWithCache,
|
||||
BotWithHelpersPlugin} from './deps.ts.js';
|
||||
import {
|
||||
Collection,
|
||||
createBot,
|
||||
enableCachePlugin,
|
||||
enableCacheSweepers,
|
||||
enableHelpersPlugin,
|
||||
enablePermissionsPlugin,
|
||||
GatewayIntents,
|
||||
} from './deps.ts.js';
|
||||
import type { Command } from './src/types/commands.ts.js';
|
||||
|
||||
// MAKE THE BASIC BOT OBJECT
|
||||
const bot = createBot({
|
||||
token: configs.token,
|
||||
botId: configs.botId,
|
||||
intents: GatewayIntents.Guilds,
|
||||
events: {},
|
||||
});
|
||||
|
||||
// ENABLE ALL THE PLUGINS THAT WILL HELP MAKE IT EASIER TO CODE YOUR BOT
|
||||
enableHelpersPlugin(bot);
|
||||
enableCachePlugin(bot);
|
||||
enableCacheSweepers(bot as BotWithCache);
|
||||
enablePermissionsPlugin(bot as BotWithCache);
|
||||
|
||||
export interface BotClient extends BotWithCache<BotWithHelpersPlugin> {
|
||||
commands: Collection<string, Command>;
|
||||
}
|
||||
|
||||
// THIS IS THE BOT YOU WANT TO USE EVERYWHERE IN YOUR CODE! IT HAS EVERYTHING BUILT INTO IT!
|
||||
export const Bot = bot as BotClient;
|
||||
// PREPARE COMMANDS HOLDER
|
||||
Bot.commands = new Collection();
|
||||
@@ -0,0 +1,19 @@
|
||||
import { dotEnvConfig } from './deps.ts.js';
|
||||
|
||||
// Get the .env file that the user should have created, and get the token
|
||||
const env = dotEnvConfig({ export: true, path: "./.env" });
|
||||
const token = env.BOT_TOKEN || "";
|
||||
|
||||
export interface Config {
|
||||
token: string;
|
||||
botId: bigint;
|
||||
}
|
||||
|
||||
export const configs = {
|
||||
/** Get token from ENV variable */
|
||||
token,
|
||||
/** Get the BotId from the token */
|
||||
botId: BigInt(atob(token.split(".")[0])),
|
||||
/** The server id where you develop your bot and want dev commands created. */
|
||||
devGuildId: BigInt(env.DEV_GUILD_ID!),
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "https://deno.land/x/discordeno@17.0.0/mod.ts";
|
||||
export * from "https://deno.land/x/discordeno@17.0.0/plugins/mod.ts";
|
||||
|
||||
// Terminal Colors!
|
||||
export * from "https://deno.land/std@0.117.0/fmt/colors.ts";
|
||||
// Get data from .env files
|
||||
export { config as dotEnvConfig } from "https://deno.land/x/dotenv@v3.1.0/mod.ts";
|
||||
// Database, thx Tri!
|
||||
export { decode as KwikDecode, encode as KwikEncode, Kwik } from "https://deno.land/x/kwik@v1.3.1/mod.ts";
|
||||
@@ -0,0 +1,25 @@
|
||||
import { startBot } from './deps.ts.js';
|
||||
import log from './src/utils/logger.ts.js';
|
||||
import { fileLoader, importDirectory } from './src/utils/loader.ts.js';
|
||||
import { updateApplicationCommands } from './src/utils/updateCommands.ts.js';
|
||||
// setup db
|
||||
import './src/database/mod.ts.js';
|
||||
import { Bot } from './bot.ts.js';
|
||||
|
||||
log.info("Starting bot...");
|
||||
|
||||
// Forces deno to read all the files which will fill the commands/inhibitors cache etc.
|
||||
await Promise.all(
|
||||
[
|
||||
"./src/commands",
|
||||
"./src/events",
|
||||
// "./src/tasks",
|
||||
].map((path) => importDirectory(Deno.realPathSync(path))),
|
||||
);
|
||||
await fileLoader();
|
||||
|
||||
// UPDATES YOUR COMMANDS TO LATEST COMMANDS
|
||||
await updateApplicationCommands();
|
||||
|
||||
// STARTS THE CONNECTION TO DISCORD
|
||||
await startBot(Bot);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Bot } from '../../bot.ts.js';
|
||||
import type { Command } from '../types/commands.ts.js';
|
||||
|
||||
export function createCommand(command: Command) {
|
||||
Bot.commands.set(command.name, command);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ApplicationCommandTypes, InteractionResponseTypes } from '../../deps.ts.js';
|
||||
import { snowflakeToTimestamp } from '../utils/helpers.ts.js';
|
||||
import { createCommand } from './mod.ts.js';
|
||||
|
||||
createCommand({
|
||||
name: "ping",
|
||||
description: "Ping the Bot!",
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
execute: async (Bot, interaction) => {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
|
||||
await Bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `🏓 Pong! ${ping}ms`,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Kwik, KwikDecode, KwikEncode } from '../../deps.ts.js';
|
||||
import { logger } from '../utils/logger.ts.js';
|
||||
|
||||
const log = logger({ name: "DB Manager" });
|
||||
|
||||
log.info("Initializing Database");
|
||||
|
||||
const kwik = new Kwik();
|
||||
|
||||
// Add BigInt Support
|
||||
kwik.msgpackExtensionCodec.register({
|
||||
type: 0,
|
||||
encode: (object: unknown): Uint8Array | null => {
|
||||
if (typeof object === "bigint") {
|
||||
if (object <= Number.MAX_SAFE_INTEGER && object >= Number.MIN_SAFE_INTEGER) {
|
||||
return KwikEncode(parseInt(object.toString(), 10), {});
|
||||
} else {
|
||||
return KwikEncode(object.toString(), {});
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
decode: (data: Uint8Array) => {
|
||||
return BigInt(KwikDecode(data, {}) as string);
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize the Database
|
||||
await kwik.init();
|
||||
|
||||
log.info("Database Initialized!");
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Bot } from '../../bot.ts.js';
|
||||
import { InteractionTypes } from '../../deps.ts.js';
|
||||
import log from '../utils/logger.ts.js';
|
||||
|
||||
Bot.events.interactionCreate = (_, interaction) => {
|
||||
if (!interaction.data) return;
|
||||
|
||||
switch (interaction.type) {
|
||||
case InteractionTypes.ApplicationCommand:
|
||||
log.info(
|
||||
`[Application Command] ${interaction.data.name} command executed.`,
|
||||
);
|
||||
Bot.commands.get(interaction.data.name!)?.execute(Bot, interaction);
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Bot } from '../../bot.ts.js';
|
||||
import log from '../utils/logger.ts.js';
|
||||
|
||||
Bot.events.ready = (_, payload) => {
|
||||
log.info(`[READY] Shard ID ${payload.shardId} of ${Bot.gateway.lastShardId + 1} shards is ready!`);
|
||||
|
||||
if (payload.shardId === Bot.gateway.lastShardId) {
|
||||
botFullyReady();
|
||||
}
|
||||
};
|
||||
|
||||
// This function lets you run custom code when all your bot's shards are online.
|
||||
function botFullyReady() {
|
||||
// DO STUFF YOU WANT HERE ONCE BOT IS FULLY ONLINE.
|
||||
log.info("[READY] Bot is fully online.");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { BotClient } from '../../bot.ts.js';
|
||||
import type { ApplicationCommandOption, ApplicationCommandTypes, Interaction } from '../../deps.ts.js';
|
||||
|
||||
export interface Command {
|
||||
/** The name of this command. */
|
||||
name: string;
|
||||
/** What does this command do? */
|
||||
description: string;
|
||||
/** The type of command this is. */
|
||||
type: ApplicationCommandTypes;
|
||||
/** Whether or not this command is for the dev server only. */
|
||||
devOnly?: boolean;
|
||||
/** The options for this command */
|
||||
options?: ApplicationCommandOption[];
|
||||
/** This will be executed when the command is run. */
|
||||
execute: (bot: BotClient, interaction: Interaction) => unknown;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file will export all of the types in this directory.
|
||||
|
||||
export * from './commands.ts.js';
|
||||
@@ -0,0 +1,3 @@
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import log from './logger.ts.js';
|
||||
|
||||
// Very important to make sure files are reloaded properly
|
||||
let uniqueFilePathCounter = 0;
|
||||
let paths: string[] = [];
|
||||
|
||||
/** This function allows reading all files in a folder. Useful for loading/reloading commands, monitors etc */
|
||||
export async function importDirectory(path: string) {
|
||||
path = path.replaceAll("\\", "/");
|
||||
const files = Deno.readDirSync(Deno.realPathSync(path));
|
||||
const folder = path.substring(path.indexOf("/src/") + 5);
|
||||
|
||||
if (!folder.includes("/")) log.info(`Loading ${folder}...`);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.name) continue;
|
||||
|
||||
const currentPath = `${path}/${file.name}`;
|
||||
if (file.isFile) {
|
||||
if (!currentPath.endsWith(".ts")) continue;
|
||||
paths.push(
|
||||
`import "${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf("/"))}/${
|
||||
currentPath.substring(
|
||||
currentPath.indexOf("src/"),
|
||||
)
|
||||
}#${uniqueFilePathCounter}";`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await importDirectory(currentPath);
|
||||
}
|
||||
|
||||
uniqueFilePathCounter++;
|
||||
}
|
||||
|
||||
/** Imports all everything in fileloader.ts */
|
||||
export async function fileLoader() {
|
||||
await Deno.writeTextFile(
|
||||
"fileloader.ts",
|
||||
paths.join("\n").replaceAll("\\", "/"),
|
||||
);
|
||||
await import(
|
||||
`${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf("/"))}/fileloader.ts#${uniqueFilePathCounter}`
|
||||
);
|
||||
paths = [];
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { bold, cyan, gray, italic, red, yellow } from '../../deps.ts.js';
|
||||
|
||||
export enum LogLevels {
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
const prefixes = new Map<LogLevels, string>([
|
||||
[LogLevels.Debug, "DEBUG"],
|
||||
[LogLevels.Info, "INFO"],
|
||||
[LogLevels.Warn, "WARN"],
|
||||
[LogLevels.Error, "ERROR"],
|
||||
[LogLevels.Fatal, "FATAL"],
|
||||
]);
|
||||
|
||||
const noColor: (str: string) => string = (msg) => msg;
|
||||
const colorFunctions = new Map<LogLevels, (str: string) => string>([
|
||||
[LogLevels.Debug, gray],
|
||||
[LogLevels.Info, cyan],
|
||||
[LogLevels.Warn, yellow],
|
||||
[LogLevels.Error, (str: string) => red(str)],
|
||||
[LogLevels.Fatal, (str: string) => red(bold(italic(str)))],
|
||||
]);
|
||||
|
||||
export function logger({
|
||||
logLevel = LogLevels.Info,
|
||||
name,
|
||||
}: {
|
||||
logLevel?: LogLevels;
|
||||
name?: string;
|
||||
} = {}) {
|
||||
function log(level: LogLevels, ...args: any[]) {
|
||||
if (level < logLevel) return;
|
||||
|
||||
let color = colorFunctions.get(level);
|
||||
if (!color) color = noColor;
|
||||
|
||||
const date = new Date();
|
||||
const log = [
|
||||
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
|
||||
color(prefixes.get(level) || "DEBUG"),
|
||||
name ? `${name} >` : ">",
|
||||
...args,
|
||||
];
|
||||
|
||||
switch (level) {
|
||||
case LogLevels.Debug:
|
||||
return console.debug(...log);
|
||||
case LogLevels.Info:
|
||||
return console.info(...log);
|
||||
case LogLevels.Warn:
|
||||
return console.warn(...log);
|
||||
case LogLevels.Error:
|
||||
return console.error(...log);
|
||||
case LogLevels.Fatal:
|
||||
return console.error(...log);
|
||||
default:
|
||||
return console.log(...log);
|
||||
}
|
||||
}
|
||||
|
||||
function setLevel(level: LogLevels) {
|
||||
logLevel = level;
|
||||
}
|
||||
|
||||
function debug(...args: any[]) {
|
||||
log(LogLevels.Debug, ...args);
|
||||
}
|
||||
|
||||
function info(...args: any[]) {
|
||||
log(LogLevels.Info, ...args);
|
||||
}
|
||||
|
||||
function warn(...args: any[]) {
|
||||
log(LogLevels.Warn, ...args);
|
||||
}
|
||||
|
||||
function error(...args: any[]) {
|
||||
log(LogLevels.Error, ...args);
|
||||
}
|
||||
|
||||
function fatal(...args: any[]) {
|
||||
log(LogLevels.Fatal, ...args);
|
||||
}
|
||||
|
||||
return {
|
||||
log,
|
||||
setLevel,
|
||||
debug,
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
fatal,
|
||||
};
|
||||
}
|
||||
|
||||
export const log = logger({ name: "Main" });
|
||||
export default log;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Bot } from '../../bot.ts.js';
|
||||
import { configs } from '../../configs.ts.js';
|
||||
|
||||
export async function updateApplicationCommands() {
|
||||
await Bot.helpers.upsertGlobalApplicationCommands(
|
||||
Bot.commands
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter((command) => !command.devOnly)
|
||||
.array(),
|
||||
);
|
||||
|
||||
await Bot.helpers.upsertGuildApplicationCommands(
|
||||
configs.devGuildId,
|
||||
Bot.commands
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter((command) => !!command.devOnly)
|
||||
.array(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
# For Prisma use. Remember to remove the [ and ]
|
||||
DATABASE_URL=postgres:#[username]:[password]@[host]:[port]/[db]
|
||||
|
||||
# For other configs, update src/configs.ts
|
||||
|
||||
# General Configurations
|
||||
# Whether or not this process is a local development version. Change to false for the main production bot. */
|
||||
# SETUP-DD-TEMP: Change this to false in your server for production bot. Keep true in local testing.
|
||||
DEVELOPMENT=true
|
||||
|
||||
# The server id where you develop/test the bot. */
|
||||
# SETUP-DD-TEMP: Change the id to a server where you develop the bot privately.
|
||||
DEV_SERVER_ID=
|
||||
|
||||
# The discord bot token, without the BOT prefix. */
|
||||
# SETUP-DD-TEMP: Add the bot token here.
|
||||
DISCORD_TOKEN=
|
||||
|
||||
# Bot Configurations
|
||||
# The secret passcode that the bot code (event handler) is listening for. This is used to prevent someone else from trying to send malicious messages to your bot. */
|
||||
# SETUP-DD-TEMP: Add a secret passcode here.
|
||||
EVENT_HANDLER_AUTHORIZATION=SuperSecretPassword
|
||||
|
||||
# The host where the event handler will run. Must follow https:#nodejs.org/api/net.html#serverlistenoptions-callback. */
|
||||
# SETUP-DD-TEMP: Set the event handler's host here.
|
||||
EVENT_HANDLER_HOST=localhost
|
||||
|
||||
# The port where the event handler is listening for events. */
|
||||
# SETUP-DD-TEMP: Set the desired port where events will be sent to be processed.
|
||||
EVENT_HANDLER_PORT=8081
|
||||
|
||||
# The full webhook url where the bot can send errors to alert you that the bot is missing translations. */
|
||||
# SETUP-DD-TEMP: Set a full discord webhook url here.
|
||||
MISSING_TRANSLATION_WEBHOOK=
|
||||
|
||||
# The full webhook url where the bot can send errors to alert you that the bot is throwing errors. */
|
||||
# SETUP-DD-TEMP: Set a full discord webhook url here.
|
||||
BUGS_ERRORS_REPORT_WEBHOOK=
|
||||
|
||||
# Rest Proxy Configurations
|
||||
# The authorization code that the REST proxy will check for to make sure the requests are coming from you. */
|
||||
# SETUP-DD-TEMP: Add a secret passcode here.
|
||||
REST_AUTHORIZATION=password123
|
||||
|
||||
# The host where the REST proxy will run. Must follow https:#nodejs.org/api/net.html#serverlistenoptions-callback. */
|
||||
# SETUP-DD-TEMP: Set the REST proxy's host here.
|
||||
REST_HOST=localhost
|
||||
|
||||
# The port that will run the REST proxy. */
|
||||
# SETUP-DD-TEMP: Choose the port number here that will be used for the REST proxy.
|
||||
REST_PORT=8000
|
||||
|
||||
# Gateway Proxy Configurations
|
||||
|
||||
# The amount of shards to start. Useful with multiple servers where each server is handling a portion of your bot. */
|
||||
# SETUP-DD-TEMP: To start all bots, leave it as undefined. Specify he number of shards this process should handle.
|
||||
TOTAL_SHARDS=
|
||||
|
||||
# The amount of shards to start per worker. */
|
||||
# SETUP-DD-TEMP: Choose how many shards to start per worker. If you are not sure just stick to 16.
|
||||
SHARDS_PER_WORKER=16
|
||||
|
||||
# The total amount of workers to start. Generally this should be equal to the number of cores your server has. */
|
||||
# SETUP-DD-TEMP: Choose how many workers to start up. If you are not sure, check how many cores your server has.
|
||||
TOTAL_WORKERS=4
|
||||
|
||||
# The secret passcode that the gateway is listening for. This is used to prevent someone else from trying to send malicious messages to your bot. */
|
||||
# SETUP-DD-TEMP: Add a secret passcode here.
|
||||
GATEWAY_AUTHORIZATION=
|
||||
|
||||
# The host where the gateway will run. Must follow https:#nodejs.org/api/net.html#serverlistenoptions-callback. */
|
||||
# SETUP-DD-TEMP: Set the gateways's host here.
|
||||
GATEWAY_HOST=localhost
|
||||
|
||||
# The port where the gateway will run. This is where the bot will send its messages to the gateway. */
|
||||
# SETUP-DD-TEMP: Set the gateways's port here.
|
||||
GATEWAY_PORT=8080
|
||||
|
||||
# Messsage queue / RabbitMQ configuration
|
||||
# enable using messages queue to send messages from gateway to bot
|
||||
MESSAGEQUEUE_ENABLE=false
|
||||
|
||||
# The url of the message queue
|
||||
MESSAGEQUEUE_URL=rabbitmq:5672
|
||||
|
||||
# username and password for the message queue
|
||||
MESSAGEQUEUE_USERNAME=guest
|
||||
MESSAGEQUEUE_PASSWORD=guest
|
||||
|
||||
# Database Configurations
|
||||
|
||||
# These INFLUX configs are only if you wish to enable analytics. */
|
||||
# SETUP-DD-TEMP: This is optional. If you want to build analytics, add influxdb here.
|
||||
INFLUX_ORG=
|
||||
INFLUX_BUCKET=
|
||||
INFLUX_TOKEN=
|
||||
INFLUX_URL=http://influxdb:8086
|
||||
|
||||
DOCKER_INFLUXDB_INIT_MODE=setup
|
||||
DOCKER_INFLUXDB_INIT_USERNAME=skillz
|
||||
DOCKER_INFLUXDB_INIT_PASSWORD=ILoveskillz
|
||||
DOCKER_INFLUXDB_INIT_ORG=discordeno
|
||||
DOCKER_INFLUXDB_INIT_BUCKET=discordeno
|
||||
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=skillzPrefersID
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"minify": true,
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"target": "es2022",
|
||||
"keepClassNames": true,
|
||||
"loose": true,
|
||||
"minify": {
|
||||
"compress": {
|
||||
"unused": true
|
||||
},
|
||||
"mangle": true
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6",
|
||||
"strict": false,
|
||||
"strictMode": true,
|
||||
"lazy": false,
|
||||
"noInterop": false
|
||||
},
|
||||
"sourceMaps": "inline"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
FROM node:16.18.0-alpine3.16 AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json .swcrc ./
|
||||
RUN npm install
|
||||
|
||||
FROM node:16.18.0-alpine3.16 as rest
|
||||
COPY --from=deps /app /app
|
||||
WORKDIR /app
|
||||
COPY src/analytics.ts src/configs.ts src/
|
||||
COPY src/rest src/rest
|
||||
RUN npm run build
|
||||
EXPOSE 8000
|
||||
CMD ["npm","run","startr"]
|
||||
|
||||
FROM node:16.18.0-alpine3.16 as gateway
|
||||
COPY --from=deps /app /app
|
||||
WORKDIR /app
|
||||
COPY src/configs.ts src/
|
||||
COPY src/gateway src/gateway
|
||||
RUN npm run build
|
||||
EXPOSE 8080
|
||||
CMD ["npm","run","startg"]
|
||||
|
||||
FROM node:16.18.0-alpine3.16 as bot
|
||||
COPY --from=deps /app /app
|
||||
WORKDIR /app
|
||||
COPY prisma prisma
|
||||
COPY node_modules/.prisma/client node_modules/.prisma/client
|
||||
COPY src/analytics.ts src/prisma.ts src/configs.ts src/
|
||||
COPY src/bot src/bot
|
||||
RUN npm run build
|
||||
EXPOSE 8080
|
||||
CMD ["npm","run","startb"]
|
||||
|
||||
FROM rabbitmq:3.11.2-management-alpine as rabbitmq
|
||||
COPY src/rabbitmq/plugins plugins
|
||||
RUN rabbitmq-plugins enable rabbitmq_message_deduplication
|
||||
@@ -0,0 +1,66 @@
|
||||
# Discordeno Big Bot Template
|
||||
|
||||
Support: <https://discord.gg/ddeno>
|
||||
|
||||
This template is designed for bots that aim or are already in millions of Discord servers. It is written with Node.js as
|
||||
currently Deno & Bun are not ready to run something at such a scale. The general idea of this template can be modified
|
||||
for any other runtime if this improves in the future.
|
||||
|
||||
Make sure to install the latest version when you use it.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Run a find all for `// SETUP-DD-TEMP:` and follow all instructions and delete the comments as you finish them.
|
||||
|
||||
## Startup
|
||||
|
||||
There are two ways to start your bot, using docker and node. Using docker will be the simplest and easiest way to start
|
||||
your bot. The default configuation will be set for Docker.
|
||||
|
||||
### Using Docker
|
||||
|
||||
The docker compose file include the discordeno bot and influxdb, this would create an enviroment more close to the
|
||||
production enviroment.
|
||||
|
||||
First, rename the .env.example file to .env, and set the discord token and your dev guild id, change the `REST_URL` to
|
||||
`rest` and `EVENT_HANDLER_URL` to `bot`, set `MESSAGEQUEUE_ENABLE` to true to use message queue, copy the value of
|
||||
`DOCKER_INFLUXDB_INIT_xxxx` to `INFLUX_xxxx`
|
||||
|
||||
Then, run ... to build/rebuild the bot
|
||||
|
||||
- `docker-compose build`
|
||||
|
||||
And, run ... to start
|
||||
|
||||
- `docker-compose up -d`
|
||||
|
||||
Your bot should be running now, you can check the rest/bot process fetch analytics (methods, status...) in influxdb's
|
||||
webgui - <http://localhost:8086> with the username and password in the .env file, message queue's information (number of
|
||||
events...) at <http://localhost:15672> with user: guest and pass: guest.
|
||||
|
||||
### Using Node
|
||||
|
||||
you will need to start a few processes. The instructions below will use `node` but you can use something like `pm2` to
|
||||
help keep your processes alive.
|
||||
|
||||
First, rename the .env.example file to .env, and set the discord token and your dev guild id, change the `REST_URL` and
|
||||
`EVENT_HANDLER_URL` to `localhost`
|
||||
|
||||
Then compile everything with `npm run build`.
|
||||
|
||||
After that, you can start your bot one by one with the following order.
|
||||
|
||||
- Start REST
|
||||
- `npm run startr`
|
||||
- Start Bot
|
||||
- `npm run startb`
|
||||
- Start Gateway
|
||||
- `npm run startg`
|
||||
|
||||
Other things you can add:
|
||||
|
||||
- InfluxDB for logging fetch analytics, by change value of `INFLUX_xxxx` to your influxdb config, leave it empty will
|
||||
disable it.
|
||||
- RabbitMQ for using message queues instead of fetch calls, by change value of `MESSAGEQUEUE_ENABLE` to true, and
|
||||
`MESSAGEQUEUE_xxx` of your rabbitmq config <br/> Note: the RabbitMQ must installed the
|
||||
[RabbitMQ Message Deduplication Plugin](https://github.com/noxdafox/rabbitmq-message-deduplication)
|
||||
@@ -0,0 +1,44 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
influxdb:
|
||||
image: "influxdb:2.4.0-alpine"
|
||||
ports:
|
||||
- 127.0.0.1:8086:8086
|
||||
env_file:
|
||||
- .env
|
||||
rabbitmq:
|
||||
build:
|
||||
context: .
|
||||
target: rabbitmq
|
||||
ports:
|
||||
- 127.0.0.1:15672:15672
|
||||
rest:
|
||||
build:
|
||||
context: .
|
||||
target: rest
|
||||
deploy:
|
||||
replicas: 2
|
||||
depends_on:
|
||||
- influxdb
|
||||
env_file:
|
||||
- .env
|
||||
gateway:
|
||||
build:
|
||||
context: .
|
||||
target: gateway
|
||||
depends_on:
|
||||
- rest
|
||||
deploy:
|
||||
replicas: 2
|
||||
env_file:
|
||||
- .env
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
target: bot
|
||||
depends_on:
|
||||
- rest
|
||||
deploy:
|
||||
replicas: 2
|
||||
env_file:
|
||||
- .env
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": "./src/**/*.ts",
|
||||
"ext ": "env,ts",
|
||||
"signal": "SIGKILL",
|
||||
"exec": "npm run build && node"
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "dd-big-bot",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"devbg": "npx prisma generate && tsc --watch",
|
||||
"fmt": "eslint --fix \"src/**/*.ts*\"",
|
||||
"devg": "nodemon --ignore ./src/bot/**/* --ignore ./src/rest/**/* --ignore ./dist/**/* -e ts dist/gateway/index.js",
|
||||
"devr": "nodemon --ignore ./src/bot/**/* --ignore ./src/gateway/**/* --ignore ./dist/**/* -e ts dist/rest/index.js",
|
||||
"devb": "nodemon --ignore ./src/rest/**/* --ignore ./src/gateway/**/* --ignore ./dist/**/* -e ts dist/bot/index.js",
|
||||
"type": "tsc --noEmit",
|
||||
"build": "swc --delete-dir-on-start src --out-dir dist",
|
||||
"startr": "node dist/rest/index.js",
|
||||
"startg": "node dist/gateway/index.js",
|
||||
"startb": "node dist/bot/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@influxdata/influxdb-client": "^1.29.0",
|
||||
"@prisma/client": "^3.15.2",
|
||||
"amqplib": "^0.10.3",
|
||||
"colorette": "^2.0.19",
|
||||
"discordeno": "^16.0.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.1",
|
||||
"fastify": "^4.10.2",
|
||||
"nanoid": "^4.0.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"web-worker": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.57",
|
||||
"@swc/core": "^1.3.9",
|
||||
"@types/amqplib": "^0.8.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/ws": "^8.5.3",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "2.6.2",
|
||||
"prisma": "^4.2.1",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "all",
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Guilds {
|
||||
/// The server id
|
||||
id BigInt @unique @id
|
||||
/// The language the server uses.
|
||||
language String @default("english")
|
||||
}
|
||||
|
||||
model Commands {
|
||||
/// The server id
|
||||
id BigInt @unique @id
|
||||
/// The version number this server is using for it's commands.
|
||||
version Int
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { InfluxDB, Point } from "@influxdata/influxdb-client";
|
||||
import type { RestManager } from "discordeno/rest";
|
||||
|
||||
const INFLUX_ORG = process.env.INFLUX_ORG as string;
|
||||
const INFLUX_BUCKET = process.env.INFLUX_BUCKET as string;
|
||||
const INFLUX_TOKEN = process.env.INFLUX_TOKEN as string;
|
||||
const INFLUX_URL = process.env.INFLUX_URL as string;
|
||||
|
||||
export const influxDB = INFLUX_URL && INFLUX_TOKEN ? new InfluxDB({ url: INFLUX_URL, token: INFLUX_TOKEN }) : undefined;
|
||||
export const Influx = influxDB?.getWriteApi(INFLUX_ORG, INFLUX_BUCKET);
|
||||
|
||||
export const setupAnalyticsHooks = (rest: RestManager) => {
|
||||
// If influxdb data is provided, enable analytics in this proxy.
|
||||
if (Influx) {
|
||||
rest.fetching = function (options) {
|
||||
Influx?.writePoint(
|
||||
new Point("restEvents")
|
||||
// MARK THE TIME WHEN EVENT ARRIVED
|
||||
.timestamp(new Date())
|
||||
// SET THE GUILD ID
|
||||
.stringField("type", "REQUEST_FETCHING")
|
||||
.tag("method", options.method)
|
||||
.tag("url", options.url)
|
||||
.tag("bucket", options.bucketId ?? "NA"),
|
||||
);
|
||||
};
|
||||
|
||||
rest.fetched = function (options, response) {
|
||||
Influx?.writePoint(
|
||||
new Point("restEvents")
|
||||
// MARK THE TIME WHEN EVENT ARRIVED
|
||||
.timestamp(new Date())
|
||||
// SET THE GUILD ID
|
||||
.stringField("type", "REQUEST_FETCHED")
|
||||
.tag("method", options.method)
|
||||
.tag("url", options.url)
|
||||
.tag("bucket", options.bucketId ?? "NA")
|
||||
.intField("status", response.status)
|
||||
.tag("statusText", response.statusText),
|
||||
);
|
||||
};
|
||||
|
||||
setInterval(() => {
|
||||
console.log(`[Influx - REST] Saving events...`);
|
||||
Influx?.flush()
|
||||
.then(() => {
|
||||
console.log(`[Influx - REST] Saved events!`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`[Influx - REST] Error saving events!`, error);
|
||||
});
|
||||
// Every 30seconds
|
||||
}, 30000);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
# Bot Code
|
||||
|
||||
This folder will contain the code for our bot. When events are received from the gateway, they will be handled here.
|
||||
|
||||
## Further Steps
|
||||
|
||||
- Express framework to create the listener however, you can replace it with anything you like. Express is quite a
|
||||
bloated framework. Feel free to optimize to a better framework.
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Bot} from "discordeno";
|
||||
import { Collection, createBot, createRestManager } from "discordeno";
|
||||
import enableHelpersPlugin from "discordeno/helpers-plugin";
|
||||
import { createLogger } from "discordeno/logger";
|
||||
import { setupAnalyticsHooks } from "../analytics.js";
|
||||
import { INTENTS, REST_URL } from "../configs.js";
|
||||
import { setupEventHandlers } from "./events/mod.js";
|
||||
import type { MessageCollector } from "./utils/collectors.js";
|
||||
import { customizeInternals } from "./utils/internals/mod.js";
|
||||
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
||||
const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
|
||||
|
||||
export const bot = enableHelpersPlugin(
|
||||
customizeBot(
|
||||
createBot({
|
||||
token: DISCORD_TOKEN,
|
||||
intents: INTENTS,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/** Add custom props to your `bot` here */
|
||||
// SETUP-DD-TEMP: If you want to add any custom props to `bot` you can do so here. Please make sure to also add them in the type below. As an example, i have added a `logger` property. You can add any useful methods or props you wish to have easily available.
|
||||
function customizeBot<B extends Bot = Bot>(bot: B): BotWithCustomProps {
|
||||
const customized = bot as unknown as BotWithCustomProps;
|
||||
customized.logger = createLogger({ name: "[Bot]" });
|
||||
customized.collectors = {
|
||||
messages: new Collection(),
|
||||
};
|
||||
customized.commandVersions = new Collection();
|
||||
|
||||
return customized;
|
||||
}
|
||||
|
||||
// SETUP-DD-TEMP: If you want to add any custom props to `bot` you can do so here. Please make sure to also add them in the function above. Run a find all and change this to your Bot's name. For example, if your bot's name is Gamer change BotWithCustomProps to Gamer. This way whenever you need to provide the type for the Bot with your custom props it is your bots name.
|
||||
// Note: ALWAYS edit the function above first before adding the type here.
|
||||
export type BotWithCustomProps<B extends Bot = Bot> = B & {
|
||||
/** A easy to use logger to make clean log messages. */
|
||||
logger: ReturnType<typeof createLogger>;
|
||||
/** Collectors that can be used to get input from users. */
|
||||
collectors: {
|
||||
/** Holds the pending messages collectors that users can respond to. */
|
||||
messages: Collection<bigint, MessageCollector>;
|
||||
};
|
||||
/** The command versions for each guild id. */
|
||||
commandVersions: Collection<bigint, number>;
|
||||
};
|
||||
|
||||
// Example of how to customize internal discordeno stuff easily.
|
||||
customizeInternals(bot);
|
||||
|
||||
// Setup event handlers.
|
||||
setupEventHandlers();
|
||||
|
||||
bot.rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
});
|
||||
|
||||
// Add send fetching analytics hook to rest
|
||||
setupAnalyticsHooks(bot.rest);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApplicationCommandOptionTypes } from "discordeno";
|
||||
import { prisma } from "../../prisma.js";
|
||||
import languages from "../languages/languages.js";
|
||||
import { serverLanguages, translate } from "../languages/translate.js";
|
||||
import { createCommand } from "../utils/slash/createCommand.js";
|
||||
|
||||
export default createCommand({
|
||||
name: "LANGUAGE_NAME",
|
||||
description: "LANGUAGE_DESCRIPTION",
|
||||
options: [
|
||||
{
|
||||
name: "LANGUAGE_KEY_NAME",
|
||||
description: "LANGUAGE_KEY_DESCRIPTION",
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
choices: Object.keys(languages).map((key) => ({ name: key, value: key })),
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
execute: async function (_, interaction, args) {
|
||||
if (!interaction.guildId) return;
|
||||
|
||||
// Set the new language in cache
|
||||
serverLanguages.set(interaction.guildId, args.name);
|
||||
// Let the user know its been updated.
|
||||
await interaction.reply(translate(interaction.guildId!, "LANGUAGE_UPDATED", args.name));
|
||||
// Update the db
|
||||
return await prisma.guilds.upsert({
|
||||
where: { id: interaction.guildId },
|
||||
create: { language: args.name, id: interaction.guildId },
|
||||
update: { language: args.name },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import language from "./language.js";
|
||||
import ping from "./ping.js";
|
||||
|
||||
export const COMMANDS = {
|
||||
language,
|
||||
ping,
|
||||
};
|
||||
|
||||
export default COMMANDS;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { translate } from "../languages/translate.js";
|
||||
import { createCommand } from "../utils/slash/createCommand.js";
|
||||
|
||||
export default createCommand({
|
||||
name: "PING_NAME",
|
||||
description: "PING_DESCRIPTION",
|
||||
execute: async function (_, interaction) {
|
||||
return await interaction.reply(
|
||||
translate(interaction.guildId!, "PING_RESPONSE_WITH_TIME", Date.now() - snowflakeToTimestamp(interaction.id)),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: This should be deleted once this is available in the helpers plugin.
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Interaction } from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../bot.js";
|
||||
|
||||
export async function executeButtonClick(bot: BotWithCustomProps, interaction: Interaction) {
|
||||
if (!interaction.data) return;
|
||||
|
||||
bot.logger.info(
|
||||
`[Button] The ${interaction.data.customId} button was clicked in Guild: ${interaction.guildId} by ${interaction.user.id}.`,
|
||||
);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Insert any functions you wish to run when a user clicks a button.
|
||||
]).catch(console.log);
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import { bgBlack, bgGreen, bgMagenta, bgYellow, black, green, red, white } from "colorette";
|
||||
import type {
|
||||
ApplicationCommandOption,
|
||||
Bot,
|
||||
Channel,
|
||||
ChannelTypes,
|
||||
Interaction,
|
||||
InteractionDataOption,
|
||||
Member,
|
||||
Role,
|
||||
User} from "discordeno";
|
||||
import {
|
||||
ApplicationCommandOptionTypes,
|
||||
InteractionResponseTypes
|
||||
} from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../bot.js";
|
||||
import { bot } from "../../bot.js";
|
||||
import COMMANDS from "../../commands/mod.js";
|
||||
import type { translationKeys } from "../../languages/translate.js";
|
||||
import { getLanguage, loadLanguage, serverLanguages, translate } from "../../languages/translate.js";
|
||||
import type { InteractionWithCustomProps } from "../../typings/discordeno.js";
|
||||
import type { Command, ConvertArgumentDefinitionsToArgs } from "../../utils/slash/createCommand.js";
|
||||
|
||||
function logCommand(
|
||||
info: Interaction,
|
||||
type: "Failure" | "Success" | "Trigger" | "Slowmode" | "Missing" | "Inhibit",
|
||||
commandName: string,
|
||||
) {
|
||||
const command = `[COMMAND: ${bgYellow(black(commandName || "Unknown"))} - ${
|
||||
bgBlack(
|
||||
["Failure", "Slowmode", "Missing"].includes(type) ? red(type) : type === "Success" ? green(type) : white(type),
|
||||
)
|
||||
}]`;
|
||||
|
||||
const user = bgGreen(
|
||||
black(`${info.user.username}#${info.user.discriminator.toString().padStart(4, "0")}(${info.id})`),
|
||||
);
|
||||
const guild = bgMagenta(black(`${info.guildId ? `Guild ID: (${info.guildId})` : "DM"}`));
|
||||
|
||||
bot.logger.info(`${command} by ${user} in ${guild} with MessageID: ${info.id}`);
|
||||
}
|
||||
|
||||
export async function executeSlashCommand(bot: BotWithCustomProps, interaction: InteractionWithCustomProps) {
|
||||
const data = interaction.data;
|
||||
const name = data?.name as keyof typeof COMMANDS;
|
||||
|
||||
const command: Command<any> | undefined = COMMANDS[name];
|
||||
|
||||
// Command could not be found
|
||||
if (!command?.execute) {
|
||||
return await interaction
|
||||
.reply(translate(interaction.guildId!, "EXECUTE_COMMAND_NOT_FOUND"))
|
||||
.catch(bot.logger.error);
|
||||
}
|
||||
|
||||
// HAVE TO CONVERT OUTSIDE OF TRY SO IT CAN BE USED IN CATCH TOO
|
||||
try {
|
||||
logCommand(interaction, "Trigger", name);
|
||||
|
||||
// Check subcommand permissions and options
|
||||
if (!(await commandAllowed(interaction, command))) return;
|
||||
|
||||
// Load the language for this guild
|
||||
if (interaction.guildId && !serverLanguages.has(interaction.guildId)) {
|
||||
// Todo: make command.execute reply change to editReply after running this
|
||||
// await interaction.reply({
|
||||
// type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
// });
|
||||
await loadLanguage(interaction.guildId);
|
||||
} // Load the language for this guild
|
||||
else if (command.acknowledge) {
|
||||
// Acknowledge the command
|
||||
await interaction.reply({
|
||||
type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
});
|
||||
}
|
||||
|
||||
// FIRST GET THE TRANSLATIONS FOR ALL OPTIONS
|
||||
const translatedOptionNames = interaction.guildId && command.options
|
||||
? translateOptionNames(bot, interaction.guildId, command.options)
|
||||
: {};
|
||||
|
||||
// PARSE THE OPTIONS TO A NICE OBJECT AND TRANSLATE THE KEYS TO ENGLISH
|
||||
const parsedArguments = optionParser(interaction, translatedOptionNames);
|
||||
|
||||
await command.execute(bot, interaction, parsedArguments as ConvertArgumentDefinitionsToArgs<any>);
|
||||
logCommand(interaction, "Success", name);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logCommand(interaction, "Failure", name);
|
||||
|
||||
try {
|
||||
console.log("try");
|
||||
// try to reply the interaction, becuase we don't know if it replied or deffered
|
||||
return await interaction.reply(translate(interaction.id, "EXECUTE_COMMAND_ERROR"));
|
||||
} catch {
|
||||
console.log("catch");
|
||||
// edit the reply or deffered reply of interaction
|
||||
return await interaction.editReply(translate(interaction.id, "EXECUTE_COMMAND_ERROR")).catch(bot.logger.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs the inhibitors to see if a command is allowed to run. */
|
||||
export async function commandAllowed(interaction: InteractionWithCustomProps, command: Command<any>) {
|
||||
// CHECK WHETHER THE USER/GUILD IS VIP
|
||||
if (command.vipOnly) {
|
||||
// SETUP-DD-TEMP: Check if this server/user is a vip.
|
||||
const isVIP = true;
|
||||
|
||||
if (!isVIP) {
|
||||
await interaction.reply(translate(interaction.id, "NEED_VIP")).catch(bot.logger.error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mapped by `language-commandName`
|
||||
const translatedOptionNamesCache = new Map<string, Record<string, string>>();
|
||||
|
||||
/** Translates all options of the command to an object: translatedOptionName: optionName */
|
||||
export function translateOptionNames(
|
||||
bot: Bot,
|
||||
guildId: bigint,
|
||||
options: ApplicationCommandOption[],
|
||||
commandName?: string,
|
||||
): Record<string, string> {
|
||||
const language = getLanguage(guildId);
|
||||
// RETURN THE ALREADY TRANSLATED OPTIONS WHICH ARE IN CACHE
|
||||
if (commandName && translatedOptionNamesCache.has(`${language}-${commandName}`)) {
|
||||
return translatedOptionNamesCache.get(`${language}-${commandName}`)!;
|
||||
}
|
||||
|
||||
// TRANSLATE ALL OPTIONS
|
||||
let translated: Record<string, string> = {};
|
||||
for (const option of options) {
|
||||
translated[translate(guildId, option.name as translationKeys).toLowerCase()] = translate(
|
||||
"english",
|
||||
option.name as translationKeys,
|
||||
);
|
||||
if (option.options) {
|
||||
translated = {
|
||||
...translated,
|
||||
...translateOptionNames(bot, guildId, option.options),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// SAVE THE TRANSLATED OPTIONS IN CACHE FOR FASTER ACCESS
|
||||
if (commandName) {
|
||||
translatedOptionNamesCache.set(`${language}-${commandName}`, translated);
|
||||
}
|
||||
|
||||
return translated;
|
||||
}
|
||||
|
||||
function convertOptionValue(
|
||||
interaction: Interaction,
|
||||
option: InteractionDataOption,
|
||||
translateOptions?: Record<string, string>,
|
||||
): [
|
||||
string,
|
||||
(
|
||||
| { user: User; member: Member }
|
||||
| Role
|
||||
| {
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
),
|
||||
] {
|
||||
// THE OPTION IS A CHANNEL
|
||||
if (option.type === ApplicationCommandOptionTypes.Channel) {
|
||||
const channel = interaction.data?.resolved?.channels?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, channel];
|
||||
}
|
||||
|
||||
// THE OPTION IS A ROLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Role) {
|
||||
const role = interaction.data?.resolved?.roles?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, role];
|
||||
}
|
||||
|
||||
// THE OPTION IS A USER
|
||||
if (option.type === ApplicationCommandOptionTypes.User) {
|
||||
const user = interaction.data?.resolved?.users?.get(BigInt(option.value as string));
|
||||
const member = interaction.data?.resolved?.members?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [
|
||||
translateOptions?.[option.name] ?? option.name,
|
||||
{
|
||||
member,
|
||||
user,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// THE OPTION IS A MENTIONABLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Mentionable) {
|
||||
const role = interaction.data?.resolved?.roles?.get(BigInt(option.value as string));
|
||||
const user = interaction.data?.resolved?.users?.get(BigInt(option.value as string));
|
||||
const member = interaction.data?.resolved?.members?.get(BigInt(option.value as string));
|
||||
|
||||
const final = user && member ? { user, member } : role;
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, final];
|
||||
}
|
||||
|
||||
// THE REST OF OPTIONS DON'T NEED ANY CONVERTION
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
// @ts-expect-error
|
||||
return [translateOptions?.[option.name] ?? option.name, option.value];
|
||||
}
|
||||
|
||||
/** Parse the options to a nice object.
|
||||
* NOTE: this does not work with subcommands
|
||||
*/
|
||||
export function optionParser(
|
||||
interaction: Interaction,
|
||||
translateOptions?: Record<string, string>,
|
||||
):
|
||||
| InteractionCommandArgs
|
||||
| { [key: string]: InteractionCommandArgs }
|
||||
| { [key: string]: { [key: string]: InteractionCommandArgs } } {
|
||||
// OPTIONS CAN BE UNDEFINED SO WE JUST RETURN AN EMPTY OBJECT
|
||||
if (!interaction.data?.options) return {};
|
||||
|
||||
// A SUBCOMMAND WAS USED
|
||||
if (interaction.data.options[0]?.type === ApplicationCommandOptionTypes.SubCommand) {
|
||||
const convertedOptions: Record<
|
||||
string,
|
||||
| { user: User; member: Member }
|
||||
| Role
|
||||
| {
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
> = {};
|
||||
// CONVERT ALL THE OPTIONS
|
||||
for (const option of interaction.data.options[0].options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return {
|
||||
[translateOptions?.[interaction.data.options[0].name] ?? interaction.data.options[0].name]: convertedOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// A SUBCOMMAND GROUP WAS USED
|
||||
if (interaction.data.options[0]?.type === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
const convertedOptions: Record<string, Member | Role | Channel | boolean | string | number> = {};
|
||||
// CONVERT ALL THE OPTIONS
|
||||
for (const option of interaction.data.options[0]?.options![0]?.options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
// @ts-expect-error
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return {
|
||||
[translateOptions?.[interaction.data.options[0].name] ?? interaction.data.options[0].name]: {
|
||||
[
|
||||
translateOptions?.[interaction.data.options[0]!.options![0]!.name] ??
|
||||
interaction.data.options[0]!.options![0]!.name
|
||||
]: convertedOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// A NORMAL COMMAND WAS USED
|
||||
const convertedOptions: Record<
|
||||
string,
|
||||
Member | Role | Record<string, Pick<Channel, "id" | "name" | "type" | "permissions">> | boolean | string | number
|
||||
> = {};
|
||||
for (const option of interaction.data.options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
// @ts-expect-error
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
return convertedOptions;
|
||||
}
|
||||
|
||||
/** The interaction arguments.
|
||||
* Important the members `deaf` and `mute` properties will always be false.
|
||||
*/
|
||||
export type InteractionCommandArgs = Record<
|
||||
string,
|
||||
Member | Role | Record<string, Pick<Channel, "id" | "name" | "type" | "permissions">> | boolean | string | number
|
||||
>;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { InteractionTypes, MessageComponentTypes } from "discordeno";
|
||||
import { bot } from "../../bot.js";
|
||||
import type { InteractionWithCustomProps } from "../../typings/discordeno.js";
|
||||
import { executeButtonClick } from "./button.js";
|
||||
import { executeSlashCommand } from "./command.js";
|
||||
import { executeModalSubmit } from "./modal.js";
|
||||
|
||||
export function setInteractionCreateEvent() {
|
||||
bot.events.interactionCreate = async function (_, interaction) {
|
||||
if (interaction.type === InteractionTypes.ApplicationCommand) {
|
||||
await executeSlashCommand(bot, interaction as InteractionWithCustomProps);
|
||||
} else if (interaction.type === InteractionTypes.MessageComponent) {
|
||||
if (!interaction.data) return;
|
||||
|
||||
// THE INTERACTION CAME FROM A BUTTON
|
||||
if (interaction.data.componentType === MessageComponentTypes.Button) {
|
||||
await executeButtonClick(bot, interaction);
|
||||
}
|
||||
} else if (interaction.type === InteractionTypes.ModalSubmit) {
|
||||
await executeModalSubmit(bot, interaction);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Interaction } from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../bot.js";
|
||||
|
||||
export async function executeModalSubmit(bot: BotWithCustomProps, interaction: Interaction) {
|
||||
if (!interaction.data) return;
|
||||
|
||||
bot.logger.info(
|
||||
`[Modal] The ${
|
||||
interaction.data?.customId || "UNKNWON"
|
||||
} modal was submitted in Guild: ${interaction.guildId} by ${interaction.user.id}.`,
|
||||
);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Insert any functions you wish to run when a user clicks a button.
|
||||
]).catch(console.log);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { bot } from "../../bot.js";
|
||||
import { processMessageCollectors } from "../../utils/collectors.js";
|
||||
|
||||
export function setMessageCreateEvent() {
|
||||
bot.events.messageCreate = async function (_, message) {
|
||||
processMessageCollectors(message);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Add any functions you want to run on every message here. For example, automoderation filters.
|
||||
]).catch(console.log);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { setInteractionCreateEvent } from "./interactions/mod.js";
|
||||
import { setMessageCreateEvent } from "./messages/create.js";
|
||||
import { setRawEvent } from "./raw.js";
|
||||
|
||||
export function setupEventHandlers() {
|
||||
setInteractionCreateEvent();
|
||||
setRawEvent();
|
||||
setMessageCreateEvent();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { DiscordUnavailableGuild } from "discordeno";
|
||||
import { prisma } from "../../prisma.js";
|
||||
import { bot } from "../bot.js";
|
||||
import { updateGuildCommands, usesLatestCommandVersion } from "../utils/slash/updateCommands.js";
|
||||
|
||||
/** To prevent updating every guild when a shard goes ready we have to ignore them using this */
|
||||
// export const initialyLoadingGuildIds = new Set<bigint>()
|
||||
|
||||
export function setRawEvent() {
|
||||
bot.events.raw = async function (_, data) {
|
||||
if (data.t === "GUILD_DELETE") {
|
||||
const id = (data.d as DiscordUnavailableGuild).id;
|
||||
|
||||
return await prisma.commands.delete({ where: { id: bot.transformers.snowflake(id) } });
|
||||
}
|
||||
|
||||
const id = bot.transformers.snowflake(
|
||||
(data.t && ["GUILD_UPDATE", "GUILD_CREATE"].includes(data.t)
|
||||
// deno-lint-ignore no-explicit-any
|
||||
? (data.d )?.id
|
||||
// deno-lint-ignore no-explicit-any
|
||||
: (data.d )?.guild_id) ?? "",
|
||||
);
|
||||
|
||||
// The GUILD_CREATE event came from a shard loaded event so ignore it
|
||||
if (["READY", "GUILD_LOADED_DD", null].includes(data.t)) return;
|
||||
|
||||
// console.log({ id, v: await usesLatestCommandVersion(id) })
|
||||
|
||||
if (!id || (await usesLatestCommandVersion(id))) return;
|
||||
// dev guild
|
||||
if (id === 547046977578336286n) return;
|
||||
|
||||
// NEW GUILD AVAILABLE
|
||||
bot.logger.info(`[Slash Setup] Installing Slash commands on Guild ${id} event type: ${data.t}`);
|
||||
await updateGuildCommands(bot, id).catch(bot.logger.error);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import type { DiscordGatewayPayload } from "discordeno";
|
||||
// ReferenceError: publishMessage is not defined
|
||||
// import Embeds from "discordeno/embeds";
|
||||
import amqplib from "amqplib";
|
||||
import express from "express";
|
||||
import { BOT_ID, EVENT_HANDLER_URL } from "../configs.js";
|
||||
import { bot } from "./bot.js";
|
||||
import { updateDevCommands } from "./utils/slash/updateCommands.js";
|
||||
import { webhookURLToIDAndToken } from "./utils/webhook.js";
|
||||
dotenv.config();
|
||||
|
||||
const BUGS_ERRORS_REPORT_WEBHOOK = process.env.BUGS_ERRORS_REPORT_WEBHOOK;
|
||||
const EVENT_HANDLER_AUTHORIZATION = process.env.EVENT_HANDLER_AUTHORIZATION as string;
|
||||
const EVENT_HANDLER_PORT = process.env.EVENT_HANDLER_PORT as string;
|
||||
|
||||
process
|
||||
.on("unhandledRejection", (error) => {
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
// DO NOT SEND ERRORS FROM NON PRODUCTION
|
||||
if (BOT_ID !== 270010330782892032n) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// An unhandled error occurred on the bot in production
|
||||
console.error(error ?? `An unhandled rejection error occurred but error was null or undefined`);
|
||||
|
||||
if (!error) return;
|
||||
|
||||
// ReferenceError: publishMessage is not defined
|
||||
/*
|
||||
const embeds = new Embeds()
|
||||
.setDescription(["```js", error, "```"].join(`\n`))
|
||||
.setTimestamp()
|
||||
.setFooter("Unhandled Rejection Error Occurred");
|
||||
|
||||
// SEND ERROR TO THE LOG CHANNEL ON THE DEV SERVER
|
||||
return bot.helpers.sendWebhookMessage(bot.transformers.snowflake(id), token, { embeds }).catch(console.error);
|
||||
*/
|
||||
})
|
||||
.on("uncaughtException", async (error) => {
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
// DO NOT SEND ERRORS FROM NON PRODUCTION
|
||||
if (BOT_ID !== 270010330782892032n) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// An unhandled error occurred on the bot in production
|
||||
console.error(error ?? `An unhandled exception occurred but error was null or undefined`);
|
||||
|
||||
if (!error) process.exit(1);
|
||||
|
||||
/*
|
||||
const embeds = new Embeds()
|
||||
.setDescription(["```js", error.stack, "```"].join(`\n`))
|
||||
.setTimestamp()
|
||||
.setFooter("Unhandled Exception Error Occurred");
|
||||
// SEND ERROR TO THE LOG CHANNEL ON THE DEV SERVER
|
||||
await bot.helpers.sendWebhookMessage(bot.transformers.snowflake(id), token, { embeds }).catch(console.error);
|
||||
*/
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
if (process.env.DEVELOPMENT === "true") {
|
||||
bot.logger.info(`[DEV MODE] Updating slash commands for dev server.`);
|
||||
updateDevCommands(bot);
|
||||
}
|
||||
|
||||
// Handle events from the gateway
|
||||
const handleEvent = async (message: DiscordGatewayPayload, shardId: number) => {
|
||||
// EMITS RAW EVENT
|
||||
bot.events.raw(bot, message, shardId);
|
||||
|
||||
if (message.t && message.t !== "RESUMED") {
|
||||
// When a guild or something isnt in cache this will fetch it before doing anything else
|
||||
if (!["READY", "GUILD_LOADED_DD"].includes(message.t)) {
|
||||
await bot.events.dispatchRequirements(bot, message, shardId);
|
||||
}
|
||||
|
||||
bot.handlers[message.t]?.(bot, message, shardId);
|
||||
}
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.all("/", async (req, res) => {
|
||||
try {
|
||||
if (!EVENT_HANDLER_AUTHORIZATION || EVENT_HANDLER_AUTHORIZATION !== req.headers.authorization) {
|
||||
return res.status(401).json({ error: "Invalid authorization key." });
|
||||
}
|
||||
|
||||
const json = req.body as {
|
||||
message: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
|
||||
await handleEvent(json.message, json.shardId);
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error: any) {
|
||||
bot.logger.error(error);
|
||||
res.status(error.code).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(EVENT_HANDLER_PORT, () => {
|
||||
console.log(`Bot is listening at ${EVENT_HANDLER_URL};`);
|
||||
});
|
||||
|
||||
const connectRabbitmq = async () => {
|
||||
let connection: amqplib.Connection | undefined;
|
||||
|
||||
try {
|
||||
connection = await amqplib.connect(
|
||||
`amqp://${process.env.MESSAGEQUEUE_USERNAME}:${process.env.MESSAGEQUEUE_PASSWORD}@${process.env.MESSAGEQUEUE_URL}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
}
|
||||
|
||||
if (!connection) return;
|
||||
connection.on("error", (err) => {
|
||||
console.error(err);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
|
||||
connection.on("close", () => {
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
const channel = await connection.createChannel();
|
||||
|
||||
await channel.assertExchange(
|
||||
"gatewayMessage",
|
||||
"x-message-deduplication",
|
||||
{
|
||||
durable: true,
|
||||
arguments: {
|
||||
"x-cache-size": 1000,
|
||||
"x-cache-ttl": 500,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await channel.assertQueue("gatewayMessageQueue");
|
||||
await channel.bindQueue("gatewayMessageQueue", "gatewayMessage", "");
|
||||
await channel.consume(
|
||||
"gatewayMessageQueue",
|
||||
async (msg) => {
|
||||
if (!msg) return;
|
||||
const json = JSON.parse(msg.content.toString()) as {
|
||||
message: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
|
||||
await handleEvent(json.message, json.shardId);
|
||||
|
||||
await channel.ack(msg);
|
||||
},
|
||||
{
|
||||
noAck: false,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.MESSAGEQUEUE_ENABLE === "true") {
|
||||
connectRabbitmq();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const english = {
|
||||
// Permissions
|
||||
NEED_VIP: "❌ Only VIP users or servers can use this feature.",
|
||||
|
||||
// Execute Command
|
||||
EXECUTE_COMMAND_NOT_FOUND: "❌ Something went wrong. I was not able to find this command.",
|
||||
EXECUTE_COMMAND_ERROR: "❌ Something went wrong. The command execution has thrown an error.",
|
||||
|
||||
// Language Command
|
||||
LANGUAGE_NAME: "language",
|
||||
LANGUAGE_DESCRIPTION: "⚙️ Change the bots language.",
|
||||
LANGUAGE_KEY_NAME: "name",
|
||||
LANGUAGE_KEY_DESCRIPTION: "What language would you like to set?",
|
||||
LANGUAGE_UPDATED: (language: string) => `The language has been updated to ${language}`,
|
||||
|
||||
// Ping Command
|
||||
PING_NAME: "ping",
|
||||
PING_DESCRIPTION: "🏓 Check whether the bot is online and responsive.",
|
||||
PING_RESPONSE: "🏓 Pong! I am online and responsive! :clock10:",
|
||||
PING_RESPONSE_WITH_TIME: (time: number) => `🏓 Pong! ${time / 1000} seconds! I am online and responsive! :clock10:`,
|
||||
} as const;
|
||||
|
||||
export default english;
|
||||
@@ -0,0 +1,20 @@
|
||||
import english from "./english.js";
|
||||
// import french from './french'
|
||||
// import german from './german'
|
||||
// import portuguese from './portuguese'
|
||||
// import russian from './russian'
|
||||
// import spanish from './spanish'
|
||||
|
||||
const languages: Record<LanguageNames, Language> & Record<string, Language> = {
|
||||
english,
|
||||
// french,
|
||||
// german,
|
||||
// portuguese,
|
||||
// russian,
|
||||
// spanish,
|
||||
};
|
||||
|
||||
export default languages;
|
||||
|
||||
export type Language = Record<string, string | string[] | ((...args: any[]) => string)>;
|
||||
export type LanguageNames = "english";
|
||||
@@ -0,0 +1,84 @@
|
||||
import Embeds from "discordeno/embeds";
|
||||
import { bot } from "../bot.js";
|
||||
import { webhookURLToIDAndToken } from "../utils/webhook.js";
|
||||
import type english from "./english.js";
|
||||
import languages from "./languages.js";
|
||||
|
||||
const MISSING_TRANSLATION_WEBHOOK = process.env.MISSING_TRANSLATION_WEBHOOK;
|
||||
|
||||
/** This should hold the language names per guild id. <guildId, language> */
|
||||
export const serverLanguages = new Map<bigint, keyof typeof languages>();
|
||||
|
||||
export function translate<K extends translationKeys>(
|
||||
guildIdOrLanguage: bigint | keyof typeof languages,
|
||||
key: K,
|
||||
...params: getArgs<K>
|
||||
): string {
|
||||
const language = getLanguage(guildIdOrLanguage);
|
||||
let value: string | ((...any: any[]) => string) | string[] | undefined = languages[language]?.[key];
|
||||
|
||||
// Was not able to be translated
|
||||
if (!value) {
|
||||
// Check if this key is available in english
|
||||
if (language !== "english") {
|
||||
value = languages.english[key];
|
||||
}
|
||||
|
||||
// Still not found in english so default to using the KEY_ITSELF
|
||||
if (!value) value = key;
|
||||
|
||||
// Send a log webhook so the devs know sth is missing
|
||||
missingTranslation(language, key);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) return value.join("\n");
|
||||
|
||||
if (typeof value === "function") return value(...(params || []));
|
||||
|
||||
return value ;
|
||||
}
|
||||
|
||||
/** Get the language this guild has set, will always return "english" if it is not in cache */
|
||||
export function getLanguage(guildIdOrLanguage: bigint | keyof typeof languages) {
|
||||
return typeof guildIdOrLanguage === "string"
|
||||
? guildIdOrLanguage
|
||||
: serverLanguages.get(guildIdOrLanguage) ?? "english";
|
||||
}
|
||||
|
||||
export async function loadLanguage(guildId: bigint) {
|
||||
// TODO: add this settings
|
||||
// const settings = await database.findOne('guilds', guildId)
|
||||
const settings = { language: "undefined" };
|
||||
|
||||
if (settings?.language && languages[settings.language]) {
|
||||
serverLanguages.set(guildId, settings.language);
|
||||
} else serverLanguages.set(guildId, "english");
|
||||
}
|
||||
|
||||
/** Send a webhook for a missing translation key */
|
||||
export async function missingTranslation(language: keyof typeof languages, key: string) {
|
||||
if (!MISSING_TRANSLATION_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(MISSING_TRANSLATION_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
const embeds = new Embeds()
|
||||
.setTitle("Missing Translation")
|
||||
.setColor("RANDOM")
|
||||
.addField("Language", language, true)
|
||||
.addField("Key", key, true);
|
||||
|
||||
await bot.helpers
|
||||
.sendWebhookMessage(bot.transformers.snowflake(id), token, {
|
||||
// SETUP-DD-TEMP: If you wish to make it @ mention you, please edit the next line.
|
||||
// content: `<@${owner id here}>`,
|
||||
embeds,
|
||||
wait: false,
|
||||
})
|
||||
.catch(bot.logger.error);
|
||||
}
|
||||
|
||||
// type translationKeys = keyof typeof english | string
|
||||
export type translationKeys = keyof typeof english;
|
||||
type getArgs<K extends translationKeys> = typeof english[K] extends (...any: any[]) => unknown
|
||||
? Parameters<typeof english[K]>
|
||||
: [];
|
||||
@@ -0,0 +1,12 @@
|
||||
// This file allows you to tell typescript about any additions you have made to the internal discordeno objects.
|
||||
import type { Interaction, InteractionCallbackData, InteractionResponse, Message } from "discordeno";
|
||||
|
||||
export interface InteractionWithCustomProps extends Interaction {
|
||||
// Normally, to send a response you would have to do something like bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, data: { content: "text here" } })
|
||||
// But with this reply method we added, it is as simple as interaction.reply("text here").
|
||||
// Feel free to delete these comments once you have understood the concept.
|
||||
/** Send a reply to an interaction. */
|
||||
reply: (response: InteractionResponse | string) => Promise<Message | undefined>;
|
||||
/** Edit a deferred reply of an interaction. */
|
||||
editReply: (response: InteractionCallbackData | string) => Promise<Message | undefined>;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { Interaction, Member, Message } from "discordeno";
|
||||
import { bot } from "../bot.js";
|
||||
|
||||
export async function needMessage(
|
||||
memberId: bigint,
|
||||
channelId: bigint,
|
||||
options: MessageCollectorOptions & { amount?: 1 },
|
||||
): Promise<Message>;
|
||||
export async function needMessage(
|
||||
memberId: bigint,
|
||||
channelId: bigint,
|
||||
options: MessageCollectorOptions & { amount?: number },
|
||||
): Promise<Message[]>;
|
||||
export async function needMessage(memberId: bigint, channelId: bigint): Promise<Message>;
|
||||
export async function needMessage(memberId: bigint, channelId: bigint, options?: MessageCollectorOptions) {
|
||||
const messages = await collectMessages({
|
||||
key: memberId,
|
||||
channelId,
|
||||
createdAt: Date.now(),
|
||||
filter: options?.filter || ((msg) => memberId === msg.authorId),
|
||||
amount: options?.amount || 1,
|
||||
duration: options?.duration || (1000 * 60 * 5),
|
||||
});
|
||||
|
||||
return (options?.amount || 1) > 1 ? messages : messages[0];
|
||||
}
|
||||
|
||||
export async function collectMessages(options: CollectMessagesOptions): Promise<Message[]> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
bot.collectors.messages.get(options.key)?.reject(
|
||||
"A new collector began before the user responded to the previous one.",
|
||||
);
|
||||
|
||||
bot.collectors.messages.set(options.key, {
|
||||
...options,
|
||||
messages: [],
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function processMessageCollectors(message: Message) {
|
||||
// IGNORE DMS
|
||||
if (!message.guildId) return;
|
||||
|
||||
const collector = bot.collectors.messages.get(message.authorId);
|
||||
// This user has no collectors pending or the message is in a different channel
|
||||
if (!collector || message.channelId !== collector.channelId) return;
|
||||
// This message is a response to a collector. Now running the filter function.
|
||||
if (!collector.filter(message)) return;
|
||||
|
||||
// If the necessary amount has been collected
|
||||
if (collector.amount === 1 || collector.amount === collector.messages.length + 1) {
|
||||
// Remove the collector
|
||||
bot.collectors.messages.delete(message.authorId);
|
||||
// Resolve the collector
|
||||
return collector.resolve([...collector.messages, message]);
|
||||
}
|
||||
|
||||
// More messages still need to be collected
|
||||
collector.messages.push(message);
|
||||
}
|
||||
|
||||
export interface BaseCollectorOptions {
|
||||
/** The amount of messages to collect before resolving. Defaults to 1 */
|
||||
amount?: number;
|
||||
/** The amount of milliseconds this should collect for before expiring. Defaults to 5 minutes. */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface MessageCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (message: Message) => boolean;
|
||||
/** The amount of messages to collect before resolving. Defaults to 1 */
|
||||
amount?: number;
|
||||
/** The amount of milliseconds this should collect for before expiring. Defaults to 5 minutes. */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface ReactionCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (userId: bigint, reaction: string, message: Message | { id: string }) => boolean;
|
||||
}
|
||||
|
||||
export interface BaseCollectorCreateOptions {
|
||||
/** The unique key that will be used to get responses for this. Ideally, meant to be for member id. */
|
||||
key: bigint;
|
||||
/** The amount of messages to collect before resolving. */
|
||||
amount: number;
|
||||
/** The timestamp when this collector was created */
|
||||
createdAt: number;
|
||||
/** The duration in milliseconds how long this collector should last. */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface CollectMessagesOptions extends BaseCollectorCreateOptions {
|
||||
/** The channel Id where this is listening to */
|
||||
channelId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (message: Message) => boolean;
|
||||
}
|
||||
|
||||
export interface CollectReactionsOptions extends BaseCollectorCreateOptions {
|
||||
/** The message Id where this is listening to */
|
||||
messageId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (userId: bigint, reaction: string, message: Message | { id: string }) => boolean;
|
||||
}
|
||||
|
||||
export interface MessageCollector extends CollectMessagesOptions {
|
||||
resolve: (value: Message[] | PromiseLike<Message[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the messages are stored if the amount to collect is more than 1. */
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface ReactionCollector extends CollectReactionsOptions {
|
||||
resolve: (value: string[] | PromiseLike<string[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the reactions are stored if the amount to collect is more than 1. */
|
||||
reactions: string[];
|
||||
}
|
||||
|
||||
export interface CollectButtonOptions extends BaseCollectorCreateOptions {
|
||||
/** The message Id where this is listening to */
|
||||
messageId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (message: Message, member?: Member) => boolean;
|
||||
}
|
||||
|
||||
export interface ButtonCollector extends CollectButtonOptions {
|
||||
resolve: (value: ButtonCollectorReturn[] | PromiseLike<ButtonCollectorReturn[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the buttons are stored if the amount to collect is more than 1. */
|
||||
buttons: ButtonCollectorReturn[];
|
||||
}
|
||||
|
||||
export interface ButtonCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (message: Message, member?: Member) => boolean;
|
||||
}
|
||||
|
||||
export interface ButtonCollectorReturn {
|
||||
customId: string;
|
||||
interaction: Omit<Interaction, "member">;
|
||||
member?: Member;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { BotWithCustomProps } from "../../bot.js";
|
||||
import { customizeTransformers } from "./transformers/mod.js";
|
||||
|
||||
export function customizeInternals(bot: BotWithCustomProps) {
|
||||
customizeTransformers(bot);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// SETUP-DD-TEMP: This file is an example, of how to customize an object properties that you do not want.
|
||||
// Only keep the properties your bot uses. If your bot does not use emojis in cache, you can save all that memory.
|
||||
// This file is currently disabled, but you can enable it should you choose when you go the customizer file.
|
||||
// Feel free to delete this comment or file as you wish.
|
||||
import type { Guild } from "discordeno";
|
||||
import { Collection } from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../../bot.js";
|
||||
|
||||
export function customizeGuildTransformer(bot: BotWithCustomProps) {
|
||||
bot.transformers.guild = function (bot, payload) {
|
||||
const guildId = bot.transformers.snowflake(payload.guild.id);
|
||||
|
||||
return {
|
||||
name: payload.guild.name,
|
||||
joinedAt: payload.guild.joined_at ? Date.parse(payload.guild.joined_at) : undefined,
|
||||
memberCount: payload.guild.member_count ?? 0,
|
||||
shardId: payload.shardId,
|
||||
icon: payload.guild.icon ? bot.utils.iconHashToBigInt(payload.guild.icon) : undefined,
|
||||
roles: new Collection(
|
||||
payload.guild.roles?.map((role) => {
|
||||
const result = bot.transformers.role(bot, { role, guildId });
|
||||
return [result.id, result];
|
||||
}),
|
||||
),
|
||||
id: guildId,
|
||||
ownerId: bot.transformers.snowflake(payload.guild.owner_id),
|
||||
} as unknown as Guild;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// SETUP-DD-TEMP: This file serves as an example, of how to customize internal discordeno objects. Feel free to use, add more or remove as desired.
|
||||
import type { InteractionCallbackData, InteractionResponse} from "discordeno";
|
||||
import { InteractionResponseTypes } from "discordeno";
|
||||
import type { BotWithCustomProps } from "../../../bot.js";
|
||||
|
||||
export function customizeInteractionTransformer(bot: BotWithCustomProps) {
|
||||
// Store the internal transformer function
|
||||
const oldInteraction = bot.transformers.interaction;
|
||||
|
||||
// Overwrite the internal function.
|
||||
bot.transformers.interaction = function (_, payload) {
|
||||
// Run the old function to get the internal value.
|
||||
const interaction = oldInteraction(bot, payload);
|
||||
|
||||
// Add anything to this object. In this case we add a Interaction.reply() method.
|
||||
Object.defineProperty(interaction, "reply", {
|
||||
value: function (response: InteractionResponse | string) {
|
||||
if (typeof response === "string") {
|
||||
response = { type: InteractionResponseTypes.ChannelMessageWithSource, data: { content: response } };
|
||||
}
|
||||
|
||||
return bot.helpers.sendInteractionResponse(interaction.id, interaction.token, response);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(interaction, "editReply", {
|
||||
value: function (response: InteractionCallbackData | string) {
|
||||
if (typeof response === "string") {
|
||||
response = { content: response };
|
||||
}
|
||||
|
||||
return bot.helpers.editOriginalInteractionResponse(interaction.token, response);
|
||||
},
|
||||
});
|
||||
// Add as many properties or methods you would like here.
|
||||
// NOTE: Whenever you add anything here, in order to get nice autocomplete you should also add it to the src/types/discordeno.ts file.
|
||||
|
||||
// Return the new customized object.
|
||||
return interaction;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { BotWithCustomProps } from "../../../bot.js";
|
||||
// SETUP-DD-TEMP: Enable this comment if you want to enable this customizer.
|
||||
// import { customizeGuildTransformer } from "./guild.js";
|
||||
import { customizeInteractionTransformer } from "./interaction.js";
|
||||
|
||||
export function customizeTransformers(bot: BotWithCustomProps) {
|
||||
customizeInteractionTransformer(bot);
|
||||
// SETUP-DD-TEMP: Enable this comment if you want to enable this customizer.
|
||||
// customizeGuildTransformer(bot);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import type {
|
||||
ApplicationCommandOptionTypes,
|
||||
ApplicationCommandTypes,
|
||||
Bot,
|
||||
Channel,
|
||||
Interaction,
|
||||
Member,
|
||||
PermissionStrings,
|
||||
Role,
|
||||
User,
|
||||
} from "discordeno";
|
||||
import type english from "../../languages/english.js";
|
||||
import type { translationKeys } from "../../languages/translate.js";
|
||||
import type { InteractionWithCustomProps } from "../../typings/discordeno.js";
|
||||
import type { PermissionLevelHandlers } from "./permLevels.js";
|
||||
|
||||
export function createCommand<T extends readonly ArgumentDefinition[]>(command: Command<T>) {
|
||||
return command;
|
||||
}
|
||||
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
||||
|
||||
type Identity<T> = { [P in keyof T]: T[P] };
|
||||
|
||||
// TODO: make required by default true
|
||||
// Define each of the types here
|
||||
interface BaseDefinition {
|
||||
description: translationKeys;
|
||||
}
|
||||
|
||||
// Subcommand
|
||||
type SubcommandArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.SubCommand;
|
||||
// options: Omit<ArgumentDefinition, 'SubcommandArgumentDefinition' | 'SubcommandGroupArgumentDefinition'>[]
|
||||
options?: readonly ArgumentDefinition[];
|
||||
};
|
||||
|
||||
// SubcommandGroup
|
||||
type SubcommandGroupArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.SubCommandGroup;
|
||||
options: readonly SubcommandArgumentDefinition[];
|
||||
};
|
||||
|
||||
// String
|
||||
type StringArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: ReadonlyArray<{ name: string; value: string }>;
|
||||
required?: true;
|
||||
};
|
||||
type StringOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: ReadonlyArray<{ name: string; value: string }>;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// Integer
|
||||
type IntegerArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: ReadonlyArray<{ name: string; value: number }>;
|
||||
required: true;
|
||||
};
|
||||
type IntegerOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: ReadonlyArray<{ name: string; value: number }>;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// BOOLEAN
|
||||
type BooleanArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required: true;
|
||||
};
|
||||
type BooleanOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// USER
|
||||
type UserArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required: true;
|
||||
};
|
||||
type UserOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// CHANNEL
|
||||
type ChannelArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required: true;
|
||||
};
|
||||
type ChannelOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// ROLE
|
||||
type RoleArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required: true;
|
||||
};
|
||||
type RoleOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// MENTIONABLE
|
||||
type MentionableArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required: true;
|
||||
};
|
||||
type MentionableOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// Add each of known ArgumentDefinitions to this union.
|
||||
export type ArgumentDefinition =
|
||||
| StringArgumentDefinition
|
||||
| StringOptionalArgumentDefinition
|
||||
| IntegerArgumentDefinition
|
||||
| IntegerOptionalArgumentDefinition
|
||||
| BooleanArgumentDefinition
|
||||
| BooleanOptionalArgumentDefinition
|
||||
| UserArgumentDefinition
|
||||
| UserOptionalArgumentDefinition
|
||||
| ChannelArgumentDefinition
|
||||
| ChannelOptionalArgumentDefinition
|
||||
| RoleArgumentDefinition
|
||||
| RoleOptionalArgumentDefinition
|
||||
| MentionableArgumentDefinition
|
||||
| MentionableOptionalArgumentDefinition
|
||||
| SubcommandArgumentDefinition
|
||||
| SubcommandGroupArgumentDefinition;
|
||||
|
||||
type getName<K extends translationKeys> = typeof english[K] extends string ? typeof english[K] : never;
|
||||
|
||||
// OPTIONALS MUST BE FIRST!!!
|
||||
export type ConvertArgumentDefinitionsToArgs<T extends readonly ArgumentDefinition[]> = Identity<
|
||||
UnionToIntersection<
|
||||
{
|
||||
[P in keyof T]: T[P] extends StringOptionalArgumentDefinition<infer N> // STRING
|
||||
? {
|
||||
// @ts-expect-error TODO: fix this some day
|
||||
[_ in getName<N>]?: T[P]["choices"] extends ReadonlyArray<{ name: string; value: string }> // @ts-expect-error
|
||||
? T[P]["choices"][number]["value"]
|
||||
: string;
|
||||
}
|
||||
: T[P] extends StringArgumentDefinition<infer N> ? {
|
||||
// @ts-expect-error TODO: fix this some day
|
||||
[_ in getName<N>]: T[P]["choices"] extends ReadonlyArray<{ name: string; value: string }> // @ts-expect-error
|
||||
? T[P]["choices"][number]["value"]
|
||||
: string;
|
||||
}
|
||||
// INTEGER
|
||||
: T[P] extends IntegerOptionalArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: T[P]["choices"] extends ReadonlyArray<{ name: string; value: number }> // @ts-expect-error
|
||||
? T[P]["choices"][number]["value"]
|
||||
: number;
|
||||
}
|
||||
: T[P] extends IntegerArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]: T[P]["choices"] extends ReadonlyArray<{ name: string; value: number }> // @ts-expect-error
|
||||
? T[P]["choices"][number]["value"]
|
||||
: number;
|
||||
}
|
||||
// BOOLEAN
|
||||
: T[P] extends BooleanOptionalArgumentDefinition<infer N> ? { [_ in getName<N>]?: boolean }
|
||||
: T[P] extends BooleanArgumentDefinition<infer N> ? { [_ in getName<N>]: boolean }
|
||||
// USER
|
||||
: T[P] extends UserOptionalArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: T[P] extends UserArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]: {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
// CHANNEL
|
||||
: T[P] extends ChannelOptionalArgumentDefinition<infer N> ? { [_ in getName<N>]?: Channel }
|
||||
: T[P] extends ChannelArgumentDefinition<infer N> ? { [_ in getName<N>]: Channel }
|
||||
// ROLE
|
||||
: T[P] extends RoleOptionalArgumentDefinition<infer N> ? { [_ in getName<N>]?: Role }
|
||||
: T[P] extends RoleArgumentDefinition<infer N> ? { [_ in getName<N>]: Role }
|
||||
// MENTIONABLE
|
||||
: T[P] extends MentionableOptionalArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?:
|
||||
| Role
|
||||
| {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: T[P] extends MentionableArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]:
|
||||
| Role
|
||||
| {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
// SUBCOMMAND
|
||||
: T[P] extends SubcommandArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: T[P]["options"] extends readonly ArgumentDefinition[] // @ts-expect-error somehow this check does not work
|
||||
? ConvertArgumentDefinitionsToArgs<T[P]["options"]>
|
||||
: {};
|
||||
}
|
||||
// SUBCOMMANDGROUP
|
||||
: T[P] extends SubcommandGroupArgumentDefinition<infer N> ? {
|
||||
[_ in getName<N>]?: ConvertArgumentDefinitionsToArgs<T[P]["options"]>;
|
||||
}
|
||||
: never;
|
||||
}[number]
|
||||
>
|
||||
>;
|
||||
|
||||
export interface Command<T extends readonly ArgumentDefinition[]> {
|
||||
/** The name of the command, used for both slash and message commands. */
|
||||
name: translationKeys;
|
||||
/** The type of command. */
|
||||
type?: ApplicationCommandTypes;
|
||||
/** The description of the command */
|
||||
description: translationKeys;
|
||||
// TODO: consider type being a string like "number" | "user" for better ux
|
||||
/** The options for the command, used for both slash and message commands. */
|
||||
// options?: ApplicationCommandOption[];
|
||||
options?: T;
|
||||
execute: (bot: Bot, data: InteractionWithCustomProps, args: ConvertArgumentDefinitionsToArgs<T>) => unknown;
|
||||
subcommands?: Record<string, Omit<Command<any>, "subcommands"> & { group?: string }>;
|
||||
/** Whether the command should have a cooldown */
|
||||
cooldown?: {
|
||||
/** How long the user needs to wait after the first execution until he can use the command again */
|
||||
seconds: number;
|
||||
/** How often the user is allowed to use the command until he is in cooldown */
|
||||
allowedUses?: number;
|
||||
};
|
||||
nsfw?: boolean;
|
||||
/** By default false */
|
||||
global?: boolean;
|
||||
/** Dm only by default false */
|
||||
dmOnly?: boolean;
|
||||
|
||||
/** VIP only by default false */
|
||||
vipOnly?: boolean;
|
||||
|
||||
advanced?: boolean;
|
||||
|
||||
/** Whether or not this slash command should be enabled right now. Defaults to true. */
|
||||
enabled?: boolean;
|
||||
/** Whether or not this command is still in development and should be setup in the dev server for testing. */
|
||||
dev?: boolean;
|
||||
/** Whether or not this command will take longer than 3s and need to acknowledge to discord. */
|
||||
acknowledge?: boolean;
|
||||
|
||||
permissionLevels?:
|
||||
| Array<keyof typeof PermissionLevelHandlers>
|
||||
| ((data: Interaction, command: Command<T>) => boolean | Promise<boolean>);
|
||||
botServerPermissions?: PermissionStrings[];
|
||||
botChannelPermissions?: PermissionStrings[];
|
||||
userServerPermissions?: PermissionStrings[];
|
||||
userChannelPermissions?: PermissionStrings[];
|
||||
}
|
||||
|
||||
export enum PermissionLevels {
|
||||
Member,
|
||||
Moderator,
|
||||
Admin,
|
||||
ServerOwner,
|
||||
BotSupporter,
|
||||
BotDev,
|
||||
BotOwner,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import COMMANDS from "../../commands/mod.js";
|
||||
|
||||
export async function validateSlashLimits() {
|
||||
const MAX_ALLOWED_CHARACTERS = 4000;
|
||||
|
||||
const commands = await fetch("https://cmd-counter-play.deno.dev/", {
|
||||
body: JSON.stringify(COMMANDS),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}).then(async (res) => await res.json()).catch(() => undefined);
|
||||
|
||||
if (!commands) return;
|
||||
|
||||
const invalidCommandNames: string[] = [];
|
||||
|
||||
if (commands[0]?.characters > MAX_ALLOWED_CHARACTERS) {
|
||||
for (const command of commands) {
|
||||
if (command.characters <= MAX_ALLOWED_CHARACTERS) continue;
|
||||
|
||||
invalidCommandNames.push(command.name);
|
||||
console.log(
|
||||
`[Invalid Command] The ${command.name} is not a valid command. It's total characters are (${command.characters}) which is more than the max allowed ${MAX_ALLOWED_CHARACTERS}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidCommandNames.length) throw new Error(`[Startup] Invalid commands: ${invalidCommandNames.join(", ")}`);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Interaction } from "discordeno";
|
||||
import { validatePermissions } from "discordeno/permissions-plugin";
|
||||
import type { Command } from "./createCommand.js";
|
||||
|
||||
export default async function hasPermissionLevel(command: Command<any>, payload: Interaction) {
|
||||
// This command doesnt require a perm level so allow the command.
|
||||
if (!command.permissionLevels) return true;
|
||||
|
||||
// If a custom function was provided
|
||||
if (typeof command.permissionLevels === "function") {
|
||||
return await command.permissionLevels(payload, command);
|
||||
}
|
||||
|
||||
// If an array of perm levels was provided
|
||||
for (const permlevel of command.permissionLevels) {
|
||||
// If this user has one of the allowed perm level, the loop is canceled and command is allowed.
|
||||
if (await PermissionLevelHandlers[permlevel](payload, command)) return true;
|
||||
}
|
||||
|
||||
// None of the perm levels were met. So cancel the command
|
||||
return false;
|
||||
}
|
||||
|
||||
export const PermissionLevelHandlers: Record<
|
||||
keyof typeof PermissionLevels,
|
||||
(payload: Interaction, command: Command<any>) => boolean | Promise<boolean>
|
||||
> = {
|
||||
MEMBER: () => true,
|
||||
MODERATOR: (payload) =>
|
||||
Boolean(payload.member?.permissions) && validatePermissions(payload.member!.permissions!, ["MANAGE_GUILD"]),
|
||||
ADMIN: (payload) =>
|
||||
Boolean(payload.member?.permissions) && validatePermissions(payload.member!.permissions!, ["ADMINISTRATOR"]),
|
||||
// TODO(cache): fix this
|
||||
SERVER_OWNER: () => false,
|
||||
BOT_SUPPORT: () => false,
|
||||
BOT_DEVS: () => false,
|
||||
BOT_OWNERS: (payload) => [130136895395987456n, 615542460151496705n].includes(payload.user.id),
|
||||
};
|
||||
|
||||
export enum PermissionLevels {
|
||||
MEMBER,
|
||||
MODERATOR,
|
||||
ADMIN,
|
||||
SERVER_OWNER,
|
||||
BOT_SUPPORT,
|
||||
BOT_DEVS,
|
||||
BOT_OWNERS,
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import type { ApplicationCommandOption, Bot } from "discordeno";
|
||||
import { ApplicationCommandTypes } from "discordeno";
|
||||
import { prisma } from "../../../prisma.js";
|
||||
import { bot } from "../../bot.js";
|
||||
import COMMANDS from "../../commands/mod.js";
|
||||
import { serverLanguages, translate } from "../../languages/translate.js";
|
||||
import type { ArgumentDefinition } from "./createCommand.js";
|
||||
|
||||
const DEV_SERVER_ID = process.env.DEV_SERVER_ID as string;
|
||||
|
||||
export async function updateDevCommands(bot: Bot) {
|
||||
const cmds = Object.entries(COMMANDS)
|
||||
// ONLY DEV COMMANDS
|
||||
.filter(([_name, command]) => command?.dev);
|
||||
|
||||
if (!cmds.length) return;
|
||||
|
||||
// DEV RELATED COMMANDS, USE upsertGlobalApplicationCommands TO UPDATE GLOBALLY
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
bot.transformers.snowflake(DEV_SERVER_ID),
|
||||
cmds.map(([name, command]) => {
|
||||
const translatedName = translate(DEV_SERVER_ID, command.name);
|
||||
const translatedDescription = command.description ? translate(DEV_SERVER_ID, command.description) : "";
|
||||
|
||||
if (command.type && command.type !== ApplicationCommandTypes.ChatInput) {
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
type: command.type,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
description: translatedDescription || command.description,
|
||||
options: command.options
|
||||
? createOptions(bot.transformers.snowflake(DEV_SERVER_ID), command.options, command.name)
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// SETUP-DD-TEMP: You can make this able to be updated dynicamally by moving this value to something in the database and having a command to update it on the fly or as part of CI.
|
||||
export const CURRENT_SLASH_COMMAND_VERSION = 1;
|
||||
|
||||
/** Whether the guild has the latest slash command version */
|
||||
export async function usesLatestCommandVersion(guildId: bigint): Promise<boolean> {
|
||||
return (await getCurrentCommandVersion(guildId)) === CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
|
||||
/** Get the current slash command version for this guild */
|
||||
export async function getCurrentCommandVersion(guildId: bigint): Promise<number> {
|
||||
if (bot.commandVersions.has(guildId)) return bot.commandVersions.get(guildId)!;
|
||||
|
||||
const commandVersion = await prisma.commands.findUnique({ where: { id: guildId } });
|
||||
if (commandVersion) bot.commandVersions.set(guildId, commandVersion.version);
|
||||
|
||||
return commandVersion?.version ?? 0;
|
||||
}
|
||||
|
||||
export async function updateCommandVersion(guildId: bigint): Promise<number> {
|
||||
// UPDATE THE VERSION SAVED IN THE DB
|
||||
await prisma.commands.upsert({
|
||||
where: { id: guildId },
|
||||
create: { id: guildId, version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
update: { version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
});
|
||||
|
||||
bot.commandVersions.set(guildId, CURRENT_SLASH_COMMAND_VERSION);
|
||||
return CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
|
||||
export async function updateGuildCommands(bot: Bot, guildId: bigint) {
|
||||
if (guildId === 547046977578336286n) return await updateDevCommands(bot);
|
||||
|
||||
await updateCommandVersion(guildId);
|
||||
|
||||
// GUILD RELATED COMMANDS
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
guildId,
|
||||
Object.entries(COMMANDS)
|
||||
// ONLY GUILD COMMANDS
|
||||
.filter(([_name, command]) => !command.global && !command.dev)
|
||||
.map(([name, command]) => {
|
||||
// USER OPTED TO USE BASIC VERSION ONLY
|
||||
if (command.advanced === false) {
|
||||
return {
|
||||
name,
|
||||
description: translate("english", command.description),
|
||||
options: command.options ? createOptions("english", command.options, command.name) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ADVANCED VERSION WILL ALLOW TRANSLATION
|
||||
const translatedName = translate(guildId, command.name);
|
||||
const translatedDescription = translate(guildId, command.description);
|
||||
|
||||
return {
|
||||
name: translatedName.toLowerCase(),
|
||||
description: translatedDescription,
|
||||
options: command.options ? createOptions(guildId, command.options, command.name) : undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// USED TO CACHE CONVERTED COMMANDS AFTER START TO PREVENT UNNECESSARY LOOPS
|
||||
const convertedCache = new Map<string, ApplicationCommandOption[]>();
|
||||
|
||||
/** Creates the commands options including subcommands. Also translates them. */
|
||||
function createOptions(
|
||||
guildId: bigint | "english",
|
||||
options: readonly ArgumentDefinition[],
|
||||
commandName?: string,
|
||||
): ApplicationCommandOption[] | undefined {
|
||||
const language = guildId === "english" ? "english" : serverLanguages.get(guildId) ?? "english";
|
||||
if (commandName && convertedCache.has(`${language}-${commandName}`)) {
|
||||
return convertedCache.get(`${language}-${commandName}`)!;
|
||||
}
|
||||
|
||||
const newOptions: ApplicationCommandOption[] = [];
|
||||
|
||||
for (const option of options || []) {
|
||||
const optionName = translate(guildId, option.name);
|
||||
const optionDescription = translate(guildId, option.description);
|
||||
|
||||
// TODO: remove this ts ignore
|
||||
// @ts-expect-error
|
||||
const choices = option.choices?.map((choice) => ({
|
||||
...choice,
|
||||
name: translate(guildId, choice.name),
|
||||
}));
|
||||
|
||||
newOptions.push({
|
||||
...option,
|
||||
name: optionName.toLowerCase(),
|
||||
description: optionDescription || "No description available.",
|
||||
choices,
|
||||
// @ts-expect-error fix this
|
||||
options: option.options
|
||||
// @ts-expect-error fix this
|
||||
? createOptions(bot, guildId, option.options)
|
||||
: undefined,
|
||||
} as ApplicationCommandOption);
|
||||
}
|
||||
if (commandName) convertedCache.set(`${language}-${commandName}`, newOptions);
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** Get the webhook id and token from a webhook url. */
|
||||
export function webhookURLToIDAndToken(url: string) {
|
||||
const [id, token] = url.substring(url.indexOf("webhooks/") + 9).split(
|
||||
"/",
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
token,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getBotIdFromToken, Intents } from "discordeno";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
/** The bot id, derived from the bot token. */
|
||||
export const BOT_ID = getBotIdFromToken(process.env.DISCORD_TOKEN as string);
|
||||
export const EVENT_HANDLER_URL = `http://${process.env.EVENT_HANDLER_HOST}:${process.env.EVENT_HANDLER_PORT}`;
|
||||
export const REST_URL = `http://${process.env.REST_HOST}:${process.env.REST_PORT}`;
|
||||
export const GATEWAY_URL = `http://${process.env.GATEWAY_HOST}:${process.env.GATEWAY_PORT}`;
|
||||
|
||||
// Gateway Proxy Configurations
|
||||
/** The gateway intents you would like to use. */
|
||||
export const INTENTS: Intents =
|
||||
// SETUP-DD-TEMP: Add the intents you want enabled here. Or Delete the intents you don't want in your bot.
|
||||
Intents.DirectMessageReactions |
|
||||
Intents.DirectMessageTyping |
|
||||
Intents.DirectMessages |
|
||||
Intents.GuildBans |
|
||||
Intents.GuildEmojis |
|
||||
Intents.GuildIntegrations |
|
||||
Intents.GuildInvites |
|
||||
Intents.GuildMembers |
|
||||
Intents.GuildMessageReactions |
|
||||
Intents.GuildMessageTyping |
|
||||
Intents.GuildMessages |
|
||||
Intents.GuildPresences |
|
||||
Intents.GuildVoiceStates |
|
||||
Intents.GuildWebhooks |
|
||||
Intents.Guilds;
|
||||
@@ -0,0 +1,8 @@
|
||||
# Big Bot Gateway Proxy Server
|
||||
|
||||
This folder will contain the code for our gateway. When events are received from Discord they are sent here and this
|
||||
proxy will forward them to our event handler.
|
||||
|
||||
## Further Steps
|
||||
|
||||
- Resharding
|
||||
@@ -0,0 +1,198 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { Collection, createBot, createGatewayManager, createRestManager } from "discordeno";
|
||||
import { createLogger } from "discordeno/logger";
|
||||
import fastify from "fastify";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Worker } from "worker_threads";
|
||||
import { EVENT_HANDLER_URL, INTENTS, REST_URL } from "../configs.js";
|
||||
import type { WorkerCreateData, WorkerGetShardInfo, WorkerMessage, WorkerShardInfo, WorkerShardPayload } from "./worker.js";
|
||||
dotenv.config();
|
||||
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
||||
const EVENT_HANDLER_AUTHORIZATION = process.env.EVENT_HANDLER_AUTHORIZATION as string;
|
||||
const GATEWAY_AUTHORIZATION = process.env.GATEWAY_AUTHORIZATION as string;
|
||||
const GATEWAY_PORT = Number(process.env.GATEWAY_PORT as string);
|
||||
const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
|
||||
const SHARDS_PER_WORKER = Number(process.env.SHARDS_PER_WORKER as string);
|
||||
const TOTAL_SHARDS = process.env.TOTAL_SHARDS ? Number(process.env.TOTAL_SHARDS) : undefined;
|
||||
const TOTAL_WORKERS = Number(process.env.TOTAL_WORKERS as string);
|
||||
|
||||
const log = createLogger({ name: "[MANAGER]" });
|
||||
|
||||
const bot = createBot({
|
||||
token: DISCORD_TOKEN,
|
||||
});
|
||||
|
||||
bot.rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
});
|
||||
|
||||
const gatewayBot = await bot.helpers.getGatewayBot();
|
||||
|
||||
const gateway = createGatewayManager({
|
||||
gatewayBot,
|
||||
gatewayConfig: {
|
||||
token: DISCORD_TOKEN,
|
||||
intents: INTENTS,
|
||||
},
|
||||
// force the total amount of shards
|
||||
totalShards: TOTAL_SHARDS,
|
||||
shardsPerWorker: SHARDS_PER_WORKER,
|
||||
totalWorkers: TOTAL_WORKERS,
|
||||
|
||||
handleDiscordPayload: () => {},
|
||||
|
||||
tellWorkerToIdentify: async (_gateway, workerId, shardId, _bucketId) => {
|
||||
log.info("TELL TO IDENTIFY", { workerId, shardId, _bucketId });
|
||||
|
||||
let worker = workers.get(workerId);
|
||||
if (!worker) {
|
||||
worker = createWorker(workerId);
|
||||
workers.set(workerId, worker);
|
||||
}
|
||||
|
||||
const identify: WorkerMessage = {
|
||||
type: "IDENTIFY_SHARD",
|
||||
shardId,
|
||||
};
|
||||
|
||||
worker.postMessage(identify);
|
||||
},
|
||||
});
|
||||
|
||||
const workers = new Collection<number, Worker>();
|
||||
const nonces = new Collection<string, (data: any) => void>();
|
||||
|
||||
function createWorker(workerId: number) {
|
||||
console.log(TOTAL_SHARDS, gateway.manager.totalShards, "SHARDS");
|
||||
|
||||
const workerData: WorkerCreateData = {
|
||||
intents: gateway.manager.gatewayConfig.intents ?? 0,
|
||||
token: DISCORD_TOKEN,
|
||||
handlerUrls: [EVENT_HANDLER_URL],
|
||||
handlerAuthorization: EVENT_HANDLER_AUTHORIZATION,
|
||||
path: "./worker.ts",
|
||||
totalShards: gateway.manager.totalShards,
|
||||
workerId,
|
||||
};
|
||||
|
||||
const worker = new Worker("./dist/gateway/worker.js", {
|
||||
workerData,
|
||||
});
|
||||
|
||||
worker.on("message", async (data: ManagerMessage) => {
|
||||
log.info({ data });
|
||||
switch (data.type) {
|
||||
case "REQUEST_IDENTIFY": {
|
||||
log.info("REQUESTING IDENTIFY #", data.shardId);
|
||||
await gateway.manager.requestIdentify(data.shardId);
|
||||
|
||||
const allowIdentify: WorkerMessage = {
|
||||
type: "ALLOW_IDENTIFY",
|
||||
shardId: data.shardId,
|
||||
};
|
||||
|
||||
worker.postMessage(allowIdentify);
|
||||
|
||||
break;
|
||||
}
|
||||
case "NONCE_REPLY": {
|
||||
nonces.get(data.nonce)?.(data.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return worker;
|
||||
}
|
||||
|
||||
gateway.spawnShards();
|
||||
|
||||
const server = fastify();
|
||||
|
||||
server.post("/", async (request, reply) => {
|
||||
if (request.headers.authorization !== GATEWAY_AUTHORIZATION) {
|
||||
reply.code(StatusCodes.Unauthorized);
|
||||
|
||||
return reply.send({ processing: false, error: false, message: "Invalid authorization header." });
|
||||
}
|
||||
|
||||
if (!request.body) {
|
||||
reply.code(StatusCodes.BadRequest);
|
||||
|
||||
return reply.send({ processing: false, error: false, message: "Empty body." });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = request.body as WorkerShardPayload | Omit<WorkerGetShardInfo, "nonce">;
|
||||
switch (data.type) {
|
||||
case "SHARD_PAYLOAD": {
|
||||
const workerId = gateway.calculateWorkerId(data.shardId);
|
||||
const worker = workers.get(workerId);
|
||||
|
||||
worker?.postMessage(data);
|
||||
|
||||
break;
|
||||
}
|
||||
case "GET_SHARD_INFO": {
|
||||
const infos = await Promise.all(
|
||||
workers.map(async (worker) => {
|
||||
const nonce = nanoid();
|
||||
|
||||
return await new Promise<WorkerShardInfo[]>((resolve) => {
|
||||
worker.postMessage({ type: "GET_SHARD_INFO", nonce });
|
||||
|
||||
nonces.set(nonce, resolve);
|
||||
});
|
||||
}),
|
||||
).then((res) =>
|
||||
res.reduce((acc, cur) => {
|
||||
acc.push(...cur);
|
||||
return acc;
|
||||
}, [] as WorkerShardInfo[])
|
||||
);
|
||||
|
||||
reply.code(StatusCodes.Ok);
|
||||
|
||||
return reply.send(infos);
|
||||
}
|
||||
}
|
||||
|
||||
reply.code(StatusCodes.Ok);
|
||||
|
||||
return reply.send({ processing: true });
|
||||
} catch {
|
||||
reply.code(StatusCodes.BadRequest);
|
||||
|
||||
return reply.send({ processing: false, error: true, message: "Failed to parse body." });
|
||||
}
|
||||
});
|
||||
|
||||
server.listen({ port: GATEWAY_PORT }).catch((error) => {
|
||||
log.error(["[FASTIFY ERROR", error].join("\n"));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
export type ManagerMessage = ManagerRequestIdentify | ManagerNonceReply<WorkerShardInfo[]>;
|
||||
|
||||
export interface ManagerRequestIdentify {
|
||||
type: "REQUEST_IDENTIFY";
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface ManagerNonceReply<T> {
|
||||
type: "NONCE_REPLY";
|
||||
nonce: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
enum StatusCodes {
|
||||
Ok = 200,
|
||||
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
|
||||
InternalServerError = 500,
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import amqplib from "amqplib";
|
||||
import type {
|
||||
DiscordGuild,
|
||||
DiscordReady,
|
||||
DiscordUnavailableGuild,
|
||||
Shard,
|
||||
ShardSocketRequest,
|
||||
ShardState} from "discordeno";
|
||||
import {
|
||||
createShardManager,
|
||||
GatewayEventNames
|
||||
} from "discordeno";
|
||||
import { createLogger } from "discordeno/logger";
|
||||
import fetch from "node-fetch";
|
||||
import crypto from "node:crypto";
|
||||
import { parentPort, workerData } from "worker_threads";
|
||||
import type { ManagerMessage } from './index.js';
|
||||
dotenv.config();
|
||||
|
||||
if (!parentPort) {
|
||||
throw new Error("Parent port is null");
|
||||
}
|
||||
|
||||
const script: WorkerCreateData = workerData;
|
||||
|
||||
const log = createLogger({ name: `[WORKER #${script.workerId}]` });
|
||||
|
||||
const identifyPromises = new Map<number, () => void>();
|
||||
|
||||
let channel: amqplib.Channel | undefined;
|
||||
|
||||
const useMessageQueue = process.env.MESSAGEQUEUE_ENABLE === "true";
|
||||
|
||||
// Store guild ids, loading guild ids to change GUILD_CREATE event to GUILD_LOADED_DD if needed.
|
||||
const guildIds: Set<bigint> = new Set();
|
||||
const loadingGuildIds: Set<bigint> = new Set();
|
||||
|
||||
const manager = createShardManager({
|
||||
gatewayConfig: {
|
||||
intents: script.intents,
|
||||
token: script.token,
|
||||
},
|
||||
shardIds: [],
|
||||
totalShards: script.totalShards,
|
||||
handleMessage: async (shard, message) => {
|
||||
const url = script.handlerUrls[shard.id % script.handlerUrls.length];
|
||||
if (!url) return console.log("ERROR: NO URL FOUND TO SEND MESSAGE");
|
||||
|
||||
if (message.t === "READY") {
|
||||
// Marks which guilds the bot in when initial loading in cache.
|
||||
(message.d as DiscordReady).guilds.forEach((g) => loadingGuildIds.add(BigInt(g.id)));
|
||||
}
|
||||
|
||||
// If GUILD_CREATE event came from a shard loaded event, change event to GUILD_LOADED_DD.
|
||||
if (message.t === "GUILD_CREATE") {
|
||||
const guild = message.d as DiscordGuild;
|
||||
const id = BigInt(guild.id);
|
||||
|
||||
const existing = guildIds.has(id);
|
||||
if (existing) return;
|
||||
|
||||
if (loadingGuildIds.has(id)) {
|
||||
(message.t ) = "GUILD_LOADED_DD";
|
||||
|
||||
loadingGuildIds.delete(id);
|
||||
}
|
||||
|
||||
guildIds.add(id);
|
||||
}
|
||||
|
||||
// Delete guild id from cache so GUILD_CREATE from the same guild later works properly.
|
||||
if (message.t === "GUILD_DELETE") {
|
||||
const guild = message.d as DiscordUnavailableGuild;
|
||||
|
||||
if (guild.unavailable) return;
|
||||
|
||||
guildIds.delete(BigInt(guild.id));
|
||||
}
|
||||
|
||||
if (useMessageQueue) {
|
||||
if (!channel) return;
|
||||
await channel.publish(
|
||||
"gatewayMessage",
|
||||
"",
|
||||
Buffer.from(JSON.stringify({ shard, message })),
|
||||
{
|
||||
contentType: "application/json",
|
||||
headers: {
|
||||
"x-deduplication-header": crypto.createHash("md5").update(JSON.stringify(message.d)).digest("hex"),
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message, shardId: shard.id }),
|
||||
headers: { "Content-Type": "application/json", Authorization: script.handlerAuthorization },
|
||||
}).catch((error) => log.error(error));
|
||||
}
|
||||
|
||||
log.debug({ shardId: shard.id, message });
|
||||
},
|
||||
requestIdentify: async function (shardId: number): Promise<void> {
|
||||
return await new Promise((resolve) => {
|
||||
identifyPromises.set(shardId, resolve);
|
||||
|
||||
const identifyRequest: ManagerMessage = {
|
||||
type: "REQUEST_IDENTIFY",
|
||||
shardId,
|
||||
};
|
||||
|
||||
parentPort?.postMessage(identifyRequest);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function buildShardInfo(shard: Shard): WorkerShardInfo {
|
||||
return {
|
||||
workerId: script.workerId,
|
||||
shardId: shard.id,
|
||||
rtt: shard.heart.rtt || -1,
|
||||
state: shard.state,
|
||||
};
|
||||
}
|
||||
|
||||
parentPort.on("message", async (data: WorkerMessage) => {
|
||||
switch (data.type) {
|
||||
case "IDENTIFY_SHARD": {
|
||||
log.info(`starting to identify shard #${data.shardId}`);
|
||||
await manager.identify(data.shardId);
|
||||
|
||||
break;
|
||||
}
|
||||
case "ALLOW_IDENTIFY": {
|
||||
identifyPromises.get(data.shardId)?.();
|
||||
identifyPromises.delete(data.shardId);
|
||||
|
||||
break;
|
||||
}
|
||||
case "SHARD_PAYLOAD": {
|
||||
manager.shards.get(data.shardId)?.send(data.data);
|
||||
|
||||
break;
|
||||
}
|
||||
case "GET_SHARD_INFO": {
|
||||
const infos = manager.shards.map(buildShardInfo);
|
||||
|
||||
parentPort?.postMessage({ type: "NONCE_REPLY", nonce: data.nonce, data: infos });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type WorkerMessage = WorkerIdentifyShard | WorkerAllowIdentify | WorkerShardPayload | WorkerGetShardInfo;
|
||||
|
||||
export interface WorkerIdentifyShard {
|
||||
type: "IDENTIFY_SHARD";
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface WorkerAllowIdentify {
|
||||
type: "ALLOW_IDENTIFY";
|
||||
shardId: number;
|
||||
}
|
||||
|
||||
export interface WorkerShardPayload {
|
||||
type: "SHARD_PAYLOAD";
|
||||
shardId: number;
|
||||
data: ShardSocketRequest;
|
||||
}
|
||||
|
||||
export interface WorkerGetShardInfo {
|
||||
type: "GET_SHARD_INFO";
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
export interface WorkerCreateData {
|
||||
intents: number;
|
||||
token: string;
|
||||
handlerUrls: string[];
|
||||
handlerAuthorization: string;
|
||||
path: string;
|
||||
totalShards: number;
|
||||
workerId: number;
|
||||
}
|
||||
|
||||
export interface WorkerShardInfo {
|
||||
workerId: number;
|
||||
shardId: number;
|
||||
rtt: number;
|
||||
state: ShardState;
|
||||
}
|
||||
|
||||
const connectRabbitmq = async () => {
|
||||
let connection: amqplib.Connection | undefined;
|
||||
|
||||
try {
|
||||
connection = await amqplib.connect(
|
||||
`amqp://${process.env.MESSAGEQUEUE_USERNAME}:${process.env.MESSAGEQUEUE_PASSWORD}@${process.env.MESSAGEQUEUE_URL}`,
|
||||
);
|
||||
} catch (error) {
|
||||
channel = undefined;
|
||||
log.error(error);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
}
|
||||
|
||||
if (!connection) return;
|
||||
connection.on("error", (err) => {
|
||||
channel = undefined;
|
||||
log.error(err);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
|
||||
connection.on("close", () => {
|
||||
channel = undefined;
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
channel = await connection.createChannel();
|
||||
await channel.assertExchange(
|
||||
"gatewayMessage",
|
||||
"x-message-deduplication",
|
||||
{
|
||||
durable: true,
|
||||
arguments: {
|
||||
"x-cache-size": 1000,
|
||||
"x-cache-ttl": 500,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
channel = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (useMessageQueue) {
|
||||
connectRabbitmq();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
# REST Proxy
|
||||
|
||||
This folder will contain the code for our REST proxy. This is going to become the single source that all of our bot will
|
||||
use to communciate to the Discord API.
|
||||
|
||||
## Further Steps
|
||||
|
||||
- Express framework to create the listener however, you can replace it with anything you like. Express is quite a
|
||||
bloated framework. Feel free to optimize to a better framework.
|
||||
@@ -0,0 +1,60 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { BASE_URL, createRestManager } from "discordeno";
|
||||
import express from "express";
|
||||
import { setupAnalyticsHooks } from "../analytics.js";
|
||||
import { REST_URL } from "../configs.js";
|
||||
dotenv.config();
|
||||
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
||||
const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
|
||||
const REST_PORT = process.env.REST_PORT as string;
|
||||
|
||||
const rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
debug: console.log,
|
||||
});
|
||||
|
||||
// Add send fetching analytics hook to rest
|
||||
setupAnalyticsHooks(rest);
|
||||
|
||||
// @ts-expect-error
|
||||
rest.convertRestError = (errorStack, data) => {
|
||||
if (!data) return { message: errorStack.message };
|
||||
return { ...data, message: errorStack.message };
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.all("/*", async (req, res) => {
|
||||
if (!REST_AUTHORIZATION || REST_AUTHORIZATION !== req.headers.authorization) {
|
||||
return res.status(401).json({ error: "Invalid authorization key." });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await rest.runMethod(rest, req.method , `${BASE_URL}${req.url}`, req.body);
|
||||
|
||||
if (result) {
|
||||
res.status(200).json(result);
|
||||
} else {
|
||||
res.status(204).json();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(REST_PORT, () => {
|
||||
console.log(`REST listening at ${REST_URL}`);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"allowUnusedLabels": false,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"strict": true,
|
||||
"stripInternal": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"allowUnreachableCode": false,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
".env"
|
||||
],
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"experimentalSpecifierResolution": "node",
|
||||
"swc": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"lint": {
|
||||
"rules": {
|
||||
"tags": ["recommended"],
|
||||
"include": ["ban-untagged-todo"],
|
||||
"exclude": ["no-unused-vars"]
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"lineWidth": 120,
|
||||
"indentWidth": 2,
|
||||
"singleQuote": false,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
BOT_TOKEN=''
|
||||
@@ -0,0 +1,13 @@
|
||||
# Minimal Bot Template
|
||||
|
||||
Just the minimum to get a working bot using interactions.
|
||||
|
||||
Make sure to install the latest version when you use it.
|
||||
|
||||
## Setup
|
||||
|
||||
Just rename `.env.example` to `.env` and fill it with your bot token.
|
||||
|
||||
## Run Bot
|
||||
|
||||
- deno run -A mod.ts
|
||||
@@ -0,0 +1,5 @@
|
||||
import { dotEnvConfig } from './deps.ts.js';
|
||||
|
||||
dotEnvConfig({ export: true });
|
||||
export const BOT_TOKEN = process.env.BOT_TOKEN || "";
|
||||
export const BOT_ID = BigInt(atob(BOT_TOKEN.split(".")[0]));
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "https://deno.land/x/discordeno@17.0.0/mod.ts";
|
||||
export * from "https://deno.land/x/discordeno@17.0.0/plugins/mod.ts";
|
||||
export { config as dotEnvConfig } from "https://deno.land/x/dotenv@v3.1.0/mod.ts";
|
||||
export * from "https://deno.land/std@0.117.0/fmt/colors.ts";
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
ActivityTypes,
|
||||
createBot,
|
||||
enableCachePlugin,
|
||||
enableCacheSweepers,
|
||||
fastFileLoader,
|
||||
GatewayIntents,
|
||||
startBot,
|
||||
} from './deps.ts.js';
|
||||
import { BOT_ID, BOT_TOKEN } from './configs.ts.js';
|
||||
import { logger } from './src/utils/logger.ts.js';
|
||||
import { events } from './src/events/mod.ts.js';
|
||||
import { updateCommands } from './src/utils/helpers.ts.js';
|
||||
|
||||
const log = logger({ name: "Main" });
|
||||
|
||||
log.info("Starting Bot, this might take a while...");
|
||||
|
||||
const paths = ["./src/events", "./src/commands"];
|
||||
await fastFileLoader(paths).catch((err) => {
|
||||
log.fatal(`Unable to Import ${paths}`);
|
||||
log.fatal(err);
|
||||
Deno.exit(1);
|
||||
});
|
||||
|
||||
export const bot = enableCachePlugin(
|
||||
createBot({
|
||||
token: BOT_TOKEN,
|
||||
botId: BOT_ID,
|
||||
intents: GatewayIntents.Guilds,
|
||||
events,
|
||||
}),
|
||||
);
|
||||
|
||||
// @ts-nocheck: no-updated-depencdencies
|
||||
enableCacheSweepers(bot);
|
||||
|
||||
bot.gateway.manager.createShardOptions.makePresence = (shardId: number) => {
|
||||
return {
|
||||
shardId,
|
||||
status: "online",
|
||||
activities: [
|
||||
{
|
||||
name: "Discordeno is the Best Lib",
|
||||
type: ActivityTypes.Game,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
await startBot(bot);
|
||||
|
||||
await updateCommands(bot);
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ApplicationCommandOption, ApplicationCommandTypes, Bot, Interaction } from '../../deps.ts.js';
|
||||
import { Collection } from '../../deps.ts.js';
|
||||
|
||||
export type subCommand = Omit<Command, "subcommands">;
|
||||
export interface subCommandGroup {
|
||||
name: string;
|
||||
subCommands: subCommand[];
|
||||
}
|
||||
export interface Command {
|
||||
name: string;
|
||||
description: string;
|
||||
usage?: string[];
|
||||
options?: ApplicationCommandOption[];
|
||||
type: ApplicationCommandTypes;
|
||||
/** Defaults to `Guild` */
|
||||
scope?: "Global" | "Guild";
|
||||
execute: (bot: Bot, interaction: Interaction) => unknown;
|
||||
subcommands?: Array<subCommandGroup | subCommand>;
|
||||
}
|
||||
|
||||
export const commands = new Collection<string, Command>();
|
||||
|
||||
export function createCommand(command: Command) {
|
||||
commands.set(command.name, command);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ApplicationCommandTypes, InteractionResponseTypes } from '../../deps.ts.js';
|
||||
import { humanizeMilliseconds, snowflakeToTimestamp } from '../utils/helpers.ts.js';
|
||||
import { createCommand } from './mod.ts.js';
|
||||
|
||||
createCommand({
|
||||
name: "ping",
|
||||
description: "Ping the Bot!",
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
scope: "Global",
|
||||
execute: async (bot, interaction) => {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id);
|
||||
await bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { events } from './mod.ts.js';
|
||||
import { updateGuildCommands } from '../utils/helpers.ts.js';
|
||||
|
||||
events.guildCreate = async (bot, guild) => await updateGuildCommands(bot, guild);
|
||||
@@ -0,0 +1,249 @@
|
||||
import type {
|
||||
BotWithCache,
|
||||
Guild} from '../../deps.ts.js';
|
||||
import {
|
||||
ApplicationCommandOptionTypes,
|
||||
bgBlack,
|
||||
bgYellow,
|
||||
black,
|
||||
green,
|
||||
red,
|
||||
white,
|
||||
yellow,
|
||||
} from '../../deps.ts.js';
|
||||
import { events } from './mod.ts.js';
|
||||
import { logger } from '../utils/logger.ts.js';
|
||||
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.ts.js';
|
||||
import type { Command} from '../commands/mod.ts.js';
|
||||
import { commands } from '../commands/mod.ts.js';
|
||||
|
||||
const log = logger({ name: "Event: InteractionCreate" });
|
||||
|
||||
events.interactionCreate = async (rawBot, interaction) => {
|
||||
const bot = rawBot as BotWithCache;
|
||||
|
||||
if (interaction.data && interaction.id) {
|
||||
let guildName = "Direct Message";
|
||||
let guild = {} as Guild;
|
||||
|
||||
// Set guild, if there was an error getting the guild, then just say it was a DM. (What else are we going to do?)
|
||||
if (interaction.guildId) {
|
||||
const guildOrVoid = await getGuildFromId(bot, interaction.guildId).catch(
|
||||
(err) => {
|
||||
log.error(err);
|
||||
},
|
||||
);
|
||||
if (guildOrVoid) {
|
||||
guild = guildOrVoid;
|
||||
guildName = guild.name;
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${
|
||||
bgBlack(white(`Trigger`))
|
||||
}] by ${interaction.user.username}#${interaction.user.discriminator} in ${guildName}${
|
||||
guildName !== "Direct Message" ? ` (${guild.id})` : ``
|
||||
}`,
|
||||
);
|
||||
|
||||
let command: undefined | Command = interaction.data.name ? commands.get(interaction.data.name) : undefined;
|
||||
let commandName = command?.name;
|
||||
|
||||
if (command !== undefined) {
|
||||
if (interaction.data.name) {
|
||||
if (interaction.data.options?.[0]) {
|
||||
const optionType = interaction.data.options[0].type;
|
||||
|
||||
if (optionType === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!command.subcommands) return;
|
||||
|
||||
// Try to find the subcommand group
|
||||
const subCommandGroup = command.subcommands?.find(
|
||||
(command) => command.name == interaction.data?.options?.[0].name,
|
||||
);
|
||||
if (!subCommandGroup) return;
|
||||
|
||||
if (isSubCommand(subCommandGroup)) return;
|
||||
|
||||
// Get name of the command which we are looking for
|
||||
const targetCmdName = interaction.data.options?.[0].options?.[0].name ||
|
||||
interaction.data.options?.[0].options?.[0].name;
|
||||
if (!targetCmdName) return;
|
||||
|
||||
// Try to find the command
|
||||
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName);
|
||||
|
||||
commandName += ` ${subCommandGroup.name} ${command?.name}`;
|
||||
|
||||
// Normal
|
||||
}
|
||||
|
||||
if (optionType === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!command?.subcommands) return;
|
||||
|
||||
// Try to find the command
|
||||
const found = command.subcommands.find((command) => command.name == interaction.data?.options?.[0].name);
|
||||
if (!found) return;
|
||||
|
||||
if (isSubCommandGroup(found)) return;
|
||||
|
||||
command = found;
|
||||
commandName += ` ${command?.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (command) {
|
||||
command.execute(rawBot, interaction);
|
||||
log.info(
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${
|
||||
bgBlack(green(`Success`))
|
||||
}] by ${interaction.user.username}#${interaction.user.discriminator} in ${guildName}${
|
||||
guildName !== "Direct Message" ? ` (${guild.id})` : ``
|
||||
}`,
|
||||
);
|
||||
} else {
|
||||
throw "";
|
||||
}
|
||||
} catch (err) {
|
||||
log.error(
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${
|
||||
bgBlack(red(`Error`))
|
||||
}] by ${interaction.user.username}#${interaction.user.discriminator} in ${guildName}${
|
||||
guildName !== "Direct Message" ? ` (${guild.id})` : ``
|
||||
}`,
|
||||
);
|
||||
err.length ? log.error(err) : undefined;
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${
|
||||
bgBlack(yellow(`Not Found`))
|
||||
}] by ${interaction.user.username}#${interaction.user.discriminator} in ${guildName}${
|
||||
guildName !== "Direct Message" ? ` (${guild.id})` : ``
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
// Handle subcommands
|
||||
let cmdName = cmd.name;
|
||||
|
||||
// Group
|
||||
if (interaction.data?.options?.[0].type === DiscordApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!cmd.subcommands) return;
|
||||
|
||||
// Try to find the subcommand group
|
||||
const subCmdGroup = cmd.subcommands?.find((cmd) => cmd.name == interaction?.data?.options?.[0].name);
|
||||
if (!subCmdGroup) return;
|
||||
|
||||
if (isSubCommand(subCmdGroup)) return;
|
||||
|
||||
// Get name of the command which we are looking for
|
||||
const targetCmdName =
|
||||
interaction.data.options?.[0].options?.[0].name || interaction.data.options?.[0].options?.[0].name;
|
||||
if (!targetCmdName) return;
|
||||
|
||||
// Try to find the command
|
||||
cmd = subCmdGroup.subCommands.find((c) => c.name === targetCmdName);
|
||||
|
||||
cmdName += ` ${subCmdGroup.name} ${cmd?.name}`;
|
||||
|
||||
// Normal
|
||||
} else if (interaction.data?.options?.[0].type === DiscordApplicationCommandOptionTypes.SubCommand) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!cmd.subcommands) return;
|
||||
|
||||
// Try to find the command
|
||||
const found = cmd.subcommands.find((cmd) => cmd.name == interaction.data?.options?.[0].name);
|
||||
if (!found) return;
|
||||
|
||||
if (isSubCommandGroup(found)) return;
|
||||
|
||||
cmd = found;
|
||||
cmdName += ` ${cmd?.name}`;
|
||||
}
|
||||
if (!cmd) return;
|
||||
|
||||
// Get options
|
||||
const options =
|
||||
interaction.data?.options?.[0].type === DiscordApplicationCommandOptionTypes.SubCommandGroup
|
||||
? interaction.data?.options?.[0].options?.[0].options
|
||||
: interaction.data?.options?.[0].type === DiscordApplicationCommandOptionTypes.SubCommand
|
||||
? interaction.data?.options[0].options
|
||||
: interaction.data?.options;
|
||||
|
||||
// Prepare info for logs
|
||||
const user = member || interaction.user;
|
||||
const guild = interaction.guildId
|
||||
? await customCacheHandlers.get("guilds", snowflakeToBigint(interaction.guildId))
|
||||
: undefined;
|
||||
|
||||
// Log cmd trigger
|
||||
logSlashCommand(cmdName, user, guild, "trigger");
|
||||
|
||||
// Check inhibitors
|
||||
for await (const inhibitor of bot.inhibitors.values()) {
|
||||
const inhibited = await inhibitor(cmd, interaction, member);
|
||||
if (inhibited) {
|
||||
// Log cmd inhibition
|
||||
logSlashCommand(cmdName, user, guild, "inhibited");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if command has execute
|
||||
if (!cmd.execute) {
|
||||
logger.error(`Command ${cmdName} is missing execute.`);
|
||||
sendBasicResponse(data.id, data.token, "This command is not configured to be executed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Command context
|
||||
const cmdCtx = createCommandCtx(interaction, undefined, undefined, options, member);
|
||||
|
||||
// Execute command
|
||||
let err;
|
||||
try {
|
||||
if (cmd.longExecution)
|
||||
// Thinking....
|
||||
await sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: DiscordInteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
});
|
||||
|
||||
await cmd.execute(cmdCtx);
|
||||
} catch (error) {
|
||||
err = true;
|
||||
|
||||
// Log command fail
|
||||
logSlashCommand(cmdName, user, guild, "failed", error);
|
||||
|
||||
// Log the error
|
||||
logger.error(`Error ${cmdName}`, error);
|
||||
|
||||
// Send error message to user
|
||||
sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: DiscordInteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
embeds: [
|
||||
createSimpleEmbed("error")
|
||||
.setDescription("Unexpected Error")
|
||||
.setFooter("This error has been reported to the developers of this bot."),
|
||||
],
|
||||
},
|
||||
}).catch(logger.error);
|
||||
}
|
||||
|
||||
// Log success
|
||||
if (!err) logSlashCommand(cmdName, user, guild, "success");
|
||||
},
|
||||
});
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { EventHandlers } from '../../deps.ts.js';
|
||||
|
||||
export const events: Partial<EventHandlers> = {};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { events } from './mod.ts.js';
|
||||
import { logger } from '../utils/logger.ts.js';
|
||||
|
||||
const log = logger({ name: "Event: Ready" });
|
||||
|
||||
events.ready = () => {
|
||||
log.info("Bot Ready");
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import type {
|
||||
Bot,
|
||||
BotWithCache,
|
||||
CreateApplicationCommand,
|
||||
Guild,
|
||||
MakeRequired} from '../../deps.ts.js';
|
||||
import {
|
||||
getGuild,
|
||||
hasProperty,
|
||||
upsertGuildApplicationCommands,
|
||||
} from '../../deps.ts.js';
|
||||
import { logger } from './logger.ts.js';
|
||||
import type { subCommand, subCommandGroup } from '../commands/mod.ts.js';
|
||||
import { commands } from '../commands/mod.ts.js';
|
||||
|
||||
const log = logger({ name: "Helpers" });
|
||||
|
||||
/** This function will update all commands, or the defined scope */
|
||||
export async function updateCommands(bot: BotWithCache, scope?: "Guild" | "Global") {
|
||||
const globalCommands: Array<MakeRequired<CreateApplicationCommand, "name">> = [];
|
||||
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, "name">> = [];
|
||||
|
||||
for (const command of commands.values()) {
|
||||
if (command.scope) {
|
||||
if (command.scope === "Guild") {
|
||||
perGuildCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
});
|
||||
} else if (command.scope === "Global") {
|
||||
globalCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
perGuildCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (globalCommands.length && (scope === "Global" || scope === undefined)) {
|
||||
log.info("Updating Global Commands, changes should apply in short...");
|
||||
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(log.error);
|
||||
}
|
||||
|
||||
if (perGuildCommands.length && (scope === "Guild" || scope === undefined)) {
|
||||
await bot.guilds.forEach(async (guild: Guild) => {
|
||||
await upsertGuildApplicationCommands(bot, guild.id, perGuildCommands);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Update commands for a guild */
|
||||
export async function updateGuildCommands(bot: Bot, guild: Guild) {
|
||||
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, "name">> = [];
|
||||
|
||||
for (const command of commands.values()) {
|
||||
if (command.scope) {
|
||||
if (command.scope === "Guild") {
|
||||
perGuildCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (perGuildCommands.length) {
|
||||
await upsertGuildApplicationCommands(bot, guild.id, perGuildCommands);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGuildFromId(bot: BotWithCache, guildId: bigint): Promise<Guild> {
|
||||
let returnValue: Guild = {} as Guild;
|
||||
|
||||
if (guildId !== 0n) {
|
||||
if (bot.guilds.get(guildId)) {
|
||||
returnValue = bot.guilds.get(guildId) as Guild;
|
||||
}
|
||||
|
||||
await getGuild(bot, guildId).then((guild) => {
|
||||
if (guild) bot.guilds.set(guildId, guild);
|
||||
if (guild) returnValue = guild;
|
||||
});
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
}
|
||||
|
||||
export function humanizeMilliseconds(milliseconds: number) {
|
||||
// Gets ms into seconds
|
||||
const time = milliseconds / 1000;
|
||||
if (time < 1) return "1s";
|
||||
|
||||
const days = Math.floor(time / 86400);
|
||||
const hours = Math.floor((time % 86400) / 3600);
|
||||
const minutes = Math.floor(((time % 86400) % 3600) / 60);
|
||||
const seconds = Math.floor(((time % 86400) % 3600) % 60);
|
||||
|
||||
const dayString = days ? `${days}d ` : "";
|
||||
const hourString = hours ? `${hours}h ` : "";
|
||||
const minuteString = minutes ? `${minutes}m ` : "";
|
||||
const secondString = seconds ? `${seconds}s ` : "";
|
||||
|
||||
return `${dayString}${hourString}${minuteString}${secondString}`;
|
||||
}
|
||||
|
||||
export function isSubCommand(data: subCommand | subCommandGroup): data is subCommand {
|
||||
return !hasProperty(data, "subCommands");
|
||||
}
|
||||
|
||||
export function isSubCommandGroup(data: subCommand | subCommandGroup): data is subCommandGroup {
|
||||
return hasProperty(data, "subCommands");
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { bold, cyan, gray, italic, red, yellow } from '../../deps.ts.js';
|
||||
|
||||
export enum LogLevels {
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
const prefixes = new Map<LogLevels, string>([
|
||||
[LogLevels.Debug, "DEBUG"],
|
||||
[LogLevels.Info, "INFO"],
|
||||
[LogLevels.Warn, "WARN"],
|
||||
[LogLevels.Error, "ERROR"],
|
||||
[LogLevels.Fatal, "FATAL"],
|
||||
]);
|
||||
|
||||
const noColor: (str: string) => string = (msg) => msg;
|
||||
const colorFunctions = new Map<LogLevels, (str: string) => string>([
|
||||
[LogLevels.Debug, gray],
|
||||
[LogLevels.Info, cyan],
|
||||
[LogLevels.Warn, yellow],
|
||||
[LogLevels.Error, (str: string) => red(str)],
|
||||
[LogLevels.Fatal, (str: string) => red(bold(italic(str)))],
|
||||
]);
|
||||
|
||||
export function logger({
|
||||
logLevel = LogLevels.Info,
|
||||
name,
|
||||
}: {
|
||||
logLevel?: LogLevels;
|
||||
name?: string;
|
||||
} = {}) {
|
||||
function log(level: LogLevels, ...args: any[]) {
|
||||
if (level < logLevel) return;
|
||||
|
||||
let color = colorFunctions.get(level);
|
||||
if (!color) color = noColor;
|
||||
|
||||
const date = new Date();
|
||||
const log = [
|
||||
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
|
||||
color(prefixes.get(level) || "DEBUG"),
|
||||
name ? `${name} >` : ">",
|
||||
...args,
|
||||
];
|
||||
|
||||
switch (level) {
|
||||
case LogLevels.Debug:
|
||||
return console.debug(...log);
|
||||
case LogLevels.Info:
|
||||
return console.info(...log);
|
||||
case LogLevels.Warn:
|
||||
return console.warn(...log);
|
||||
case LogLevels.Error:
|
||||
return console.error(...log);
|
||||
case LogLevels.Fatal:
|
||||
return console.error(...log);
|
||||
default:
|
||||
return console.log(...log);
|
||||
}
|
||||
}
|
||||
|
||||
function setLevel(level: LogLevels) {
|
||||
logLevel = level;
|
||||
}
|
||||
|
||||
function debug(...args: any[]) {
|
||||
log(LogLevels.Debug, ...args);
|
||||
}
|
||||
|
||||
function info(...args: any[]) {
|
||||
log(LogLevels.Info, ...args);
|
||||
}
|
||||
|
||||
function warn(...args: any[]) {
|
||||
log(LogLevels.Warn, ...args);
|
||||
}
|
||||
|
||||
function error(...args: any[]) {
|
||||
log(LogLevels.Error, ...args);
|
||||
}
|
||||
|
||||
function fatal(...args: any[]) {
|
||||
log(LogLevels.Fatal, ...args);
|
||||
}
|
||||
|
||||
return {
|
||||
log,
|
||||
setLevel,
|
||||
debug,
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
fatal,
|
||||
};
|
||||
}
|
||||
|
||||
export const log = logger();
|
||||
@@ -0,0 +1 @@
|
||||
TOKEN=
|
||||
@@ -0,0 +1,137 @@
|
||||
const resolveFolder = (folderName) => path.resolve(__dirname, ".", folderName);
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
class CommandManager {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.cache = new Map();
|
||||
this.aliases = new Map();
|
||||
}
|
||||
|
||||
load(options = {}) {
|
||||
const commandFolderPath = options.path || "../Plugins";
|
||||
const commandFolder = resolveFolder(commandFolderPath);
|
||||
if (options.category === undefined) options.category = true;
|
||||
if (options.plugins === undefined) options.plugins = true;
|
||||
// PluginMode will iterate through all SubFolders
|
||||
fs.readdirSync(commandFolder).map(async (dir) => {
|
||||
if (dir.endsWith(".txt")) return;
|
||||
if (!options.category && dir.endsWith(".js")) {
|
||||
const commandPath = path.join(commandFolder, dir);
|
||||
this.loadCommand(commandPath);
|
||||
} else {
|
||||
fs.readdirSync(path.join(commandFolder, dir)).map((cmd) => {
|
||||
if (cmd.endsWith(".js") && !options.plugins) {
|
||||
const commandPath = path.join(commandFolder, dir, cmd);
|
||||
this.loadCommand(commandPath);
|
||||
} else if (commandFolderPath === "../Plugins") {
|
||||
if (cmd !== "commands") return;
|
||||
fs.readdirSync(path.join(commandFolder, dir, cmd)).map((cmdfile) => {
|
||||
if (!cmdfile.endsWith(".js")) return;
|
||||
const commandPath = path.join(commandFolder, dir, cmd, cmdfile);
|
||||
this.loadCommand(commandPath);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadCommand(commandPath) {
|
||||
const pull = require(path.join(commandPath));
|
||||
if (pull.name) {
|
||||
pull.path = commandPath;
|
||||
this.cache.set(pull.name, pull);
|
||||
}
|
||||
if (pull.aliases) {
|
||||
pull.aliases.map((p) => this.aliases.set(p, pull));
|
||||
}
|
||||
return pull;
|
||||
}
|
||||
|
||||
reloadCommand(commandName) {
|
||||
const command = this.cache.get(commandName);
|
||||
if (!command) return;
|
||||
const commandPath = path.join(command.path);
|
||||
delete require.cache[require.resolve(commandPath)];
|
||||
return this.loadCommand(commandPath);
|
||||
}
|
||||
|
||||
isCommand(message) {
|
||||
if (message.isFromBot) return false;
|
||||
const prefix = "!";
|
||||
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const prefixRegex = new RegExp(`^(<@!?${this.client.id}>|${escapeRegex(prefix)})\\s*`);
|
||||
if (!prefixRegex.test(message.content)) return false;
|
||||
|
||||
const [, matchedPrefix] = message.content.match(prefixRegex);
|
||||
const args = message.content.slice(matchedPrefix.length).trim().split(/ +/);
|
||||
|
||||
this.onMessage(message, prefix, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
isInteraction(interaction) {
|
||||
if (interaction.type !== 2) return;
|
||||
this.onInteraction(interaction);
|
||||
}
|
||||
|
||||
async onMessage(message, guild, args) {
|
||||
const commandName = args.shift().toLowerCase();
|
||||
const command = this.cache.get(commandName); // || this.cache.find(cmd => cmd.aliases && cmd.aliases.includes(commandName));
|
||||
if (!command && message.content.includes(this.client.id)) {
|
||||
// Handle, when Command has not been found
|
||||
const options = { content: "I did not found the Command!" };
|
||||
this.client.helpers.sendMessage(message.channelId, options);
|
||||
}
|
||||
if (!command) return;
|
||||
|
||||
const messagecommand = new command({
|
||||
manager: this,
|
||||
message,
|
||||
client: this.client,
|
||||
args,
|
||||
settings: {},
|
||||
commandName: command.name,
|
||||
});
|
||||
messagecommand.execute()?.catch?.((error) => {
|
||||
console.log(error);
|
||||
// Call Function on CommandResponse.js, handle the error
|
||||
return messagecommand.onError(error ?? "custom");
|
||||
});
|
||||
}
|
||||
|
||||
async onInteraction(interaction) {
|
||||
const command = this.cache.get(interaction.data.name);
|
||||
if (!command) return;
|
||||
|
||||
const args = [];
|
||||
// Map all Values and Args
|
||||
interaction.data.options?.map((o) => {
|
||||
if (o.name) args.push(o.name);
|
||||
if (o.options) {
|
||||
o.options.map((o2) => {
|
||||
if (o2.value) return args.push(o2.value);
|
||||
if (o2.name) args.push(o2.name);
|
||||
if (o2.options) o2.options.map((v) => args.push(v.value));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const messagecommand = new command({
|
||||
manager: this,
|
||||
interaction,
|
||||
client: this.client,
|
||||
args,
|
||||
settings: {},
|
||||
commandName: command.name,
|
||||
});
|
||||
messagecommand.execute()?.catch?.((error) => {
|
||||
console.log(error);
|
||||
// Call Function on CommandResponse.js, handle the error
|
||||
return messagecommand.onError(error ?? "custom");
|
||||
});
|
||||
}
|
||||
}
|
||||
module.exports = CommandManager;
|
||||
@@ -0,0 +1,32 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const resolveFolder = (folderName) => path.resolve(__dirname, ".", folderName);
|
||||
|
||||
const EventEmitter = require("events");
|
||||
|
||||
class EventManager extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.cache = new Map();
|
||||
this.allEvents = {};
|
||||
}
|
||||
|
||||
load(options = {}) {
|
||||
const eventsFolder = resolveFolder("../events");
|
||||
let i = 0;
|
||||
fs.readdirSync(eventsFolder).map(async (file) => {
|
||||
if (!file.endsWith(".js")) return;
|
||||
i++;
|
||||
const fileName = path.join(eventsFolder, file);
|
||||
const event = require(fileName);
|
||||
const eventName = file.split(".")[0];
|
||||
|
||||
this.allEvents[`${eventName}`] = (...args) => {
|
||||
this.emit(eventName, ...args);
|
||||
return event(...args);
|
||||
};
|
||||
});
|
||||
return this.allEvents;
|
||||
}
|
||||
}
|
||||
module.exports = EventManager;
|
||||
@@ -0,0 +1,51 @@
|
||||
const Discord = require("discordeno.js");
|
||||
|
||||
const BaseCommand = require("../../../Structures/BaseCommand.js");
|
||||
class evalcommand extends BaseCommand {
|
||||
static name = "eval";
|
||||
static description = "danger !!!";
|
||||
static category = "Developer";
|
||||
static slash = { name: "eval", category: "dev" };
|
||||
constructor(data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
if (!this.client.config.owners.includes(String(this.user.id))) return;
|
||||
if (!(this.args.length > 0)) return this.reply({ content: "**You must provide something to eval!**" });
|
||||
|
||||
const inputOfEval = this.args.join(" ");
|
||||
let outputOfEval;
|
||||
let typeOfEval;
|
||||
|
||||
try {
|
||||
if (this.args.includes("await")) {
|
||||
outputOfEval = await eval("(async () => {" + inputOfEval + "})()");
|
||||
} else {
|
||||
outputOfEval = await eval(inputOfEval);
|
||||
}
|
||||
} catch (e) {
|
||||
outputOfEval = e.message;
|
||||
typeOfEval = e.name;
|
||||
}
|
||||
|
||||
const seen = [];
|
||||
outputOfEval = typeof outputOfEval === "object"
|
||||
? JSON.stringify(outputOfEval, (_, value) => {
|
||||
if (value == `Bot ${this.client.config.token}`) return `BOT_TOKEN`;
|
||||
if (typeof value === "bigint") value = value.toString();
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.indexOf(value) !== -1) return;
|
||||
else seen.push(value);
|
||||
}
|
||||
return value;
|
||||
}, 1)
|
||||
: outputOfEval;
|
||||
|
||||
const embed = new Discord.Embed()
|
||||
.addField({ name: "Input", value: "```js\n" + inputOfEval + "```" })
|
||||
.addField({ name: "Output", value: "```json\n" + `${outputOfEval}`.slice(0, 1000) + "```" });
|
||||
this.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
module.exports = evalcommand;
|
||||
@@ -0,0 +1,19 @@
|
||||
const BaseCommand = require("../../../Structures/BaseCommand.js");
|
||||
class reloadcommand extends BaseCommand {
|
||||
static name = "reload";
|
||||
static description = "Reloads a Command";
|
||||
static category = "Developer";
|
||||
static slash = { name: "reload", category: "dev" };
|
||||
constructor(data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
if (!this.client.config.owners.includes(String(this.user.id))) return;
|
||||
if (!this.args[0]) return this.reply({ content: "**You must provide a command to reload!**" });
|
||||
const op = this.client.commands.reloadCommand(this.args[0]);
|
||||
if (!op) return this.reply({ content: "**That command doesn't exist!**" });
|
||||
return this.reply({ content: "**Reloaded Command: `" + this.args[0] + "`**" });
|
||||
}
|
||||
}
|
||||
module.exports = reloadcommand;
|
||||
@@ -0,0 +1,25 @@
|
||||
const BaseCommand = require("../../../Structures/BaseCommand.js");
|
||||
const Discord = require("discordeno.js");
|
||||
class pingcommand extends BaseCommand {
|
||||
static name = "ping";
|
||||
static description = "See if the bot latency is okay";
|
||||
static usage = "";
|
||||
static category = "General";
|
||||
static slash = { name: "ping", category: "info" };
|
||||
constructor(data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const msg = await this.channel.send({ content: `Pinging...` });
|
||||
// Assign properties to the response
|
||||
const ping = msg.timestamp - (this.message ? this.message.timestamp : this.interaction.timestamp);
|
||||
|
||||
const embed = new Discord.Embed()
|
||||
.setTitle(`The Bots ping is ${ping} ms`)
|
||||
.toJSON();
|
||||
// Edit Message with the Embed
|
||||
return this.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
module.exports = pingcommand;
|
||||
@@ -0,0 +1,41 @@
|
||||
const BaseCommand = require("../../../Structures/BaseCommand.js");
|
||||
const Discord = require("discordeno.js");
|
||||
|
||||
class bancommand extends BaseCommand {
|
||||
static name = "ban";
|
||||
static description = "Ban a user from the server";
|
||||
static usage = "";
|
||||
static category = "Moderation";
|
||||
static slash = { name: "ban", category: "mod" };
|
||||
constructor(data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
// Show Case Modal
|
||||
|
||||
// Because no permission system has not been added
|
||||
if (!this.client.config.owners.includes(String(this.user.id))) return;
|
||||
|
||||
const textinput = new Discord.Component()
|
||||
.setType("TEXT_INPUT")
|
||||
.setStyle("SHORT")
|
||||
.setCustomId("t1")
|
||||
.setLabel("User ID")
|
||||
.setPlaceholder("User ID")
|
||||
.setRequired(true)
|
||||
.setMaxLength(20)
|
||||
.setMinLength(1)
|
||||
.setValue(this.args[0])
|
||||
.toJSON();
|
||||
const textinput2 = new Discord.Component().setType("TEXT_INPUT").setStyle("PARAGRAPH").setCustomId("t2")
|
||||
.setLabel("Reason").setPlaceholder("Reason for Ban").setRequired(false)
|
||||
.setMaxLength(300).toJSON();
|
||||
|
||||
const actionrow = new Discord.Component().setType(1).setComponents(textinput).toJSON();
|
||||
const actionrow2 = new Discord.Component().setType(1).setComponents(textinput2).toJSON();
|
||||
|
||||
this.interaction.popupModal({ customId: "ban_modal", title: "Ban User", components: [actionrow, actionrow2] });
|
||||
}
|
||||
}
|
||||
module.exports = bancommand;
|
||||
@@ -0,0 +1,82 @@
|
||||
const BaseCommand = require("../../../Structures/BaseCommand.js");
|
||||
|
||||
const { Interaction, Collector, ComponentOptions, Embed, Component } = require("discordeno.js");
|
||||
|
||||
class warncommand extends BaseCommand {
|
||||
static name = "warn";
|
||||
static description = "Warn a user from the server";
|
||||
static usage = "";
|
||||
static category = "Moderation";
|
||||
static slash = { name: "warn", category: "mod" };
|
||||
constructor(data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
// Show Case Modal
|
||||
if (!this.interaction) return this.reply("You currently can just use this command as slash command.");
|
||||
|
||||
if (!this.interaction.member.permissions.has("KICK_MEMBERS")) {
|
||||
return this.reply("You need the permission `KICK_MEMBERS` to use this command.");
|
||||
}
|
||||
|
||||
const textinput = new Component()
|
||||
.setType("TEXT_INPUT")
|
||||
.setStyle("SHORT")
|
||||
.setCustomId("t1")
|
||||
.setLabel("User ID")
|
||||
.setPlaceholder("User ID")
|
||||
.setRequired(true)
|
||||
.setMaxLength(20)
|
||||
.setMinLength(1)
|
||||
.setValue(this.args[0])
|
||||
.toJSON();
|
||||
const textinput2 = new Component().setType("TEXT_INPUT").setStyle("PARAGRAPH").setCustomId("t2")
|
||||
.setLabel("Reason").setPlaceholder("Reason for Warning").setRequired(false)
|
||||
.setMaxLength(300).toJSON();
|
||||
|
||||
const actionrow = new Component().setType(1).setComponents(textinput).toJSON();
|
||||
const actionrow2 = new Component().setType(1).setComponents(textinput2).toJSON();
|
||||
|
||||
this.interaction.popupModal({ customId: "warn_modal", title: "Warn User", components: [actionrow, actionrow2] });
|
||||
|
||||
const filter = (m) => m.data?.customId === "warn_modal";
|
||||
const collector = new Collector("interactionCreate", { client: this.client, timeout: 60000, filter });
|
||||
collector.on("collect", (m) => {
|
||||
const options = new ComponentOptions(m.data.components);
|
||||
const i = new Interaction(this.client, m);
|
||||
collector.stop();
|
||||
|
||||
const memberId = options.get("t1").value;
|
||||
const reason = options.get("t2").value;
|
||||
|
||||
const embed = new Embed()
|
||||
.setTitle("Warned User:")
|
||||
.setDescription(`User ID: <@${memberId}> \n Reason: ${reason}`)
|
||||
.setColor(0x00ff00)
|
||||
.toJSON();
|
||||
|
||||
const warnMessage = new Embed()
|
||||
.setTitle("Warning:")
|
||||
.setDescription(`You have been warned in **${this.guild.name}** for ${"`" + reason + "`"}`)
|
||||
.toJSON();
|
||||
|
||||
this.guild.members.fetch(memberId).then((m) => {
|
||||
m.send({ embeds: [warnMessage] }).then(() => {
|
||||
i.reply({ embeds: [embed] });
|
||||
}).catch((e) => {
|
||||
console.log(e);
|
||||
i.reply({ content: `Could not warn user ${"<@" + m.id + ">"} | They likely do not have their DMs open.` });
|
||||
});
|
||||
}).catch((e) => {
|
||||
const embed = new Embed()
|
||||
.setTitle("Member not found")
|
||||
.setDescription(`The member with the ID of ${"`" + memberId + "`"} has not been found in this Server.`)
|
||||
.setColor(0xff0000)
|
||||
.toJSON();
|
||||
i.reply({ embeds: [embed] });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
module.exports = warncommand;
|
||||
@@ -0,0 +1,13 @@
|
||||
# Nodejs Bot Template
|
||||
|
||||
This template is a minimal template that uses [discordeno.js](https://www.npmjs.com/package/discordeno.js), a wrapper
|
||||
around discordeno, which brings up a djs-like interface but also includes the core Features (Cross Ratelimit, Zero
|
||||
Downtime Restart, Resharding...)
|
||||
|
||||
## Setup
|
||||
|
||||
Rename `.env.example` to `.env` and fill your bot token there.
|
||||
|
||||
## Run Bot
|
||||
|
||||
- node index.js
|
||||
@@ -0,0 +1,15 @@
|
||||
const UtilCommand = require("./CommandResponse.js");
|
||||
class BaseCommand extends UtilCommand {
|
||||
constructor(data) {
|
||||
super(data);
|
||||
this.message = data.message;
|
||||
this.interaction = data.interaction;
|
||||
this.user = this.message ? this.message.author : this.interaction.user;
|
||||
this.guild = this.message ? this.message.guild : this.interaction.guild;
|
||||
this.member = this.message ? this.message.member : this.interaction.member;
|
||||
this.channel = this.message ? this.message.channel : this.interaction.channel;
|
||||
this.client = data.client;
|
||||
this.settings = data.settings ?? {};
|
||||
}
|
||||
}
|
||||
module.exports = BaseCommand;
|
||||
@@ -0,0 +1,50 @@
|
||||
class Responses {
|
||||
constructor(data) {
|
||||
this.manager = data.manager;
|
||||
this.args = this._validateArguments(data.args);
|
||||
this.replied = false;
|
||||
}
|
||||
|
||||
async reply(content) {
|
||||
// When just a string is passed, we assume it's the content -> transform to correct formatted payload
|
||||
if (typeof content === "string") content = { content };
|
||||
if (this.interaction) {
|
||||
if (this.replied) return this.followUp(content);
|
||||
const reply = await this.interaction.reply(content);
|
||||
this.replied = true;
|
||||
return {};
|
||||
}
|
||||
if (this.message) {
|
||||
if (this.replied) return this.followUp(content);
|
||||
|
||||
const msg = await this.message.channel.send(content);
|
||||
|
||||
// Assign properties to the response
|
||||
const response = this.client.messages.forge(msg);
|
||||
this.replied = true;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
async followUp(content) {
|
||||
if (this.interaction) {
|
||||
const reply = await this.interaction.followUp(content);
|
||||
return {};
|
||||
}
|
||||
if (this.message) {
|
||||
const msg = await this.message.channel.send(content);
|
||||
const response = this.client.messages.forge(msg);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
onError(error) {
|
||||
return this.reply({ content: `A unknown Error happend: \n> ${error}` });
|
||||
}
|
||||
|
||||
_validateArguments(args) {
|
||||
this.args = args;
|
||||
return args;
|
||||
}
|
||||
}
|
||||
module.exports = Responses;
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = async (client, interaction) => {
|
||||
interaction = client.interactions.forge(interaction);
|
||||
client.commands.isInteraction(interaction);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = async (client, message) => {
|
||||
message = client.messages.forge(message);
|
||||
client.commands.isCommand(message);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = async (client, payload) => {
|
||||
client.user = client.users.forge(payload.user);
|
||||
if (payload.shardId === client.gateway.lastShardId) {
|
||||
// All Shards are ready
|
||||
console.log("Successfully connected to the gateway as " + client.user.tag);
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user