Compare commits

..

124 Commits

Author SHA1 Message Date
Majora8120
601a2eb65a
add .portable to the yuzu bot (#174)
ports .portable from the citra bot to the yuzu bot
2024-02-27 20:41:33 -07:00
liushuyu
6e27ab6c91
server: fix the workaround logic issue 2024-02-27 17:15:55 -07:00
liushuyu
31d188933b
deps: update dependencies 2024-02-27 16:43:09 -07:00
liushuyu
c5bf06fafe
server: temporarily making some commands as admin-only 2024-02-27 16:41:27 -07:00
Schplee
fde0b14b58
Update citra.json 2024-02-27 14:46:38 -08:00
Schplee
9e3e756adc
Update yuzu.json 2024-02-27 14:46:13 -08:00
SleepingSnakezzz
a6e642d638
Merge pull request #172 from citra-emu/Add-custom-drivers-command
Adding custom drivers command.
2024-02-23 15:46:06 +01:00
SleepingSnakezzz
7fa5a6aa6f
Adding custom drivers command.
Requested by team member, as this is asked often by users.
2024-02-22 13:23:38 +01:00
sakuramipha
027bf47f0f
Merge pull request #171 from Majora8120/usecase-port
Add .usecase command to the yuzu bot
2024-02-21 20:23:18 +00:00
Majora8120
9b204a914f Add .usecase command to the yuzu bot
I'm tired of pirates asking "then what is the point of the emulator."

this ports the command from the citra bot to the yuzu bot
2024-02-20 15:09:46 -06:00
Jaffa
df9d06313f
Update .requirements (#169)
* Remove old citra.json

* Update citra.json
2024-02-16 23:33:41 +00:00
Jaffa
81c50236c7
Add Dumping command (#167)
* Add files via upload

* add dumping command

* add .dumping command

* Update citra.json

* Delete src/responses/citra.json

* Revert order of commands

* Delete src/responses/citra.json

* Add files via upload

* Add files via upload

* Delete src/responses/citra.json

* Delete citra.json (the correct one this time)

* Re-add the correct JSON file

* Update src/responses/citra.json

Co-authored-by: sakuramipha <86921268+sakuramipha@users.noreply.github.com>

---------

Co-authored-by: sakuramipha <86921268+sakuramipha@users.noreply.github.com>
2024-02-06 14:08:28 +00:00
liushuyu
d6a4e978f7
deps: update dependencies 2024-02-01 22:35:07 -07:00
liushuyu
cd44a47749 deps: update dependencies 2024-02-01 22:34:46 -07:00
Tobias
d3aa111ffc
Merge pull request #163 from citra-emu/revert-160-patch-1
Revert "Update citra bot .apk command"
2023-12-21 10:07:28 +01:00
Tobias
031c3741df Revert "Update citra bot .apk command"
This reverts commit 904c196a18ee304c52f902c889e6c9f4bd400902.
2023-12-21 09:33:38 +01:00
liushuyu
9de48ffe3c
deps: update dependencies 2023-11-29 12:12:52 -07:00
liushuyu
0432fbe881
server: avoid pinging the user when logging the command 2023-11-25 20:33:29 -07:00
liushuyu
aa54213734 deps: update dependencies 2023-11-20 17:48:22 -07:00
TGP17
904c196a18
Update citra bot .apk command
This updates the .apk command, by saying to download the .apk.zip, while it previously said to download the .apk, which isn't correct anymore
2023-11-20 19:47:08 +00:00
liushuyu
04ef587bc9
server: beautify command logging embed 2023-10-19 17:06:30 -06:00
liushuyu
1e191894ca
deps: update dependencies 2023-10-19 16:44:34 -06:00
liushuyu
72639ee6ed
deps: update dependencies 2023-09-19 13:51:03 -06:00
liushuyu
e3c51adf20
server: send command usage to logging channel 2023-09-19 13:49:27 -06:00
TGP17
35a3204866
citra: describe what link to click to get the apk (#159)
* Describe what link to click to get the apk

* Update src/responses/citra.json

Co-authored-by: sakuramipha <86921268+sakuramipha@users.noreply.github.com>

---------

Co-authored-by: sakuramipha <86921268+sakuramipha@users.noreply.github.com>
2023-09-08 19:28:28 +01:00
liushuyu
659a323a60
CI: use Node.js 20 for TypeScript checking 2023-09-07 18:06:12 -06:00
liushuyu
55101858e7
tsconfig: update to match Node.js 20 preset 2023-09-07 18:04:24 -06:00
liushuyu
abad013754 deps: update dependencies 2023-09-07 18:04:17 -06:00
sakuramipha
eac709db26
issue commands (#158) 2023-09-08 00:49:19 +01:00
Moonlacer
ab72a9c170
address feedback (#155)
You can read!

responses\yuzu: add multiplayer command

You can read!
2023-09-05 10:25:03 +01:00
SleepingSnakezzz
5c73b7b8db
Update portable command (#157)
* Update portable command

* Update src/responses/citra.json

Co-authored-by: sakuramipha <86921268+sakuramipha@users.noreply.github.com>

---------

Co-authored-by: sakuramipha <86921268+sakuramipha@users.noreply.github.com>
2023-09-02 15:39:15 +01:00
sakuramipha
6a1d4e77d4
responses/citra.json: Edit .states command to be clearer to user (#154)
* edit .states command

* Update src/responses/citra.json

Co-authored-by: SleepingSnakezzz <71992016+SleepingSnakezzz@users.noreply.github.com>

---------

Co-authored-by: SleepingSnakezzz <71992016+SleepingSnakezzz@users.noreply.github.com>
2023-08-28 19:37:17 +01:00
liushuyu
869070d149
deps: update dependencies 2023-08-23 10:45:54 -06:00
liushuyu
b047824f59 Upgrade to Node.js 20 ...
... removing node-fetch and use built-in fetch function
2023-08-23 10:45:51 -06:00
liushuyu
27aba40ba2 deps: update dependencies 2023-08-23 10:45:51 -06:00
Efadd
d9c78786f1
Pr bs resolver 9001 (#153)
* Revert 132, add folder command from 133, and add eshop command

* Revert 132, add folder command from 133, and add eshop command

* Update citra.json

I blame Github Desktop

* Usage command

* Update src/responses/citra.json

Co-authored-by: sakuramipha <86921268+sakuramipha@users.noreply.github.com>

---------

Co-authored-by: sakuramipha <86921268+sakuramipha@users.noreply.github.com>
2023-08-23 17:35:47 +01:00
sakuramipha
ced9f5aaee
Merge pull request #152 from TGP17/bot-commands
Update citra.json
2023-08-11 09:13:43 +02:00
TGP17
875baa2805
Update citra.json
Change .mod command to .hack and create .modding command for game modding
2023-08-10 00:22:26 +02:00
Moonlacer
14ee40bd25
responses\yuzu: change hardware command (#147)
Changes the hardware requirement bot command to "hardware," instead of "cpu"
2023-07-01 21:15:54 +01:00
liushuyu
43a2373f85
typings: add third-party typings for logdna-winston 2023-06-26 17:36:30 -06:00
liushuyu
4cfed50b95
deps: update dependencies 2023-06-26 17:21:02 -06:00
liushuyu
9887a29256
deps: update dependencies 2023-06-10 21:14:18 -06:00
liushuyu
27359f87a7
responses/yuzu: fix JSON file structure 2023-05-30 16:23:04 -06:00
liushuyu
9f0324706f server: simplify if conditions 2023-05-30 16:13:22 -06:00
liushuyu
f10457e5d0 deps: update dependencies 2023-05-30 16:13:22 -06:00
SleepingSnakezzz
51aa05660d
Update Android command.json (#144)
* Update Android command.json

* Update yuzu.json
2023-05-30 15:09:20 -07:00
liushuyu
d5bf77f4f1
deps: update dependencies 2023-05-12 16:14:30 -06:00
liushuyu
9513c8626e
common: display human-readable role name 2023-05-10 21:19:58 -06:00
sakuramipha
173f996bb1
Add legacy builds command to citra.json (#142)
Links to a wiki page outlining all of the legacy builds of Citra one may need to use, such as if they're on macOS or Windows7/8.1

Removes the .macos command as that is not needed anymore, the information for such is now found on the legacy builds page.
2023-05-10 16:45:46 +05:30
SleepingSnakezzz
dd755b9b44
Update iOS command.json (#141)
* Update citra.json

* Remove the /"

* Remove "yet".
2023-05-09 12:42:55 -06:00
Efadd
d0db0afd27 Update citra.json
* Upstream merge error resolution

* Python instincts vs json conventions clean-up

* Updated Cheats command because I'm in here anyway

* Thanks Snake!

* Fair, Autumn

* Obfuscate how many iOS forks there are
2023-05-06 15:53:59 +02:00
Matías Locatti
2e4f8fc288
Add firmware installation to bot call (#140)
* Add firmware installation to bot call

* Update yuzu.json
2023-05-06 07:25:58 +05:30
liushuyu
7baa540663 commands/grantTester: new command 2023-04-28 14:41:45 +02:00
liushuyu
c61fafdca0 common: extract the grantRole function 2023-04-28 14:41:45 +02:00
liushuyu
ffb697c323 deps: update dependencies 2023-04-28 14:41:45 +02:00
liushuyu
65b36aaa5d commands: lint code according to ESLint 2023-04-28 14:41:45 +02:00
liushuyu
3a997d9b5c deps: update dependencies 2023-04-28 14:41:45 +02:00
Matías Locatti
4c440dec41
Add recommended settings (#138) 2023-03-11 23:26:19 -07:00
liushuyu
bccd1e1c15
deps: update dependencies 2023-03-11 00:08:38 -07:00
SleepingSnakezzz
ffa3c72566
Remove a part of the .legal and .piracy commands (#137)
Since https://github.com/citra-emu/citra/pull/6269 is a thing now, it can be confusing to users who know about this feature.
2023-03-07 15:44:59 -07:00
liushuyu
77bc0d5780
deps: update dependencies 2023-02-25 13:43:38 -07:00
Matías Locatti
bd67f4a4c3
Update keys and firmware following new quickstart guide (#136) 2023-02-22 20:03:21 -08:00
liushuyu
196ad81bcc
deps: update dependencies 2023-02-16 16:28:06 -07:00
liushuyu
199e000317
esbuild: remove whitespace in the bundled file 2023-02-08 22:36:45 -07:00
liushuyu
815ceb358f
deps: update dependencies 2023-02-08 22:36:35 -07:00
Efadd
0d82c9a915
Revert #132, add folder command from 133, and add eshop command (#134)
* Revert 132, add folder command from 133, and add eshop command

* Revert 132, add folder command from 133, and add eshop command

* Update citra.json

I blame Github Desktop
2023-02-08 22:14:20 -07:00
liushuyu
3b2c77ac34
deps: update dependencies 2023-01-28 03:36:02 -07:00
liushuyu
bd9a9ef390
Merge pull request #132 from Efadd/master
Added "Yes, we know the eshop is shuttered. No, we don't care" to piracy, legal, and rule 2 commands
2023-01-27 18:41:00 -07:00
Efadd
b89ebe1623 Update citra.json 2023-01-27 17:35:31 -06:00
liushuyu
f9e5bedee4
deps: update dependencies 2023-01-22 17:13:37 -07:00
liushuyu
32e725dd37
build: switch to esbuild instead of rollup 2023-01-14 19:31:23 -07:00
liushuyu
19fe3581fb
deps: update dependencies ...
... also make CI test on Node.js 18
2023-01-14 19:16:43 -07:00
liushuyu
34754878bb
rollup: update rollup plugins 2022-12-31 02:39:17 -07:00
liushuyu
1ca72ceb00
tree-wide: update to Node.js 18 LTS 2022-12-31 02:05:32 -07:00
liushuyu
718d5d54f0
triggers/pingBomb: exclude moderators from the auto-ban 2022-12-31 02:03:10 -07:00
liushuyu
4f8bf4574f
tree-wide: make sources more bundler friendly 2022-12-31 02:02:40 -07:00
Efadd
e46432ac22 Update citra.json 2022-12-10 18:31:14 -07:00
liushuyu
3ec6af6368
deps: update dependencies 2022-12-10 18:18:27 -07:00
liushuyu
08abde2d7c
deps: update dependencies ...
... and use a newer terser plugin for rollup
2022-12-06 18:49:05 -07:00
liushuyu
9cfbf3ed1c
Merge pull request #129 from sakuramipha/master
Small bot commands overhaul
2022-12-06 18:21:55 -07:00
sakuramipha
c8c8c19c0c
Update cheat codes wording
Added additional instructions just for clarity on what to do to add a cheat code.
2022-12-06 16:25:13 +00:00
sakuramipha
08f4aa24b4
Edit cheats command.
Changed instructions from going through the files to using the cheats UI.
2022-12-06 16:10:04 +00:00
sakuramipha
c07f5b8546
Update Citra Android download link.
Changes the download link from the citra-emu.org downloads page to the Nightly repo, as that is more updated than the current Play Store version.
2022-12-06 15:40:13 +00:00
sakuramipha
9ac189a8af
update keys wording again 2022-11-29 21:31:45 +00:00
sakuramipha
63223bf1c4
Quick edit to .keys
Updated wording due to not being able to actually obtain some keys from the 3DS.
2022-11-29 21:00:41 +00:00
sakuramipha
47dd6aacad
Small Citra bot commands overhaul
Fixed some grammar and spelling issues. Edited some commands for clarity and formatting. Deleted some unused commands and commands that are outdated now.
2022-11-29 20:51:48 +00:00
liushuyu
941037a3e8
CI: upgrade various action scripts 2022-11-18 21:04:35 -07:00
liushuyu
5cc3656f3b
deps: update dependencies 2022-11-18 20:58:31 -07:00
liushuyu
cbfea20517 tree-wide: code clean-up ...
* remove commented-out code blocks
* remove usages of deprecated JS functions
2022-11-18 20:56:38 -07:00
liushuyu
0c3a42e5c7 tree-wide: await all the await-able functions 2022-11-18 20:56:38 -07:00
Efadd
10f80bf1bc
Clean up message phrasing for piracy command (#128)
* Clean up message phrasing

* See Discord
2022-11-18 20:55:34 -07:00
Efadd
a3c1b64ed8
more whitespace on piracy command (#127)
* Update citra.json
2022-11-06 15:01:15 -07:00
Efadd
8c4083fe24
Add '.piracy' command because we all know how much these 3 messages are bundled -_- (#126)
* Update README.md

* Added .multiplayer command

* Update README.md

* Add portable, iOS, and macOS commads

* Update citra.json

* Update citra.json

* Update citra.json

* Update citra.json

* Update citra.json
2022-11-04 22:32:00 -06:00
liushuyu
92dd268da3
manifest: declare Node.js version requirements 2022-10-30 00:12:09 -06:00
liushuyu
3311053f7f
deps: update dependencies 2022-10-30 00:10:11 -06:00
liushuyu
1f91f0f55d
deps: update dependencies 2022-10-16 00:29:52 -06:00
liushuyu
c48d5b23b2
docker: copy mjs files as well 2022-10-12 18:02:14 -06:00
liushuyu
33ec2bb74e
deps: update dependencies 2022-10-12 16:39:35 -06:00
liushuyu
ff398a97fa deps: clean up dependencies 2022-10-12 16:39:33 -06:00
Efadd
964647368b
Add portable, macOS, and iOS commands (#125)
* Add portable, iOS, and macOS commads
2022-09-24 20:07:11 -06:00
liushuyu
e2c7fa776a
script: fix generateExports.js so that ...
... command mapping will have a lowercase key
2022-09-21 20:20:32 -06:00
liushuyu
23e8e1a8eb
lint: fix issues from ESLint 2022-09-18 03:52:00 -06:00
liushuyu
9dce458bf0
deps: update dependencies 2022-09-18 03:47:35 -06:00
liushuyu
280032fb1c
rollup: try to bundle Discord.js as well 2022-09-16 18:04:28 -06:00
liushuyu
0ab62ff4df
rollup: remove unneeded exports from the bundle 2022-09-16 18:01:37 -06:00
liushuyu
0609f5d5f0
CI: test build for all branches 2022-09-14 03:08:26 -06:00
liushuyu
0e5f158f11
docker: update dockerfile to use rollup 2022-09-14 03:05:55 -06:00
liushuyu
749bbdeb9d rollup: use terser to minify the bundle 2022-09-14 03:05:51 -06:00
liushuyu
3e1b010cae typings: add shim typing for checkenv 2022-09-14 03:05:51 -06:00
liushuyu
29388049ac tree-wide: use rollup to bundle 2022-09-14 03:05:51 -06:00
liushuyu
8c3e5261b5 deps: update dependencies 2022-09-14 03:05:51 -06:00
Efadd
dda1e3c64e
Add .multiplayer command (#124)
* Update README.md

* Added .multiplayer command

* Update README.md
2022-09-13 21:04:47 -06:00
liushuyu
b132f1d4be
deps: upgrade Discord.js to 14 2022-09-13 21:02:56 -06:00
liushuyu
fc966347d8
deps: update dependencies 2022-06-17 00:03:47 -06:00
liushuyu
e5f92d05da
Merge pull request #123 from Efadd/add_.mod_command_to_send_the_mod_guide
Add .mod command
2022-06-16 23:59:15 -06:00
Efadd
21d2c6f199
Add .mod command 2022-06-16 20:31:19 -05:00
Matías Locatti
12a3b6e6b7
Added ASCII rule (#121) 2022-02-25 15:16:56 -08:00
liushuyu
3f830cb6c5
yarn: downgrade colors 2022-01-09 18:01:21 -07:00
liushuyu
2642ffc3fa
deps: update discord.js 2022-01-09 17:40:12 -07:00
Morph
bb2d38c0fc responses: Update .vc command link 2022-01-09 17:34:56 -07:00
liushuyu
319479d594
deps: update dependencies 2021-11-07 03:26:37 -07:00
liushuyu
16455f3d33
server: add a missing intent for greeting new members 2021-10-28 15:59:44 -06:00
liushuyu
e2fb430042
lint: automated fix from ESLint 2021-10-28 15:38:02 -06:00
liushuyu
39c8f5979c
lint: await all the Promises 2021-10-28 15:32:32 -06:00
34 changed files with 1930 additions and 4989 deletions

View File

@ -1,6 +1,12 @@
{
"extends": ["eslint:recommended", "standard"],
"extends": ["eslint:recommended", "standard",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["@typescript-eslint"],
"rules": {
"semi": [2, "always"]
},
"parserOptions": {
"project": ["tsconfig.json"]
}
}

View File

@ -2,7 +2,7 @@ name: Node.js CI
on:
push:
branches: [ "*" ]
branches: [ '*' ]
pull_request:
branches: [ master ]
@ -13,12 +13,12 @@ jobs:
strategy:
matrix:
node-version: [16.x]
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: yarn
@ -29,15 +29,15 @@ jobs:
runs-on: ubuntu-latest
if: (github.ref == 'refs/heads/master') && (github.repository == 'citra-emu/discord-bot')
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
name: Setup Docker BuildX system
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/build-push-action@v2
- uses: docker/build-push-action@v3
name: Deploy the image
with:
push: true

4
.gitignore vendored
View File

@ -51,5 +51,5 @@ config/development.json
CMakeLists.txt.user*
/dist
# Parcel's cache
/.parcel-cache
_.ts

View File

@ -1,4 +0,0 @@
{
"extends": "@parcel/config-default",
"resolvers": ["@parcel/resolver-glob", "..."]
}

View File

@ -1,16 +1,15 @@
FROM node:16-alpine AS build
FROM node:20-alpine AS build
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies and add source files
COPY package.json yarn.lock env.json bundle.sh .parcelrc ./
COPY src/ ./src
COPY patches/ ./patches
RUN apk add python3 build-base && yarn install && sh bundle.sh
COPY package.json env.json yarn.lock tsconfig.json bundle.sh *.js *.mjs ./
COPY ./src ./src
RUN yarn install --frozen-lockfile && sh -e ./bundle.sh
# Second stage
FROM node:16-alpine
FROM node:20-alpine
WORKDIR /usr/src/app

View File

@ -1,27 +1,13 @@
#!/bin/bash -e
yarn
[ -d "dist" ] && rm -rf dist
echo "[+] Applying patches to accommodate bundler ..."
for i in patches/*.patch; do
echo "Applying $i ..."
patch -Np1 --no-backup-if-mismatch -i "$i"
done
yarn run parcel build
yarn run build
echo "[+] Installing non-bundle-able packages ..."
# DISCORD_JS="$(grep discord.js package.json | sed 's|,||')"
cd "dist"
echo '{"name": "citra-discord-bot","license": "GPL-2.0+"}' > package.json
yarn add discord.js@^13
cd ..
echo "[+] Reversing patches ..."
for i in patches/*.patch; do
echo "Reversing $i ..."
patch -Np1 -R -i "$i"
done
echo "[+] Removing patch backup files ..."
find . -name "*.orig" -print -delete
echo "{\"name\": \"citra-discord-bot\",\"license\": \"GPL-2.0+\",\"dependencies\": {}}" > package.json
yarn install

View File

@ -33,6 +33,7 @@
"description": "The unique ID for the role that the bot will *remove* when the user accepts the rules."
},
"DISCORD_DEVELOPER_ROLE": true,
"DISCORD_TESTER_ROLE": true,
"DISCORD_LOGIN_TOKEN": {
"description": "The login token of the bot."
},

42
generateExports.js Normal file
View File

@ -0,0 +1,42 @@
const fs = require('fs');
const path = require('path');
function collectModules(dir, extname) {
let modules = [];
fs.readdirSync(`./src/${dir}/`).forEach(function (file) {
// Load the module if it's a script.
if (path.extname(file) === extname) {
if (file.includes(".disabled")) {
console.info(`Did not load disabled module: ${file}`);
} else {
const moduleName = path.basename(file, extname);
if (moduleName === "_") return;
modules.push(moduleName);
console.info(`Scanning ${moduleName} from ${file} ...`);
}
}
});
return modules;
}
const header = '// GENERATED FILE. DO NOT EDIT!\n// See generateExports.js for details.\n'
console.info('Generating module loader ...');
let modules = collectModules('commands', '.ts');
let loader_content = header;
for (let mod of modules) {
loader_content += `import * as ${mod} from './${mod}';\n`;
}
let loader_map = modules.map((moduleName) => moduleName.toLowerCase() === moduleName ? moduleName : `${moduleName.toLowerCase()}: ${moduleName}`).join(', ');
loader_content += `\nexport default { ${loader_map} };\n`;
fs.writeFileSync("./src/commands/_.ts", loader_content);
let triggers = collectModules('triggers', '.ts');
loader_content = header;
for (let mod of triggers) {
loader_content += `import * as ${mod} from "./${mod}";\n`;
}
loader_map = triggers.join(', ');
loader_content += `\nexport default [ ${loader_map} ];\n`;
fs.writeFileSync("./src/triggers/_.ts", loader_content);

View File

@ -3,57 +3,44 @@
"version": "2.0.0",
"description": "Citra bot for Discord",
"author": "chris062689 <chris062689@gmail.com>",
"preferGlobal": true,
"private": true,
"subdomain": "citra-emu",
"analyze": true,
"license": "GPL-2.0+",
"source": "src/server.ts",
"main": "dist/server.js",
"engines": {
"node": ">= 16"
},
"targets": {
"main": {
"includeNodeModules": {
"discord.js": false
},
"context": "node",
"optimize": true
}
"node": ">=20.0.0"
},
"dependencies": {
"checkenv": "^1.2.2",
"discord.js": "^13.2.0",
"ip": "^1.1.5",
"logdna": "^3.5.2",
"discord.js": "^14.14.1",
"ip": "^2.0.1",
"logdna": "^3.5.3",
"logdna-winston": "^4.0.1",
"node-fetch": "^3",
"string-similarity": "^4.0.4",
"winston": "^3.3.3"
"typescript": "^5.3.3",
"winston": "^3.11.0"
},
"devDependencies": {
"@parcel/resolver-glob": "^2.0.0",
"@tsconfig/node14": "^1.0.1",
"@types/ip": "^1.1.0",
"@types/node": "^16.11.2",
"@types/node-fetch": "^3",
"@types/string-similarity": "^4.0.0",
"@types/ws": "^8.2.0",
"eslint": "^8.0.1",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-standard": "^4.1.0",
"parcel": "^2.0.0",
"ts-node": "^10.3.1",
"typescript": "^4.4.4"
"@tsconfig/node20": "^20.1.2",
"@types/ip": "^1.1.3",
"@types/node": "^20.11.21",
"@types/string-similarity": "^4.0.2",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"esbuild": "^0.20.1",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.1.1",
"ts-node": "^10.9.2"
},
"scripts": {
"postinstall": "node generateExports.js",
"build": "yarn run esbuild --bundle src/server.ts --platform=node --target=node20 --outdir=dist/ --minify-whitespace",
"check": "yarn run tsc --noEmit",
"build": "yarn run tsc",
"dist": "./bundle.sh",
"bundle": "./bundle.sh",
"serve": "yarn run ts-node ./src/server.ts"
}
}

View File

@ -1,95 +0,0 @@
From 2ae094e7ad43d9137fafe84a36eac3e9bf0a8245 Mon Sep 17 00:00:00 2001
From: liushuyu <liushuyu011@gmail.com>
Date: Wed, 15 Sep 2021 22:49:40 -0600
Subject: [PATCH] server: make it bundler friendly
---
src/server.ts | 49 ++++++++++++++++++++-----------------------------
1 file changed, 20 insertions(+), 29 deletions(-)
diff --git a/src/server.ts b/src/server.ts
index bc63ae7..7cf8825 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,5 +1,7 @@
// Check for environmental variables.
-require('checkenv').check();
+const checkenv = require('checkenv');
+checkenv.setConfig(require('../env.json'));
+checkenv.check();
import discord = require('discord.js');
import path = require('path');
@@ -9,6 +11,10 @@ import logger from './logging';
import state from './state';
import * as data from './data';
import { IModule, ITrigger } from './models/interfaces';
+// Parcel glob imports
+import commands from './commands/*.ts';
+import triggers from './triggers/*.ts';
+import responses from './responses/*.json';
interface IModuleMap {
[name: string]: IModule;
@@ -233,37 +239,16 @@ client.on('message', message => {
});
// Cache all command modules.
-cachedModules = {};
-fs.readdirSync('./commands/').forEach(function (file) {
- // Load the module if it's a script.
- if (path.extname(file) === '.js') {
- if (file.includes('.disabled')) {
- logger.info(`Did not load disabled module: ${file}`);
- } else {
- const moduleName = path.basename(file, '.js').toLowerCase();
- logger.info(`Loaded module: ${moduleName} from ${file}`);
- cachedModules[moduleName] = require(`./commands/${file}`);
- }
- }
+cachedModules = commands;
+Object.entries(commands).forEach(function (command) {
+ logger.info(`Loaded command: ${command[0]}`);
});
// Cache all triggers.
cachedTriggers = [];
-fs.readdirSync('./triggers/').forEach(function (file) {
- // Load the module if it's a script.
- if (path.extname(file) === '.js') {
- if (file.includes('.disabled')) {
- logger.info(`Did not load disabled trigger: ${file}`);
- } else {
- const moduleName = path.basename(file, '.js').toLowerCase();
- logger.info(`Loaded trigger: ${moduleName} from ${file}`);
- try {
- cachedTriggers.push(require(`./triggers/${file}`));
- } catch (e) {
- logger.error(`Could not load trigger ${moduleName}: ${e}`);
- }
- }
- }
+Object.entries(triggers).forEach((trigger: [string, ITrigger]) => {
+ cachedTriggers.push(trigger[1]);
+ logger.info(`Loaded trigger: ${trigger[0]}`);
});
data.readWarnings();
@@ -271,7 +256,13 @@ data.readBans();
// Load custom responses
if (process.env.DATA_CUSTOM_RESPONSES) {
- data.readCustomResponses();
+ // Load the responses file into the responses variable.
+ state.responses = responses[process.env.TENANT];
+ if (!state.responses) {
+ logger.error(`Failed to load ${process.env.TENANT} from cache! Custom responses are disabled.`);
+ } else {
+ logger.debug(`Loaded ${process.env.TENANT} responses from cache.`);
+ }
}
client.login(process.env.DISCORD_LOGIN_TOKEN);
--
2.33.0

View File

@ -1,11 +0,0 @@
--- a/node_modules/winston/dist/winston.js 2021-08-26 22:26:24.296000000 -0600
+++ b/node_modules/winston/dist/winston.js 2021-09-15 23:35:13.136034453 -0600
@@ -6,7 +6,7 @@
*/
'use strict';
-var logform = require('logform');
+import * as logform from 'logform/dist/browser.js';
var _require = require('./winston/common'),
warn = _require.warn;

View File

@ -1,9 +1,7 @@
import { ban } from '../common';
import discord = require('discord.js');
import * as discord from 'discord.js';
export const roles = ['Admins', 'Moderators', 'CitraBot'];
export function command (message: discord.Message) {
message.mentions.users.map((user) => {
ban(user, message.author, message.guild);
});
};
export async function command (message: discord.Message) {
return Promise.all(message.mentions.users.map(async (user) => ban(user, message.author, message.guild)));
}

View File

@ -1,21 +1,21 @@
import state from '../state';
import * as data from '../data';
import logger from '../logging';
import discord = require('discord.js');
import * as discord from 'discord.js';
export const roles = ['Admins', 'Moderators'];
export function command (message: discord.Message) {
message.mentions.users.map((user) => {
export async function command (message: discord.Message) {
return Promise.all(message.mentions.users.map(async (user) => {
const count = state.warnings.filter(x => x.id === user.id && !x.cleared);
if (count != null && count.length > 0) {
count.forEach(warning => { warning.cleared = true; });
data.flushWarnings();
message.channel.send(`${user.toString()}, your warnings have been cleared.`);
await message.channel.send(`${user.toString()}, your warnings have been cleared.`);
} else {
message.channel.send(`${user.toString()}, you have no warnings to clear.`);
await message.channel.send(`${user.toString()}, you have no warnings to clear.`);
}
logger.info(`${message.author.username} has cleared all warnings for ${user} ${user.username} [${count?.length}].`);
state.logChannel?.send(`${message.author.toString()} has cleared all warnings for ${user.toString()} [${count?.length}].`);
});
};
logger.info(`${message.author.username} has cleared all warnings for ${user.toString()} ${user.username} [${count?.length}].`);
await state.logChannel?.send(`${message.author.toString()} has cleared all warnings for ${user.toString()} [${count?.length}].`);
}));
}

View File

@ -1,6 +1,5 @@
import fetch from 'node-fetch';
import discord = require('discord.js');
import stringSimilarity = require('string-similarity');
import * as discord from 'discord.js';
import * as stringSimilarity from 'string-similarity';
import logger from '../logging';
import state from '../state';
@ -16,21 +15,21 @@ const compatStrings: ICompatList = {
1: { key: '1', name: 'Great', color: '#47d35c', description: 'Game functions with minor graphical or audio glitches and is playable from start to finish. May require some workarounds.' },
2: { key: '2', name: 'Okay', color: '#94b242', description: 'Game functions with major graphical or audio glitches, but game is playable from start to finish with workarounds.' },
3: { key: '3', name: 'Bad', color: '#f2d624', description: 'Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches even with workarounds.' },
4: { key: '4', name: 'Intro/Menu', color: 'RED', description: 'Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen.' },
4: { key: '4', name: 'Intro/Menu', color: 'Red', description: 'Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen.' },
5: { key: '5', name: "Won't Boot", color: '#828282', description: 'The game crashes when attempting to startup.' },
99: { key: '99', name: 'Not Tested', color: 'DARK_BUT_NOT_BLACK', description: 'The game has not yet been tested.' }
99: { key: '99', name: 'Not Tested', color: 'DarkButNotBlack', description: 'The game has not yet been tested.' }
};
async function updateDatabase () {
let body: any;
let body: IGameDBEntry[];
if (!targetServer) {
logger.error('Unable to download latest games list!');
return;
}
try {
let response = await fetch(targetServer);
body = await response.json();
const response = await fetch(targetServer);
body = (await response.json()) as IGameDBEntry[];
} catch (e) {
logger.error('Unable to download latest games list!');
throw e;
@ -62,15 +61,15 @@ export async function command (message: discord.Message) {
try {
await state.gameDBPromise;
} catch (e) {
message.channel.send('Game compatibility feed temporarily unavailable.');
await message.channel.send('Game compatibility feed temporarily unavailable.');
throw e;
} finally {
// We don't need this message anymore
waitMessage.then(waitMessageResult => waitMessageResult.delete());
await waitMessage.then(async waitMessageResult => await waitMessageResult.delete());
}
}
const game = message.content.substr(message.content.indexOf(' ') + 1);
const game = message.content.substring(message.content.indexOf(' ') + 1);
// Search all games. This is only linear time, so /shrug?
let bestGame: IGameDBEntry | null = null;
@ -87,7 +86,7 @@ export async function command (message: discord.Message) {
}
if (!bestGame) {
message.channel.send('Game could not be found.');
await message.channel.send('Game could not be found.');
return;
}
@ -96,13 +95,13 @@ export async function command (message: discord.Message) {
const compat = compatStrings[bestGame.compatibility];
const embed = new discord.MessageEmbed()
.addField('Status', compat.name, true)
const embed = new discord.EmbedBuilder()
.addFields({ name: 'Status', value: compat.name, inline: true })
.setTitle(bestGame.title)
.setColor(compat.color)
.setDescription(compat.description)
.setURL(url)
.setThumbnail(screenshot);
message.channel.send({embeds: [embed]});
await message.channel.send({ embeds: [embed] });
}

View File

@ -1,38 +1,20 @@
import state from '../state';
import logger from '../logging';
import discord = require('discord.js');
import * as discord from 'discord.js';
import { grantRole } from '../common';
export const roles = ['Admins', 'Moderators', 'CitraBot'];
export function command (message: discord.Message) {
export async function command (message: discord.Message) {
const role = process.env.DISCORD_DEVELOPER_ROLE;
if (!role) {
logger.error('DISCORD_DEVELOPER_ROLE suddenly became undefined?!');
return;
return Promise.resolve([]);
}
message.mentions.users.map((user) => {
message.guild?.members.fetch(user).then((member) => {
const alreadyJoined = member.roles.cache.has(role);
if (alreadyJoined) {
member.roles.remove(role).then(() => {
message.channel.send(`${user.toString()}'s speech has been revoked in the #development channel.`);
}).catch(() => {
state.logChannel?.send(`Error revoking ${user.toString()}'s developer speech...`);
logger.error(`Error revoking ${user} ${user.username}'s developer speech...`);
});
} else {
member.roles.add(role).then(() => {
message.channel.send(`${user.toString()} has been granted speech in the #development channel.`);
}).catch(() => {
state.logChannel?.send(`Error granting ${user.toString()}'s developer speech...`);
logger.error(`Error granting ${user} ${user.username}'s developer speech...`);
});
}
}).catch(() => {
message.channel.send(`User ${user.toString()} was not found in the channel.`);
});
});
return Promise.all(message.mentions.users.map(async (user) => {
return message.guild?.members.fetch(user).then((member) => grantRole(member, role, message.channel))
.catch(async () => {
await message.channel.send(`User ${user.toString()} was not found in the channel.`);
});
}));
}

View File

@ -0,0 +1,20 @@
import logger from '../logging';
import * as discord from 'discord.js';
import { grantRole } from '../common';
const role = process.env.DISCORD_TESTER_ROLE;
export const roles = ['Admins', 'Moderators', 'CitraBot'];
export async function command (message: discord.Message) {
if (!role) {
logger.error('DISCORD_TESTER_ROLE suddenly became undefined?!');
return Promise.resolve([]);
}
return Promise.all(message.mentions.users.map(async (user) => {
return message.guild?.members.fetch(user).then((member) => grantRole(member, role, message.channel))
.catch(async () => {
await message.channel.send(`User ${user.toString()} was not found in the channel.`);
});
}));
}

View File

@ -1,20 +1,20 @@
import state from '../state';
import UserBan from '../models/UserBan';
import UserWarning from '../models/UserWarning';
import discord = require('discord.js');
import * as discord from 'discord.js';
export const roles = ['Admins', 'Moderators'];
function formatWarnings (warnings: UserWarning[]) {
return warnings.map(x => `[${x.date}] ${x.warnedByUsername} warned ${x.username} [${x.priorWarnings} + 1]. ${x.silent ? '(silent)' : ''} ${x.cleared ? '(cleared)' : ''}`);
return warnings.map(x => `[${x.date.toISOString()}] ${x.warnedByUsername} warned ${x.username} [${x.priorWarnings} + 1]. ${x.silent ? '(silent)' : ''} ${x.cleared ? '(cleared)' : ''}`);
}
function formatBans (bans: UserBan[]) {
return bans.map(x => `[${x.date}] ${x.warnedByUsername} banned ${x.username} [${x.priorWarnings} + 1].`);
return bans.map(x => `[${x.date.toISOString()}] ${x.warnedByUsername} banned ${x.username} [${x.priorWarnings} + 1].`);
}
export function command (message: discord.Message) {
message.mentions.users.map((user) => {
export async function command (message: discord.Message) {
return Promise.all(message.mentions.users.map(async (user) => {
const totalWarnings = state.warnings.filter(x => x.id === user.id && x.cleared === false).length;
const warns = state.warnings.filter(x => x.id === user.id);
const bans = state.bans.filter(x => x.id === user.id);
@ -22,6 +22,6 @@ export function command (message: discord.Message) {
const warnsString = `Warns: \`\`\`${formatWarnings(warns).join('\n')}\`\`\``;
const bansString = `Bans: \`\`\`${formatBans(bans).join('\n')}\`\`\``;
message.channel.send(`\`${user.username} (${totalWarnings}) information:\`${warns.length !== 0 ? warnsString : '\n<No warnings>\n'}${bans.length !== 0 ? bansString : '<Not banned>'}`);
});
};
await message.channel.send(`\`${user.username} (${totalWarnings}) information:\`${warns.length !== 0 ? warnsString : '\n<No warnings>\n'}${bans.length !== 0 ? bansString : '<Not banned>'}`);
}));
}

View File

@ -1,13 +1,13 @@
import discord = require('discord.js');
import * as discord from 'discord.js';
export const roles = ['Admins', 'Moderators'];
export function command (message: discord.Message, reply: string) {
export async function command (message: discord.Message, reply: string | undefined) {
let replyMessage;
if (reply == null) {
replyMessage = message.content.substr(message.content.indexOf(' ') + 1);
replyMessage = message.content.substring(message.content.indexOf(' ') + 1);
} else {
replyMessage = `${message.mentions.users.map(user => `${user.toString()}`)} ${reply}`;
replyMessage = `${message.mentions.users.map(user => user.toString()).join(' ')} ${reply}`;
}
message.channel.send(replyMessage);
await message.channel.send(replyMessage);
}

View File

@ -1,33 +1,32 @@
import fetch from 'node-fetch';
import discord = require('discord.js');
import * as discord from 'discord.js';
const fetchOptions = {
headers: { 'User-Agent': 'Citra-Emu/CitraBot (Node.js)', 'Accept': 'application/vnd.github.antiope-preview+json' }
headers: { 'User-Agent': 'Citra-Emu/CitraBot (Node.js)', Accept: 'application/vnd.github.antiope-preview+json' }
};
const repo = process.env.GITHUB_REPOSITORY || 'citra-emu/citra';
export const roles = ['Admins', 'Moderators', 'Developer'];
export function command(message: discord.Message) {
const pr_number = message.content.substr(message.content.indexOf(' ') + 1).replace(/\n/g, '');
const url = `https://api.github.com/repos/${repo}/pulls/${pr_number}`;
fetch(url, fetchOptions).then(response => response.json()).then((pr: any) => {
export async function command (message: discord.Message) {
const prNumber = message.content.substring(message.content.indexOf(' ') + 1).replace(/\n/g, '');
const url = `https://api.github.com/repos/${repo}/pulls/${prNumber}`;
return fetch(url, fetchOptions).then(response => response.json()).then(async (pr: any) => {
if (!pr || pr.documentation_url || !pr.head) throw new Error('PR not found');
const headSHA = pr.head.sha;
// use the new GitHub checks API
fetch(`https://api.github.com/repos/${repo}/commits/${headSHA}/check-runs`, fetchOptions).then(response => response.json()).then((statuses: any) => {
return fetch(`https://api.github.com/repos/${repo}/commits/${headSHA}/check-runs`, fetchOptions).then(response => response.json()).then(async (statuses: any) => {
if (!statuses.check_runs || statuses.total_count < 1) throw new Error('No check runs');
let msg = new discord.MessageEmbed().setTitle(`Status for PR #${pr_number}`).setURL(pr.html_url);
let color = 'GREEN' as discord.ColorResolvable;
const msg = new discord.EmbedBuilder().setTitle(`Status for PR #${prNumber}`).setURL(pr.html_url);
let color: discord.ColorResolvable = 'Green';
statuses.check_runs.forEach((run: any) => {
msg.addField(`${run.name}`, `**[${run.status} ${run.conclusion}](${run.html_url})**`);
if (run.conclusion !== 'success') color = 'RED';
msg.addFields({ name: run.name, value: `**[${run.status} ${run.conclusion}](${run.html_url})**` });
if (run.conclusion !== 'success') color = 'Red';
});
msg.setColor(color);
message.channel.send({ embeds: [msg] });
}).catch(() => {
message.channel.send('I wasn\'t able to get the status of that PR...')
await message.channel.send({ embeds: [msg] });
}).catch(async () => {
await message.channel.send('I wasn\'t able to get the status of that PR...');
});
}).catch(() => {
message.channel.send('No such PR.');
}).catch(async () => {
await message.channel.send('No such PR.');
});
}

View File

@ -2,23 +2,23 @@ import state from '../state';
import * as data from '../data';
import logger from '../logging';
import UserWarning from '../models/UserWarning';
import discord = require('discord.js');
import * as discord from 'discord.js';
exports.roles = ['Admins', 'Moderators'];
exports.command = function (message: discord.Message) {
export const roles = ['Admins', 'Moderators'];
export async function command (message: discord.Message) {
const silent = message.content.includes('silent');
message.mentions.users.map((user) => {
return Promise.all(message.mentions.users.map(async (user) => {
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
if (silent === false) {
message.channel.send(`${user.toString()} You have been warned. Additional infractions may result in a ban.`);
if (!silent) {
await message.channel.send(`${user.toString()} You have been warned. Additional infractions may result in a ban.`);
}
logger.info(`${message.author.username} ${message.author} has warned ${user.username} ${user} [${count} + 1].`);
state.logChannel?.send(`${message.author.toString()} has warned ${user.toString()} (${user.username}) [${user}] [${count} + 1].`);
await state.logChannel?.send(`${message.author.toString()} has warned ${user.toString()} (${user.username}) [${user}] [${count} + 1].`);
state.warnings.push(new UserWarning(user.id, user.username, message.author.id, message.author.username, count, silent));
data.flushWarnings();
});
};
}));
}

View File

@ -1,9 +1,9 @@
import state from '../state';
import discord = require('discord.js');
import * as discord from 'discord.js';
exports.command = function (message: discord.Message) {
message.mentions.users.map((user) => {
export async function command (message: discord.Message) {
return Promise.all(message.mentions.users.map(async (user) => {
const warnings = state.warnings.filter(x => x.id === user.id && !x.cleared);
message.channel.send(`${user.toString()}, you have ${warnings.length} total warnings.`);
});
};
await message.channel.send(`${user.toString()}, you have ${warnings.length} total warnings.`);
}));
}

View File

@ -2,19 +2,42 @@ import state from './state';
import * as data from './data';
import logger from './logging';
import UserBan from './models/UserBan';
import discord = require('discord.js');
import * as discord from 'discord.js';
export function ban(user: discord.User, moderator: discord.User, guild: discord.Guild | null) {
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
export async function ban (user: discord.User, moderator: discord.User, guild: discord.Guild | null) {
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
logger.info(`${moderator.toString()} has banned ${user.toString()} ${user.id} ${user.username}.`);
state.logChannel?.send(`${moderator.toString()} has banned ${user.id} ${user.toString()} [${count}].`);
logger.info(`${moderator.toString()} has banned ${user.toString()} ${user.id} ${user.username}.`);
await state.logChannel?.send(`${moderator.toString()} has banned ${user.id} ${user.toString()} [${count}].`);
state.bans.push(new UserBan(user.id, user.username, moderator.id, moderator.username, count));
guild?.members?.ban(user).catch(function (error) {
state.logChannel?.send(`Error banning ${user.toString()} ${user.username}`);
logger.error(`Error banning ${user.toString()} ${user.id} ${user.username}.`, error);
});
state.bans.push(new UserBan(user.id, user.username, moderator.id, moderator.username, count));
guild?.members?.ban(user).catch(async function (error) {
await state.logChannel?.send(`Error banning ${user.toString()} ${user.username}`);
logger.error(`Error banning ${user.toString()} ${user.id} ${user.username}.`, error);
});
data.flushBans();
data.flushBans();
}
export async function grantRole (member: discord.GuildMember, role: string, channel: discord.TextBasedChannel) {
const user = member.user;
const roleDisplayName = member.guild.roles.cache.get(role)?.name;
const alreadyJoined = member.roles.cache.has(role);
if (alreadyJoined) {
member.roles.remove(role).then(async () => {
await channel.send(`${user.toString()}'s ${roleDisplayName} role has been revoked.`);
}).catch(async () => {
await state.logChannel?.send(`Error revoking ${user.toString()}'s ${roleDisplayName} permission...`);
logger.error(`Error revoking ${user.toString()} ${user.username}'s ${roleDisplayName} permission...`);
});
return;
}
member.roles.add(role).then(async () => {
await channel.send(`${user.toString()} has been granted ${roleDisplayName} role.`);
}).catch(async () => {
await state.logChannel?.send(`Error granting ${user.toString()}'s ${roleDisplayName} permission...`);
logger.error(`Error granting ${user.toString()} ${user.username}'s ${roleDisplayName} permission...`);
});
}

View File

@ -1,6 +1,12 @@
import * as fs from 'fs';
import state from './state';
import logger from './logging';
import { IResponses } from './models/interfaces';
const responses: { [index: string]: IResponses } = {
citra: require('./responses/citra.json'),
yuzu: require('./responses/yuzu.json')
};
export function readWarnings () {
// Load the warnings file into the application state.
@ -32,12 +38,14 @@ export function readBans () {
export function readCustomResponses () {
// Load the responses file into the responses variable.
try {
state.responses = require(`./responses/${process.env.TENANT}.json`);
logger.debug(`Loaded responses file for ${process.env.TENANT} from external source.`);
} catch (e) {
logger.error(`Failed to load ${process.env.TENANT}.json! Custom responses are disabled.`);
if (process.env.TENANT) {
state.responses = responses[process.env.TENANT];
if (state.responses) {
logger.debug(`Loaded responses file for ${process.env.TENANT} from external source.`);
return;
}
}
logger.error(`Failed to load ${process.env.TENANT}.json! Custom responses are disabled.`);
}
export function flushWarnings () {

View File

@ -1,6 +1,7 @@
import winston = require('winston');
import * as winston from 'winston';
import * as ip from 'ip';
import * as os from 'os';
import LogdnaWinston from 'logdna-winston';
const logger = winston.createLogger({
level: 'debug',
@ -13,15 +14,14 @@ const logger = winston.createLogger({
handleExceptions: true
})
],
exitOnError: true,
exitOnError: true
});
// Setup logging for LogDNA cloud logging.
if (process.env.LOGDNA_API_KEY) {
const logdnaWinston = require('logdna-winston');
const logLevel = process.env.LOGDNA_LEVEL || 'info';
logger.add(new logdnaWinston({
logger.add(new LogdnaWinston({
level: logLevel,
app: process.env.LOGDNA_APPNAME,
index_meta: true,

View File

@ -24,7 +24,7 @@ export interface IResponses {
export interface IModule {
readonly roles?: string[],
command: (message: Message, args?: string) => void | Promise<void>
command: (message: Message, args?: string) => Promise<void> | Promise<void[]>
}
export interface ITrigger {

View File

@ -2,26 +2,34 @@
"pmReply": "Please refer to our **Frequently Asked Questions**. <https://citra-emu.org/wiki/faq/>",
"quotes": {
"faq": { "reply": "Please refer to our **Frequently Asked Questions**. <https://citra-emu.org/wiki/faq/>" },
"cpu": { "reply": "Citra requires powerful single-core performance. Refer to your CPU in this graph. Your experience with Citra won't be enjoyable in most games if it's below 1,800. <https://www.cpubenchmark.net/singleThread.html>" },
"requirements": { "reply": "Please refer to our **Frequently Asked Questions**. The only requirements for Citra are a GPU that supports at least OpenGL 3.3 and a 64-bit OS, but you definitely want a processor with the highest possible performance per core. <https://citra-emu.org/wiki/faq/>"},
"requirements": { "reply": "Please refer to our **Frequently Asked Questions**. The only requirements for Citra are a GPU that supports at least OpenGL 4.3 or Vulkan 1.1 and a 64-bit OS, but you definitely want a processor with the highest possible performance per core. <https://citra-emu.org/wiki/faq/>"},
"roms": { "reply": "Please read our __community rules__. Warez/downloading games talk is strictly prohibited. To prevent legal issues, you are not allowed to post links or refer to any kind of ROM, NAND, ISO, game, or other copyrighted material that has been illegally obtained or shared. <https://citra-emu.org/rules/>"},
"dump-game": { "reply": "Please refer to our __game dumping guides__. \nFor Cartridges: <https://citra-emu.org/wiki/dumping-game-cartridges/> \nFor Installed Titles: <https://citra-emu.org/wiki/dumping-installed-titles/> \nTo dump DLC and Updates, use this guide <https://citra-emu.org/wiki/dumping-updates-and-dlcs/> and install the results through `File -> Install CIA...`"},
"apk": { "reply": "Official Citra for Android has been released! Download it from <https://citra-emu.org/download>. \nPlease note that the current app is a beta version and not everything works right away. We will continue to fix issues/bugs and release updates."},
"dump-system": { "reply": "Please refer to our __system dumping guide__. <https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/>"},
"dumping": {"reply": "Dumping is the act of creating an archival copy of your own legally purchased games. This process requires a hacked console to perform the dump \nDumping is legal, however sharing your dumped archival copies with anyone else is illegal."},
"apk": { "reply": "Official Citra for Android has been released! Download the .apk (the second `citra-android-universal` link) from <https://github.com/citra-emu/citra-nightly/releases>. \nIf you have the Citra Android app from the Google Play Store installed, please uninstall it before installing from our Nightly repo. Your save data will transfer over and if you have paid for Citra Premium, that will transfer over too. \n\nPlease note that the current app is a beta version and not everything works right away. We will continue to fix issues/bugs and release updates."},
"dump-system": { "reply": "Please refer to our __system dumping guide__: <https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/>"},
"compat": { "reply": "Click here to view our game compatibility list: <https://citra-emu.org/game/>. ***A Reminder***: Compatibility ratings are user submitted data and may not accurately reflect the game's playability. Your experience may or may not be the same due to differences in hardware or software environments."},
"alpha": { "reply": "*Citra* is currently in early stages of development. Some games usually run less than full-speed even on the best computers. Expect bugs and glitches to appear in most games. Many features found in more mature emulators are still in the works. For any major updates, please visit <https://citra-emu.org/>"},
"updates": { "reply": "You can check our latest updates on *Github*. <https://github.com/citra-emu/citra/pulse>"},
"download": { "reply": "Please only download from the official *Citra* website, as downloading from other sources is not supported here. <https://citra-emu.org/download/>"},
"legal": { "reply": "*Citra* is legal, we don't support illegal activities. Dumping your purchased games and system files from your 3DS is legal. Downloading them is not."},
"building": { "reply": "Please refer to our building guides.\nWindows: <https://citra-emu.org/wiki/building-for-windows> \nmacOS: <https://citra-emu.org/wiki/building-for-macos> \nLinux: <https://citra-emu.org/wiki/building-for-linux>"},
"controller": { "reply": "This forum topic tells you how to __configure your gamepad / controller__: <https://community.citra-emu.org/t/temporary-controller-configurations-for-citra/1061>"},
"issues": { "reply": "This forum topic lists __known issues in games and their workarounds__: <https://community.citra-emu.org/t/known-problems-typical-issues-and-their-workarounds/1317> \nPlease read it carefully. It includes help with most games"},
"forum": { "reply": "This question might be more suitable for the *Citra* forum. <https://community.citra-emu.org/>"},
"log": { "reply": "For assistance with your issue, please accurately describe the problem and post a log file. The following guide shows how to __get the log file__: <https://community.citra-emu.org/t/how-to-upload-the-log-file/296>"},
"canary": { "reply": "The nightly build of Citra contains already reviewed and tested features. If you require support with the installation or use of Citra, or you want to report bugs you should use this version.\nThe Canary build of Citra is the same as our nightly builds, with additional features that are still waiting on review before making it into the official Citra builds. We will not provide support for issues found only in this version. If you believe youve found a bug, please retest on our nightly builds. Both versions are still in development, so expect crashes and bugs."},
"cheats": { "reply": "Looking to add cheats to Citra? Drop a .txt file with the Title ID of the game you're playing into the Cheats folder. You can find it by clicking 'File -> Open Citra Folder'. \nCheats are stored in the gateway format. A sample collection of cheats are located at <https://github.com/iSharingan/CTRPF-AR-CHEAT-CODES/tree/master/Cheats> "},
"scam": { "reply": "If you believe that a user is attempting to scam you or others, please report them to discord trust and safety and promptly block them. \nTo report the user, please follow this link <https://dis.gd/request>. \nFor more information about how to report them, please look here: <https://support.discord.com/hc/en-us/articles/360000291932-How-to-Properly-Report-Issues-to-Trust-Safety>. \nFinally, if this is the only server you share, please report the user (including screenshots of the conversation) to our moderation team, so that we can address the issue."},
"keys": { "reply": "Users need to provide encryption keys to use encrypted ROMs on Citra. Please follow the provided instructions below to obtain the keys from your 3DS. <https://citra-emu.org/wiki/aes-keys/>"},
"legal": { "reply": "*Citra* is legal, we don't support illegal activities. Dumping your purchased games from your 3DS is legal. Downloading them is not."},
"building": { "reply": "Please refer to our __building guides__.\nWindows: <https://citra-emu.org/wiki/building-for-windows> \nmacOS: <https://citra-emu.org/wiki/building-for-macos> \nLinux: <https://citra-emu.org/wiki/building-for-linux>"},
"log": { "reply": "For assistance with your issue, please post a log file. The following guide shows how to __get the log file__: <https://community.citra-emu.org/t/how-to-upload-the-log-file/296>"},
"canary": { "reply": "The Nightly build of Citra contains already reviewed and tested features. If you require support with the installation or use of Citra, or you want to report bugs you should use this version.\n\nThe Canary build of Citra is the same as our Nightly builds, with additional features that are still waiting on review before making it into the official Citra builds. We will not provide support for issues found only in this version. If you believe youve found a bug, please retest on our Nightly builds. Both versions are still in development, so expect crashes and bugs."},
"cheats": { "reply": "Looking to add cheats? Here's how:\n\n**Desktop**\nThere are two ways to access the Cheats menu:\n1. Right click on your game in Citra's game list, then select `Properties`, and select the `Cheats` tab. \n2. After launching your game, go to `Emulation > Configure Current Game > Cheats tab`\n\nSelect **Add Cheat**, enter the name of your cheat code and the cheat code itself into the relevant boxes, and select **Save**. To enable the cheat code, tick the box next to the name of the cheat code in the `Available Cheats` list.\n\n**Android**\nLaunch your game, then press the Back button to open Citra's in-game menu, and select the `Open Cheats` option. Select the `+` button on the bottom right, then enter the name of your cheat code and the cheat code itself into the relevant boxes. Press Ok, and then press the arrow button on the top left to return to the Cheats menu. Activate the Cheat code.\n\n Cheats are stored in the gateway format. A sample collection of cheats are located at <https://github.com/iSharingan/CTRPF-AR-CHEAT-CODES/tree/master/Cheats>"},
"keys": { "reply": "Users need to provide encryption keys to use encrypted ROMs on Citra. Please follow the __AES Keys guide__ to provide the keys needed. <https://citra-emu.org/wiki/aes-keys/>"},
"hack": {"reply": "To mod your 3DS, please follow the __3DS hacking guide__: <https://3ds.hacks.guide/> \nYou will need a way to read an SD card on your device, such as a built in reader or an adapter."},
"multiplayer": {"reply": "Please refer to our __multiplayer setup guide__: <https://citra-emu.org/help/feature/multiplayer>"},
"modding": {"reply": "Please refer to our __game modding guide__: <https://citra-emu.org/help/feature/game-modding>"},
"portable": {"reply": "Go to <https://citra-emu.org/download/> and select `Manual download`. Then under **Nightly Build**, click on your operating system's icon to the right of the latest build available to download the build.\nExtract it (.7z can be extracted using Winrar or 7zip) and put it wherever you wish. Inside the extracted `nightly` folder, create a folder called `user`. This Citra should now store all of its config, save files and such inside of this `user` folder.\nCheck to make sure that this `user` folder is in the same folder that contains `citra-qt(.exe)`\nNow run the `citra-qt` executable found inside of the `nightly` folder."},
"ios": {"reply": "Citra does not have an official iOS version because iOS is a complicated operating system to develop for. We currently do not have any developers with an interest in developing for this platform. There are some iOS forks of Citra, however they are not supported by us."},
"piracy": {"reply": "*Citra* is legal, we don't support illegal activities. Dumping your purchased games from your 3DS is legal. Downloading them is not.\n\nRefer to our __game dumping guides__. \nFor Cartridges: <https://citra-emu.org/wiki/dumping-game-cartridges/> \nFor Installed Titles: <https://citra-emu.org/wiki/dumping-installed-titles/> \nTo dump DLC and Updates, use this guide <https://citra-emu.org/wiki/dumping-updates-and-dlcs/> and install the results through `File -> Install CIA...`\n\nAlso, please refresh yourself on the <#417321837916192779> you agreed to when you joined this server."},
"saves": {"reply": "Download Checkpoint and open it on the 3DS. Select the game you want, click L. If you require extdata too, not all games do, press X then press L.\n\nThese will be dumped to /3ds/Checkpoint/saves then either saves or extdata.\n\nLaunch Citra and make sure your game directory is listed on the main screen so the game shows up there. Right click on the game you want then choose Open Save Data Location or Open Extra Data Location. Make sure to launch the game at least once in Citra to create these folders first. \n\nOnce those have been open, just replace any save files in Citra with the ones dumped from your 3DS.\n<https://github.com/FlagBrew/Checkpoint>\nTo reverse this and put a Citra save on the 3DS, work backwards and use R to restore it."},
"states": {"reply": "Save states save the entirety of Citra's emulation state. As a result of this, Citra updates that add new things to serialize in the emulation state will break the loading of existing save states that were created on prior Citra versions. Additionally, saving upon save state created progress will make the emulation state gradually more unstable, since the emulation state never gets to clear out emulation bugs with a proper shut down.\nThis is why in-game save files should be used most of all when saving and loading your progress. Save states should only be used within single gaming sessions, not beyond them.\nLastly, save states cannot be transferred between different Citra installations. They may load, but they'll break the virtual file system."},
"folder": {"reply": "When you open the game directory selector, you need to choose the folder your games are in, not the games themselves. Those will auto-populate. The only exception to this are .cia ROM files, which you'll need to install using `File>Install CIA...`"},
"eshop": {"reply": "We're aware that the e-shop has closed. However, whether a game is available for purchase has no bearing on the legality of downloading that game. The games are still copyrighted, thus downloading them off of the internet is still piracy."},
"usecase": {"reply": "Emulators are for enhancing your bought game beyond what the console can offer. Which means resolution upscaling, mod support, texture packs, texture filters, speed ups, ect.\nMost importantly it's for the preservation of your games when the console will inevitably no longer be available for purchase.\nEmulators are not for people to commit theft online."},
"legacy": {"reply": "Please refer to our __legacy builds guide__ to download any older builds of Citra you may need: <https://citra-emu.org/wiki/citra-legacy-builds/>"},
"issue": {"reply": "Please refer to our __GitHub issues page__ to file an issue or a feature request: <https://github.com/citra-emu/citra/issues/new/choose>"},
"lenny": { "reply": "( ͡° ͜ʖ ͡°)"},
"( ͡° ͜ʖ ͡°)": { "reply": "lenny"},
"r1": { "reply": ":beginner: **Rule #1:** \nStay courteous and respectful to others."},
@ -32,7 +40,7 @@
"r6": { "reply": ":beginner: **Rule #6:** \nEnglish Only — This is an English only server. If you don't speak English well, please use a translation service such as <https://www.deepl.com/en/translator>."},
"r7": { "reply": ":beginner: **Rule #7:** \nNo Shitposting — No excessive posting of memes, low quality posts, copypastas, or other content deemed disruptive."},
"r8": { "reply": ":beginner: **Rule #8:** \nTrying to evade or circumvent any of this server's rules will be considered as breaking them. Doing so will result in warnings, and eventually a ban."},
"r9": { "reply": ":beginner: **Rule #9:** \nIn case of no written rule — the discretion of moderators shall take precedence."}
"r9": { "reply": ":beginner: **Rule #9:** \nIn case of no written rule — the discretion of moderators shall take precedence."},
"case": {"reply": "We do not know anything other than the public filing, and we are not able to discuss the matter at this time."}
}
}

View File

@ -4,7 +4,7 @@
"help": { "reply": "Need help? Refer to our **asking for help** guide. <https://yuzu-emu.org/help/reference/asking/>" },
"ea": { "reply": "In order to use **yuzu Early Access**, you must have an active forum account with your Patreon account linked to it. Please refer to the following guide to receive prioritized support: <https://yuzu-emu.org/help/early-access/>" },
"faq": { "reply": "For information on your issue, please refer to our **Frequently Asked Questions**. <https://yuzu-emu.org/wiki/faq/>" },
"cpu": { "reply": "For information on Hardware Requirements for yuzu, please refer to this page: <https://yuzu-emu.org/help/quickstart/#hardware-requirements>"},
"hardware": { "reply": "For information on Hardware Requirements for yuzu, please refer to this page: <https://yuzu-emu.org/help/quickstart/#hardware-requirements>"},
"roms": { "reply": "Please read our __community rules__. Warez/downloading games talk is strictly prohibited. To prevent legal issues, you are not allowed to post links or refer to any kind of ROM, NAND, ISO, game, or other copyrighted material that has been illegally obtained or shared. <https://yuzu-emu.org/rules/>"},
"dump-cart": { "reply": "Please refer to our __cartridge dumping guide__. <https://yuzu-emu.org/help/quickstart/#dumping-cartridge-games>"},
"alpha": { "reply": "*yuzu* is currently in very early stages of development. Some games usually run less than full-speed even on the best computers. Expect bugs and glitches to appear in most games. Many features found in more mature emulators are still in the works. For any major updates, please visit <https://yuzu-emu.org/> or the #announcements channel."},
@ -13,10 +13,11 @@
"legal": { "reply": "*yuzu* is legal, we don't support illegal activities. Dumping your purchased games and system files from your Switch is legal. Downloading them is not."},
"building": { "reply": "Please refer to our building guides.\nWindows: <https://yuzu-emu.org/wiki/building-for-windows> \nLinux: <https://yuzu-emu.org/wiki/building-for-linux>"},
"contributing": { "reply": "Contributing to the project is the best way to help the project move forward. If you are a developer, please refer to: \nSwitch reference guides: <https://github.com/yuzu-emu/yuzu/wiki/Switch-Hardware-and-Software> \nSwitch homebrew applications for testing: <https://github.com/yuzu-emu/yuzu/wiki/Switch-Homebrew> \nWiki, dedicated to Switch research: <http://switchbrew.org> \nyuzu contributing tips: <https://github.com/yuzu-emu/yuzu/blob/master/CONTRIBUTING.md>"},
"multiplayer": { "reply": "Please refer to our __multiplayer setup guide__: <https://yuzu-emu.org/help/feature/multiplayer/>"},
"lenny": { "reply": "( ͡° ͜ʖ ͡°)"},
"( ͡° ͜ʖ ͡°)": { "reply": "lenny"},
"format": { "reply": "A full description of game formats the yuzu supports and when to use them can be found on our wiki. <https://yuzu-emu.org/wiki/overview-of-switch-game-formats/>"},
"keys": { "reply": "Most games require encryption keys to boot. You can dump them from your Switch by following this guide. <https://yuzu-emu.org/help/quickstart/#dumping-prodkeys-and-titlekeys>"},
"keys": { "reply": "Most games require encryption keys to boot. You can dump them from your Switch by following this guide. <https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys>"},
"game-updates": { "reply": "Installing and using game updates are a separate process from the base game. Check out our updates tutorial on our wiki. <https://yuzu-emu.org/wiki/how-to-install-and-use-game-updates/>"},
"appdata": { "reply": "Please refer to the following guide to fully reinstall yuzu: <https://yuzu-emu.org/wiki/faq/#yuzu-will-not-update-further-or-starts-with-a-qt-platform-error>"},
"log": { "reply": "For assistance with your issue, please accurately describe the problem and post a log file. The following guide shows how to __get the log file__: <https://yuzu-emu.org/help/reference/log-files/>"},
@ -34,16 +35,23 @@
"r7": { "reply": ":beginner: **Rule #7:** \nNo advertising of any kind, unless permission is granted. — Exceptions being texture packs / game mods that relate to yuzu, but these are handled on a case-by-case basis."},
"r8": { "reply": ":beginner: **Rule #8:** \nEnglish Only — This is an English only server. If you don't speak English well, please use a translation service such as <https://www.deepl.com/en/translator>."},
"r9": { "reply": ":beginner: **Rule #9:** \nNo Shitposting — No excessive posting of memes, low quality posts, copypastas, or other content deemed disruptive."},
"r10": { "reply": ":beginner: **Rule #10:** \nTrying to evade or circumvent any of this server's rules will be considered as breaking them. Doing so will result in warnings, and eventually a ban."},
"r11": { "reply": ":beginner: **Rule #11:** \nIn case of no written rule — the discretion of moderators shall take precedence."},
"r10": { "reply": ":beginner: **Rule #10:** \nTo make moderation easier, users are required to use ASCII usernames. Special characters can and will be removed by staff."},
"r11": { "reply": ":beginner: **Rule #11:** \nTrying to evade or circumvent any of this server's rules will be considered as breaking them. Doing so will result in warnings, and eventually a ban."},
"r12": { "reply": ":beginner: **Rule #12:** \nIn case of no written rule — the discretion of moderators shall take precedence."},
"mods": { "reply": "For a list of useful mods for your favorite games, check our database for downloads here: <https://github.com/yuzu-emu/yuzu/wiki/Switch-Mods>"},
"vc": { "reply": "yuzu requires the latest versions of Microsoft Visual C++. Please download and install the following dependency: <https://aka.ms/vs/16/release/vc_redist.x64.exe>"},
"vc": { "reply": "yuzu requires the latest versions of Microsoft Visual C++. Please download and install the following dependency: <https://aka.ms/vs/17/release/vc_redist.x64.exe>"},
"infringe": { "reply": "We are aware of the emulator, and are pursuing legal action. They violate our license, and also ship with copyrighted Nintendo files. As such, any references or discussion of the emulator aren't allowed."},
"android": { "reply": "While we would love to see yuzu ported to Android in the future, we are currently focused on improving the emulator as a whole. Look out for any future announcements, as we'll make sure to let everyone know if an Android build is on the horizon."},
"firmware": { "reply": "Some games require a firmware dump to work properly. Click the following link for instructions on how to dump the firmware from your Nintendo Switch: <https://yuzu-emu.org/help/quickstart/#dumping-system-update-firmware>"},
"android": { "reply": "yuzu Android is available now in the Google Play Store! Download for free: <https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu> or support us by purchasing Early Access: <https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea>"},
"firmware": { "reply": "Some games require a firmware dump to work properly. Click the following link for instructions on how to dump the firmware from your Nintendo Switch: <https://yuzu-emu.org/help/quickstart/#dumping-system-firmware>\nClick the following link to learn how to install it: <https://yuzu-emu.org/help/quickstart/#setting-up-the-decryption-keys-and-firmware>"},
"raptor": { "reply": "Raptor is a 3rd party service that provides a paid alternative to Nintendo Switch Online. We are not associated with Raptor in anyway. After key emulation members and the discerning public shared their opinion with us, we cut all ties with Raptor immediately in 2020. This new fork was made without our permission or cooperation and does not have permission to use the \"yuzu\" name or branding. At this time, we have no plans to add support for any 3rd party services, including Raptor.\n\nAs per our normal policies, we only provide support for our official releases and discussion of any unofficial builds isn't allowed."},
"role": { "reply": "To claim your Patreon Discord role, please follow this guide: <https://support.patreon.com/hc/en-us/articles/212052266-Get-my-Discord-role>"},
"release": { "reply": "yuzu builds can be manually downloaded on Github: <https://github.com/yuzu-emu/yuzu-mainline/releases>"}
"release": { "reply": "yuzu builds can be manually downloaded on Github: <https://github.com/yuzu-emu/yuzu-mainline/releases>"},
"rec": { "reply": "For information on recommended settings and GPU drivers for yuzu, please refer to this page: <https://community.citra-emu.org/t/recommended-settings/319349>"},
"issue": {"reply": "Please refer to our __GitHub issues page__ to file an issue or a feature request: <https://github.com/yuzu-emu/yuzu/issues/new/choose>"},
"drivers": { "reply": "yuzu can use custom drivers for Android devices with an Adreno 6XX/7XX GPU (Snapdragon SoC). There are no custom drivers for Mediatek, Exynos, or any other SoCs currently.\n\nTo use custom drivers, head to #android-drivers, open a driver thread that corresponds with your phone's GPU, and open the latest link posted in the thread.\nIf you don't know your phone's GPU, search for your phone on <https://www.gsmarena.com/>.\nDownload the .zip file from the link, go to the GPU driver manager in the Settings tab and select the downloaded driver you'd like to install."},
"portable": {"reply": "Go to <https://yuzu-emu.org/downloads/> and scroll down to `Builds`. Then under **Builds**, click on your operating system's icon to the right of the latest build available to download the build.\nExtract it and put it wherever you wish. Inside the extracted `yuzu` folder, create a folder called `user`. This yuzu should now store all of its config, save files and such inside of this `user` folder.\nCheck to make sure that this `user` folder is in the same folder that contains `yuzu(.exe)`\nNow run the `yuzu` executable found inside of the `yuzu` folder."},
"usecase": {"reply": "Emulators are for enhancing your bought game beyond what the console can offer. Which means resolution upscaling, mod support, texture packs, texture filters, speed ups, ect.\nMost importantly it's for the preservation of your games when the console will inevitably no longer be available for purchase.\nEmulators are not for people to commit theft online."},
"case": {"reply": "We do not know anything other than the public filing, and we are not able to discuss the matter at this time."}
}
}

View File

@ -1,41 +1,43 @@
// Check for environmental variables.
require('checkenv').check();
import discord = require('discord.js');
import path = require('path');
import fs = require('fs');
import * as discord from 'discord.js';
import logger from './logging';
import state from './state';
import * as data from './data';
import { IModule, ITrigger } from './models/interfaces';
import modules from './commands/_';
import triggers from './triggers/_';
// Check for environmental variables.
import * as checkenv from 'checkenv';
import envConfig from '../env.json';
checkenv.setConfig(envConfig);
checkenv.check();
interface IModuleMap {
[name: string]: IModule;
}
let cachedModules: IModuleMap = {};
let cachedTriggers: ITrigger[] = [];
const client = new discord.Client({ intents: discord.Intents.FLAGS.GUILDS | discord.Intents.FLAGS.GUILD_BANS | discord.Intents.FLAGS.GUILD_MESSAGES });
const cachedModules: IModuleMap = modules;
const cachedTriggers: ITrigger[] = triggers;
const client = new discord.Client({ intents: discord.GatewayIntentBits.GuildMembers | discord.GatewayIntentBits.Guilds | discord.GatewayIntentBits.GuildBans | discord.GatewayIntentBits.GuildMessages | discord.GatewayIntentBits.DirectMessages | discord.GatewayIntentBits.MessageContent });
const rulesTrigger = process.env.DISCORD_RULES_TRIGGER;
const rluesRole = process.env.DISCORD_RULES_ROLE;
const rulesRole = process.env.DISCORD_RULES_ROLE;
const mediaUsers = new Map();
logger.info('Application startup. Configuring environment.');
if (!rulesTrigger) {
throw new Error('DISCORD_RULES_TRIGGER somehow became undefined.');
}
if (!rluesRole) {
if (!rulesRole) {
throw new Error('DISCORD_RULES_ROLE somehow became undefined.');
}
function findArray(haystack: string | any[], arr: any[]) {
return arr.some(function (v: any) {
return haystack.indexOf(v) >= 0;
});
function findArray (haystack: string | string[], arr: string[]) {
return arr.some((v: string) => haystack.indexOf(v) >= 0);
}
function IsIgnoredCategory(categoryName: string) {
function IsIgnoredCategory (categoryName: string) {
const IgnoredCategory = ['internal', 'team', 'development'];
return IgnoredCategory.includes(categoryName);
}
@ -45,8 +47,8 @@ client.on('ready', async () => {
if (!process.env.DISCORD_LOG_CHANNEL || !process.env.DISCORD_MSGLOG_CHANNEL) {
throw new Error('DISCORD_LOG_CHANNEL or DISCORD_MSGLOG_CHANNEL not defined.');
}
let logChannel = await client.channels.fetch(process.env.DISCORD_LOG_CHANNEL) as discord.TextChannel;
let msglogChannel = await client.channels.fetch(process.env.DISCORD_MSGLOG_CHANNEL) as discord.TextChannel;
const logChannel = await client.channels.fetch(process.env.DISCORD_LOG_CHANNEL) as discord.TextChannel;
const msglogChannel = await client.channels.fetch(process.env.DISCORD_MSGLOG_CHANNEL) as discord.TextChannel;
if (!logChannel.send) throw new Error('DISCORD_LOG_CHANNEL is not a text channel!');
if (!msglogChannel.send) throw new Error('DISCORD_MSGLOG_CHANNEL is not a text channel!');
state.logChannel = logChannel;
@ -68,90 +70,86 @@ client.on('disconnect', () => {
logger.warn('Disconnected from Discord server.');
});
client.on('guildMemberAdd', (member) => {
if (process.env.DISCORD_RULES_ROLE)
member.roles.add(process.env.DISCORD_RULES_ROLE);
client.on('guildMemberAdd', async (member) => {
if (process.env.DISCORD_RULES_ROLE) { await member.roles.add(process.env.DISCORD_RULES_ROLE); }
});
client.on('messageDelete', message => {
client.on('messageDelete', async (message) => {
const AllowedRoles = ['Administrators', 'Moderators', 'Team', 'Developer', 'Support', 'VIP'];
let authorRoles = message.member?.roles?.cache?.map(x => x.name);
const authorRoles = message.member?.roles?.cache?.map(x => x.name);
if (!authorRoles) {
logger.error(`Unable to get the roles for ${message.author}`);
return;
}
if (!findArray(authorRoles, AllowedRoles)) {
let parent = (message.channel as discord.TextChannel).parent;
if (parent && IsIgnoredCategory(parent.name) === false) {
if (((message.content && message.content.startsWith('.') === false) || (message.attachments.size > 0)) && message.author?.bot === false) {
let messageAttachment = message.attachments.first()?.proxyURL
const parent = (message.channel as discord.TextChannel).parent;
if (parent && !IsIgnoredCategory(parent.name)) {
if (((message.content && !message.content.startsWith('.')) || (message.attachments.size > 0)) && message.author?.bot === false) {
const messageAttachment = message.attachments.first()?.proxyURL;
const deletionEmbed = new discord.MessageEmbed()
.setAuthor(message.author?.tag, message.author?.displayAvatarURL())
const deletionEmbed = new discord.EmbedBuilder()
.setAuthor({ name: message.author?.tag, iconURL: message.author?.displayAvatarURL() })
.setDescription(`Message deleted in ${message.channel.toString()}`)
.addField('Content', message.cleanContent || '<no content>', false)
.addFields({ name: 'Content', value: message.cleanContent || '<no content>', inline: false })
.setTimestamp()
.setColor('RED');
.setColor('Red');
if (messageAttachment) deletionEmbed.setImage(messageAttachment)
if (messageAttachment) deletionEmbed.setImage(messageAttachment);
let userInfo = `${message.author?.toString()} (${message.author?.username}) (${message.author})`
const userInfo = `${message.author?.toString()} (${message.author?.username}) (${message.author})`;
state.msglogChannel?.send({ content: userInfo, embeds: [deletionEmbed] });
await state.msglogChannel?.send({ content: userInfo, embeds: [deletionEmbed] });
logger.info(`${message.author?.username} ${message.author} deleted message: ${message.cleanContent}.`);
}
}
}
});
client.on('messageUpdate', (oldMessage, newMessage) => {
client.on('messageUpdate', async (oldMessage, newMessage) => {
const AllowedRoles = ['Administrators', 'Moderators', 'Team', 'Developer', 'Support', 'VIP'];
let authorRoles = oldMessage.member?.roles?.cache?.map(x => x.name);
const authorRoles = oldMessage.member?.roles?.cache?.map(x => x.name);
if (!authorRoles) {
logger.error(`Unable to get the roles for ${oldMessage.author}`);
return;
}
if (!findArray(authorRoles, AllowedRoles)) {
let parent = (oldMessage.channel as discord.TextChannel).parent;
if (parent && IsIgnoredCategory(parent.name) === false) {
const parent = (oldMessage.channel as discord.TextChannel).parent;
if (parent && !IsIgnoredCategory(parent.name)) {
const oldM = oldMessage.cleanContent || '<no content>';
const newM = newMessage.cleanContent;
if (oldMessage.content !== newMessage.content && oldM && newM) {
let messageAttachment = oldMessage.attachments.first()?.proxyURL
if (oldMessage.content !== newMessage.content && newM) {
const messageAttachment = oldMessage.attachments.first()?.proxyURL;
const editedEmbed = new discord.MessageEmbed()
.setAuthor(oldMessage.author?.tag || '<unknown>', oldMessage.author?.displayAvatarURL())
const editedEmbed = new discord.EmbedBuilder()
.setAuthor({ name: oldMessage.author?.tag || '<unknown>', iconURL: oldMessage.author?.displayAvatarURL() })
.setDescription(`Message edited in ${oldMessage.channel.toString()} [Jump To Message](${newMessage.url})`)
.addField('Before', oldM, false)
.addField('After', newM, false)
.addFields({ name: 'Before', value: oldM, inline: false }, { name: 'After', value: newM, inline: false })
.setTimestamp()
.setColor('GREEN');
.setColor('Green');
if (messageAttachment) editedEmbed.setImage(messageAttachment)
if (messageAttachment) editedEmbed.setImage(messageAttachment);
let userInfo = `${oldMessage.author?.toString()} (${oldMessage.author?.username}) (${oldMessage.author})`
const userInfo = `${oldMessage.author?.toString()} (${oldMessage.author?.username}) (${oldMessage.author})`;
state.msglogChannel?.send({ content: userInfo, embeds: [editedEmbed] });
await state.msglogChannel?.send({ content: userInfo, embeds: [editedEmbed] });
logger.info(`${oldMessage.author?.username} ${oldMessage.author} edited message from: ${oldM} to: ${newM}.`);
}
}
}
});
client.on('messageCreate', message => {
if (message.author.bot && message.content.startsWith('.ban') === false) { return; }
client.on('messageCreate', async (message) => {
if (message.author.bot && !message.content.startsWith('.ban')) { return; }
if (message.guild == null && state.responses.pmReply) {
// We want to log PM attempts.
// logger.info(`${message.author.username} ${message.author} [PM]: ${message.content}`);
// state.logChannel.send(`${message.author.toString()} [PM]: ${message.content}`);
message.reply(state.responses.pmReply);
// We want to reply to PM attempts.
await message.reply(state.responses.pmReply);
return;
}
logger.verbose(`${message.author.username} ${message.author} [Channel: ${(message.channel as discord.TextChannel).name} ${message.channel}]: ${message.content}`);
let authorRoles = message.member?.roles?.cache?.map(x => x.name);
const authorRoles = message.member?.roles?.cache?.map(x => x.name);
if (message.channel.id === process.env.DISCORD_MEDIA_CHANNEL && !message.author.bot) {
const AllowedMediaRoles = ['Administrators', 'Moderators', 'Team', 'VIP'];
@ -160,13 +158,13 @@ client.on('messageCreate', message => {
return;
}
if (!findArray(authorRoles, AllowedMediaRoles)) {
const urlRegex = new RegExp(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&\/=]*)/gi);
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/gi;
if (message.attachments.size > 0 || message.content.match(urlRegex)) {
mediaUsers.set(message.author.id, true);
} else if (mediaUsers.get(message.author.id)) {
mediaUsers.set(message.author.id, false);
} else {
message.delete();
await message.delete();
mediaUsers.set(message.author.id, false);
}
}
@ -177,18 +175,18 @@ client.on('messageCreate', message => {
if (message.content.toLowerCase().includes(rulesTrigger)) {
// We want to remove the 'Unauthorized' role from them once they agree to the rules.
logger.verbose(`${message.author.username} ${message.author} has accepted the rules, removing role ${process.env.DISCORD_RULES_ROLE}.`);
message.member?.roles.remove(rluesRole, 'Accepted the rules.');
await message.member?.roles.remove(rulesRole, 'Accepted the rules.');
}
// Delete the message in the channel to force a cleanup.
message.delete();
} else if (message.content.startsWith('.') && message.content.startsWith('..') === false) {
await message.delete();
} else if (message.content.startsWith('.') && !message.content.startsWith('..')) {
// We want to make sure it's an actual command, not someone '...'-ing.
const cmd = message.content.split(' ', 1)[0].slice(1);
// Check by the name of the command.
let cachedModule = cachedModules[`${cmd.toLowerCase()}`];
let quoteResponse = null;
const cachedModule = cachedModules[`${cmd.toLowerCase()}`];
let quoteResponse: { reply: string; } | null = null;
// Check by the quotes in the configuration.
if (!cachedModule) quoteResponse = state.responses.quotes[cmd];
if (!cachedModule && !quoteResponse) return; // Not a valid command.
@ -198,69 +196,52 @@ client.on('messageCreate', message => {
logger.error(`Unable to get the roles for ${message.author}`);
return;
}
if (cachedModule && cachedModule.roles && !findArray(authorRoles, cachedModule.roles)) {
state.logChannel?.send(`${message.author.toString()} attempted to use admin command: ${message.content}`);
const allowedRoles = cmd === 'case' ? ['Admins', 'Moderators', 'Developer'] : cachedModule?.roles;
const isAllowed = (!allowedRoles) || (allowedRoles && findArray(authorRoles, allowedRoles));
if (!isAllowed) {
await state.logChannel?.send(`${message.author.toString()} attempted to use admin command: ${message.content}`);
logger.info(`${message.author.username} ${message.author} attempted to use admin command: ${message.content}`);
return;
}
logger.info(`${message.author.username} ${message.author} [Channel: ${message.channel}] executed command: ${message.content}`);
message.delete();
try {
if (!!cachedModule) {
cachedModule.command(message);
} else if (cachedModules['quote']) {
cachedModules['quote'].command(message, quoteResponse?.reply);
}
} catch (err) { logger.error(err); }
} else if (message.author.bot === false) {
// This is a normal channel message.
cachedTriggers.forEach(function (trigger) {
if (!trigger.roles || authorRoles && findArray(authorRoles, trigger.roles)) {
if (trigger.trigger(message) === true) {
logger.debug(`${message.author.username} ${message.author} [Channel: ${message.channel}] triggered: ${message.content}`);
try {
trigger.execute(message);
} catch (err) { logger.error(err); }
}
}
});
}
});
// Cache all command modules.
cachedModules = {};
fs.readdirSync('./commands/').forEach(function (file) {
// Load the module if it's a script.
if (path.extname(file) === '.js') {
if (file.includes('.disabled')) {
logger.info(`Did not load disabled module: ${file}`);
} else {
const moduleName = path.basename(file, '.js').toLowerCase();
logger.info(`Loaded module: ${moduleName} from ${file}`);
cachedModules[moduleName] = require(`./commands/${file}`);
}
}
});
// Cache all triggers.
cachedTriggers = [];
fs.readdirSync('./triggers/').forEach(function (file) {
// Load the module if it's a script.
if (path.extname(file) === '.js') {
if (file.includes('.disabled')) {
logger.info(`Did not load disabled trigger: ${file}`);
} else {
const moduleName = path.basename(file, '.js').toLowerCase();
logger.info(`Loaded trigger: ${moduleName} from ${file}`);
const executeModule = async () => {
try {
cachedTriggers.push(require(`./triggers/${file}`));
} catch (e) {
logger.error(`Could not load trigger ${moduleName}: ${e}`);
}
}
if (cachedModule) {
await cachedModule.command(message);
} else if (cachedModules.quote && quoteResponse) {
await cachedModules.quote.command(message, quoteResponse.reply);
}
} catch (err) { logger.error(err); }
};
const commandUsageEmbed = new discord.EmbedBuilder()
.setAuthor({ name: message.author.username, iconURL: message.author.displayAvatarURL() })
.setDescription(`Command used in ${message.channel.toString()} [Jump To Message](${message.url})`)
.addFields({ name: 'Command', value: `\`\`\`\n${message.content}\n\`\`\``, inline: false })
.setTimestamp()
.setColor('Blue');
const userInfo = `\`${message.author?.toString()}\` (${message.author?.username})`;
await Promise.all(
[
state.msglogChannel?.send({ content: userInfo, embeds: [commandUsageEmbed] }),
message.delete(),
executeModule()
]
);
} else if (!message.author.bot) {
// This is a normal channel message.
await Promise.all(
cachedTriggers.map(async function (trigger) {
if (!trigger.roles || (authorRoles && findArray(authorRoles, trigger.roles))) {
if (trigger.trigger(message)) {
logger.debug(`${message.author.username} ${message.author} [Channel: ${message.channel}] triggered: ${message.content}`);
try {
await trigger.execute(message);
} catch (err) { logger.error(err); }
}
}
})
);
}
});
@ -272,5 +253,5 @@ if (process.env.DATA_CUSTOM_RESPONSES) {
data.readCustomResponses();
}
client.login(process.env.DISCORD_LOGIN_TOKEN);
client.login(process.env.DISCORD_LOGIN_TOKEN).catch(err => logger.error(err));
logger.info('Startup completed. Established connection to Discord.');

View File

@ -1,7 +1,7 @@
import UserWarning from './models/UserWarning';
import UserBan from './models/UserBan';
import { IGameDBEntry, IResponses } from './models/interfaces';
import discord = require('discord.js');
import * as discord from 'discord.js';
/* Application State */
class State {

View File

@ -1,15 +1,22 @@
import { ban } from '../common';
import state from '../state';
import logger from '../logging';
import discord = require('discord.js');
import * as discord from 'discord.js';
export function trigger(message: discord.Message) {
return message.mentions.users.size > 10;
const ExemptRoles = ['Administrators', 'Moderators', 'Team', 'Developer', 'Support', 'VIP'];
export function trigger (message: discord.Message) {
return message.mentions.users.size > 10;
}
export function execute(message: discord.Message) {
const count = message.mentions.users.size;
logger.info(`${message.author.toString()} tagged ${count} users in ${message.channel.toString()}`);
state.logChannel?.send(`Ping bomb detected in ${message.channel.toString()} by ${message.author.toString()}`);
ban(message.author, message.author, message.guild);
};
export async function execute (message: discord.Message) {
const count = message.mentions.users.size;
const exempt = message.member?.roles?.cache.find(role => ExemptRoles.includes(role.name)) !== undefined;
logger.info(`${message.author.toString()} tagged ${count} users in ${message.channel.toString()}`);
await state.logChannel?.send(`Ping bomb detected in ${message.channel.toString()} by ${message.author.toString()}`);
if (exempt) {
await state.logChannel?.send(`... however ${message.author.toString()} is exempt from the banning rule.`);
} else {
await ban(message.author, message.author, message.guild);
}
}

View File

@ -1,16 +1,21 @@
{
"extends": "@tsconfig/node14/tsconfig.json",
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"module": "node16",
"noImplicitAny": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"removeComments": true,
"preserveConstEnums": true,
"outDir": "dist/",
"sourceMap": true,
"esModuleInterop": true,
"lib": [
"es2018",
"dom"
],
"typeRoots": [
"./typings",
"./node_modules/@types/"
]
},
"include": [

5
typings/checkenv/index.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module 'checkenv';
export function load(): string;
export function check(): void;
export function setConfig(config: any): void;

27
typings/logdna-winston/index.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
// Taken from https://github.com/efugulin/logdna-winston/blob/master/index.d.ts
import { ConstructorOptions } from 'logdna';
import Transport from 'winston-transport';
declare class LogDNATransport extends Transport {
constructor(options: LogDNATransport.TransportOptions);
}
declare namespace LogDNATransport {
interface TransportOptions
extends Transport.TransportStreamOptions,
ConstructorOptions {
/** The LogDNA API key. */
key: string;
/** The name of this transport (default: "LogDNA"). */
name?: string;
/** Level of messages that this transport should log (default: "debug"). */
level?: string;
/**
* Allow meta objects to be passed with each line (default: false).
* See logger ConstructorOptions for more information.
*/
index_meta?: boolean;
}
}
export = LogDNATransport;

6010
yarn.lock

File diff suppressed because it is too large Load Diff