feat: save deleted spam logs; server spam logs from a web server; update Knub to 9.6.4

This commit is contained in:
Dragory 2018-08-01 20:09:51 +03:00
parent 847ee11195
commit 16be52a5e7
10 changed files with 167 additions and 10 deletions

View file

@ -0,0 +1,15 @@
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');
};

6
package-lock.json generated
View file

@ -2181,9 +2181,9 @@
} }
}, },
"knub": { "knub": {
"version": "9.6.2", "version": "9.6.4",
"resolved": "https://registry.npmjs.org/knub/-/knub-9.6.2.tgz", "resolved": "https://registry.npmjs.org/knub/-/knub-9.6.4.tgz",
"integrity": "sha512-4Hz6xTrY8srq+tT5h1uxWIGZrb0iIvRkCP2l5OKINKfFHzm0Xn6IvkWYW6DUTQg4m37KgySGdWDpPhLSZUqVmg==", "integrity": "sha512-srwdWu/XPciBQQP3phuavJgQd4XDixccSgjl/vcvQu2kjWgH5Cgnprkfl5JuzhMum/7pVnch4YLEfZDrFzjxAw==",
"requires": { "requires": {
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"js-yaml": "^3.9.1", "js-yaml": "^3.9.1",

View file

@ -33,7 +33,7 @@
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"humanize-duration": "^3.15.0", "humanize-duration": "^3.15.0",
"knex": "^0.14.6", "knex": "^0.14.6",
"knub": "^9.6.2", "knub": "^9.6.4",
"lodash.at": "^4.6.0", "lodash.at": "^4.6.0",
"lodash.difference": "^4.5.0", "lodash.difference": "^4.5.0",
"lodash.intersection": "^4.4.0", "lodash.intersection": "^4.4.0",
@ -42,7 +42,8 @@
"moment-timezone": "^0.5.21", "moment-timezone": "^0.5.21",
"tlds": "^1.203.1", "tlds": "^1.203.1",
"ts-node": "^3.3.0", "ts-node": "^3.3.0",
"typescript": "^2.9.2" "typescript": "^2.9.2",
"uuid": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^1.17.5", "nodemon": "^1.17.5",

View file

@ -35,7 +35,7 @@
"COMMAND": "🤖 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) used command in **#{channel.name}**:\n`{command}`", "COMMAND": "🤖 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) used command in **#{channel.name}**:\n`{command}`",
"SPAM_DELETE": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) spam deleted in **#{channel.name}**: {description} (more than {limit} in {interval}s)", "SPAM_DELETE": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) spam deleted in **#{channel.name}**: {description} (more than {limit} in {interval}s) {logUrl}",
"CENSOR": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) censored message in **#{channel.name}** (`{channel.id}`) {reason}:\n```{messageText}```", "CENSOR": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) censored message in **#{channel.name}** (`{channel.id}`) {reason}:\n```{messageText}```",
"CLEAN": "🚿 **{mod.username}#{mod.discriminator}** (`{mod.id}`) cleaned **{count}** message(s) in **#{channel.name}**", "CLEAN": "🚿 **{mod.username}#{mod.discriminator}** (`{mod.id}`) cleaned **{count}** message(s) in **#{channel.name}**",

72
src/data/GuildSpamLogs.ts Normal file
View file

@ -0,0 +1,72 @@
import uuid from "uuid/v4"; // tslint:disable-line
import moment from "moment-timezone";
import knex from "../knex";
import SpamLog from "../models/SpamLog";
import { Message } from "eris";
import { formatTemplateString, stripObjectToScalars, trimLines } from "../utils";
const EXPIRY_DAYS = 7;
const MESSAGE_FORMAT =
"[{timestamp}] [{message.id}] {user.username}#{user.discriminator}: {message.content}{attachments}";
function cleanExpiredLogs() {
knex("spam_logs")
.where("expires_at", "<=", knex.raw("NOW()"))
.delete();
}
cleanExpiredLogs();
setInterval(cleanExpiredLogs, 1000 * 60 * 60); // Clean expired logs every hour
export class GuildSpamLogs {
protected guildId: string;
constructor(guildId) {
this.guildId = guildId;
}
async find(id: string): Promise<SpamLog> {
const result = await knex("spam_logs")
.where("id", id)
.first();
return result ? new SpamLog(result) : null;
}
/**
* @return ID of the created spam log entry
*/
async createFromMessages(messages: Message[], header: string = null) {
const lines = messages.map(msg => {
return formatTemplateString(MESSAGE_FORMAT, {
user: stripObjectToScalars(msg.author),
message: stripObjectToScalars(msg),
timestamp: moment(msg.timestamp).format("YYYY-MM-DD HH:mm:ss zz"),
attachments: msg.attachments.length
? ` (message contained ${msg.attachments.length} attachment(s))`
: ""
});
});
const id = uuid();
const now = moment().format("YYYY-MM-DD HH:mm:ss zz");
const expiresAt = moment().add(EXPIRY_DAYS, "days");
const body = trimLines(`
Log file generated on ${now}. Expires at ${expiresAt.format("YYYY-MM-DD HH:mm:ss zz")}.${
header ? "\n" + header : ""
}
${lines.join("\n")}
`);
await knex("spam_logs").insert({
id,
guild_id: this.guildId,
body,
expires_at: expiresAt.format("YYYY-MM-DD HH:mm:ss")
});
return id;
}
}

View file

@ -22,6 +22,7 @@ import { ReactionRolesPlugin } from "./plugins/ReactionRoles";
import { CensorPlugin } from "./plugins/Censor"; import { CensorPlugin } from "./plugins/Censor";
import { PersistPlugin } from "./plugins/Persist"; import { PersistPlugin } from "./plugins/Persist";
import { SpamPlugin } from "./plugins/Spam"; import { SpamPlugin } from "./plugins/Spam";
import { LogServerPlugin } from "./plugins/LogServer";
import knex from "./knex"; import knex from "./knex";
// Run latest database migrations // Run latest database migrations
@ -44,7 +45,8 @@ knex.migrate.latest().then(() => {
spam: SpamPlugin spam: SpamPlugin
}, },
globalPlugins: { globalPlugins: {
bot_control: BotControlPlugin bot_control: BotControlPlugin,
log_server: LogServerPlugin
}, },
options: { options: {

9
src/models/SpamLog.ts Normal file
View file

@ -0,0 +1,9 @@
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;
}

44
src/plugins/LogServer.ts Normal file
View file

@ -0,0 +1,44 @@
import http, { ServerResponse } from "http";
import { GlobalPlugin } from "knub";
import { GuildSpamLogs } from "../data/GuildSpamLogs";
const DEFAULT_PORT = 9920;
const logUrlRegex = /^\/spam-logs\/([a-z0-9\-]+)\/?$/i;
function notFound(res: ServerResponse) {
res.statusCode = 404;
res.end("Not Found");
}
/**
* A global plugin that allows bot owners to control the bot
*/
export class LogServerPlugin extends GlobalPlugin {
protected spamLogs: GuildSpamLogs;
protected server: http.Server;
onLoad() {
this.spamLogs = new GuildSpamLogs(null);
this.server = http.createServer(async (req, res) => {
const logId = req.url.match(logUrlRegex);
if (!logId) return notFound(res);
if (logId) {
const log = await this.spamLogs.find(logId[1]);
if (!log) return notFound(res);
res.setHeader("Content-Type", "text/plain; charset=UTF-8");
res.end(log.body);
}
});
this.server.listen(this.configValue("port", DEFAULT_PORT));
}
async onUnload() {
return new Promise(resolve => {
this.server.close(() => resolve());
});
}
}

View file

@ -1,7 +1,6 @@
import { decorators as d, Plugin } from "knub"; import { decorators as d, Plugin } from "knub";
import { Message, TextChannel } from "eris"; import { Message, TextChannel } from "eris";
import { import {
cleanMessagesInChannel,
getEmojiInString, getEmojiInString,
getRoleMentions, getRoleMentions,
getUrlsInString, getUrlsInString,
@ -12,6 +11,7 @@ import { LogType } from "../data/LogType";
import { GuildLogs } from "../data/GuildLogs"; import { GuildLogs } from "../data/GuildLogs";
import { ModActionsPlugin } from "./ModActions"; import { ModActionsPlugin } from "./ModActions";
import { CaseType } from "../data/CaseType"; import { CaseType } from "../data/CaseType";
import { GuildSpamLogs } from "../data/GuildSpamLogs";
enum RecentActionType { enum RecentActionType {
Message = 1, Message = 1,
@ -35,6 +35,7 @@ const MAX_INTERVAL = 300;
export class SpamPlugin extends Plugin { export class SpamPlugin extends Plugin {
protected logs: GuildLogs; protected logs: GuildLogs;
protected spamLogs: GuildSpamLogs;
protected recentActions: IRecentAction[]; protected recentActions: IRecentAction[];
@ -56,6 +57,7 @@ export class SpamPlugin extends Plugin {
onLoad() { onLoad() {
this.logs = new GuildLogs(this.guildId); this.logs = new GuildLogs(this.guildId);
this.spamLogs = new GuildSpamLogs(this.guildId);
this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60); this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60);
this.recentActions = []; this.recentActions = [];
} }
@ -112,6 +114,17 @@ export class SpamPlugin extends Plugin {
this.recentActions = this.recentActions.filter(action => action.timestamp >= expiryTimestamp); this.recentActions = this.recentActions.filter(action => action.timestamp >= expiryTimestamp);
} }
async saveSpamLogs(messages: Message[]) {
const channel = messages[0].channel as TextChannel;
const header = `Server: ${this.guild.name} (${this.guild.id}), channel: #${channel.name} (${
channel.id
})`;
const logId = await this.spamLogs.createFromMessages(messages, header);
const url = this.knub.getGlobalConfig().url;
return url ? `${url}/spam-logs/${logId}` : `Log ID: ${logId}`;
}
async detectSpam( async detectSpam(
msg: Message, msg: Message,
type: RecentActionType, type: RecentActionType,
@ -137,13 +150,15 @@ export class SpamPlugin extends Plugin {
const msgIds = recentActions.map(a => a.msg.id); const msgIds = recentActions.map(a => a.msg.id);
await this.bot.deleteMessages(msg.channel.id, msgIds); await this.bot.deleteMessages(msg.channel.id, msgIds);
const logUrl = await this.saveSpamLogs(recentActions.map(a => a.msg));
this.logs.log(LogType.SPAM_DELETE, { this.logs.log(LogType.SPAM_DELETE, {
member: stripObjectToScalars(msg.member, ["user"]), member: stripObjectToScalars(msg.member, ["user"]),
channel: stripObjectToScalars(msg.channel), channel: stripObjectToScalars(msg.channel),
description, description,
limit: spamConfig.count, limit: spamConfig.count,
interval: spamConfig.interval interval: spamConfig.interval,
logUrl
}); });
} }

View file

@ -249,7 +249,6 @@ export function getUserMentions(str: string) {
// tslint:disable-next-line // tslint:disable-next-line
while ((match = regex.exec(str)) !== null) { while ((match = regex.exec(str)) !== null) {
console.log("m", match);
userIds.push(match[1]); userIds.push(match[1]);
} }