3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +00:00

Switch from Knex to TypeORM. Update Knub.

This commit is contained in:
Dragory 2018-10-26 06:41:20 +03:00
parent e3ff4cef45
commit f9c16263ae
49 changed files with 1192 additions and 1395 deletions

View file

@ -1,34 +0,0 @@
require('dotenv').config();
const moment = require('moment-timezone');
moment.tz.setDefault('UTC');
module.exports = {
client: 'mysql2',
connection: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
charset: 'utf8mb4',
timezone: 'UTC',
supportBigNumbers: true,
bigNumberStrings: true,
dateStrings: true,
typeCast(field, next) {
if (field.type === 'DATETIME') {
const val = field.string();
return val != null ? moment(val).format('YYYY-MM-DD HH:mm:ss') : null;
}
return next();
}
},
pool: {
afterCreate(conn, cb) {
conn.query('SET time_zone = "+00:00";', err => {
cb(err, conn);
});
}
}
};

View file

@ -0,0 +1,112 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreatePreTypeORMTables1540519249973 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`archives\` (
\`id\` VARCHAR(36) NOT NULL,
\`guild_id\` VARCHAR(20) NOT NULL,
\`body\` MEDIUMTEXT NOT NULL,
\`created_at\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
\`expires_at\` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (\`id\`)
)
COLLATE='utf8mb4_general_ci'
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`cases\` (
\`id\` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
\`guild_id\` BIGINT(20) UNSIGNED NOT NULL,
\`case_number\` INT(10) UNSIGNED NOT NULL,
\`user_id\` BIGINT(20) UNSIGNED NOT NULL,
\`user_name\` VARCHAR(128) NOT NULL,
\`mod_id\` BIGINT(20) UNSIGNED NULL DEFAULT NULL,
\`mod_name\` VARCHAR(128) NULL DEFAULT NULL,
\`type\` INT(10) UNSIGNED NOT NULL,
\`audit_log_id\` BIGINT(20) NULL DEFAULT NULL,
\`created_at\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (\`id\`),
UNIQUE INDEX \`mod_actions_guild_id_case_number_unique\` (\`guild_id\`, \`case_number\`),
UNIQUE INDEX \`mod_actions_audit_log_id_unique\` (\`audit_log_id\`),
INDEX \`mod_actions_user_id_index\` (\`user_id\`),
INDEX \`mod_actions_mod_id_index\` (\`mod_id\`),
INDEX \`mod_actions_created_at_index\` (\`created_at\`)
)
COLLATE = 'utf8mb4_general_ci'
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`case_notes\` (
\`id\` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
\`case_id\` INT(10) UNSIGNED NOT NULL,
\`mod_id\` BIGINT(20) UNSIGNED NULL DEFAULT NULL,
\`mod_name\` VARCHAR(128) NULL DEFAULT NULL,
\`body\` TEXT NOT NULL,
\`created_at\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (\`id\`),
INDEX \`mod_action_notes_mod_action_id_index\` (\`case_id\`),
INDEX \`mod_action_notes_mod_id_index\` (\`mod_id\`),
INDEX \`mod_action_notes_created_at_index\` (\`created_at\`)
)
COLLATE = 'utf8mb4_general_ci'
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`mutes\` (
\`guild_id\` BIGINT(20) UNSIGNED NOT NULL,
\`user_id\` BIGINT(20) UNSIGNED NOT NULL,
\`created_at\` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
\`expires_at\` DATETIME NULL DEFAULT NULL,
\`case_id\` INT(10) UNSIGNED NULL DEFAULT NULL,
PRIMARY KEY (\`guild_id\`, \`user_id\`),
INDEX \`mutes_expires_at_index\` (\`expires_at\`),
INDEX \`mutes_case_id_foreign\` (\`case_id\`),
CONSTRAINT \`mutes_case_id_foreign\` FOREIGN KEY (\`case_id\`) REFERENCES \`cases\` (\`id\`)
ON DELETE SET NULL
)
COLLATE = 'utf8mb4_general_ci'
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`persisted_data\` (
\`guild_id\` VARCHAR(20) NOT NULL,
\`user_id\` VARCHAR(20) NOT NULL,
\`roles\` VARCHAR(1024) NULL DEFAULT NULL,
\`nickname\` VARCHAR(255) NULL DEFAULT NULL,
\`is_voice_muted\` INT(11) NOT NULL DEFAULT '0',
PRIMARY KEY (\`guild_id\`, \`user_id\`)
)
COLLATE = 'utf8mb4_general_ci'
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`reaction_roles\` (
\`guild_id\` VARCHAR(20) NOT NULL,
\`channel_id\` VARCHAR(20) NOT NULL,
\`message_id\` VARCHAR(20) NOT NULL,
\`emoji\` VARCHAR(20) NOT NULL,
\`role_id\` VARCHAR(20) NOT NULL,
PRIMARY KEY (\`guild_id\`, \`channel_id\`, \`message_id\`, \`emoji\`),
INDEX \`reaction_roles_message_id_emoji_index\` (\`message_id\`, \`emoji\`)
)
COLLATE = 'utf8mb4_general_ci'
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`tags\` (
\`guild_id\` BIGINT(20) UNSIGNED NOT NULL,
\`tag\` VARCHAR(64) NOT NULL,
\`user_id\` BIGINT(20) UNSIGNED NOT NULL,
\`body\` TEXT NOT NULL,
\`created_at\` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (\`guild_id\`, \`tag\`)
)
COLLATE = 'utf8mb4_general_ci'
`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
// No down function since we're migrating (hehe) from another migration system (knex)
}
}

View file

@ -1,34 +0,0 @@
exports.up = async function(knex) {
if (! await knex.schema.hasTable('mod_actions')) {
await knex.schema.createTable('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.integer('action_type').unsigned().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']);
});
}
if (! await knex.schema.hasTable('mod_action_notes')) {
await knex.schema.createTable('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) {
await knex.schema.dropTableIfExists('mod_action_notes');
await knex.schema.dropTableIfExists('mod_actions');
};

View file

@ -1,17 +0,0 @@
exports.up = async function(knex) {
if (! await knex.schema.hasTable('mutes')) {
await knex.schema.createTable('mutes', table => {
table.bigInteger('guild_id').unsigned().notNullable();
table.bigInteger('user_id').unsigned().notNullable();
table.dateTime('created_at').defaultTo(knex.raw('NOW()'));
table.dateTime('expires_at').nullable().defaultTo(null);
table.primary(['guild_id', 'user_id']);
table.index(['expires_at']);
});
}
};
exports.down = async function(knex) {
await knex.schema.dropTableIfExists('mutes');
};

View file

@ -1,21 +0,0 @@
exports.up = async function(knex) {
await knex.schema.renameTable('mod_actions', 'cases');
await knex.schema.renameTable('mod_action_notes', 'case_notes');
await knex.schema.table('cases', table => {
table.renameColumn('action_type', 'type');
});
await knex.schema.table('case_notes', table => {
table.renameColumn('mod_action_id', 'case_id');
});
};
exports.down = async function(knex) {
await knex.schema.table('cases', table => {
table.renameColumn('type', 'action_type');
});
await knex.schema.table('case_notes', table => {
table.renameColumn('case_id', 'mod_action_id');
});
await knex.schema.renameTable('cases', 'mod_actions');
await knex.schema.renameTable('case_notes', 'mod_action_notes');
};

View file

@ -1,18 +0,0 @@
exports.up = async function(knex, Promise) {
if (! await knex.schema.hasTable('reaction_roles')) {
await knex.schema.createTable('reaction_roles', table => {
table.string('guild_id', 20).notNullable();
table.string('channel_id', 20).notNullable();
table.string('message_id', 20).notNullable();
table.string('emoji', 20).notNullable();
table.string('role_id', 20).notNullable();
table.primary(['guild_id', 'channel_id', 'message_id', 'emoji']);
table.index(['message_id', 'emoji']);
});
}
};
exports.down = async function(knex, Promise) {
await knex.schema.dropTableIfExists('reaction_roles');
};

View file

@ -1,17 +0,0 @@
exports.up = async function(knex, Promise) {
if (! await knex.schema.hasTable('persisted_data')) {
await knex.schema.createTable('persisted_data', table => {
table.string('guild_id', 20).notNullable();
table.string('user_id', 20).notNullable();
table.string('roles', 1024).nullable().defaultTo(null);
table.string('nickname', 255).nullable().defaultTo(null);
table.integer('is_voice_muted').notNullable().defaultTo(0);
table.primary(['guild_id', 'user_id']);
});
}
};
exports.down = async function(knex, Promise) {
await knex.schema.dropTableIfExists('persisted_data');
};

View file

@ -1,15 +0,0 @@
exports.up = async function(knex, Promise) {
if (! await knex.schema.hasTable('spam_logs')) {
await knex.schema.createTable('spam_logs', table => {
table.string('id', 36).notNullable().primary();
table.string('guild_id', 20).notNullable();
table.text('body', 'mediumtext').notNullable();
table.dateTime('created_at').defaultTo(knex.raw('NOW()')).notNullable();
table.dateTime('expires_at').nullable();
});
}
};
exports.down = async function(knex, Promise) {
await knex.schema.dropTableIfExists('spam_logs');
};

View file

@ -1,12 +0,0 @@
exports.up = async function(knex, Promise) {
await knex.schema.table('mutes', table => {
table.integer('case_id').unsigned().nullable().defaultTo(null).after('user_id').references('id').inTable('cases').onDelete('SET NULL');
});
};
exports.down = async function(knex, Promise) {
await knex.schema.table('mutes', table => {
table.dropForeign('case_id');
table.dropColumn('case_id');
});
};

View file

@ -1,17 +0,0 @@
exports.up = async function(knex) {
if (! await knex.schema.hasTable('tags')) {
await knex.schema.createTable('tags', table => {
table.bigInteger('guild_id').unsigned().notNullable();
table.string('tag', 64).notNullable();
table.bigInteger('user_id').unsigned().notNullable();
table.text('body').notNullable();
table.dateTime('created_at').defaultTo(knex.raw('NOW()'));
table.primary(['guild_id', 'tag']);
});
}
};
exports.down = async function(knex) {
await knex.schema.dropTableIfExists('tags');
};

View file

@ -1,7 +0,0 @@
exports.up = async function(knex) {
await knex.schema.renameTable('spam_logs', 'archives');
};
exports.down = async function(knex) {
await knex.schema.renameTable('archives', 'spam_logs');
};

38
ormconfig.js Normal file
View file

@ -0,0 +1,38 @@
require('dotenv').config();
const moment = require('moment-timezone');
moment.tz.setDefault('UTC');
module.exports = {
type: "mysql",
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: true,
dateStrings: true,
synchronize: false,
// Entities
entities: [`${__dirname}/src/data/entities/*.ts`],
// Pool options
extra: {
typeCast(field, next) {
if (field.type === 'DATETIME') {
const val = field.string();
return val != null ? moment(val).format('YYYY-MM-DD HH:mm:ss') : null;
}
return next();
}
},
// Migrations
migrations: ["migrations/*.ts"],
cli: {
migrationsDir: "migrations"
},
};

1231
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,8 +10,7 @@
"precommit": "lint-staged",
"postcommit": "git update-index --again",
"format": "prettier --write \"./**/*.ts\"",
"db-migrate": "knex migrate:latest",
"db-rollback": "knex migrate:rollback"
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
},
"lint-staged": {
"*.ts": [
@ -23,34 +22,35 @@
"author": "",
"license": "ISC",
"dependencies": {
"@types/knex": "0.0.64",
"@types/lodash.at": "^4.6.3",
"@types/moment-timezone": "^0.5.6",
"@types/node": "^8.0.50",
"dotenv": "^4.0.0",
"emoji-regex": "^7.0.0",
"eris": "^0.8.6",
"escape-string-regexp": "^1.0.5",
"humanize-duration": "^3.15.0",
"knex": "0.12.6",
"knub": "^10.1.0",
"js-yaml": "^3.12.0",
"knub": "^12.1.1",
"lodash.at": "^4.6.0",
"lodash.chunk": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.intersection": "^4.4.0",
"lodash.isequal": "^4.5.0",
"moment-timezone": "^0.5.21",
"mysql2": "^1.6.0",
"mysql": "^2.16.0",
"reflect-metadata": "^0.1.12",
"tlds": "^1.203.1",
"ts-node": "^3.3.0",
"typescript": "^2.9.2",
"typeorm": "^0.2.8",
"typescript": "^3.1.3",
"uuid": "^3.3.2"
},
"devDependencies": {
"nodemon": "^1.17.5",
"lint-staged": "^7.2.0",
"prettier": "^1.8.2",
"@types/node": "^10.12.0",
"husky": "^0.14.3",
"lint-staged": "^7.2.0",
"nodemon": "^1.17.5",
"prettier": "^1.8.2",
"tslint": "^5.8.0",
"tslint-config-prettier": "^1.6.0"
}

13
src/SimpleError.ts Normal file
View file

@ -0,0 +1,13 @@
import util from "util";
export class SimpleError {
public message: string;
constructor(message: string) {
this.message = message;
}
[util.inspect.custom](depth, options) {
return `Error: ${this.message}`;
}
}

View file

@ -0,0 +1,50 @@
export class BaseRepository {
private static guildInstances: Map<string, any>;
private nextRelations: string[];
protected guildId: string;
constructor(guildId: string) {
this.guildId = guildId;
this.nextRelations = [];
}
/**
* Returns a cached instance of the inheriting class for the specified guildId,
* or creates a new instance if one doesn't exist yet
*/
public static getInstance<T extends typeof BaseRepository>(this: T, guildId: string): InstanceType<T> {
if (!this.guildInstances) {
this.guildInstances = new Map();
}
if (!this.guildInstances.has(guildId)) {
this.guildInstances.set(guildId, new this(guildId));
}
return this.guildInstances.get(guildId) as InstanceType<T>;
}
/**
* Primes the specified relation(s) to be used in the next database operation.
* Can be chained.
*/
public with(relations: string | string[]): this {
if (Array.isArray(relations)) {
this.nextRelations.push(...relations);
} else {
this.nextRelations.push(relations);
}
return this;
}
/**
* Gets and resets the relations primed using with()
*/
protected getRelations(): string[] {
const relations = this.nextRelations || [];
this.nextRelations = [];
return relations;
}
}

View file

@ -1,12 +1,12 @@
import { CaseType } from "./CaseType";
import { CaseTypes } from "./CaseTypes";
export const CaseTypeColors = {
[CaseType.Note]: 0x3498db,
[CaseType.Warn]: 0xdae622,
[CaseType.Mute]: 0xe6b122,
[CaseType.Unmute]: 0xa175b3,
[CaseType.Kick]: 0xe67e22,
[CaseType.Softban]: 0xe67e22,
[CaseType.Ban]: 0xcb4314,
[CaseType.Unban]: 0x9b59b6
[CaseTypes.Note]: 0x3498db,
[CaseTypes.Warn]: 0xdae622,
[CaseTypes.Mute]: 0xe6b122,
[CaseTypes.Unmute]: 0xa175b3,
[CaseTypes.Kick]: 0xe67e22,
[CaseTypes.Softban]: 0xe67e22,
[CaseTypes.Ban]: 0xcb4314,
[CaseTypes.Unban]: 0x9b59b6
};

View file

@ -1,4 +1,4 @@
export enum CaseType {
export enum CaseTypes {
Ban = 1,
Unban,
Note,

View file

@ -1,51 +1,52 @@
import uuid from "uuid/v4"; // tslint:disable-line
import moment from "moment-timezone";
import knex from "../knex";
import SpamLog from "../models/SpamLog";
import { ArchiveEntry } from "./entities/ArchiveEntry";
import { getRepository, Repository } from "typeorm";
import { BaseRepository } from "./BaseRepository";
const DEFAULT_EXPIRY_DAYS = 30;
function deleteExpiredArchives() {
knex("archives")
.where("expires_at", "<=", knex.raw("NOW()"))
.delete();
}
deleteExpiredArchives();
setInterval(deleteExpiredArchives, 1000 * 60 * 60); // Clean expired archives every hour
export class GuildArchives {
protected guildId: string;
export class GuildArchives extends BaseRepository {
protected archives: Repository<ArchiveEntry>;
constructor(guildId) {
this.guildId = guildId;
super(guildId);
this.archives = getRepository(ArchiveEntry);
// Clean expired archives at start and then every hour
this.deleteExpiredArchives();
setInterval(() => this.deleteExpiredArchives(), 1000 * 60 * 60);
}
generateNewLogId() {
return uuid();
private deleteExpiredArchives() {
this.archives
.createQueryBuilder()
.where("expires_at <= NOW()")
.delete()
.execute();
}
async find(id: string): Promise<SpamLog> {
const result = await knex("archives")
.where("id", id)
.first();
return result ? new SpamLog(result) : null;
async find(id: string): Promise<ArchiveEntry> {
return this.archives.findOne({
where: { id },
relations: this.getRelations()
});
}
async create(body: string, expiresAt: moment.Moment = null) {
const id = this.generateNewLogId();
/**
* @returns ID of the created entry
*/
async create(body: string, expiresAt: moment.Moment = null): Promise<string> {
if (!expiresAt) {
expiresAt = moment().add(DEFAULT_EXPIRY_DAYS, "days");
}
await knex("archives").insert({
id,
const result = await this.archives.insert({
guild_id: this.guildId,
body,
expires_at: expiresAt.format("YYYY-MM-DD HH:mm:ss")
});
return id;
return result.identifiers[0].id;
}
}

View file

@ -1,93 +1,78 @@
import knex from "../knex";
import Case from "../models/Case";
import CaseNote from "../models/CaseNote";
import { Case } from "./entities/Case";
import { CaseNote } from "./entities/CaseNote";
import { BaseRepository } from "./BaseRepository";
import { getRepository, In, Repository } from "typeorm";
export class GuildCases {
protected guildId: string;
export class GuildCases extends BaseRepository {
private cases: Repository<Case>;
private caseNotes: Repository<CaseNote>;
constructor(guildId) {
this.guildId = guildId;
super(guildId);
this.cases = getRepository(Case);
this.caseNotes = getRepository(CaseNote);
}
async get(ids: number[]): Promise<Case[]> {
const result = await knex("cases")
.whereIn("id", ids)
.select();
return result.map(r => new Case(r));
return this.cases.find({
relations: this.getRelations(),
where: {
guild_id: this.guildId,
id: In(ids)
}
});
}
async find(id: number): Promise<Case> {
const result = await knex("cases")
.where("guild_id", this.guildId)
.where("id", id)
.first();
return result ? new Case(result) : null;
return this.cases.findOne({
relations: this.getRelations(),
where: {
guild_id: this.guildId,
id
}
});
}
async findByCaseNumber(caseNumber: number): Promise<Case> {
const result = await knex("cases")
.where("guild_id", this.guildId)
.where("case_number", caseNumber)
.first();
return result ? new Case(result) : null;
return this.cases.findOne({
relations: this.getRelations(),
where: {
guild_id: this.guildId,
case_number: caseNumber
}
async getCaseNotes(caseId: number): Promise<CaseNote[]> {
const results = await knex("case_notes")
.where("case_id", caseId)
.select();
return results.map(r => new CaseNote(r));
});
}
async getByUserId(userId: string): Promise<Case[]> {
const results = await knex("cases")
.where("guild_id", this.guildId)
.where("user_id", userId)
.select();
return results.map(r => new Case(r));
return this.cases.find({
relations: this.getRelations(),
where: {
guild_id: this.guildId,
user_id: userId
}
async findFirstCaseNote(caseId: number): Promise<CaseNote> {
const result = await knex("case_notes")
.where("case_id", caseId)
.first();
return result ? new CaseNote(result) : null;
});
}
async create(data): Promise<number> {
return knex
.insert({
const result = await this.cases.insert({
...data,
guild_id: this.guildId,
case_number: knex.raw(
"(SELECT IFNULL(MAX(case_number)+1, 1) FROM cases AS ma2 WHERE guild_id = ?)",
this.guildId
)
})
.returning("id")
.into("cases")
.then(ids => Number(ids[0]));
case_number: () => `(SELECT IFNULL(MAX(case_number)+1, 1) FROM cases AS ma2 WHERE guild_id = ${this.guildId})`
});
return result.identifiers[0].id;
}
update(id, data) {
return knex("cases")
.where("id", id)
.update(data);
return this.cases.update(id, data);
}
createNote(caseId: number, data: any) {
return knex
.insert({
async createNote(caseId: number, data: any): Promise<number> {
const result = await this.caseNotes.insert({
...data,
case_id: caseId
})
.into("case_notes")
.return();
});
return result.identifiers[0].id;
}
}

View file

@ -1,31 +1,32 @@
import knex from "../knex";
import moment from "moment-timezone";
import Mute from "../models/Mute";
import { Mute } from "./entities/Mute";
import { BaseRepository } from "./BaseRepository";
import { getRepository, Repository, Brackets } from "typeorm";
export class GuildMutes {
protected guildId: string;
export class GuildMutes extends BaseRepository {
private mutes: Repository<Mute>;
constructor(guildId) {
this.guildId = guildId;
super(guildId);
this.mutes = getRepository(Mute);
}
async getExpiredMutes(): Promise<Mute[]> {
const result = await knex("mutes")
.where("guild_id", this.guildId)
.whereNotNull("expires_at")
.whereRaw("expires_at <= NOW()")
.select();
return result.map(r => new Mute(r));
return this.mutes
.createQueryBuilder("mutes")
.where("guild_id = :guild_id", { guild_id: this.guildId })
.where("expires_at IS NOT NULL")
.where("expires_at <= NOW()")
.getMany();
}
async findExistingMuteForUserId(userId: string): Promise<Mute> {
const result = await knex("mutes")
.where("guild_id", this.guildId)
.where("user_id", userId)
.first();
return result ? new Mute(result) : null;
return this.mutes.findOne({
where: {
guild_id: this.guildId,
user_id: userId
}
});
}
async addMute(userId, expiryTime) {
@ -35,13 +36,11 @@ export class GuildMutes {
.format("YYYY-MM-DD HH:mm:ss")
: null;
return knex
.insert({
return this.mutes.insert({
guild_id: this.guildId,
user_id: userId,
expires_at: expiresAt
})
.into("mutes");
});
}
async updateExpiryTime(userId, newExpiryTime) {
@ -51,12 +50,15 @@ export class GuildMutes {
.format("YYYY-MM-DD HH:mm:ss")
: null;
return knex("mutes")
.where("guild_id", this.guildId)
.where("user_id", userId)
.update({
return this.mutes.update(
{
guild_id: this.guildId,
user_id: userId
},
{
expires_at: expiresAt
});
}
);
}
async addOrUpdateMute(userId, expiryTime) {
@ -70,25 +72,33 @@ export class GuildMutes {
}
async getActiveMutes(): Promise<Mute[]> {
const result = await knex("mutes")
.where("guild_id", this.guildId)
.where(q => q.whereRaw("expires_at > NOW()").orWhereNull("expires_at"))
.select();
return result.map(r => new Mute(r));
return this.mutes
.createQueryBuilder("mutes")
.where("guild_id = :guild_id", { guild_id: this.guildId })
.andWhere(
new Brackets(qb => {
qb.where("expires_at > NOW()").orWhere("expires_at IS NULL");
})
)
.getMany();
}
async setCaseId(userId, caseId) {
await knex("mutes")
.where("guild_id", this.guildId)
.where("user_id", userId)
.update({ case_id: caseId });
await this.mutes.update(
{
guild_id: this.guildId,
user_id: userId
},
{
case_id: caseId
}
);
}
async clear(userId) {
return knex("mutes")
.where("guild_id", this.guildId)
.where("user_id", userId)
.delete();
await this.mutes.delete({
guild_id: this.guildId,
user_id: userId
});
}
}

View file

@ -1,5 +1,6 @@
import knex from "../knex";
import PersistedData from "../models/PersistedData";
import { PersistedData } from "./entities/PersistedData";
import { BaseRepository } from "./BaseRepository";
import { getRepository, Repository } from "typeorm";
export interface IPartialPersistData {
roles?: string[];
@ -7,20 +8,21 @@ export interface IPartialPersistData {
is_voice_muted?: boolean;
}
export class GuildPersistedData {
protected guildId: string;
export class GuildPersistedData extends BaseRepository {
private persistedData: Repository<PersistedData>;
constructor(guildId) {
this.guildId = guildId;
super(guildId);
this.persistedData = getRepository(PersistedData);
}
async find(userId: string) {
const result = await knex("persisted_data")
.where("guild_id", this.guildId)
.where("user_id", userId)
.first();
return result ? new PersistedData(result) : null;
return this.persistedData.findOne({
where: {
guild_id: this.guildId,
user_id: userId
}
});
}
async set(userId: string, data: IPartialPersistData = {}) {
@ -31,12 +33,15 @@ export class GuildPersistedData {
const existing = await this.find(userId);
if (existing) {
await knex("persisted_data")
.where("guild_id", this.guildId)
.where("user_id", userId)
.update(finalData);
await this.persistedData.update(
{
guild_id: this.guildId,
user_id: userId
},
finalData
);
} else {
await knex("persisted_data").insert({
await this.persistedData.insert({
...finalData,
guild_id: this.guildId,
user_id: userId
@ -45,9 +50,9 @@ export class GuildPersistedData {
}
async clear(userId: string) {
await knex("persisted_data")
.where("guild_id", this.guildId)
.where("user_id", userId)
.delete();
await this.persistedData.delete({
guild_id: this.guildId,
user_id: userId
});
}
}

View file

@ -1,54 +1,57 @@
import knex from "../knex";
import ReactionRole from "../models/ReactionRole";
import { ReactionRole } from "./entities/ReactionRole";
import { BaseRepository } from "./BaseRepository";
import { getRepository, Repository } from "typeorm";
export class GuildReactionRoles {
protected guildId: string;
export class GuildReactionRoles extends BaseRepository {
private reactionRoles: Repository<ReactionRole>;
constructor(guildId) {
this.guildId = guildId;
super(guildId);
this.reactionRoles = getRepository(ReactionRole);
}
async all(): Promise<ReactionRole[]> {
const results = await knex("reaction_roles")
.where("guild_id", this.guildId)
.select();
return results.map(r => new ReactionRole(r));
return this.reactionRoles.find({
where: {
guild_id: this.guildId
}
});
}
async getForMessage(messageId: string): Promise<ReactionRole[]> {
const results = await knex("reaction_roles")
.where("guild_id", this.guildId)
.where("message_id", messageId)
.select();
return results.map(r => new ReactionRole(r));
return this.reactionRoles.find({
where: {
guild_id: this.guildId,
message_id: messageId
}
});
}
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole> {
const result = await knex("reaction_roles")
.where("guild_id", this.guildId)
.where("message_id", messageId)
.where("emoji", emoji)
.first();
return result ? new ReactionRole(result) : null;
return this.reactionRoles.findOne({
where: {
guild_id: this.guildId,
message_id: messageId,
emoji
}
});
}
async removeFromMessage(messageId: string, emoji: string = null) {
let query = knex("reaction_roles")
.where("guild_id", this.guildId)
.where("message_id", messageId);
const criteria: any = {
guild_id: this.guildId,
message_id: messageId
};
if (emoji) {
query = query.where("emoji", emoji);
criteria.emoji = emoji;
}
await query.delete();
await this.reactionRoles.delete(criteria);
}
async add(channelId: string, messageId: string, emoji: string, roleId: string) {
await knex("reaction_roles").insert({
await this.reactionRoles.insert({
guild_id: this.guildId,
channel_id: channelId,
message_id: messageId,

View file

@ -1,36 +1,40 @@
import knex from "../knex";
import moment from "moment-timezone";
import Tag from "../models/Tag";
import { Tag } from "./entities/Tag";
import { getRepository, Repository } from "typeorm";
import { BaseRepository } from "./BaseRepository";
export class GuildTags {
protected guildId: string;
export class GuildTags extends BaseRepository {
private tags: Repository<Tag>;
constructor(guildId) {
this.guildId = guildId;
super(guildId);
this.tags = getRepository(Tag);
}
async find(tag): Promise<Tag> {
const result = await knex("tags")
.where("guild_id", this.guildId)
.where("tag", tag)
.first();
return result ? new Tag(result) : null;
return this.tags.findOne({
where: {
guild_id: this.guildId,
tag
}
});
}
async createOrUpdate(tag, body, userId) {
const existingTag = await this.find(tag);
if (existingTag) {
await knex("tags")
.where("guild_id", this.guildId)
.where("tag", tag)
.update({
await this.tags
.createQueryBuilder()
.update()
.set({
body,
user_id: userId,
created_at: knex.raw("NOW()")
});
created_at: () => "NOW()"
})
.where("guild_id = :guildId", { guildId: this.guildId })
.where("tag = :tag", { tag })
.execute();
} else {
await knex("tags").insert({
await this.tags.insert({
guild_id: this.guildId,
user_id: userId,
tag,
@ -40,9 +44,9 @@ export class GuildTags {
}
async delete(tag) {
await knex("tags")
.where("guild_id", this.guildId)
.where("tag", tag)
.delete();
await this.tags.delete({
guild_id: this.guildId,
tag
});
}
}

23
src/data/db.ts Normal file
View file

@ -0,0 +1,23 @@
import { SimpleError } from "../SimpleError";
import { Connection, createConnection } from "typeorm";
let connectionPromise: Promise<Connection>;
export let connection: Connection;
export function connect() {
if (!connectionPromise) {
connectionPromise = createConnection().then(newConnection => {
return newConnection.query("SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP) AS tz").then(r => {
if (r[0].tz !== "00:00:00") {
throw new SimpleError(`Database timezone must be UTC (detected ${r[0].tz})`);
}
connection = newConnection;
return newConnection;
});
});
}
return connectionPromise;
}

View file

@ -0,0 +1,16 @@
import { Entity, Column, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
@Entity("archives")
export class ArchiveEntry {
@Column()
@PrimaryGeneratedColumn("uuid")
id: string;
@Column() guild_id: string;
@Column() body: string;
@Column() created_at: string;
@Column() expires_at: string;
}

28
src/data/entities/Case.ts Normal file
View file

@ -0,0 +1,28 @@
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { CaseNote } from "./CaseNote";
@Entity("cases")
export class Case {
@PrimaryGeneratedColumn() id: number;
@Column() guild_id: string;
@Column() case_number: number;
@Column() user_id: string;
@Column() user_name: string;
@Column() mod_id: string;
@Column() mod_name: string;
@Column() type: number;
@Column() audit_log_id: string;
@Column() created_at: string;
@OneToMany(type => CaseNote, note => note.case)
notes: CaseNote[];
}

View file

@ -0,0 +1,21 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from "typeorm";
import { Case } from "./Case";
@Entity("case_notes")
export class CaseNote {
@PrimaryGeneratedColumn() id: number;
@Column() case_id: number;
@Column() mod_id: string;
@Column() mod_name: string;
@Column() body: string;
@Column() created_at: string;
@ManyToOne(type => Case, theCase => theCase.notes)
@JoinColumn({ name: "case_id" })
case: Case;
}

18
src/data/entities/Mute.ts Normal file
View file

@ -0,0 +1,18 @@
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity("mutes")
export class Mute {
@Column()
@PrimaryColumn()
guild_id: string;
@Column()
@PrimaryColumn()
user_id: string;
@Column() created_at: string;
@Column() expires_at: string;
@Column() case_id: number;
}

View file

@ -0,0 +1,18 @@
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity("persisted_data")
export class PersistedData {
@Column()
@PrimaryColumn()
guild_id: string;
@Column()
@PrimaryColumn()
user_id: string;
@Column() roles: string;
@Column() nickname: string;
@Column() is_voice_muted: number;
}

View file

@ -0,0 +1,22 @@
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity("reaction_roles")
export class ReactionRole {
@Column()
@PrimaryColumn()
guild_id: string;
@Column()
@PrimaryColumn()
channel_id: string;
@Column()
@PrimaryColumn()
message_id: string;
@Column()
@PrimaryColumn()
emoji: string;
@Column() role_id: string;
}

18
src/data/entities/Tag.ts Normal file
View file

@ -0,0 +1,18 @@
import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm";
@Entity("tags")
export class Tag {
@Column()
@PrimaryColumn()
guild_id: string;
@Column()
@PrimaryColumn()
tag: string;
@Column() user_id: string;
@Column() body: string;
@Column() created_at: string;
}

View file

@ -1,3 +1,9 @@
import path from "path";
import yaml from "js-yaml";
import _fs from "fs";
const fs = _fs.promises;
require("dotenv").config();
process.on("unhandledRejection", (reason, p) => {
@ -17,13 +23,12 @@ process.on("uncaughtException", err => {
});
// Always use UTC
// This is also set for the database in knexfile
import moment from "moment-timezone";
moment.tz.setDefault("UTC");
import { Client } from "eris";
import { Knub, logger } from "knub";
import knex from "./knex";
import { connect } from "./data/db";
// Global plugins
import { BotControlPlugin } from "./plugins/BotControl";
@ -42,7 +47,9 @@ import { TagsPlugin } from "./plugins/Tags";
// Run latest database migrations
logger.info("Running database migrations");
knex.migrate.latest().then(() => {
connect().then(async conn => {
await conn.runMigrations();
const client = new Client(process.env.TOKEN, {
getAllUsers: true
});
@ -72,6 +79,25 @@ knex.migrate.latest().then(() => {
return keys.filter(pluginName => {
return plugins[pluginName] && plugins[pluginName].enabled !== false;
});
},
async getConfig(id) {
const configFile = id ? `${id}.yml` : "global.yml";
const configPath = path.join("config", configFile);
try {
await fs.access(configPath);
} catch (e) {
return {};
}
const yamlString = await fs.readFile(configPath, { encoding: "utf8" });
return yaml.safeLoad(yamlString);
},
logFn: (level, msg) => {
if (level === "debug") return;
console.log(`[${level.toUpperCase()}] ${msg}`);
}
}
});

View file

@ -1,6 +0,0 @@
const knexfile = require("../knexfile");
import knex from "knex";
const db = knex(knexfile);
export default db;

View file

@ -1,14 +0,0 @@
import Model from "./Model";
export default class Case extends Model {
public id: number;
public guild_id: string;
public case_number: number;
public user_id: string;
public user_name: string;
public mod_id: string;
public mod_name: string;
public type: number;
public audit_log_id: string;
public created_at: string;
}

View file

@ -1,10 +0,0 @@
import Model from "./Model";
export default class CaseNote extends Model {
public id: number;
public case_id: number;
public mod_id: string;
public mod_name: string;
public body: string;
public created_at: string;
}

View file

@ -1,7 +0,0 @@
export default class Model {
constructor(props) {
for (const key in props) {
this[key] = props[key];
}
}
}

View file

@ -1,9 +0,0 @@
import Model from "./Model";
export default class Mute extends Model {
public guild_id: string;
public user_id: string;
public case_id: number;
public created_at: string;
public expires_at: string;
}

View file

@ -1,26 +0,0 @@
import Model from "./Model";
export default class PersistedData extends Model {
private _roles;
private _isVoiceMuted;
public guild_id: string;
public user_id: string;
public nickname: string;
set roles(v) {
this._roles = v ? v.split(",") : [];
}
get roles() {
return this._roles;
}
set is_voice_muted(v) {
this._isVoiceMuted = v === 1;
}
get is_voice_muted() {
return this._isVoiceMuted;
}
}

View file

@ -1,9 +0,0 @@
import Model from "./Model";
export default class ReactionRole extends Model {
public guild_id: string;
public channel_id: string;
public message_id: string;
public emoji: string;
public role_id: string;
}

View file

@ -1,9 +0,0 @@
import Model from "./Model";
export default class SpamLog extends Model {
public id: string;
public guild_id: string;
public body: string;
public created_at: string;
public expires_at: string;
}

View file

@ -1,9 +0,0 @@
import Model from "./Model";
export default class Tag extends Model {
public guild_id: string;
public tag: string;
public user_id: string;
public body: string;
public created_at: string;
}

View file

@ -11,14 +11,13 @@ import {
errorMessage,
findRelevantAuditLogEntry,
formatTemplateString,
sleep,
stripObjectToScalars,
successMessage,
trimLines
} from "../utils";
import { GuildMutes } from "../data/GuildMutes";
import Case from "../models/Case";
import { CaseType } from "../data/CaseType";
import { Case } from "../data/entities/Case";
import { CaseTypes } from "../data/CaseTypes";
import { GuildLogs } from "../data/GuildLogs";
import { LogType } from "../data/LogType";
import Timer = NodeJS.Timer;
@ -47,8 +46,8 @@ export class ModActionsPlugin extends Plugin {
protected ignoredEvents: IIgnoredEvent[];
async onLoad() {
this.cases = new GuildCases(this.guildId);
this.mutes = new GuildMutes(this.guildId);
this.cases = GuildCases.getInstance(this.guildId);
this.mutes = GuildMutes.getInstance(this.guildId);
this.serverLogs = new GuildLogs(this.guildId);
this.ignoredEvents = [];
@ -156,9 +155,9 @@ export class ModActionsPlugin extends Plugin {
const modId = relevantAuditLogEntry.user.id;
const auditLogId = relevantAuditLogEntry.id;
await this.createCase(user.id, modId, CaseType.Ban, auditLogId, relevantAuditLogEntry.reason, true);
await this.createCase(user.id, modId, CaseTypes.Ban, auditLogId, relevantAuditLogEntry.reason, true);
} else {
await this.createCase(user.id, null, CaseType.Ban);
await this.createCase(user.id, null, CaseTypes.Ban);
}
}
@ -183,9 +182,9 @@ export class ModActionsPlugin extends Plugin {
const modId = relevantAuditLogEntry.user.id;
const auditLogId = relevantAuditLogEntry.id;
await this.createCase(user.id, modId, CaseType.Unban, auditLogId, null, true);
await this.createCase(user.id, modId, CaseTypes.Unban, auditLogId, null, true);
} else {
await this.createCase(user.id, null, CaseType.Unban);
await this.createCase(user.id, null, CaseTypes.Unban);
}
}
@ -228,7 +227,7 @@ export class ModActionsPlugin extends Plugin {
this.createCase(
member.id,
kickAuditLogEntry.user.id,
CaseType.Kick,
CaseTypes.Kick,
kickAuditLogEntry.id,
kickAuditLogEntry.reason,
true
@ -274,12 +273,13 @@ export class ModActionsPlugin extends Plugin {
const user = await this.bot.users.get(args.userId);
const userName = user ? `${user.username}#${user.discriminator}` : "member";
await this.createCase(args.userId, msg.author.id, CaseType.Note, null, args.note);
await this.createCase(args.userId, msg.author.id, CaseTypes.Note, null, args.note);
msg.channel.createMessage(successMessage(`Note added on ${userName}`));
}
@d.command("warn", "<member:Member> <reason:string$>")
@d.permission("warn")
@d.nonBlocking()
async warnCmd(msg: Message, args: any) {
// Make sure we're allowed to warn this member
if (!this.canActOn(msg.member, args.member)) {
@ -307,7 +307,7 @@ export class ModActionsPlugin extends Plugin {
}
}
await this.createCase(args.member.id, msg.author.id, CaseType.Warn, null, args.reason);
await this.createCase(args.member.id, msg.author.id, CaseTypes.Warn, null, args.reason);
msg.channel.createMessage(
successMessage(`Warned **${args.member.user.username}#${args.member.user.discriminator}**`)
@ -363,7 +363,7 @@ export class ModActionsPlugin extends Plugin {
}
} else {
// Create a case
const caseId = await this.createCase(args.member.id, msg.author.id, CaseType.Mute, null, args.reason);
const caseId = await this.createCase(args.member.id, msg.author.id, CaseTypes.Mute, null, args.reason);
await this.mutes.setCaseId(args.member.id, caseId);
}
@ -458,7 +458,7 @@ export class ModActionsPlugin extends Plugin {
}
// Create a case
await this.createCase(args.member.id, msg.author.id, CaseType.Unmute, null, args.reason);
await this.createCase(args.member.id, msg.author.id, CaseTypes.Unmute, null, args.reason);
// Log the action
this.serverLogs.log(LogType.MEMBER_UNMUTE, {
@ -568,7 +568,7 @@ export class ModActionsPlugin extends Plugin {
args.member.kick(args.reason);
// Create a case for this action
await this.createCase(args.member.id, msg.author.id, CaseType.Kick, null, args.reason);
await this.createCase(args.member.id, msg.author.id, CaseTypes.Kick, null, args.reason);
// Confirm the action to the moderator
let response = `Kicked **${args.member.user.username}#${args.member.user.discriminator}**`;
@ -613,7 +613,7 @@ export class ModActionsPlugin extends Plugin {
args.member.ban(1, args.reason);
// Create a case for this action
await this.createCase(args.member.id, msg.author.id, CaseType.Ban, null, args.reason);
await this.createCase(args.member.id, msg.author.id, CaseTypes.Ban, null, args.reason);
// Confirm the action to the moderator
let response = `Banned **${args.member.user.username}#${args.member.user.discriminator}**`;
@ -646,7 +646,7 @@ export class ModActionsPlugin extends Plugin {
await this.guild.unbanMember(args.member.id);
// Create a case for this action
await this.createCase(args.member.id, msg.author.id, CaseType.Softban, null, args.reason);
await this.createCase(args.member.id, msg.author.id, CaseTypes.Softban, null, args.reason);
// Confirm the action to the moderator
msg.channel.createMessage(
@ -677,7 +677,7 @@ export class ModActionsPlugin extends Plugin {
msg.channel.createMessage(successMessage("Member unbanned!"));
// Create a case
this.createCase(args.userId, msg.author.id, CaseType.Unban, null, args.reason);
this.createCase(args.userId, msg.author.id, CaseTypes.Unban, null, args.reason);
// Log the action
this.serverLogs.log(LogType.MEMBER_UNBAN, {
@ -710,7 +710,7 @@ export class ModActionsPlugin extends Plugin {
msg.channel.createMessage(successMessage("Member forcebanned!"));
// Create a case
this.createCase(args.userId, msg.author.id, CaseType.Ban, null, args.reason);
this.createCase(args.userId, msg.author.id, CaseTypes.Ban, null, args.reason);
// Log the action
this.serverLogs.log(LogType.MEMBER_FORCEBAN, {
@ -721,6 +721,7 @@ export class ModActionsPlugin extends Plugin {
@d.command("massban", "<userIds:string...>")
@d.permission("massban")
@d.nonBlocking()
async massbanCmd(msg: Message, args: { userIds: string[] }) {
// Limit to 100 users at once (arbitrary?)
if (args.userIds.length > 100) {
@ -763,7 +764,7 @@ export class ModActionsPlugin extends Plugin {
for (const userId of args.userIds) {
try {
await this.guild.banMember(userId);
await this.createCase(userId, msg.author.id, CaseType.Ban, null, `Mass ban: ${banReason}`, false, false);
await this.createCase(userId, msg.author.id, CaseTypes.Ban, null, `Mass ban: ${banReason}`, false, false);
} catch (e) {
failedBans.push(userId);
}
@ -811,13 +812,13 @@ export class ModActionsPlugin extends Plugin {
// Verify the case type is valid
const type: string = args.type[0].toUpperCase() + args.type.slice(1).toLowerCase();
if (!CaseType[type]) {
if (!CaseTypes[type]) {
msg.channel.createMessage(errorMessage("Cannot add case: invalid case type"));
return;
}
// Create the case
const caseId = await this.createCase(args.target, msg.author.id, CaseType[type], null, args.reason);
const caseId = await this.createCase(args.target, msg.author.id, CaseTypes[type], null, args.reason);
const theCase = await this.cases.find(caseId);
// Log the action
@ -852,7 +853,7 @@ export class ModActionsPlugin extends Plugin {
@d.command(/cases|usercases/, "<userId:userId> [expanded:string]")
@d.permission("view")
async usercasesCmd(msg: Message, args: { userId: string; expanded?: string }) {
const cases = await this.cases.getByUserId(args.userId);
const cases = await this.cases.with("notes").getByUserId(args.userId);
const user = this.bot.users.get(args.userId);
const userName = user ? `${user.username}#${user.discriminator}` : "Unknown#0000";
const prefix = this.knub.getGuildData(this.guildId).config.prefix;
@ -869,7 +870,8 @@ export class ModActionsPlugin extends Plugin {
// Compact view (= regular message with a preview of each case)
const lines = [];
for (const theCase of cases) {
const firstNote = await this.cases.findFirstCaseNote(theCase.id);
theCase.notes.sort((a, b) => (a.created_at > b.created_at ? 1 : -1));
const firstNote = theCase.notes[0];
let reason = firstNote ? firstNote.body : "";
if (reason.length > CASE_LIST_REASON_MAX_LENGTH) {
@ -882,7 +884,7 @@ export class ModActionsPlugin extends Plugin {
reason = disableLinkPreviews(reason);
lines.push(`Case \`#${theCase.case_number}\` __${CaseType[theCase.type]}__ ${reason}`);
lines.push(`Case \`#${theCase.case_number}\` __${CaseTypes[theCase.type]}__ ${reason}`);
}
const finalMessage = trimLines(`
@ -945,7 +947,7 @@ export class ModActionsPlugin extends Plugin {
protected async displayCase(caseOrCaseId: Case | number, channelId: string) {
let theCase: Case;
if (typeof caseOrCaseId === "number") {
theCase = await this.cases.find(caseOrCaseId);
theCase = await this.cases.with("notes").find(caseOrCaseId);
} else {
theCase = caseOrCaseId;
}
@ -953,10 +955,8 @@ export class ModActionsPlugin extends Plugin {
if (!theCase) return;
if (!this.guild.channels.get(channelId)) return;
const notes = await this.cases.getCaseNotes(theCase.id);
const createdAt = moment(theCase.created_at);
const actionTypeStr = CaseType[theCase.type].toUpperCase();
const actionTypeStr = CaseTypes[theCase.type].toUpperCase();
const embed: any = {
title: `${actionTypeStr} - Case #${theCase.case_number}`,
@ -981,8 +981,8 @@ export class ModActionsPlugin extends Plugin {
embed.color = CaseTypeColors[theCase.type];
}
if (notes.length) {
notes.forEach((note: any) => {
if (theCase.notes.length) {
theCase.notes.forEach((note: any) => {
const noteDate = moment(note.created_at);
embed.fields.push({
name: `${note.mod_name} at ${noteDate.format("YYYY-MM-DD [at] HH:mm")}:`,
@ -1014,7 +1014,7 @@ export class ModActionsPlugin extends Plugin {
public async createCase(
userId: string,
modId: string,
caseType: CaseType,
caseType: CaseTypes,
auditLogId: string = null,
reason: string = null,
automatic = false,

View file

@ -21,7 +21,7 @@ export class PersistPlugin extends Plugin {
}
onLoad() {
this.persistedData = new GuildPersistedData(this.guildId);
this.persistedData = GuildPersistedData.getInstance(this.guildId);
this.logs = new GuildLogs(this.guildId);
}

View file

@ -30,20 +30,17 @@ export class ReactionRolesPlugin extends Plugin {
}
async onLoad() {
this.reactionRoles = new GuildReactionRoles(this.guildId);
this.reactionRoles = GuildReactionRoles.getInstance(this.guildId);
return;
// Pre-fetch all messages with reaction roles so we get their events
const reactionRoles = await this.reactionRoles.all();
const channelMessages: Map<string, Set<string>> = reactionRoles.reduce(
(map: Map<string, Set<string>>, row) => {
const channelMessages: Map<string, Set<string>> = reactionRoles.reduce((map: Map<string, Set<string>>, row) => {
if (!map.has(row.channel_id)) map.set(row.channel_id, new Set());
map.get(row.channel_id).add(row.message_id);
return map;
},
new Map()
);
}, new Map());
const msgLoadPromises = [];
@ -62,10 +59,7 @@ export class ReactionRolesPlugin extends Plugin {
@d.command("reaction_roles", "<channel:channel> <messageId:string> <reactionRolePairs:string$>")
@d.permission("manage")
async reactionRolesCmd(
msg: Message,
args: { channel: Channel; messageId: string; reactionRolePairs: string }
) {
async reactionRolesCmd(msg: Message, args: { channel: Channel; messageId: string; reactionRolePairs: string }) {
if (!(args.channel instanceof TextChannel)) {
msg.channel.createMessage(errorMessage("Channel must be a text channel!"));
return;
@ -100,9 +94,7 @@ export class ReactionRolesPlugin extends Plugin {
// Verify the specified emojis and roles are valid
for (const pair of newRolePairs) {
if (isSnowflake(pair[0]) && !guildEmojiIds.includes(pair[0])) {
msg.channel.createMessage(
errorMessage("I can only use regular emojis and custom emojis from this server")
);
msg.channel.createMessage(errorMessage("I can only use regular emojis and custom emojis from this server"));
return;
}
@ -113,9 +105,7 @@ export class ReactionRolesPlugin extends Plugin {
}
const oldReactionRoles = await this.reactionRoles.getForMessage(targetMessage.id);
const oldRolePairs: ReactionRolePair[] = oldReactionRoles.map(
r => [r.emoji, r.role_id] as ReactionRolePair
);
const oldRolePairs: ReactionRolePair[] = oldReactionRoles.map(r => [r.emoji, r.role_id] as ReactionRolePair);
// Remove old reaction/role pairs that weren't included in the new pairs or were changed in some way
const toRemove = oldRolePairs.filter(
@ -154,10 +144,7 @@ export class ReactionRolesPlugin extends Plugin {
@d.event("messageReactionAdd")
async onAddReaction(msg: Message, emoji: CustomEmoji, userId: string) {
const matchingReactionRole = await this.reactionRoles.getByMessageAndEmoji(
msg.id,
emoji.id || emoji.name
);
const matchingReactionRole = await this.reactionRoles.getByMessageAndEmoji(msg.id, emoji.id || emoji.name);
if (!matchingReactionRole) return;
const member = this.guild.members.get(userId);
@ -168,10 +155,7 @@ export class ReactionRolesPlugin extends Plugin {
@d.event("messageReactionRemove")
async onRemoveReaction(msg: Message, emoji: CustomEmoji, userId: string) {
const matchingReactionRole = await this.reactionRoles.getByMessageAndEmoji(
msg.id,
emoji.id || emoji.name
);
const matchingReactionRole = await this.reactionRoles.getByMessageAndEmoji(msg.id, emoji.id || emoji.name);
if (!matchingReactionRole) return;
const member = this.guild.members.get(userId);

View file

@ -1,19 +1,18 @@
import { decorators as d, Plugin } from "knub";
import { Channel, Message, TextChannel, User } from "eris";
import { Channel, Message, User } from "eris";
import {
formatTemplateString,
getEmojiInString,
getRoleMentions,
getUrlsInString,
getUserMentions,
sleep,
stripObjectToScalars,
trimLines
} from "../utils";
import { LogType } from "../data/LogType";
import { GuildLogs } from "../data/GuildLogs";
import { ModActionsPlugin } from "./ModActions";
import { CaseType } from "../data/CaseType";
import { CaseTypes } from "../data/CaseTypes";
import { GuildArchives } from "../data/GuildArchives";
import moment from "moment-timezone";
@ -23,7 +22,8 @@ enum RecentActionType {
Link,
Attachment,
Emoji,
Newline
Newline,
Censor
}
interface IRecentAction {
@ -96,7 +96,7 @@ export class SpamPlugin extends Plugin {
onLoad() {
this.logs = new GuildLogs(this.guildId);
this.archives = new GuildArchives(this.guildId);
this.archives = GuildArchives.getInstance(this.guildId);
this.recentActions = [];
this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60);
@ -260,7 +260,7 @@ export class SpamPlugin extends Plugin {
const logUrl = await this.saveSpamArchives(uniqueMessages, msg.channel, msg.author);
// Create a case and log the actions taken above
const caseType = spamConfig.mute ? CaseType.Mute : CaseType.Note;
const caseType = spamConfig.mute ? CaseTypes.Mute : CaseTypes.Note;
const caseText = trimLines(`
Automatic spam detection: ${description} (over ${spamConfig.count} in ${spamConfig.interval}s)
${logUrl}
@ -297,6 +297,14 @@ export class SpamPlugin extends Plugin {
);
}
// For interoperability with the Censor plugin
async logCensor(msg: Message) {
const spamConfig = this.configValueForMsg(msg, "max_censor");
if (spamConfig) {
this.logAndDetectSpam(msg, RecentActionType.Censor, spamConfig, 1, "too many censored messages");
}
}
@d.event("messageCreate")
async onMessageCreate(msg: Message) {
if (msg.author.bot) return;

View file

@ -1,5 +1,5 @@
import { Plugin, decorators as d } from "knub";
import { Channel, Message, TextChannel } from "eris";
import { Message } from "eris";
import { errorMessage, successMessage } from "../utils";
import { GuildTags } from "../data/GuildTags";
@ -29,7 +29,7 @@ export class TagsPlugin extends Plugin {
}
onLoad() {
this.tags = new GuildTags(this.guildId);
this.tags = GuildTags.getInstance(this.guildId);
}
@d.command("tag", "<tag:string> <body:string$>")

View file

@ -1,19 +1,12 @@
import { Plugin, decorators as d, reply } from "knub";
import { Channel, EmbedOptions, Message, TextChannel, User, VoiceChannel } from "eris";
import {
embedPadding,
errorMessage,
getMessages,
stripObjectToScalars,
successMessage,
trimLines
} from "../utils";
import { embedPadding, errorMessage, getMessages, stripObjectToScalars, successMessage, trimLines } from "../utils";
import { GuildLogs } from "../data/GuildLogs";
import { LogType } from "../data/LogType";
import moment from "moment-timezone";
import humanizeDuration from "humanize-duration";
import { GuildCases } from "../data/GuildCases";
import { CaseType } from "../data/CaseType";
import { CaseTypes } from "../data/CaseTypes";
const MAX_SEARCH_RESULTS = 15;
const MAX_CLEAN_COUNT = 50;
@ -54,7 +47,7 @@ export class UtilityPlugin extends Plugin {
onLoad() {
this.logs = new GuildLogs(this.guildId);
this.cases = new GuildCases(this.guildId);
this.cases = GuildCases.getInstance(this.guildId);
if (activeReloads && activeReloads.has(this.guildId)) {
activeReloads.get(this.guildId).createMessage(successMessage("Reloaded!"));
@ -80,9 +73,7 @@ export class UtilityPlugin extends Plugin {
}
const level = this.getMemberLevel(member);
msg.channel.createMessage(
`The permission level of ${member.username}#${member.discriminator} is **${level}**`
);
msg.channel.createMessage(`The permission level of ${member.username}#${member.discriminator} is **${level}**`);
}
@d.command("search", "<query:string$>")
@ -126,9 +117,7 @@ export class UtilityPlugin extends Plugin {
});
lines = lines.slice(from, to);
const footer = paginated
? "Add a page number to the end of the command to browse results"
: "";
const footer = paginated ? "Add a page number to the end of the command to browse results" : "";
msg.channel.createMessage(`${header}\n\`\`\`${lines.join("\n")}\`\`\`${footer}`);
} else {
@ -152,25 +141,17 @@ export class UtilityPlugin extends Plugin {
@d.permission("clean")
async cleanAllCmd(msg: Message, args: { count: number }) {
if (args.count > MAX_CLEAN_COUNT || args.count <= 0) {
msg.channel.createMessage(
errorMessage(`Clean count must be between 1 and ${MAX_CLEAN_COUNT}`)
);
msg.channel.createMessage(errorMessage(`Clean count must be between 1 and ${MAX_CLEAN_COUNT}`));
return;
}
const messagesToClean = await getMessages(
msg.channel as TextChannel,
m => m.id !== msg.id,
args.count
);
const messagesToClean = await getMessages(msg.channel as TextChannel, m => m.id !== msg.id, args.count);
if (messagesToClean.length > 0) {
await this.cleanMessages(msg.channel, messagesToClean.map(m => m.id), msg.author);
}
msg.channel.createMessage(
successMessage(
`Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`
)
successMessage(`Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`)
);
}
@ -178,9 +159,7 @@ export class UtilityPlugin extends Plugin {
@d.permission("clean")
async cleanUserCmd(msg: Message, args: { userId: string; count: number }) {
if (args.count > MAX_CLEAN_COUNT || args.count <= 0) {
msg.channel.createMessage(
errorMessage(`Clean count must be between 1 and ${MAX_CLEAN_COUNT}`)
);
msg.channel.createMessage(errorMessage(`Clean count must be between 1 and ${MAX_CLEAN_COUNT}`));
return;
}
@ -194,9 +173,7 @@ export class UtilityPlugin extends Plugin {
}
msg.channel.createMessage(
successMessage(
`Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`
)
successMessage(`Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`)
);
}
@ -204,9 +181,7 @@ export class UtilityPlugin extends Plugin {
@d.permission("clean")
async cleanBotCmd(msg: Message, args: { count: number }) {
if (args.count > MAX_CLEAN_COUNT || args.count <= 0) {
msg.channel.createMessage(
errorMessage(`Clean count must be between 1 and ${MAX_CLEAN_COUNT}`)
);
msg.channel.createMessage(errorMessage(`Clean count must be between 1 and ${MAX_CLEAN_COUNT}`));
return;
}
@ -220,9 +195,7 @@ export class UtilityPlugin extends Plugin {
}
msg.channel.createMessage(
successMessage(
`Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`
)
successMessage(`Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`)
);
}
@ -283,7 +256,7 @@ export class UtilityPlugin extends Plugin {
});
const caseSummaries = cases.map(c => {
return `${CaseType[c.type]} (#${c.case_number})`;
return `${CaseTypes[c.type]} (#${c.case_number})`;
});
embed.fields.push({

View file

@ -5,7 +5,8 @@
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"target": "ES2017",
"emitDecoratorMetadata": true,
"target": "ES6",
"lib": [
"es6",
"es7",