Initial commit in new repository
This commit is contained in:
commit
23c78f2c9c
15 changed files with 4048 additions and 0 deletions
7
.editorconfig
Normal file
7
.editorconfig
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
69
.gitignore
vendored
Normal file
69
.gitignore
vendored
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Node template
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Knub data
|
||||||
|
/data
|
||||||
|
|
||||||
|
# PHPStorm
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
/convert.js
|
33
README.md
Normal file
33
README.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
### Config format example
|
||||||
|
|
||||||
|
Config files are currently located at `data/guilds/<guildId>.yml` (and `data/guilds/global.yml` for global plugins).
|
||||||
|
|
||||||
|
```yml
|
||||||
|
levels:
|
||||||
|
50: mod
|
||||||
|
100: admin
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
mod_plugin:
|
||||||
|
config:
|
||||||
|
kick_message: 'You have been kicked'
|
||||||
|
permissions:
|
||||||
|
kick: false
|
||||||
|
overrides:
|
||||||
|
- level: '>=50'
|
||||||
|
permissions:
|
||||||
|
kick: true
|
||||||
|
- level: '>=100'
|
||||||
|
config:
|
||||||
|
kick_message: 'You have been kicked by an admin'
|
||||||
|
spam:
|
||||||
|
config:
|
||||||
|
filter_words: ['heck']
|
||||||
|
overrides:
|
||||||
|
- channel: '1234'
|
||||||
|
config:
|
||||||
|
+filter_words: ['foo']
|
||||||
|
- level: '>=50'
|
||||||
|
config:
|
||||||
|
-filter_words: ['heck']
|
||||||
|
```
|
13
knexfile.js
Normal file
13
knexfile.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
client: 'mariasql',
|
||||||
|
connection: {
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
db: process.env.DB_DATABASE,
|
||||||
|
timezone: 'UTC',
|
||||||
|
charset: 'utf8mb4'
|
||||||
|
}
|
||||||
|
};
|
30
migrations/20171221220235_create_mod_action_tables.js
Normal file
30
migrations/20171221220235_create_mod_action_tables.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
exports.up = async function(knex, Promise) {
|
||||||
|
await knex.schema.createTableIfNotExists('mod_actions', table => {
|
||||||
|
table.increments('id');
|
||||||
|
table.bigInteger('guild_id').unsigned().notNullable();
|
||||||
|
table.integer('case_number').unsigned().notNullable();
|
||||||
|
table.bigInteger('user_id').index().unsigned().notNullable();
|
||||||
|
table.string('user_name', 128).notNullable();
|
||||||
|
table.bigInteger('mod_id').index().unsigned().nullable().defaultTo(null);
|
||||||
|
table.string('mod_name', 128).nullable().defaultTo(null);
|
||||||
|
table.string('action_type', 16).notNullable();
|
||||||
|
table.bigInteger('audit_log_id').unique().nullable().defaultTo(null);
|
||||||
|
table.dateTime('created_at').index().defaultTo(knex.raw('NOW()')).notNullable();
|
||||||
|
|
||||||
|
table.unique(['guild_id', 'case_number']);
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.createTableIfNotExists('mod_action_notes', table => {
|
||||||
|
table.increments('id');
|
||||||
|
table.integer('mod_action_id').unsigned().notNullable().index().references('id').inTable('mod_actions').onDelete('CASCADE');
|
||||||
|
table.bigInteger('mod_id').index().unsigned().nullable().defaultTo(null);
|
||||||
|
table.string('mod_name', 128).nullable().defaultTo(null);
|
||||||
|
table.text('body').notNullable();
|
||||||
|
table.dateTime('created_at').index().defaultTo(knex.raw('NOW()')).notNullable();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function(knex, Promise) {
|
||||||
|
await knex.schema.dropTableIfExists('mod_action_notes');
|
||||||
|
await knex.schema.dropTableIfExists('mod_actions');
|
||||||
|
};
|
3251
package-lock.json
generated
Normal file
3251
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
44
package.json
Normal file
44
package.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "newmodbot",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "ts-node src/index.ts",
|
||||||
|
"precommit": "lint-staged",
|
||||||
|
"postcommit": "git update-index --again",
|
||||||
|
"format": "prettier --write \"./**/*.ts\""
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.ts": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cli-table2": "^0.2.1",
|
||||||
|
"@types/knex": "0.0.64",
|
||||||
|
"@types/koa": "^2.0.43",
|
||||||
|
"@types/koa-router": "^7.0.27",
|
||||||
|
"@types/node": "^8.0.50",
|
||||||
|
"cli-table3": "^0.5.0",
|
||||||
|
"dotenv": "^4.0.0",
|
||||||
|
"eris": "^0.8.6",
|
||||||
|
"husky": "^0.14.3",
|
||||||
|
"knex": "^0.14.6",
|
||||||
|
"knub": "^9.3.0",
|
||||||
|
"lint-staged": "^7.2.0",
|
||||||
|
"mariasql": "^0.2.6",
|
||||||
|
"moment": "^2.20.1",
|
||||||
|
"prettier": "^1.8.2",
|
||||||
|
"randomstring": "^1.1.5",
|
||||||
|
"ts-node": "^3.3.0",
|
||||||
|
"tslint": "^5.8.0",
|
||||||
|
"tslint-config-prettier": "^1.6.0",
|
||||||
|
"typescript": "^2.6.1"
|
||||||
|
}
|
||||||
|
}
|
67
src/data/GuildModActions.ts
Normal file
67
src/data/GuildModActions.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import knex from "../knex";
|
||||||
|
|
||||||
|
export class GuildModActions {
|
||||||
|
protected guildId: string;
|
||||||
|
|
||||||
|
constructor(guildId) {
|
||||||
|
this.guildId = guildId;
|
||||||
|
}
|
||||||
|
|
||||||
|
find(id: number) {
|
||||||
|
return knex("mod_actions")
|
||||||
|
.where("guild_id", this.guildId)
|
||||||
|
.where("id", id)
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
findByCaseNumber(caseNumber: number) {
|
||||||
|
return knex("mod_actions")
|
||||||
|
.where("guild_id", this.guildId)
|
||||||
|
.where("case_number", caseNumber)
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
getActionNotes(actionId: number) {
|
||||||
|
return knex("mod_action_notes")
|
||||||
|
.where("mod_action_id", actionId)
|
||||||
|
.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
getByUserId(userId: string) {
|
||||||
|
return knex("mod_actions")
|
||||||
|
.where("guild_id", this.guildId)
|
||||||
|
.where("user_id", userId)
|
||||||
|
.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
create(data) {
|
||||||
|
return knex
|
||||||
|
.insert({
|
||||||
|
...data,
|
||||||
|
guild_id: this.guildId,
|
||||||
|
case_number: knex.raw(
|
||||||
|
"(SELECT IFNULL(MAX(case_number)+1, 1) FROM mod_actions AS ma2 WHERE guild_id = ?)",
|
||||||
|
this.guildId
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.returning("id")
|
||||||
|
.into("mod_actions")
|
||||||
|
.then(ids => Number(ids[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id, data) {
|
||||||
|
return knex("mod_actions")
|
||||||
|
.where("id", id)
|
||||||
|
.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNote(modActionId: number, data: any) {
|
||||||
|
return knex
|
||||||
|
.insert({
|
||||||
|
...data,
|
||||||
|
mod_action_id: modActionId
|
||||||
|
})
|
||||||
|
.into("mod_action_notes")
|
||||||
|
.return();
|
||||||
|
}
|
||||||
|
}
|
31
src/index.ts
Normal file
31
src/index.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason, p) => {
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(reason);
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
import { Client } from "eris";
|
||||||
|
import { Knub } from "knub";
|
||||||
|
import { BotControlPlugin } from "./plugins/BotControl";
|
||||||
|
import { ModActionsPlugin } from "./plugins/ModActions";
|
||||||
|
import { UtilityPlugin } from "./plugins/Utility";
|
||||||
|
import knex from "./knex";
|
||||||
|
|
||||||
|
// Run latest database migrations
|
||||||
|
knex.migrate.latest().then(() => {
|
||||||
|
const client = new Client(process.env.TOKEN);
|
||||||
|
|
||||||
|
const bot = new Knub(client, {
|
||||||
|
plugins: {
|
||||||
|
utility: UtilityPlugin,
|
||||||
|
mod_notes: ModActionsPlugin
|
||||||
|
},
|
||||||
|
globalPlugins: {
|
||||||
|
bot_control: BotControlPlugin
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.run();
|
||||||
|
});
|
6
src/knex.ts
Normal file
6
src/knex.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const knexfile = require("../knexfile");
|
||||||
|
import * as knex from "knex";
|
||||||
|
|
||||||
|
const db = knex(knexfile);
|
||||||
|
|
||||||
|
export default db;
|
46
src/plugins/BotControl.ts
Normal file
46
src/plugins/BotControl.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { decorators as d, GlobalPlugin } from "knub";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { Message } from "eris";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A global plugin that allows bot owners to control the bot
|
||||||
|
*/
|
||||||
|
export class BotControlPlugin extends GlobalPlugin {
|
||||||
|
getDefaultOptions() {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
owners: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner(userId) {
|
||||||
|
return this.configValue("owners").includes(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@d.command("bot_update")
|
||||||
|
async updateCmd(msg: Message) {
|
||||||
|
if (!this.isOwner(msg.author.id)) return;
|
||||||
|
|
||||||
|
const updateCmd = this.configValue("update_cmd");
|
||||||
|
if (!updateCmd) {
|
||||||
|
msg.channel.createMessage("Update command not specified!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.channel.createMessage("Updating...");
|
||||||
|
const updater = child_process.exec(updateCmd, { cwd: process.cwd() });
|
||||||
|
updater.stderr.on("data", data => {
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@d.command("bot_reload")
|
||||||
|
async reloadCmd(msg: Message) {
|
||||||
|
if (!this.isOwner(msg.author.id)) return;
|
||||||
|
|
||||||
|
msg.channel.createMessage("Reloading...");
|
||||||
|
this.knub.reloadGuild(this.guildId);
|
||||||
|
}
|
||||||
|
}
|
384
src/plugins/ModActions.ts
Normal file
384
src/plugins/ModActions.ts
Normal file
|
@ -0,0 +1,384 @@
|
||||||
|
import { Plugin, decorators as d } from "knub";
|
||||||
|
import {
|
||||||
|
Guild,
|
||||||
|
GuildAuditLogEntry,
|
||||||
|
Member,
|
||||||
|
Message,
|
||||||
|
TextChannel,
|
||||||
|
User
|
||||||
|
} from "eris";
|
||||||
|
import * as moment from "moment";
|
||||||
|
import { GuildModActions } from "../data/GuildModActions";
|
||||||
|
|
||||||
|
enum ActionType {
|
||||||
|
Ban = 1,
|
||||||
|
Unban,
|
||||||
|
Note,
|
||||||
|
Warn,
|
||||||
|
Kick
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms: number): Promise<void> => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ModActionsPlugin extends Plugin {
|
||||||
|
protected modActions: GuildModActions;
|
||||||
|
|
||||||
|
async onLoad() {
|
||||||
|
this.modActions = new GuildModActions(this.guildId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultOptions() {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
dm_on_warn: true,
|
||||||
|
dm_on_mute: true,
|
||||||
|
dm_on_kick: false,
|
||||||
|
dm_on_ban: false,
|
||||||
|
message_on_warn: false,
|
||||||
|
message_on_mute: false,
|
||||||
|
message_on_kick: false,
|
||||||
|
message_on_ban: false,
|
||||||
|
message_channel: null,
|
||||||
|
warn_message: "You have received a warning on {guildName}: {reason}",
|
||||||
|
mute_message: "You have been muted on {guildName} for {reason}",
|
||||||
|
kick_message: "You have been kicked from {guildName} for {reason}",
|
||||||
|
ban_message: "You have been banned from {guildName} for {reason}",
|
||||||
|
log_automatic_actions: true,
|
||||||
|
action_log_channel: null,
|
||||||
|
alert_on_rejoin: false,
|
||||||
|
alert_channel: null
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
note: false,
|
||||||
|
warn: false,
|
||||||
|
mute: false,
|
||||||
|
kick: false,
|
||||||
|
ban: false,
|
||||||
|
view: false
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
level: ">=50",
|
||||||
|
permissions: {
|
||||||
|
note: true,
|
||||||
|
warn: true,
|
||||||
|
mute: true,
|
||||||
|
kick: true,
|
||||||
|
ban: true,
|
||||||
|
view: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a BAN action automatically when a user is banned.
|
||||||
|
* Attempts to find the ban's details in the audit log.
|
||||||
|
*/
|
||||||
|
@d.event("guildBanAdd")
|
||||||
|
async onGuildBanAdd(guild: Guild, user: User) {
|
||||||
|
await sleep(1000); // Wait a moment for the audit log to update
|
||||||
|
const relevantAuditLogEntry = await this.findRelevantAuditLogEntry(
|
||||||
|
"MEMBER_BAN_ADD",
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let modActionId;
|
||||||
|
|
||||||
|
if (relevantAuditLogEntry) {
|
||||||
|
const modId = relevantAuditLogEntry.user.id;
|
||||||
|
const auditLogId = relevantAuditLogEntry.id;
|
||||||
|
|
||||||
|
modActionId = await this.createModAction(
|
||||||
|
user.id,
|
||||||
|
modId,
|
||||||
|
ActionType.Ban,
|
||||||
|
auditLogId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relevantAuditLogEntry.reason) {
|
||||||
|
await this.createModActionNote(
|
||||||
|
modActionId,
|
||||||
|
modId,
|
||||||
|
relevantAuditLogEntry.reason
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
modActionId = await this.createModAction(user.id, null, ActionType.Ban);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.displayModAction(modActionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an UNBAN mod action automatically when a user is unbanned.
|
||||||
|
* Attempts to find the unban's details in the audit log.
|
||||||
|
*/
|
||||||
|
@d.event("guildBanRemove")
|
||||||
|
async onGuildBanRemove(guild: Guild, user: User) {
|
||||||
|
const relevantAuditLogEntry = await this.findRelevantAuditLogEntry(
|
||||||
|
"MEMBER_BAN_REMOVE",
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let modActionId;
|
||||||
|
|
||||||
|
if (relevantAuditLogEntry) {
|
||||||
|
const modId = relevantAuditLogEntry.user.id;
|
||||||
|
const auditLogId = relevantAuditLogEntry.id;
|
||||||
|
|
||||||
|
modActionId = await this.createModAction(
|
||||||
|
user.id,
|
||||||
|
modId,
|
||||||
|
ActionType.Unban,
|
||||||
|
auditLogId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
modActionId = await this.createModAction(user.id, null, ActionType.Unban);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.displayModAction(modActionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an alert if a member with prior notes joins the server
|
||||||
|
*/
|
||||||
|
@d.event("guildMemberAdd")
|
||||||
|
async onGuildMemberAdd(member: Member) {
|
||||||
|
if (! this.configValue('alert_on_rejoin')) return;
|
||||||
|
|
||||||
|
const alertChannelId = this.configValue('alert_channel');
|
||||||
|
if (! alertChannelId) return;
|
||||||
|
|
||||||
|
const actions = await this.modActions.getByUserId(member.id);
|
||||||
|
|
||||||
|
if (actions.length) {
|
||||||
|
const alertChannel: any = this.guild.channels.get(alertChannelId);
|
||||||
|
alertChannel.send(
|
||||||
|
`<@!${member.id}> (${member.user.username}#${
|
||||||
|
member.user.discriminator
|
||||||
|
} \`${member.id}\`) joined with ${actions.length} prior record(s)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified case by adding more details to it
|
||||||
|
*/
|
||||||
|
@d.command("update", "<caseNumber:number> <note:string$>")
|
||||||
|
@d.permission("note")
|
||||||
|
async updateCmd(msg: Message, args: any) {
|
||||||
|
const action = await this.modActions.findByCaseNumber(args.caseNumber);
|
||||||
|
if (!action) {
|
||||||
|
msg.channel.createMessage("Case not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.mod_id === null) {
|
||||||
|
// If the action has no moderator information, assume the first one to update it did the action
|
||||||
|
await this.modActions.update(action.id, {
|
||||||
|
mod_id: msg.author.id,
|
||||||
|
mod_name: `${msg.author.username}#${msg.author.discriminator}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.createModActionNote(action.id, msg.author.id, args.note);
|
||||||
|
|
||||||
|
this.displayModAction(action.id, msg.channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new NOTE type mod action and add the specified note to it
|
||||||
|
*/
|
||||||
|
@d.command("note", "<userId:string> <note:string$>")
|
||||||
|
@d.permission("note")
|
||||||
|
async noteCmd(msg: Message, args: any) {
|
||||||
|
const actionId = await this.createModAction(
|
||||||
|
args.userId,
|
||||||
|
msg.author.id,
|
||||||
|
ActionType.Note
|
||||||
|
);
|
||||||
|
await this.createModActionNote(actionId, msg.author.id, args.note);
|
||||||
|
|
||||||
|
this.displayModAction(actionId, msg.channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a case or list of cases
|
||||||
|
* If the argument passed is a case id, display that case
|
||||||
|
* If the argument passed is a user id, show all cases on that user
|
||||||
|
*/
|
||||||
|
@d.command("/showcase|case|cases|usercases/", "<caseNumberOrUserId:string>")
|
||||||
|
@d.permission("view")
|
||||||
|
async showcaseCmd(msg: Message, args: any) {
|
||||||
|
if (args.caseNumberOrUserId.length >= 17) {
|
||||||
|
// Assume user id
|
||||||
|
const actions = await this.modActions.getByUserId(args.userId);
|
||||||
|
|
||||||
|
if (actions.length === 0) {
|
||||||
|
msg.channel.createMessage("No cases found for the specified user!");
|
||||||
|
} else {
|
||||||
|
for (const action of actions) {
|
||||||
|
await this.displayModAction(action, msg.channel.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Assume case id
|
||||||
|
const action = await this.modActions.findByCaseNumber(args.caseNumber);
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
msg.channel.createMessage("Case not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.displayModAction(action.id, msg.channel.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows information about the specified action in a message embed.
|
||||||
|
* If no channelId is specified, uses the channel id from config.
|
||||||
|
*/
|
||||||
|
protected async displayModAction(actionOrId: any, channelId: string = null) {
|
||||||
|
let action;
|
||||||
|
if (typeof actionOrId === "number") {
|
||||||
|
action = await this.modActions.find(actionOrId);
|
||||||
|
} else {
|
||||||
|
action = actionOrId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
if (!channelId) {
|
||||||
|
channelId = this.configValue('action_log_channel');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channelId) return;
|
||||||
|
|
||||||
|
const notes = await this.modActions.getActionNotes(action.id);
|
||||||
|
|
||||||
|
const createdAt = moment(action.created_at);
|
||||||
|
const actionTypeStr = ActionType[action.action_type].toUpperCase();
|
||||||
|
|
||||||
|
const embed: any = {
|
||||||
|
title: `${actionTypeStr} - Case #${action.case_number}`,
|
||||||
|
footer: {
|
||||||
|
text: `Case created at ${createdAt.format("YYYY-MM-DD [at] HH:mm")}`
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "User",
|
||||||
|
value: `${action.user_name}\n<@!${action.user_id}>`,
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Moderator",
|
||||||
|
value: `${action.mod_name}\n<@!${action.mod_id}>`,
|
||||||
|
inline: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (actionTypeStr === "BAN") {
|
||||||
|
embed.color = 0xe67e22;
|
||||||
|
} else if (actionTypeStr === "UNBAN") {
|
||||||
|
embed.color = 0x9b59b6;
|
||||||
|
} else if (actionTypeStr === "NOTE") {
|
||||||
|
embed.color = 0x3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.length) {
|
||||||
|
notes.forEach((note: any) => {
|
||||||
|
const noteDate = moment(note.created_at);
|
||||||
|
embed.addField(
|
||||||
|
`${note.mod_name} at ${noteDate.format("YYYY-MM-DD [at] HH:mm")}:`,
|
||||||
|
note.body
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
embed.addField("!!! THIS CASE HAS NO NOTES !!!", "\u200B");
|
||||||
|
}
|
||||||
|
|
||||||
|
(this.bot.guilds
|
||||||
|
.get(this.guildId)
|
||||||
|
.channels.get(channelId) as TextChannel).createMessage({
|
||||||
|
embed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to find a relevant audit log entry for the given user and action. Only accepts audit log entries from the past 10 minutes.
|
||||||
|
*/
|
||||||
|
protected async findRelevantAuditLogEntry(
|
||||||
|
actionType: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<GuildAuditLogEntry> {
|
||||||
|
const auditLogEntries = await this.bot.getGuildAuditLogs(
|
||||||
|
this.guildId,
|
||||||
|
5,
|
||||||
|
actionType
|
||||||
|
);
|
||||||
|
|
||||||
|
auditLogEntries.entries.sort((a, b) => {
|
||||||
|
if (a.createdAt > b.createdAt) return -1;
|
||||||
|
if (a.createdAt > b.createdAt) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cutoffDate = new Date();
|
||||||
|
cutoffDate.setTime(cutoffDate.getTime() - 1000 * 15);
|
||||||
|
const cutoffTS = cutoffDate.getTime();
|
||||||
|
|
||||||
|
return auditLogEntries.entries.find(entry => {
|
||||||
|
return entry.target.id === userId && entry.createdAt >= cutoffTS;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createModAction(
|
||||||
|
userId: string,
|
||||||
|
modId: string,
|
||||||
|
actionType: ActionType,
|
||||||
|
auditLogId: string = null
|
||||||
|
): Promise<number> {
|
||||||
|
const user = this.bot.users.get(userId);
|
||||||
|
const userName = user
|
||||||
|
? `${user.username}#${user.discriminator}`
|
||||||
|
: "Unknown#0000";
|
||||||
|
|
||||||
|
const mod = this.bot.users.get(modId);
|
||||||
|
const modName = mod
|
||||||
|
? `${mod.username}#${mod.discriminator}`
|
||||||
|
: "Unknown#0000";
|
||||||
|
|
||||||
|
return this.modActions.create({
|
||||||
|
user_id: userId,
|
||||||
|
user_name: userName,
|
||||||
|
mod_id: modId,
|
||||||
|
mod_name: modName,
|
||||||
|
action_type: actionType,
|
||||||
|
audit_log_id: auditLogId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createModActionNote(
|
||||||
|
modActionId: number,
|
||||||
|
modId: string,
|
||||||
|
body: string
|
||||||
|
) {
|
||||||
|
const mod = this.bot.users.get(modId);
|
||||||
|
const modName = mod
|
||||||
|
? `${mod.username}#${mod.discriminator}`
|
||||||
|
: "Unknown#0000";
|
||||||
|
|
||||||
|
return this.modActions.createNote(modActionId, {
|
||||||
|
mod_id: modId,
|
||||||
|
mod_name: modName,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
29
src/plugins/Utility.ts
Normal file
29
src/plugins/Utility.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Plugin, decorators as d } from "knub";
|
||||||
|
import { Message, TextChannel } from "eris";
|
||||||
|
|
||||||
|
export class UtilityPlugin extends Plugin {
|
||||||
|
getDefaultOptions() {
|
||||||
|
return {
|
||||||
|
permissions: {
|
||||||
|
roles: false
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
level: ">=50",
|
||||||
|
permissions: {
|
||||||
|
roles: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@d.command("roles")
|
||||||
|
@d.permission("roles")
|
||||||
|
async rolesCmd(msg: Message) {
|
||||||
|
const roles = (msg.channel as TextChannel).guild.roles.map(
|
||||||
|
role => `${role.name} ${role.id}`
|
||||||
|
);
|
||||||
|
msg.channel.createMessage("```" + roles.join("\n") + "```");
|
||||||
|
}
|
||||||
|
}
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": [
|
||||||
|
"es6",
|
||||||
|
"es7",
|
||||||
|
"ES2017"
|
||||||
|
],
|
||||||
|
"baseUrl": "./"
|
||||||
|
}
|
||||||
|
}
|
23
tslint.json
Normal file
23
tslint.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"tslint:latest",
|
||||||
|
"tslint-config-prettier"
|
||||||
|
],
|
||||||
|
|
||||||
|
"rules": {
|
||||||
|
"no-var-requires": false,
|
||||||
|
"object-literal-sort-keys": false,
|
||||||
|
"max-classes-per-file": [false],
|
||||||
|
"prefer-conditional-expression": false,
|
||||||
|
"prefer-object-spread": false,
|
||||||
|
"forin": false,
|
||||||
|
"member-access": false,
|
||||||
|
"member-ordering": false,
|
||||||
|
"variable-name": false,
|
||||||
|
"ordered-imports": false,
|
||||||
|
"curly": [true, "ignore-same-line"],
|
||||||
|
"no-console": {
|
||||||
|
"severity": "warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue