mirror of
https://github.com/yuzu-emu/Command-fix.git
synced 2024-12-22 23:35:36 +00:00
meta: rewrite the bot in Typescript
This commit is contained in:
parent
f29653e6a8
commit
7d54dc1a78
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -49,3 +49,5 @@ config/development.json
|
|||
.vs/
|
||||
.vscode/
|
||||
CMakeLists.txt.user*
|
||||
|
||||
/dist
|
||||
|
|
20
Dockerfile
20
Dockerfile
|
@ -3,12 +3,20 @@ FROM mhart/alpine-node:latest
|
|||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
COPY package.json ./
|
||||
RUN yarn install
|
||||
# Install app dependencies and add source files
|
||||
COPY package.json yarn.lock tsconfig.json ./
|
||||
COPY src/ ./src
|
||||
RUN yarn install && yarn build && rm -f dist/*.map
|
||||
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
# Second stage
|
||||
FROM mhart/alpine-node:latest
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy artifacts
|
||||
COPY --from=0 /usr/src/app/dist/ ./
|
||||
COPY --from=0 /usr/src/app/node_modules ./node_modules
|
||||
COPY env.json ./
|
||||
|
||||
RUN addgroup -S app -g 50000 && \
|
||||
adduser -S -g app -u 50000 app && \
|
||||
|
@ -16,4 +24,4 @@ RUN addgroup -S app -g 50000 && \
|
|||
|
||||
USER app
|
||||
|
||||
ENTRYPOINT [ "node", "src/server.js" ]
|
||||
ENTRYPOINT [ "node", "server.js" ]
|
||||
|
|
10
README.md
10
README.md
|
@ -10,17 +10,17 @@ Invite Bot to your Server (see **Creating a Bot User** below).
|
|||
Make bot a Moderator.
|
||||
|
||||
# Install & Dependencies
|
||||
Install Node.js and NPM.
|
||||
Install Node.js and Yarn.
|
||||
|
||||
Install forever (task scheduler).
|
||||
```sh
|
||||
npm install -g forever
|
||||
yarn global add forever
|
||||
```
|
||||
Clone repository and install package dependencies.
|
||||
```sh
|
||||
git clone https://github.com/citra-emu/discord-bot.git
|
||||
cd discord-bot
|
||||
npm install
|
||||
yarn
|
||||
```
|
||||
Create new JSON file for bot config with the following contents in the directory specified below:
|
||||
|
||||
|
@ -48,11 +48,13 @@ Copy App Bot User token to `"clientLoginToken": ""`
|
|||
|
||||
##### For Production
|
||||
|
||||
First yo need to build the project by running `yarn build`.
|
||||
|
||||
`./start.sh` Requires a config/production.json file.
|
||||
|
||||
##### For Development
|
||||
|
||||
`node server.js` Requires a config/development.json file.
|
||||
`yarn serve` Requires a config/development.json file.
|
||||
|
||||
# License
|
||||
GNU General Public License v2.0
|
||||
|
|
39
package.json
39
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "citra-discord-bot",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Citra bot for Discord",
|
||||
"author": "chris062689 <chris062689@gmail.com>",
|
||||
"preferGlobal": true,
|
||||
|
@ -10,21 +10,32 @@
|
|||
"license": "GPL-2.0+",
|
||||
"dependencies": {
|
||||
"checkenv": "^1.2.2",
|
||||
"discord.js": "^11.3.0",
|
||||
"discord.js": "^12.2.0",
|
||||
"ip": "^1.1.5",
|
||||
"logdna": "^1.2.3",
|
||||
"node-schedule": "^1.2.3",
|
||||
"request": "^2.79.0",
|
||||
"request-promise-native": "^1.0.5",
|
||||
"string-similarity": "^1.2.0",
|
||||
"winston": "^2.3.0"
|
||||
"logdna": "^3.5.0",
|
||||
"logdna-winston": "^2.3.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-schedule": "^1.3.2",
|
||||
"string-similarity": "^4.0.1",
|
||||
"typescript": "^3.8.3",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^4.8.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-node": "^5.2.0",
|
||||
"eslint-plugin-promise": "^3.5.0",
|
||||
"eslint-plugin-standard": "^3.0.1"
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^13.13.4",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/string-similarity": "^3.0.0",
|
||||
"@types/ws": "^7.2.4",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"ts-node": "^8.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn run tsc",
|
||||
"serve": "yarn run ts-node ./src/server.ts"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
const state = require('../state.js');
|
||||
const data = require('../data.js');
|
||||
const logger = require('../logging.js');
|
||||
const UserBan = require('../models/UserBan.js');
|
||||
import state from '../state';
|
||||
import * as data from '../data';
|
||||
import logger from '../logging';
|
||||
import UserBan from '../models/UserBan';
|
||||
import discord = require('discord.js');
|
||||
|
||||
exports.roles = ['Admins', 'Moderators', 'CitraBot'];
|
||||
exports.command = function (message) {
|
||||
export const roles = ['Admins', 'Moderators', 'CitraBot'];
|
||||
export function command (message: discord.Message) {
|
||||
message.mentions.users.map((user) => {
|
||||
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
const state = require('../state.js');
|
||||
const data = require('../data.js');
|
||||
const logger = require('../logging.js');
|
||||
import state from '../state';
|
||||
import * as data from '../data';
|
||||
import logger from '../logging';
|
||||
import discord = require('discord.js');
|
||||
|
||||
exports.roles = ['Admins', 'Moderators'];
|
||||
exports.command = function (message) {
|
||||
export const roles = ['Admins', 'Moderators'];
|
||||
export function command (message: discord.Message) {
|
||||
message.mentions.users.map((user) => {
|
||||
const count = state.warnings.filter(x => x.id === user.id && !x.cleared);
|
||||
if (count != null && count.length > 0) {
|
||||
|
@ -14,7 +15,7 @@ exports.command = function (message) {
|
|||
message.channel.send(`${user}, you have no warnings to clear.`);
|
||||
}
|
||||
|
||||
logger.info(`${message.author.toString()} has cleared all warnings for ${user.toString()} [${count.length}].`);
|
||||
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}].`);
|
||||
});
|
||||
};
|
|
@ -1,99 +0,0 @@
|
|||
const request = require('request-promise-native');
|
||||
const discord = require('discord.js');
|
||||
const stringSimilarity = require('string-similarity');
|
||||
|
||||
const logger = require('../logging.js');
|
||||
const state = require('../state.js');
|
||||
|
||||
const targetServer = process.env.COMPAT_DB_SOURCE;
|
||||
const refreshTime = process.env.COMPAT_REFRESH_TIME ? parseInt(process.env.COMPAT_REFRESH_TIME) : 1000 * 60 * 20;
|
||||
const iconBase = process.env.COMPAT_ICON_BASE;
|
||||
const urlBase = process.env.COMPAT_URL_BASE;
|
||||
|
||||
const compatStrings = {
|
||||
0: { "key": "0", "name": "Perfect", "color": "#5c93ed", "description": "Game functions flawless with no audio or graphical glitches, all tested functionality works as intended without any workarounds needed." },
|
||||
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." },
|
||||
5: { "key": "5", "name": "Won't Boot", "color": "#828282", "description": "The game crashes when attempting to startup." },
|
||||
99: { "key": "99", "name": "Not Tested", "color": "black", "description": "The game has not yet been tested." }
|
||||
};
|
||||
|
||||
async function updateDatabase () {
|
||||
let body;
|
||||
try {
|
||||
body = await request(targetServer);
|
||||
} catch (e) {
|
||||
logger.error("Unable to download latest games list!");
|
||||
throw e;
|
||||
}
|
||||
|
||||
state.gameDB = JSON.parse(body).map(x => {
|
||||
return {
|
||||
directory: x.directory,
|
||||
title: x.title,
|
||||
compatibility: x.compatibility
|
||||
};
|
||||
});
|
||||
|
||||
state.lastGameDBUpdate = Date.now();
|
||||
logger.info(`Updated games list (${state.gameDB.length} games)`);
|
||||
|
||||
state.gameDBPromise = null;
|
||||
}
|
||||
|
||||
exports.command = async function (message) {
|
||||
if (Date.now() - state.lastGameDBUpdate > refreshTime) {
|
||||
// Update remote list of games locally.
|
||||
const waitMessage = message.channel.send("This will take a second...");
|
||||
|
||||
if (state.gameDBPromise == null) {
|
||||
state.gameDBPromise = updateDatabase(message);
|
||||
}
|
||||
|
||||
try {
|
||||
await state.gameDBPromise;
|
||||
} catch (e) {
|
||||
message.channel.send("Game compatibility feed temporarily unavailable.");
|
||||
throw e;
|
||||
} finally {
|
||||
// We don't need this message anymore
|
||||
waitMessage.then(waitMessageResult => waitMessageResult.delete());
|
||||
}
|
||||
}
|
||||
|
||||
const game = message.content.substr(message.content.indexOf(' ') + 1);
|
||||
|
||||
// Search all games. This is only linear time, so /shrug?
|
||||
let bestGame = undefined;
|
||||
let bestScore = 0.5; // Game names must have at least a 50% similarity to be matched
|
||||
|
||||
state.gameDB.forEach(testGame => {
|
||||
const newDistance = stringSimilarity.compareTwoStrings(game.toLowerCase(), testGame.title.toLowerCase());
|
||||
if (newDistance > bestScore) {
|
||||
bestGame = testGame;
|
||||
bestScore = newDistance;
|
||||
}
|
||||
});
|
||||
|
||||
if (bestGame === undefined) {
|
||||
message.channel.send("Game could not be found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const screenshot = `${iconBase}${bestGame.directory}.png`;
|
||||
const url = `${urlBase}${bestGame.directory}/`;
|
||||
|
||||
const compat = compatStrings[bestGame.compatibility];
|
||||
|
||||
const embed = new discord.RichEmbed()
|
||||
.addField("Status", compat.name, true)
|
||||
.setTitle(bestGame.title)
|
||||
.setColor(compat.color)
|
||||
.setDescription(compat.description)
|
||||
.setURL(url)
|
||||
.setThumbnail(screenshot);
|
||||
|
||||
message.channel.send(embed);
|
||||
};
|
101
src/commands/game.ts
Normal file
101
src/commands/game.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import fetch from 'node-fetch';
|
||||
import discord = require('discord.js');
|
||||
import stringSimilarity = require('string-similarity');
|
||||
|
||||
import logger from '../logging';
|
||||
import state from '../state';
|
||||
import { IGameDBEntry, ICompatList } from '../models/interfaces';
|
||||
|
||||
const targetServer = process.env.COMPAT_DB_SOURCE;
|
||||
const refreshTime = process.env.COMPAT_REFRESH_TIME ? parseInt(process.env.COMPAT_REFRESH_TIME) : 1000 * 60 * 20;
|
||||
const iconBase = process.env.COMPAT_ICON_BASE;
|
||||
const urlBase = process.env.COMPAT_URL_BASE;
|
||||
|
||||
const compatStrings: ICompatList = {
|
||||
0: { key: '0', name: 'Perfect', color: '#5c93ed', description: 'Game functions flawless with no audio or graphical glitches, all tested functionality works as intended without any workarounds needed.' },
|
||||
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.' },
|
||||
5: { key: '5', name: "Won't Boot", color: '#828282', description: 'The game crashes when attempting to startup.' },
|
||||
99: { key: '99', name: 'Not Tested', color: 'black', description: 'The game has not yet been tested.' }
|
||||
};
|
||||
|
||||
async function updateDatabase () {
|
||||
let body;
|
||||
try {
|
||||
let response = await fetch(targetServer);
|
||||
body = await response.json();
|
||||
} catch (e) {
|
||||
logger.error('Unable to download latest games list!');
|
||||
throw e;
|
||||
}
|
||||
|
||||
state.gameDB = body.map((x: IGameDBEntry) => {
|
||||
return {
|
||||
directory: x.directory,
|
||||
title: x.title,
|
||||
compatibility: x.compatibility
|
||||
};
|
||||
});
|
||||
|
||||
state.lastGameDBUpdate = Date.now();
|
||||
logger.info(`Updated games list (${state.gameDB.length} games)`);
|
||||
|
||||
state.gameDBPromise = null;
|
||||
}
|
||||
|
||||
export async function command (message: discord.Message) {
|
||||
if (Date.now() - state.lastGameDBUpdate > refreshTime) {
|
||||
// Update remote list of games locally.
|
||||
const waitMessage = message.channel.send('This will take a second...');
|
||||
|
||||
if (state.gameDBPromise == null) {
|
||||
state.gameDBPromise = updateDatabase();
|
||||
}
|
||||
|
||||
try {
|
||||
await state.gameDBPromise;
|
||||
} catch (e) {
|
||||
message.channel.send('Game compatibility feed temporarily unavailable.');
|
||||
throw e;
|
||||
} finally {
|
||||
// We don't need this message anymore
|
||||
waitMessage.then(waitMessageResult => waitMessageResult.delete());
|
||||
}
|
||||
}
|
||||
|
||||
const game = message.content.substr(message.content.indexOf(' ') + 1);
|
||||
|
||||
// Search all games. This is only linear time, so /shrug?
|
||||
let bestGame: IGameDBEntry;
|
||||
let bestScore = 0.5; // Game names must have at least a 50% similarity to be matched
|
||||
|
||||
state.gameDB.forEach(testGame => {
|
||||
const newDistance = stringSimilarity.compareTwoStrings(game.toLowerCase(), testGame.title.toLowerCase());
|
||||
if (newDistance > bestScore) {
|
||||
bestGame = testGame;
|
||||
bestScore = newDistance;
|
||||
}
|
||||
});
|
||||
|
||||
if (!bestGame) {
|
||||
message.channel.send('Game could not be found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const screenshot = `${iconBase}${bestGame.directory}.png`;
|
||||
const url = `${urlBase}${bestGame.directory}/`;
|
||||
|
||||
const compat = compatStrings[bestGame.compatibility];
|
||||
|
||||
const embed = new discord.MessageEmbed()
|
||||
.addField('Status', compat.name, true)
|
||||
.setTitle(bestGame.title)
|
||||
.setColor(compat.color)
|
||||
.setDescription(compat.description)
|
||||
.setURL(url)
|
||||
.setThumbnail(screenshot);
|
||||
|
||||
message.channel.send(embed);
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
exports.roles = ['Admins', 'Moderators', 'CitraBot'];
|
||||
exports.command = function (message) {
|
||||
const role = process.env.DISCORD_DEVELOPER_ROLE;
|
||||
message.mentions.users.map((user) => {
|
||||
let member = message.guild.member(user);
|
||||
let alreadyJoined = member.roles.has(role);
|
||||
|
||||
if (alreadyJoined) {
|
||||
member.removeRole(role);
|
||||
message.channel.send(`${user}'s speech has been revoked in the #development channel.`);
|
||||
} else {
|
||||
member.addRole(role);
|
||||
message.channel.send(`${user} has been granted speech in the #development channel.`);
|
||||
}
|
||||
});
|
||||
};
|
18
src/commands/grantDeveloper.ts
Normal file
18
src/commands/grantDeveloper.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import discord = require('discord.js');
|
||||
|
||||
export const roles = ['Admins', 'Moderators', 'CitraBot'];
|
||||
export function command (message: discord.Message) {
|
||||
const role = process.env.DISCORD_DEVELOPER_ROLE;
|
||||
message.mentions.users.map((user) => {
|
||||
const member = message.guild.member(user);
|
||||
const alreadyJoined = member.roles.cache.has(role);
|
||||
|
||||
if (alreadyJoined) {
|
||||
member.roles.remove(role);
|
||||
message.channel.send(`${user.toString()}'s speech has been revoked in the #development channel.`);
|
||||
} else {
|
||||
member.roles.remove(role);
|
||||
message.channel.send(`${user.toString()} has been granted speech in the #development channel.`);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
const state = require('../state.js');
|
||||
exports.roles = ['Admins', 'Moderators'];
|
||||
|
||||
function formatWarnings(warnings) {
|
||||
return warnings.map(x => `[${x.date}] ${x.warnedByUsername} warned ${x.username} [${x.priorWarnings} + 1]. ${x.silent ? '(silent)' : ''} ${x.cleared ? '(cleared)' : ''}`)
|
||||
}
|
||||
|
||||
function formatBans(bans) {
|
||||
return bans.map(x => `[${x.date}] ${x.warnedByUsername} banned ${x.username} [${x.priorWarnings} + 1].`)
|
||||
}
|
||||
|
||||
exports.command = function (message) {
|
||||
message.mentions.users.map((user) => {
|
||||
const totalWarnings = state.warnings.filter(x => x.id === user.id && x.cleared == false).length;
|
||||
let warns = state.warnings.filter(x => x.id == user.id)
|
||||
let bans = state.bans.filter(x => x.id == user.id)
|
||||
|
||||
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 : ''}${bans.length != 0 ? bansString : ''}`)
|
||||
});
|
||||
}
|
27
src/commands/info.ts
Normal file
27
src/commands/info.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import state from '../state';
|
||||
import UserBan from '../models/UserBan';
|
||||
import UserWarning from '../models/UserWarning';
|
||||
import discord = require('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)' : ''}`);
|
||||
}
|
||||
|
||||
function formatBans (bans: UserBan[]) {
|
||||
return bans.map(x => `[${x.date}] ${x.warnedByUsername} banned ${x.username} [${x.priorWarnings} + 1].`);
|
||||
}
|
||||
|
||||
export function command (message: discord.Message) {
|
||||
message.mentions.users.map((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);
|
||||
|
||||
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 : ''}${bans.length !== 0 ? bansString : ''}`);
|
||||
});
|
||||
};
|
|
@ -1,5 +1,7 @@
|
|||
exports.roles = ['Admins', 'Moderators'];
|
||||
exports.command = function (message, reply) {
|
||||
import discord = require('discord.js');
|
||||
|
||||
export const roles = ['Admins', 'Moderators'];
|
||||
export function command (message: discord.Message, reply: string) {
|
||||
let replyMessage = 'Hello.';
|
||||
if (reply == null) {
|
||||
replyMessage = message.content.substr(message.content.indexOf(' ') + 1);
|
||||
|
@ -8,4 +10,4 @@ exports.command = function (message, reply) {
|
|||
}
|
||||
|
||||
message.channel.send(replyMessage);
|
||||
};
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
const request = require('request');
|
||||
|
||||
exports.roles = ['Admins', 'Moderators', 'Developers'];
|
||||
exports.command = function (message) {
|
||||
let pr = message.content.substr(message.content.indexOf(' ') + 1).replace(/\n/g, '');
|
||||
|
||||
let repo = process.env.GITHUB_REPOSITORY || "citra-emu/citra";
|
||||
let url = `https://api.github.com/repos/${repo}/pulls/${pr}`;
|
||||
|
||||
request({ url: url, headers: { 'User-Agent': 'Citra-Emu/CitraBot (Node.js)' } }, function (error, response, body) {
|
||||
if (!error) {
|
||||
const pr = JSON.parse(body);
|
||||
request({ url: pr.statuses_url, headers: { 'User-Agent': 'Citra-Emu/CitraBot (Node.js)' } }, function (error, response, body) {
|
||||
const statuses = JSON.parse(body);
|
||||
|
||||
if (statuses.length === 0) return;
|
||||
|
||||
// Travis CI will give you multiple, identical target URLs so we might as well just check the first one...
|
||||
const status = statuses[0];
|
||||
status.target_url = status.target_url.substr(0, status.target_url.indexOf('?'));
|
||||
message.channel.sendMessage(`${status.context}: ${status.target_url}: **${status.state}**`);
|
||||
});
|
||||
} else {
|
||||
message.channel.sendMessage('No such PR.');
|
||||
}
|
||||
});
|
||||
};
|
27
src/commands/status.ts
Normal file
27
src/commands/status.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import fetch from 'node-fetch';
|
||||
import discord = require('discord.js');
|
||||
|
||||
const fetchOptions = {
|
||||
headers: { 'User-Agent': 'Citra-Emu/CitraBot (Node.js)' }
|
||||
};
|
||||
const repo = process.env.GITHUB_REPOSITORY || 'citra-emu/citra';
|
||||
|
||||
export const roles = ['Admins', 'Moderators', 'Developers'];
|
||||
export function command(message: discord.Message) {
|
||||
const pr = message.content.substr(message.content.indexOf(' ') + 1).replace(/\n/g, '');
|
||||
const url = `https://api.github.com/repos/${repo}/pulls/${pr}`;
|
||||
fetch(url, fetchOptions).then(response => response.json()).then(pr => {
|
||||
if (pr.documentation_url) throw new Error('PR not found');
|
||||
fetch(pr.statuses_url, fetchOptions).then(response => response.json()).then(statuses => {
|
||||
if (statuses.length === 0) return;
|
||||
// Travis CI will give you multiple, identical target URLs so we might as well just check the first one...
|
||||
const status = statuses[0];
|
||||
status.target_url = status.target_url.substr(0, status.target_url.indexOf('?'));
|
||||
message.channel.send(`${status.context}: ${status.target_url}: **${status.state}**`);
|
||||
}).catch(() => {
|
||||
message.channel.send('I wasn\'t able to get the status of that PR...')
|
||||
});
|
||||
}).catch(() => {
|
||||
message.channel.send('No such PR.');
|
||||
});
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
const state = require('../state.js');
|
||||
const data = require('../data.js');
|
||||
const logger = require('../logging.js');
|
||||
const UserWarning = require('../models/UserWarning.js');
|
||||
|
||||
exports.roles = ['Admins', 'Moderators'];
|
||||
exports.command = function (message) {
|
||||
const silent = message.content.includes('silent')
|
||||
|
||||
message.mentions.users.map((user) => {
|
||||
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
|
||||
|
||||
if (silent == false) {
|
||||
message.channel.send(`${user} 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} has warned ${user} [${count} + 1].`);
|
||||
|
||||
state.warnings.push(new UserWarning(user.id, user.username, message.author.id, message.author.username, count, silent));
|
||||
data.flushWarnings();
|
||||
});
|
||||
};
|
24
src/commands/warn.ts
Normal file
24
src/commands/warn.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import state from '../state';
|
||||
import * as data from '../data';
|
||||
import logger from '../logging';
|
||||
import UserWarning from '../models/UserWarning';
|
||||
import discord = require('discord.js');
|
||||
|
||||
exports.roles = ['Admins', 'Moderators'];
|
||||
exports.command = function (message: discord.Message) {
|
||||
const silent = message.content.includes('silent');
|
||||
|
||||
message.mentions.users.map((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.`);
|
||||
}
|
||||
|
||||
logger.info(`${message.author.username} ${message.author} has warned ${user.username} ${user} [${count} + 1].`);
|
||||
state.logChannel.send(`${message.author.toString()} has warned ${user.toString()} [${count} + 1].`);
|
||||
|
||||
state.warnings.push(new UserWarning(user.id, user.username, message.author.id, message.author.username, count, silent));
|
||||
data.flushWarnings();
|
||||
});
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
const state = require('../state.js');
|
||||
import state from '../state';
|
||||
import discord = require('discord.js');
|
||||
|
||||
exports.command = function (message) {
|
||||
exports.command = function (message: discord.Message) {
|
||||
message.mentions.users.map((user) => {
|
||||
const warnings = state.warnings.filter(x => x.id === user.id && !x.cleared);
|
||||
message.channel.send(`${user}, you have ${warnings.length} total warnings.`);
|
|
@ -1,8 +1,8 @@
|
|||
const fs = require('fs');
|
||||
const state = require('./state.js');
|
||||
const logger = require('./logging.js');
|
||||
import * as fs from 'fs';
|
||||
import state from './state';
|
||||
import logger from './logging';
|
||||
|
||||
function readWarnings () {
|
||||
export function readWarnings () {
|
||||
// Load the warnings file into the application state.
|
||||
const readFilePath = '/data/discordWarnings.json';
|
||||
fs.readFile(readFilePath, 'utf8', function (err, data) {
|
||||
|
@ -16,7 +16,7 @@ function readWarnings () {
|
|||
});
|
||||
}
|
||||
|
||||
function readBans () {
|
||||
export function readBans () {
|
||||
// Load the ban file into the application state.
|
||||
const readFilePath = '/data/discordBans.json';
|
||||
fs.readFile(readFilePath, 'utf8', function (err, data) {
|
||||
|
@ -30,24 +30,22 @@ function readBans () {
|
|||
});
|
||||
}
|
||||
|
||||
function readCustomResponses () {
|
||||
export function readCustomResponses () {
|
||||
// Load the responses file into the responses variable.
|
||||
state.responses = require(`./responses/${process.env.TENANT}.json`);
|
||||
logger.debug(`Loaded responses file for ${process.env.TENANT} from external source.`);
|
||||
}
|
||||
|
||||
function flushWarnings () {
|
||||
export function flushWarnings () {
|
||||
const warningsJson = JSON.stringify(state.warnings, null, 4);
|
||||
fs.writeFile('/data/discordWarnings.json', warningsJson, 'utf8', function (err) {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
}
|
||||
|
||||
function flushBans () {
|
||||
export function flushBans () {
|
||||
const bansJson = JSON.stringify(state.bans, null, 4);
|
||||
fs.writeFile('/data/discordBans.json', bansJson, 'utf8', function (err) {
|
||||
if (err) { throw err; }
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { readWarnings: readWarnings, readBans: readBans, readCustomResponses: readCustomResponses, flushWarnings: flushWarnings, flushBans: flushBans };
|
|
@ -1,35 +1,36 @@
|
|||
const winston = require('winston');
|
||||
const ip = require('ip');
|
||||
const os = require('os');
|
||||
import winston = require('winston');
|
||||
import * as ip from 'ip';
|
||||
import * as os from 'os';
|
||||
|
||||
winston.emitErrs = true;
|
||||
|
||||
const logger = new winston.Logger({
|
||||
const logger = winston.createLogger({
|
||||
level: 'debug',
|
||||
transports: [
|
||||
new (winston.transports.Console)()
|
||||
new (winston.transports.Console)({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
),
|
||||
handleExceptions: true
|
||||
})
|
||||
],
|
||||
handleExceptions: true,
|
||||
humanReadableUnhandledException: true,
|
||||
exitOnError: false,
|
||||
meta: true
|
||||
});
|
||||
|
||||
// Setup logging for LogDNA cloud logging.
|
||||
if (process.env.LOGDNA_API_KEY) {
|
||||
require('logdna');
|
||||
const logdnaWinston = require('logdna-winston');
|
||||
const logLevel = process.env.LOGDNA_LEVEL || 'info';
|
||||
|
||||
logger.add(winston.transports.Logdna, {
|
||||
logger.add(new logdnaWinston({
|
||||
level: logLevel,
|
||||
app: process.env.LOGDNA_APPNAME,
|
||||
index_meta: true,
|
||||
key: process.env.LOGDNA_API_KEY,
|
||||
ip: ip.address(),
|
||||
hostname: os.hostname()
|
||||
});
|
||||
}));
|
||||
|
||||
logger.info(`[core] Started LogDNA winston transport. Running at log level ${logLevel}.`);
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
export default logger;
|
|
@ -1,12 +0,0 @@
|
|||
class UserBan {
|
||||
constructor (id, username, warnedBy, warnedByUsername, priorWarnings) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.date = new Date();
|
||||
this.warnedBy = warnedBy;
|
||||
this.warnedByUsername = warnedByUsername;
|
||||
this.priorWarnings = priorWarnings;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserBan;
|
19
src/models/UserBan.ts
Normal file
19
src/models/UserBan.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
class UserBan {
|
||||
id: string;
|
||||
username: string;
|
||||
date: Date;
|
||||
warnedBy: string;
|
||||
warnedByUsername: string;
|
||||
priorWarnings: number;
|
||||
|
||||
constructor (id: string, username: string, warnedBy: string, warnedByUsername: string, priorWarnings: number) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.date = new Date();
|
||||
this.warnedBy = warnedBy;
|
||||
this.warnedByUsername = warnedByUsername;
|
||||
this.priorWarnings = priorWarnings;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserBan;
|
|
@ -1,13 +0,0 @@
|
|||
class UserWarning {
|
||||
constructor (id, username, warnedBy, warnedByUsername, priorWarnings, silent) {
|
||||
this.id = id
|
||||
this.username = username
|
||||
this.date = new Date()
|
||||
this.warnedBy = warnedBy
|
||||
this.warnedByUsername = warnedByUsername
|
||||
this.priorWarnings = priorWarnings
|
||||
this.silent = silent
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserWarning;
|
23
src/models/UserWarning.ts
Normal file
23
src/models/UserWarning.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
class UserWarning {
|
||||
id: string;
|
||||
username: string;
|
||||
date: Date;
|
||||
warnedBy: string;
|
||||
warnedByUsername: string;
|
||||
priorWarnings: number;
|
||||
silent: boolean;
|
||||
cleared: boolean;
|
||||
|
||||
constructor (id: string, username: string, warnedBy: string, warnedByUsername: string, priorWarnings: number, silent: boolean) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.date = new Date();
|
||||
this.warnedBy = warnedBy;
|
||||
this.warnedByUsername = warnedByUsername;
|
||||
this.priorWarnings = priorWarnings;
|
||||
this.silent = silent;
|
||||
this.cleared = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserWarning;
|
14
src/models/interfaces.ts
Normal file
14
src/models/interfaces.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export interface IGameDBEntry {
|
||||
directory: string;
|
||||
title: string;
|
||||
compatibility: number;
|
||||
}
|
||||
|
||||
export interface ICompatList {
|
||||
[key: number]: {
|
||||
key: string,
|
||||
name: string,
|
||||
color: string,
|
||||
description: string
|
||||
}
|
||||
}
|
|
@ -1,50 +1,48 @@
|
|||
// Check for environmental variables.
|
||||
require('checkenv').check();
|
||||
|
||||
const discord = require('discord.js');
|
||||
const path = require('path');
|
||||
const schedule = require('node-schedule');
|
||||
const fs = require('fs');
|
||||
import discord = require('discord.js');
|
||||
import path = require('path');
|
||||
// const schedule = require('node-schedule');
|
||||
import fs = require('fs');
|
||||
|
||||
const logger = require('./logging.js');
|
||||
const state = require('./state.js');
|
||||
const data = require('./data.js');
|
||||
import logger from './logging';
|
||||
import state from './state';
|
||||
import * as data from './data';
|
||||
|
||||
state.responses = require('./responses.json');
|
||||
|
||||
let cachedModules = [];
|
||||
let cachedTriggers = [];
|
||||
interface IModuleMap {
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
let cachedModules: IModuleMap = {};
|
||||
let cachedTriggers: any[] = [];
|
||||
const client = new discord.Client();
|
||||
|
||||
let mediaUsers = new Map();
|
||||
const mediaUsers = new Map();
|
||||
|
||||
logger.info('Application startup. Configuring environment.');
|
||||
|
||||
process.on('unhandledRejection', (error, promise) => {
|
||||
logger.error(`Unhandled promise rejection: ${error.message}.`, { meta: error });
|
||||
});
|
||||
|
||||
process.on('uncaughtException', error => {
|
||||
logger.error(`Unhandled exception: ${error.message}.`, { meta: error });
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
function findArray (haystack, arr) {
|
||||
return arr.some(function (v) {
|
||||
function findArray(haystack: string | any[], arr: any[]) {
|
||||
return arr.some(function (v: any) {
|
||||
return haystack.indexOf(v) >= 0;
|
||||
});
|
||||
}
|
||||
|
||||
function IsIgnoredCategory (categoryName) {
|
||||
function IsIgnoredCategory(categoryName: string) {
|
||||
const IgnoredCategory = ['welcome', 'team', 'website-team'];
|
||||
return IgnoredCategory.includes(categoryName);
|
||||
}
|
||||
|
||||
client.on('ready', () => {
|
||||
client.on('ready', async () => {
|
||||
// Initialize app channels.
|
||||
state.logChannel = client.channels.get(process.env.DISCORD_LOG_CHANNEL);
|
||||
state.msglogChannel = client.channels.get(process.env.DISCORD_MSGLOG_CHANNEL);
|
||||
state.guild = state.logChannel.guild;
|
||||
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;
|
||||
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;
|
||||
state.msglogChannel = msglogChannel;
|
||||
|
||||
logger.info('Bot is now online and connected to server.');
|
||||
});
|
||||
|
@ -63,19 +61,17 @@ client.on('debug', (x) => null);
|
|||
client.on('disconnect', () => {
|
||||
logger.warn('Disconnected from Discord server.');
|
||||
});
|
||||
client.on('reconnecting', () => {
|
||||
logger.warn('Reconnecting...');
|
||||
});
|
||||
|
||||
client.on('guildMemberAdd', (member) => {
|
||||
member.addRole(process.env.DISCORD_RULES_ROLE);
|
||||
member.roles.add(process.env.DISCORD_RULES_ROLE);
|
||||
});
|
||||
|
||||
client.on('messageDelete', message => {
|
||||
if (IsIgnoredCategory(message.channel.parent.name) == false) {
|
||||
if (message.content && message.content.startsWith('.') == false && message.author.bot == false) {
|
||||
const deletionEmbed = new discord.RichEmbed()
|
||||
.setAuthor(message.author.tag, message.author.displayAvatarURL)
|
||||
let parent = (message.channel as discord.TextChannel).parent;
|
||||
if (parent && IsIgnoredCategory(parent.name) === false) {
|
||||
if (message.content && message.content.startsWith('.') === false && message.author.bot === false) {
|
||||
const deletionEmbed = new discord.MessageEmbed()
|
||||
.setAuthor(message.author.tag, message.author.displayAvatarURL())
|
||||
.setDescription(`Message deleted in ${message.channel}`)
|
||||
.addField('Content', message.cleanContent, false)
|
||||
.setTimestamp()
|
||||
|
@ -89,13 +85,14 @@ client.on('messageDelete', message => {
|
|||
|
||||
client.on('messageUpdate', (oldMessage, newMessage) => {
|
||||
const AllowedRoles = ['Administrators', 'Moderators', 'Team', 'VIP'];
|
||||
if (!findArray(oldMessage.member.roles.map(function (x) { return x.name; }), AllowedRoles)) {
|
||||
if (IsIgnoredCategory(oldMessage.channel.parent.name) == false) {
|
||||
if (!findArray(oldMessage.member.roles.cache.map(x => x.name), AllowedRoles)) {
|
||||
let parent = (oldMessage.channel as discord.TextChannel).parent;
|
||||
if (parent && IsIgnoredCategory(parent.name) === false) {
|
||||
const oldM = oldMessage.cleanContent;
|
||||
const newM = newMessage.cleanContent;
|
||||
if (oldMessage.content != newMessage.content && oldM && newM) {
|
||||
const editedEmbed = new discord.RichEmbed()
|
||||
.setAuthor(oldMessage.author.tag, oldMessage.author.displayAvatarURL)
|
||||
if (oldMessage.content !== newMessage.content && oldM && newM) {
|
||||
const editedEmbed = new discord.MessageEmbed()
|
||||
.setAuthor(oldMessage.author.tag, oldMessage.author.displayAvatarURL())
|
||||
.setDescription(`Message edited in ${oldMessage.channel} [Jump To Message](${newMessage.url})`)
|
||||
.addField('Before', oldM, false)
|
||||
.addField('After', newM, false)
|
||||
|
@ -120,11 +117,11 @@ client.on('message', message => {
|
|||
return;
|
||||
}
|
||||
|
||||
logger.verbose(`${message.author.username} ${message.author} [Channel: ${message.channel.name} ${message.channel}]: ${message.content}`);
|
||||
logger.verbose(`${message.author.username} ${message.author} [Channel: ${(message.channel as discord.TextChannel).name} ${message.channel}]: ${message.content}`);
|
||||
|
||||
if (message.channel.id === process.env.DISCORD_MEDIA_CHANNEL && !message.author.bot) {
|
||||
const AllowedMediaRoles = ['Administrators', 'Moderators', 'Team', 'VIP'];
|
||||
if (!findArray(message.member.roles.map(function (x) { return x.name; }), AllowedMediaRoles)) {
|
||||
if (!findArray(message.member.roles.cache.map(x => x.name), AllowedMediaRoles)) {
|
||||
const urlRegex = new RegExp(/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);
|
||||
|
@ -142,15 +139,14 @@ client.on('message', message => {
|
|||
if (message.content.toLowerCase().includes(process.env.DISCORD_RULES_TRIGGER)) {
|
||||
// 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.removeRole(process.env.DISCORD_RULES_ROLE, 'Accepted the rules.');
|
||||
message.member.roles.remove(process.env.DISCORD_RULES_ROLE, 'Accepted the rules.');
|
||||
}
|
||||
|
||||
// Delete the message in the channel to force a cleanup.
|
||||
message.delete();
|
||||
return;
|
||||
} else if (message.content.startsWith('.') && message.content.startsWith('..') === false) {
|
||||
// We want to make sure it's an actual command, not someone '...'-ing.
|
||||
let cmd = message.content.split(' ')[0].slice(1);
|
||||
const cmd = message.content.split(' ', 1)[0].slice(1);
|
||||
|
||||
// Check by the name of the command.
|
||||
let cachedModule = cachedModules[`${cmd}.js`];
|
||||
|
@ -158,9 +154,10 @@ client.on('message', message => {
|
|||
// Check by the quotes in the configuration.
|
||||
if (cachedModule == null) { cachedModule = state.responses.quotes[cmd]; cachedModuleType = 'Quote'; }
|
||||
|
||||
if (cachedModule) {
|
||||
if (!cachedModule) return; // Not a valid command.
|
||||
|
||||
// Check access permissions.
|
||||
if (cachedModule.roles !== undefined && findArray(message.member.roles.map(function (x) { return x.name; }), cachedModule.roles) === false) {
|
||||
if (cachedModule.roles !== undefined && findArray(message.member.roles.cache.map(x => x.name), cachedModule.roles) === false) {
|
||||
state.logChannel.send(`${message.author} attempted to use admin command: ${message.content}`);
|
||||
logger.info(`${message.author.username} ${message.author} attempted to use admin command: ${message.content}`);
|
||||
return false;
|
||||
|
@ -182,20 +179,18 @@ client.on('message', message => {
|
|||
// Check if the command requires a warning.
|
||||
if (cmd !== 'warn' && cachedModule.warn === true) {
|
||||
// Access check to see if the user has privileges to warn.
|
||||
let warnCommand = cachedModules['warn.js'];
|
||||
if (findArray(message.member.roles.map(function (x) { return x.name; }), warnCommand.roles)) {
|
||||
const warnCommand = cachedModules['warn.js'];
|
||||
if (findArray(message.member.roles.cache.map(x => x.name), warnCommand.roles)) {
|
||||
// They are allowed to warn because they are in warn's roles.
|
||||
warnCommand.command(message);
|
||||
}
|
||||
}
|
||||
} catch (err) { logger.error(err); }
|
||||
} else {
|
||||
// Not a valid command.
|
||||
}
|
||||
|
||||
} else if (message.author.bot === false) {
|
||||
// This is a normal channel message.
|
||||
cachedTriggers.forEach(function (trigger) {
|
||||
if (trigger.roles === undefined || findArray(message.member.roles.map(function (x) { return x.name; }), trigger.roles)) {
|
||||
if (trigger.roles === undefined || findArray(message.member.roles.cache.map(x => x.name), trigger.roles)) {
|
||||
if (trigger.trigger(message) === true) {
|
||||
logger.debug(`${message.author.username} ${message.author} [Channel: ${message.channel}] triggered: ${message.content}`);
|
||||
try {
|
||||
|
@ -208,8 +203,8 @@ client.on('message', message => {
|
|||
});
|
||||
|
||||
// Cache all command modules.
|
||||
cachedModules = [];
|
||||
fs.readdirSync('./src/commands/').forEach(function (file) {
|
||||
cachedModules = {};
|
||||
fs.readdirSync('./commands/').forEach(function (file) {
|
||||
// Load the module if it's a script.
|
||||
if (path.extname(file) === '.js') {
|
||||
if (file.includes('.disabled')) {
|
20
src/state.js
20
src/state.js
|
@ -1,20 +0,0 @@
|
|||
/* Application State */
|
||||
const State = function () {
|
||||
this.guild = null;
|
||||
this.logChannel = null;
|
||||
this.msglogChannel = null;
|
||||
this.warnings = [];
|
||||
this.responses = null;
|
||||
this.bans = [];
|
||||
this.stats = {
|
||||
joins: 0,
|
||||
ruleAccepts: 0,
|
||||
leaves: 0,
|
||||
warnings: 0
|
||||
};
|
||||
this.lastGameDBUpdate = 0;
|
||||
this.gameDB = [];
|
||||
this.gameDBPromise = null;
|
||||
};
|
||||
|
||||
module.exports = new State();
|
36
src/state.ts
Normal file
36
src/state.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import UserWarning from './models/UserWarning';
|
||||
import UserBan from './models/UserBan';
|
||||
import { IGameDBEntry } from './models/interfaces';
|
||||
import discord = require('discord.js');
|
||||
|
||||
/* Application State */
|
||||
class State {
|
||||
logChannel: discord.TextChannel | discord.DMChannel;
|
||||
msglogChannel: discord.TextChannel | discord.DMChannel;
|
||||
warnings: UserWarning[];
|
||||
responses: any;
|
||||
bans: UserBan[];
|
||||
stats: { joins: number; ruleAccepts: number; leaves: number; warnings: number; };
|
||||
lastGameDBUpdate: number;
|
||||
gameDB: IGameDBEntry[];
|
||||
gameDBPromise: Promise<void>;
|
||||
|
||||
constructor () {
|
||||
this.logChannel = null;
|
||||
this.msglogChannel = null;
|
||||
this.warnings = [];
|
||||
this.responses = null;
|
||||
this.bans = [];
|
||||
this.stats = {
|
||||
joins: 0,
|
||||
ruleAccepts: 0,
|
||||
leaves: 0,
|
||||
warnings: 0
|
||||
};
|
||||
this.lastGameDBUpdate = 0;
|
||||
this.gameDB = [];
|
||||
this.gameDBPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default new State();
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"noImplicitAny": true,
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"outDir": "dist/",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue