Compare commits
124 Commits
use-parcel
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
601a2eb65a | ||
|
6e27ab6c91 | ||
|
31d188933b | ||
|
c5bf06fafe | ||
|
fde0b14b58 | ||
|
9e3e756adc | ||
|
a6e642d638 | ||
|
7fa5a6aa6f | ||
|
027bf47f0f | ||
|
9b204a914f | ||
|
df9d06313f | ||
|
81c50236c7 | ||
|
d6a4e978f7 | ||
|
cd44a47749 | ||
|
d3aa111ffc | ||
|
031c3741df | ||
|
9de48ffe3c | ||
|
0432fbe881 | ||
|
aa54213734 | ||
|
904c196a18 | ||
|
04ef587bc9 | ||
|
1e191894ca | ||
|
72639ee6ed | ||
|
e3c51adf20 | ||
|
35a3204866 | ||
|
659a323a60 | ||
|
55101858e7 | ||
|
abad013754 | ||
|
eac709db26 | ||
|
ab72a9c170 | ||
|
5c73b7b8db | ||
|
6a1d4e77d4 | ||
|
869070d149 | ||
|
b047824f59 | ||
|
27aba40ba2 | ||
|
d9c78786f1 | ||
|
ced9f5aaee | ||
|
875baa2805 | ||
|
14ee40bd25 | ||
|
43a2373f85 | ||
|
4cfed50b95 | ||
|
9887a29256 | ||
|
27359f87a7 | ||
|
9f0324706f | ||
|
f10457e5d0 | ||
|
51aa05660d | ||
|
d5bf77f4f1 | ||
|
9513c8626e | ||
|
173f996bb1 | ||
|
dd755b9b44 | ||
|
d0db0afd27 | ||
|
2e4f8fc288 | ||
|
7baa540663 | ||
|
c61fafdca0 | ||
|
ffb697c323 | ||
|
65b36aaa5d | ||
|
3a997d9b5c | ||
|
4c440dec41 | ||
|
bccd1e1c15 | ||
|
ffa3c72566 | ||
|
77bc0d5780 | ||
|
bd67f4a4c3 | ||
|
196ad81bcc | ||
|
199e000317 | ||
|
815ceb358f | ||
|
0d82c9a915 | ||
|
3b2c77ac34 | ||
|
bd9a9ef390 | ||
|
b89ebe1623 | ||
|
f9e5bedee4 | ||
|
32e725dd37 | ||
|
19fe3581fb | ||
|
34754878bb | ||
|
1ca72ceb00 | ||
|
718d5d54f0 | ||
|
4f8bf4574f | ||
|
e46432ac22 | ||
|
3ec6af6368 | ||
|
08abde2d7c | ||
|
9cfbf3ed1c | ||
|
c8c8c19c0c | ||
|
08f4aa24b4 | ||
|
c07f5b8546 | ||
|
9ac189a8af | ||
|
63223bf1c4 | ||
|
47dd6aacad | ||
|
941037a3e8 | ||
|
5cc3656f3b | ||
|
cbfea20517 | ||
|
0c3a42e5c7 | ||
|
10f80bf1bc | ||
|
a3c1b64ed8 | ||
|
8c4083fe24 | ||
|
92dd268da3 | ||
|
3311053f7f | ||
|
1f91f0f55d | ||
|
c48d5b23b2 | ||
|
33ec2bb74e | ||
|
ff398a97fa | ||
|
964647368b | ||
|
e2c7fa776a | ||
|
23e8e1a8eb | ||
|
9dce458bf0 | ||
|
280032fb1c | ||
|
0ab62ff4df | ||
|
0609f5d5f0 | ||
|
0e5f158f11 | ||
|
749bbdeb9d | ||
|
3e1b010cae | ||
|
29388049ac | ||
|
8c3e5261b5 | ||
|
dda1e3c64e | ||
|
b132f1d4be | ||
|
fc966347d8 | ||
|
e5f92d05da | ||
|
21d2c6f199 | ||
|
12a3b6e6b7 | ||
|
3f830cb6c5 | ||
|
2642ffc3fa | ||
|
bb2d38c0fc | ||
|
319479d594 | ||
|
16455f3d33 | ||
|
e2fb430042 | ||
|
39c8f5979c |
@ -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"]
|
||||
}
|
||||
}
|
16
.github/workflows/nodejs.yml
vendored
16
.github/workflows/nodejs.yml
vendored
@ -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
4
.gitignore
vendored
@ -51,5 +51,5 @@ config/development.json
|
||||
CMakeLists.txt.user*
|
||||
|
||||
/dist
|
||||
# Parcel's cache
|
||||
/.parcel-cache
|
||||
|
||||
_.ts
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "@parcel/config-default",
|
||||
"resolvers": ["@parcel/resolver-glob", "..."]
|
||||
}
|
11
Dockerfile
11
Dockerfile
@ -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
|
||||
|
||||
|
24
bundle.sh
24
bundle.sh
@ -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
|
||||
|
1
env.json
1
env.json
@ -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
42
generateExports.js
Normal 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);
|
59
package.json
59
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
@ -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)));
|
||||
}
|
||||
|
@ -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}].`);
|
||||
}));
|
||||
}
|
||||
|
@ -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] });
|
||||
}
|
||||
|
@ -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.`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
20
src/commands/grantTester.ts
Normal file
20
src/commands/grantTester.ts
Normal 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.`);
|
||||
});
|
||||
}));
|
||||
}
|
@ -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>'}`);
|
||||
}));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.');
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
@ -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.`);
|
||||
}));
|
||||
}
|
||||
|
@ -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...`);
|
||||
});
|
||||
}
|
||||
|
18
src/data.ts
18
src/data.ts
@ -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 () {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 you’ve 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 you’ve 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."}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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."}
|
||||
}
|
||||
}
|
||||
|
||||
|
221
src/server.ts
221
src/server.ts
@ -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.');
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
5
typings/checkenv/index.d.ts
vendored
Normal 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
27
typings/logdna-winston/index.d.ts
vendored
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user