mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
feat: save deleted spam logs; server spam logs from a web server; update Knub to 9.6.4
This commit is contained in:
parent
847ee11195
commit
16be52a5e7
10 changed files with 167 additions and 10 deletions
15
migrations/20180801185500_create_spam_logs_table.js
Normal file
15
migrations/20180801185500_create_spam_logs_table.js
Normal 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
6
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
72
src/data/GuildSpamLogs.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
9
src/models/SpamLog.ts
Normal 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
44
src/plugins/LogServer.ts
Normal 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue