mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
Rename SpamLogs to Archives. Tweak spam archive format.
This commit is contained in:
parent
c9db802638
commit
e3ff4cef45
5 changed files with 116 additions and 92 deletions
|
@ -0,0 +1,7 @@
|
||||||
|
exports.up = async function(knex) {
|
||||||
|
await knex.schema.renameTable('spam_logs', 'archives');
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function(knex) {
|
||||||
|
await knex.schema.renameTable('archives', 'spam_logs');
|
||||||
|
};
|
51
src/data/GuildArchives.ts
Normal file
51
src/data/GuildArchives.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import uuid from "uuid/v4"; // tslint:disable-line
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import knex from "../knex";
|
||||||
|
import SpamLog from "../models/SpamLog";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
constructor(guildId) {
|
||||||
|
this.guildId = guildId;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateNewLogId() {
|
||||||
|
return uuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(id: string): Promise<SpamLog> {
|
||||||
|
const result = await knex("archives")
|
||||||
|
.where("id", id)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return result ? new SpamLog(result) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(body: string, expiresAt: moment.Moment = null) {
|
||||||
|
const id = this.generateNewLogId();
|
||||||
|
if (!expiresAt) {
|
||||||
|
expiresAt = moment().add(DEFAULT_EXPIRY_DAYS, "days");
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex("archives").insert({
|
||||||
|
id,
|
||||||
|
guild_id: this.guildId,
|
||||||
|
body,
|
||||||
|
expires_at: expiresAt.format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,74 +0,0 @@
|
||||||
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 = 90;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
generateNewLogId() {
|
|
||||||
return uuid();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,10 @@
|
||||||
import http, { ServerResponse } from "http";
|
import http, { ServerResponse } from "http";
|
||||||
import { GlobalPlugin } from "knub";
|
import { GlobalPlugin } from "knub";
|
||||||
import { GuildSpamLogs } from "../data/GuildSpamLogs";
|
import { GuildArchives } from "../data/GuildArchives";
|
||||||
import { sleep } from "../utils";
|
import { sleep } from "../utils";
|
||||||
|
|
||||||
const DEFAULT_PORT = 9920;
|
const DEFAULT_PORT = 9920;
|
||||||
const logUrlRegex = /^\/spam-logs\/([a-z0-9\-]+)\/?$/i;
|
const archivesRegex = /^\/(spam-logs|archives)\/([a-z0-9\-]+)\/?$/i;
|
||||||
|
|
||||||
function notFound(res: ServerResponse) {
|
function notFound(res: ServerResponse) {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
|
@ -15,18 +15,26 @@ function notFound(res: ServerResponse) {
|
||||||
* A global plugin that allows bot owners to control the bot
|
* A global plugin that allows bot owners to control the bot
|
||||||
*/
|
*/
|
||||||
export class LogServerPlugin extends GlobalPlugin {
|
export class LogServerPlugin extends GlobalPlugin {
|
||||||
protected spamLogs: GuildSpamLogs;
|
protected archives: GuildArchives;
|
||||||
protected server: http.Server;
|
protected server: http.Server;
|
||||||
|
|
||||||
async onLoad() {
|
async onLoad() {
|
||||||
this.spamLogs = new GuildSpamLogs(null);
|
this.archives = new GuildArchives(null);
|
||||||
|
|
||||||
this.server = http.createServer(async (req, res) => {
|
this.server = http.createServer(async (req, res) => {
|
||||||
const logId = req.url.match(logUrlRegex);
|
const pathMatch = req.url.match(archivesRegex);
|
||||||
if (!logId) return notFound(res);
|
if (!pathMatch) return notFound(res);
|
||||||
|
|
||||||
if (logId) {
|
const logId = pathMatch[2];
|
||||||
const log = await this.spamLogs.find(logId[1]);
|
|
||||||
|
if (pathMatch[1] === "spam-logs") {
|
||||||
|
res.statusCode = 301;
|
||||||
|
res.setHeader("Location", `/archives/${logId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathMatch) {
|
||||||
|
const log = await this.archives.find(logId);
|
||||||
if (!log) return notFound(res);
|
if (!log) return notFound(res);
|
||||||
|
|
||||||
res.setHeader("Content-Type", "text/plain; charset=UTF-8");
|
res.setHeader("Content-Type", "text/plain; charset=UTF-8");
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { decorators as d, Plugin } from "knub";
|
import { decorators as d, Plugin } from "knub";
|
||||||
import { Message, TextChannel } from "eris";
|
import { Channel, Message, TextChannel, User } from "eris";
|
||||||
import {
|
import {
|
||||||
|
formatTemplateString,
|
||||||
getEmojiInString,
|
getEmojiInString,
|
||||||
getRoleMentions,
|
getRoleMentions,
|
||||||
getUrlsInString,
|
getUrlsInString,
|
||||||
|
@ -13,7 +14,8 @@ 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";
|
import { GuildArchives } from "../data/GuildArchives";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
|
||||||
enum RecentActionType {
|
enum RecentActionType {
|
||||||
Message = 1,
|
Message = 1,
|
||||||
|
@ -35,9 +37,21 @@ interface IRecentAction {
|
||||||
|
|
||||||
const MAX_INTERVAL = 300;
|
const MAX_INTERVAL = 300;
|
||||||
|
|
||||||
|
const ARCHIVE_EXPIRY_DAYS = 90;
|
||||||
|
const ARCHIVE_HEADER_FORMAT = trimLines(`
|
||||||
|
Server: {guild.name} ({guild.id})
|
||||||
|
Channel: #{channel.name} ({channel.id})
|
||||||
|
User: {user.username}#{user.discriminator} ({user.id})
|
||||||
|
`);
|
||||||
|
const ARCHIVE_MESSAGE_FORMAT = "[MSG ID {message.id}] [{timestamp}] {user.username}: {message.content}{attachments}";
|
||||||
|
const ARCHIVE_FOOTER_FORMAT = trimLines(`
|
||||||
|
Log file generated on {timestamp}
|
||||||
|
Expires at {expires}
|
||||||
|
`);
|
||||||
|
|
||||||
export class SpamPlugin extends Plugin {
|
export class SpamPlugin extends Plugin {
|
||||||
protected logs: GuildLogs;
|
protected logs: GuildLogs;
|
||||||
protected spamLogs: GuildSpamLogs;
|
protected archives: GuildArchives;
|
||||||
|
|
||||||
// Handle spam detection with a queue so we don't have overlapping detections on the same user
|
// Handle spam detection with a queue so we don't have overlapping detections on the same user
|
||||||
protected spamDetectionQueue: Promise<void>;
|
protected spamDetectionQueue: Promise<void>;
|
||||||
|
@ -82,7 +96,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.archives = new GuildArchives(this.guildId);
|
||||||
|
|
||||||
this.recentActions = [];
|
this.recentActions = [];
|
||||||
this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60);
|
this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60);
|
||||||
|
@ -138,13 +152,31 @@ 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[]) {
|
async saveSpamArchives(messages: Message[], channel: Channel, user: User) {
|
||||||
const channel = messages[0].channel as TextChannel;
|
const expiresAt = moment().add(ARCHIVE_EXPIRY_DAYS, "days");
|
||||||
const header = `Server: ${this.guild.name} (${this.guild.id}), channel: #${channel.name} (${channel.id})`;
|
|
||||||
const logId = await this.spamLogs.createFromMessages(messages, header);
|
const headerStr = formatTemplateString(ARCHIVE_HEADER_FORMAT, {
|
||||||
|
guild: this.guild,
|
||||||
|
channel,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
const msgLines = messages.map(msg => {
|
||||||
|
return formatTemplateString(ARCHIVE_MESSAGE_FORMAT, {
|
||||||
|
message: msg,
|
||||||
|
timestamp: moment(msg.timestamp, "x").format("HH:mm:ss"),
|
||||||
|
user
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const messagesStr = msgLines.join("\n");
|
||||||
|
const footerStr = formatTemplateString(ARCHIVE_FOOTER_FORMAT, {
|
||||||
|
timestamp: moment().format("YYYY-MM-DD [at] HH:mm:ss (Z)"),
|
||||||
|
expires: expiresAt.format("YYYY-MM-DD [at] HH:mm:ss (Z)")
|
||||||
|
});
|
||||||
|
|
||||||
|
const logId = await this.archives.create([headerStr, messagesStr, footerStr].join("\n\n"), expiresAt);
|
||||||
|
|
||||||
const url = this.knub.getGlobalConfig().url;
|
const url = this.knub.getGlobalConfig().url;
|
||||||
return url ? `${url}/spam-logs/${logId}` : `Log ID: ${logId}`;
|
return url ? `${url}/archives/${logId}` : `Archive ID: ${logId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async logAndDetectSpam(
|
async logAndDetectSpam(
|
||||||
|
@ -225,7 +257,7 @@ export class SpamPlugin extends Plugin {
|
||||||
this.clearRecentUserActions(type, msg.author.id, msg.channel.id);
|
this.clearRecentUserActions(type, msg.author.id, msg.channel.id);
|
||||||
|
|
||||||
// Generate a log from the detected messages
|
// Generate a log from the detected messages
|
||||||
const logUrl = await this.saveSpamLogs(uniqueMessages);
|
const logUrl = await this.saveSpamArchives(uniqueMessages, msg.channel, msg.author);
|
||||||
|
|
||||||
// Create a case and log the actions taken above
|
// Create a case and log the actions taken above
|
||||||
const caseType = spamConfig.mute ? CaseType.Mute : CaseType.Note;
|
const caseType = spamConfig.mute ? CaseType.Mute : CaseType.Note;
|
||||||
|
|
Loading…
Add table
Reference in a new issue