mirror of
https://github.com/yuzu-emu/Command-fix.git
synced 2024-11-22 09:45:39 +01: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/
|
.vs/
|
||||||
.vscode/
|
.vscode/
|
||||||
CMakeLists.txt.user*
|
CMakeLists.txt.user*
|
||||||
|
|
||||||
|
/dist
|
||||||
|
20
Dockerfile
20
Dockerfile
@ -3,12 +3,20 @@ FROM mhart/alpine-node:latest
|
|||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
# Install app dependencies
|
# Install app dependencies and add source files
|
||||||
COPY package.json ./
|
COPY package.json yarn.lock tsconfig.json ./
|
||||||
RUN yarn install
|
COPY src/ ./src
|
||||||
|
RUN yarn install && yarn build && rm -f dist/*.map
|
||||||
|
|
||||||
# Bundle app source
|
# Second stage
|
||||||
COPY . .
|
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 && \
|
RUN addgroup -S app -g 50000 && \
|
||||||
adduser -S -g app -u 50000 app && \
|
adduser -S -g app -u 50000 app && \
|
||||||
@ -16,4 +24,4 @@ RUN addgroup -S app -g 50000 && \
|
|||||||
|
|
||||||
USER app
|
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.
|
Make bot a Moderator.
|
||||||
|
|
||||||
# Install & Dependencies
|
# Install & Dependencies
|
||||||
Install Node.js and NPM.
|
Install Node.js and Yarn.
|
||||||
|
|
||||||
Install forever (task scheduler).
|
Install forever (task scheduler).
|
||||||
```sh
|
```sh
|
||||||
npm install -g forever
|
yarn global add forever
|
||||||
```
|
```
|
||||||
Clone repository and install package dependencies.
|
Clone repository and install package dependencies.
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/citra-emu/discord-bot.git
|
git clone https://github.com/citra-emu/discord-bot.git
|
||||||
cd discord-bot
|
cd discord-bot
|
||||||
npm install
|
yarn
|
||||||
```
|
```
|
||||||
Create new JSON file for bot config with the following contents in the directory specified below:
|
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
|
##### For Production
|
||||||
|
|
||||||
|
First yo need to build the project by running `yarn build`.
|
||||||
|
|
||||||
`./start.sh` Requires a config/production.json file.
|
`./start.sh` Requires a config/production.json file.
|
||||||
|
|
||||||
##### For Development
|
##### For Development
|
||||||
|
|
||||||
`node server.js` Requires a config/development.json file.
|
`yarn serve` Requires a config/development.json file.
|
||||||
|
|
||||||
# License
|
# License
|
||||||
GNU General Public License v2.0
|
GNU General Public License v2.0
|
||||||
|
39
package.json
39
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "citra-discord-bot",
|
"name": "citra-discord-bot",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"description": "Citra bot for Discord",
|
"description": "Citra bot for Discord",
|
||||||
"author": "chris062689 <chris062689@gmail.com>",
|
"author": "chris062689 <chris062689@gmail.com>",
|
||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
@ -10,21 +10,32 @@
|
|||||||
"license": "GPL-2.0+",
|
"license": "GPL-2.0+",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"checkenv": "^1.2.2",
|
"checkenv": "^1.2.2",
|
||||||
"discord.js": "^11.3.0",
|
"discord.js": "^12.2.0",
|
||||||
"ip": "^1.1.5",
|
"ip": "^1.1.5",
|
||||||
"logdna": "^1.2.3",
|
"logdna": "^3.5.0",
|
||||||
"node-schedule": "^1.2.3",
|
"logdna-winston": "^2.3.1",
|
||||||
"request": "^2.79.0",
|
"node-fetch": "^2.6.0",
|
||||||
"request-promise-native": "^1.0.5",
|
"node-schedule": "^1.3.2",
|
||||||
"string-similarity": "^1.2.0",
|
"string-similarity": "^4.0.1",
|
||||||
"winston": "^2.3.0"
|
"typescript": "^3.8.3",
|
||||||
|
"winston": "^3.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^4.8.0",
|
"@types/ip": "^1.1.0",
|
||||||
"eslint-config-standard": "^10.2.1",
|
"@types/node": "^13.13.4",
|
||||||
"eslint-plugin-import": "^2.7.0",
|
"@types/node-fetch": "^2.5.7",
|
||||||
"eslint-plugin-node": "^5.2.0",
|
"@types/string-similarity": "^3.0.0",
|
||||||
"eslint-plugin-promise": "^3.5.0",
|
"@types/ws": "^7.2.4",
|
||||||
"eslint-plugin-standard": "^3.0.1"
|
"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');
|
import state from '../state';
|
||||||
const data = require('../data.js');
|
import * as data from '../data';
|
||||||
const logger = require('../logging.js');
|
import logger from '../logging';
|
||||||
const UserBan = require('../models/UserBan.js');
|
import UserBan from '../models/UserBan';
|
||||||
|
import discord = require('discord.js');
|
||||||
|
|
||||||
exports.roles = ['Admins', 'Moderators', 'CitraBot'];
|
export const roles = ['Admins', 'Moderators', 'CitraBot'];
|
||||||
exports.command = function (message) {
|
export function command (message: discord.Message) {
|
||||||
message.mentions.users.map((user) => {
|
message.mentions.users.map((user) => {
|
||||||
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
|
const count = state.warnings.filter(x => x.id === user.id && !x.cleared).length || 0;
|
||||||
|
|
@ -1,9 +1,10 @@
|
|||||||
const state = require('../state.js');
|
import state from '../state';
|
||||||
const data = require('../data.js');
|
import * as data from '../data';
|
||||||
const logger = require('../logging.js');
|
import logger from '../logging';
|
||||||
|
import discord = require('discord.js');
|
||||||
|
|
||||||
exports.roles = ['Admins', 'Moderators'];
|
export const roles = ['Admins', 'Moderators'];
|
||||||
exports.command = function (message) {
|
export function command (message: discord.Message) {
|
||||||
message.mentions.users.map((user) => {
|
message.mentions.users.map((user) => {
|
||||||
const count = state.warnings.filter(x => x.id === user.id && !x.cleared);
|
const count = state.warnings.filter(x => x.id === user.id && !x.cleared);
|
||||||
if (count != null && count.length > 0) {
|
if (count != null && count.length > 0) {
|
||||||
@ -14,7 +15,7 @@ exports.command = function (message) {
|
|||||||
message.channel.send(`${user}, you have no warnings to clear.`);
|
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}].`);
|
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'];
|
import discord = require('discord.js');
|
||||||
exports.command = function (message, reply) {
|
|
||||||
|
export const roles = ['Admins', 'Moderators'];
|
||||||
|
export function command (message: discord.Message, reply: string) {
|
||||||
let replyMessage = 'Hello.';
|
let replyMessage = 'Hello.';
|
||||||
if (reply == null) {
|
if (reply == null) {
|
||||||
replyMessage = message.content.substr(message.content.indexOf(' ') + 1);
|
replyMessage = message.content.substr(message.content.indexOf(' ') + 1);
|
||||||
@ -8,4 +10,4 @@ exports.command = function (message, reply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message.channel.send(replyMessage);
|
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) => {
|
message.mentions.users.map((user) => {
|
||||||
const warnings = state.warnings.filter(x => x.id === user.id && !x.cleared);
|
const warnings = state.warnings.filter(x => x.id === user.id && !x.cleared);
|
||||||
message.channel.send(`${user}, you have ${warnings.length} total warnings.`);
|
message.channel.send(`${user}, you have ${warnings.length} total warnings.`);
|
@ -1,8 +1,8 @@
|
|||||||
const fs = require('fs');
|
import * as fs from 'fs';
|
||||||
const state = require('./state.js');
|
import state from './state';
|
||||||
const logger = require('./logging.js');
|
import logger from './logging';
|
||||||
|
|
||||||
function readWarnings () {
|
export function readWarnings () {
|
||||||
// Load the warnings file into the application state.
|
// Load the warnings file into the application state.
|
||||||
const readFilePath = '/data/discordWarnings.json';
|
const readFilePath = '/data/discordWarnings.json';
|
||||||
fs.readFile(readFilePath, 'utf8', function (err, data) {
|
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.
|
// Load the ban file into the application state.
|
||||||
const readFilePath = '/data/discordBans.json';
|
const readFilePath = '/data/discordBans.json';
|
||||||
fs.readFile(readFilePath, 'utf8', function (err, data) {
|
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.
|
// Load the responses file into the responses variable.
|
||||||
state.responses = require(`./responses/${process.env.TENANT}.json`);
|
state.responses = require(`./responses/${process.env.TENANT}.json`);
|
||||||
logger.debug(`Loaded responses file for ${process.env.TENANT} from external source.`);
|
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);
|
const warningsJson = JSON.stringify(state.warnings, null, 4);
|
||||||
fs.writeFile('/data/discordWarnings.json', warningsJson, 'utf8', function (err) {
|
fs.writeFile('/data/discordWarnings.json', warningsJson, 'utf8', function (err) {
|
||||||
if (err) { throw err; }
|
if (err) { throw err; }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushBans () {
|
export function flushBans () {
|
||||||
const bansJson = JSON.stringify(state.bans, null, 4);
|
const bansJson = JSON.stringify(state.bans, null, 4);
|
||||||
fs.writeFile('/data/discordBans.json', bansJson, 'utf8', function (err) {
|
fs.writeFile('/data/discordBans.json', bansJson, 'utf8', function (err) {
|
||||||
if (err) { throw 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');
|
import winston = require('winston');
|
||||||
const ip = require('ip');
|
import * as ip from 'ip';
|
||||||
const os = require('os');
|
import * as os from 'os';
|
||||||
|
|
||||||
winston.emitErrs = true;
|
const logger = winston.createLogger({
|
||||||
|
|
||||||
const logger = new winston.Logger({
|
|
||||||
level: 'debug',
|
level: 'debug',
|
||||||
transports: [
|
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,
|
exitOnError: false,
|
||||||
meta: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup logging for LogDNA cloud logging.
|
// Setup logging for LogDNA cloud logging.
|
||||||
if (process.env.LOGDNA_API_KEY) {
|
if (process.env.LOGDNA_API_KEY) {
|
||||||
require('logdna');
|
const logdnaWinston = require('logdna-winston');
|
||||||
const logLevel = process.env.LOGDNA_LEVEL || 'info';
|
const logLevel = process.env.LOGDNA_LEVEL || 'info';
|
||||||
|
|
||||||
logger.add(winston.transports.Logdna, {
|
logger.add(new logdnaWinston({
|
||||||
level: logLevel,
|
level: logLevel,
|
||||||
app: process.env.LOGDNA_APPNAME,
|
app: process.env.LOGDNA_APPNAME,
|
||||||
index_meta: true,
|
index_meta: true,
|
||||||
key: process.env.LOGDNA_API_KEY,
|
key: process.env.LOGDNA_API_KEY,
|
||||||
ip: ip.address(),
|
ip: ip.address(),
|
||||||
hostname: os.hostname()
|
hostname: os.hostname()
|
||||||
});
|
}));
|
||||||
|
|
||||||
logger.info(`[core] Started LogDNA winston transport. Running at log level ${logLevel}.`);
|
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.
|
// Check for environmental variables.
|
||||||
require('checkenv').check();
|
require('checkenv').check();
|
||||||
|
|
||||||
const discord = require('discord.js');
|
import discord = require('discord.js');
|
||||||
const path = require('path');
|
import path = require('path');
|
||||||
const schedule = require('node-schedule');
|
// const schedule = require('node-schedule');
|
||||||
const fs = require('fs');
|
import fs = require('fs');
|
||||||
|
|
||||||
const logger = require('./logging.js');
|
import logger from './logging';
|
||||||
const state = require('./state.js');
|
import state from './state';
|
||||||
const data = require('./data.js');
|
import * as data from './data';
|
||||||
|
|
||||||
state.responses = require('./responses.json');
|
state.responses = require('./responses.json');
|
||||||
|
|
||||||
let cachedModules = [];
|
interface IModuleMap {
|
||||||
let cachedTriggers = [];
|
[name: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedModules: IModuleMap = {};
|
||||||
|
let cachedTriggers: any[] = [];
|
||||||
const client = new discord.Client();
|
const client = new discord.Client();
|
||||||
|
|
||||||
let mediaUsers = new Map();
|
const mediaUsers = new Map();
|
||||||
|
|
||||||
logger.info('Application startup. Configuring environment.');
|
logger.info('Application startup. Configuring environment.');
|
||||||
|
|
||||||
process.on('unhandledRejection', (error, promise) => {
|
function findArray(haystack: string | any[], arr: any[]) {
|
||||||
logger.error(`Unhandled promise rejection: ${error.message}.`, { meta: error });
|
return arr.some(function (v: any) {
|
||||||
});
|
|
||||||
|
|
||||||
process.on('uncaughtException', error => {
|
|
||||||
logger.error(`Unhandled exception: ${error.message}.`, { meta: error });
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
function findArray (haystack, arr) {
|
|
||||||
return arr.some(function (v) {
|
|
||||||
return haystack.indexOf(v) >= 0;
|
return haystack.indexOf(v) >= 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function IsIgnoredCategory (categoryName) {
|
function IsIgnoredCategory(categoryName: string) {
|
||||||
const IgnoredCategory = ['welcome', 'team', 'website-team'];
|
const IgnoredCategory = ['welcome', 'team', 'website-team'];
|
||||||
return IgnoredCategory.includes(categoryName);
|
return IgnoredCategory.includes(categoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on('ready', () => {
|
client.on('ready', async () => {
|
||||||
// Initialize app channels.
|
// Initialize app channels.
|
||||||
state.logChannel = client.channels.get(process.env.DISCORD_LOG_CHANNEL);
|
let logChannel = await client.channels.fetch(process.env.DISCORD_LOG_CHANNEL) as discord.TextChannel;
|
||||||
state.msglogChannel = client.channels.get(process.env.DISCORD_MSGLOG_CHANNEL);
|
let msglogChannel = await client.channels.fetch(process.env.DISCORD_MSGLOG_CHANNEL) as discord.TextChannel;
|
||||||
state.guild = state.logChannel.guild;
|
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.');
|
logger.info('Bot is now online and connected to server.');
|
||||||
});
|
});
|
||||||
@ -63,19 +61,17 @@ client.on('debug', (x) => null);
|
|||||||
client.on('disconnect', () => {
|
client.on('disconnect', () => {
|
||||||
logger.warn('Disconnected from Discord server.');
|
logger.warn('Disconnected from Discord server.');
|
||||||
});
|
});
|
||||||
client.on('reconnecting', () => {
|
|
||||||
logger.warn('Reconnecting...');
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('guildMemberAdd', (member) => {
|
client.on('guildMemberAdd', (member) => {
|
||||||
member.addRole(process.env.DISCORD_RULES_ROLE);
|
member.roles.add(process.env.DISCORD_RULES_ROLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('messageDelete', message => {
|
client.on('messageDelete', message => {
|
||||||
if (IsIgnoredCategory(message.channel.parent.name) == false) {
|
let parent = (message.channel as discord.TextChannel).parent;
|
||||||
if (message.content && message.content.startsWith('.') == false && message.author.bot == false) {
|
if (parent && IsIgnoredCategory(parent.name) === false) {
|
||||||
const deletionEmbed = new discord.RichEmbed()
|
if (message.content && message.content.startsWith('.') === false && message.author.bot === false) {
|
||||||
.setAuthor(message.author.tag, message.author.displayAvatarURL)
|
const deletionEmbed = new discord.MessageEmbed()
|
||||||
|
.setAuthor(message.author.tag, message.author.displayAvatarURL())
|
||||||
.setDescription(`Message deleted in ${message.channel}`)
|
.setDescription(`Message deleted in ${message.channel}`)
|
||||||
.addField('Content', message.cleanContent, false)
|
.addField('Content', message.cleanContent, false)
|
||||||
.setTimestamp()
|
.setTimestamp()
|
||||||
@ -89,13 +85,14 @@ client.on('messageDelete', message => {
|
|||||||
|
|
||||||
client.on('messageUpdate', (oldMessage, newMessage) => {
|
client.on('messageUpdate', (oldMessage, newMessage) => {
|
||||||
const AllowedRoles = ['Administrators', 'Moderators', 'Team', 'VIP'];
|
const AllowedRoles = ['Administrators', 'Moderators', 'Team', 'VIP'];
|
||||||
if (!findArray(oldMessage.member.roles.map(function (x) { return x.name; }), AllowedRoles)) {
|
if (!findArray(oldMessage.member.roles.cache.map(x => x.name), AllowedRoles)) {
|
||||||
if (IsIgnoredCategory(oldMessage.channel.parent.name) == false) {
|
let parent = (oldMessage.channel as discord.TextChannel).parent;
|
||||||
|
if (parent && IsIgnoredCategory(parent.name) === false) {
|
||||||
const oldM = oldMessage.cleanContent;
|
const oldM = oldMessage.cleanContent;
|
||||||
const newM = newMessage.cleanContent;
|
const newM = newMessage.cleanContent;
|
||||||
if (oldMessage.content != newMessage.content && oldM && newM) {
|
if (oldMessage.content !== newMessage.content && oldM && newM) {
|
||||||
const editedEmbed = new discord.RichEmbed()
|
const editedEmbed = new discord.MessageEmbed()
|
||||||
.setAuthor(oldMessage.author.tag, oldMessage.author.displayAvatarURL)
|
.setAuthor(oldMessage.author.tag, oldMessage.author.displayAvatarURL())
|
||||||
.setDescription(`Message edited in ${oldMessage.channel} [Jump To Message](${newMessage.url})`)
|
.setDescription(`Message edited in ${oldMessage.channel} [Jump To Message](${newMessage.url})`)
|
||||||
.addField('Before', oldM, false)
|
.addField('Before', oldM, false)
|
||||||
.addField('After', newM, false)
|
.addField('After', newM, false)
|
||||||
@ -120,11 +117,11 @@ client.on('message', message => {
|
|||||||
return;
|
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) {
|
if (message.channel.id === process.env.DISCORD_MEDIA_CHANNEL && !message.author.bot) {
|
||||||
const AllowedMediaRoles = ['Administrators', 'Moderators', 'Team', 'VIP'];
|
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);
|
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)) {
|
if (message.attachments.size > 0 || message.content.match(urlRegex)) {
|
||||||
mediaUsers.set(message.author.id, true);
|
mediaUsers.set(message.author.id, true);
|
||||||
@ -142,15 +139,14 @@ client.on('message', message => {
|
|||||||
if (message.content.toLowerCase().includes(process.env.DISCORD_RULES_TRIGGER)) {
|
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.
|
// 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}.`);
|
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.
|
// Delete the message in the channel to force a cleanup.
|
||||||
message.delete();
|
message.delete();
|
||||||
return;
|
|
||||||
} else if (message.content.startsWith('.') && message.content.startsWith('..') === false) {
|
} else if (message.content.startsWith('.') && message.content.startsWith('..') === false) {
|
||||||
// We want to make sure it's an actual command, not someone '...'-ing.
|
// 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.
|
// Check by the name of the command.
|
||||||
let cachedModule = cachedModules[`${cmd}.js`];
|
let cachedModule = cachedModules[`${cmd}.js`];
|
||||||
@ -158,44 +154,43 @@ client.on('message', message => {
|
|||||||
// Check by the quotes in the configuration.
|
// Check by the quotes in the configuration.
|
||||||
if (cachedModule == null) { cachedModule = state.responses.quotes[cmd]; cachedModuleType = 'Quote'; }
|
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) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`${message.author.username} ${message.author} [Channel: ${message.channel}] executed command: ${message.content}`);
|
// Check access permissions.
|
||||||
message.delete();
|
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}`);
|
||||||
try {
|
logger.info(`${message.author.username} ${message.author} attempted to use admin command: ${message.content}`);
|
||||||
if (cachedModuleType === 'Command') {
|
return false;
|
||||||
cachedModule.command(message);
|
|
||||||
} else if (cachedModuleType === 'Quote') {
|
|
||||||
cachedModules['quote.js'].command(message, cachedModule.reply);
|
|
||||||
}
|
|
||||||
} catch (err) { logger.error(err); }
|
|
||||||
|
|
||||||
// Warn after running command?
|
|
||||||
try {
|
|
||||||
// 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)) {
|
|
||||||
// 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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`${message.author.username} ${message.author} [Channel: ${message.channel}] executed command: ${message.content}`);
|
||||||
|
message.delete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (cachedModuleType === 'Command') {
|
||||||
|
cachedModule.command(message);
|
||||||
|
} else if (cachedModuleType === 'Quote') {
|
||||||
|
cachedModules['quote.js'].command(message, cachedModule.reply);
|
||||||
|
}
|
||||||
|
} catch (err) { logger.error(err); }
|
||||||
|
|
||||||
|
// Warn after running command?
|
||||||
|
try {
|
||||||
|
// Check if the command requires a warning.
|
||||||
|
if (cmd !== 'warn' && cachedModule.warn === true) {
|
||||||
|
// Access check to see if the user has privileges to warn.
|
||||||
|
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 if (message.author.bot === false) {
|
} else if (message.author.bot === false) {
|
||||||
// This is a normal channel message.
|
// This is a normal channel message.
|
||||||
cachedTriggers.forEach(function (trigger) {
|
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) {
|
if (trigger.trigger(message) === true) {
|
||||||
logger.debug(`${message.author.username} ${message.author} [Channel: ${message.channel}] triggered: ${message.content}`);
|
logger.debug(`${message.author.username} ${message.author} [Channel: ${message.channel}] triggered: ${message.content}`);
|
||||||
try {
|
try {
|
||||||
@ -208,8 +203,8 @@ client.on('message', message => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Cache all command modules.
|
// Cache all command modules.
|
||||||
cachedModules = [];
|
cachedModules = {};
|
||||||
fs.readdirSync('./src/commands/').forEach(function (file) {
|
fs.readdirSync('./commands/').forEach(function (file) {
|
||||||
// Load the module if it's a script.
|
// Load the module if it's a script.
|
||||||
if (path.extname(file) === '.js') {
|
if (path.extname(file) === '.js') {
|
||||||
if (file.includes('.disabled')) {
|
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 New Issue
Block a user