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

Add censor plugin. Various other fixes/features. Update Knub to 9.4.13.

This commit is contained in:
Dragory 2018-07-30 01:44:03 +03:00
parent 0c806f32fd
commit e7734c558c
9 changed files with 283 additions and 43 deletions

11
package-lock.json generated
View file

@ -2176,9 +2176,9 @@
}
},
"knub": {
"version": "9.4.11",
"resolved": "https://registry.npmjs.org/knub/-/knub-9.4.11.tgz",
"integrity": "sha512-C/Ps3jegzgVfaKfcyumUhPdFd269t4yuAUWnXf71S3d/6MCLhVMfGehG0Ma2AMb5M85DqN8ge9n3OME3k1zwJw==",
"version": "9.4.13",
"resolved": "https://registry.npmjs.org/knub/-/knub-9.4.13.tgz",
"integrity": "sha512-4m5IMbctg1xAe6DoYSkk1jdQNWpUb6ZkjKxJPxHEmbXtIZm11qt/AmIcASgG5pvZOM7Q/PnsbLfRyzlUTbvOLA==",
"requires": {
"escape-string-regexp": "^1.0.5",
"js-yaml": "^3.9.1",
@ -3670,6 +3670,11 @@
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=",
"dev": true
},
"tlds": {
"version": "1.203.1",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.203.1.tgz",
"integrity": "sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw=="
},
"to-object-path": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",

View file

@ -29,14 +29,16 @@
"@types/node": "^8.0.50",
"dotenv": "^4.0.0",
"eris": "^0.8.6",
"escape-string-regexp": "^1.0.5",
"humanize-duration": "^3.15.0",
"knex": "^0.14.6",
"knub": "^9.4.11",
"knub": "^9.4.13",
"lodash.at": "^4.6.0",
"lodash.difference": "^4.5.0",
"lodash.isequal": "^4.5.0",
"mariasql": "^0.2.6",
"moment-timezone": "^0.5.21",
"tlds": "^1.203.1",
"ts-node": "^3.3.0",
"typescript": "^2.9.2"
},

View file

@ -24,17 +24,18 @@
"ROLE_EDIT": "🖊 Role **{role.name}** was edited",
"MESSAGE_EDIT": "✏ **{member.user.username}#{member.user.discriminator}** (`{member.id}`) message edited in **{channel.name}**:\n`B:` {before}\n`A:` {after}",
"MESSAGE_DELETE": "🗑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) message deleted in **{channel.name}**:\n{messageText}{attachments}",
"MESSAGE_DELETE": "🗑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) message deleted in **{channel.name}**:\n```{messageText}```",
"MESSAGE_DELETE_BULK": "🗑 **{count}** messages deleted in **{channel.name}**",
"MESSAGE_DELETE_BARE": "🗑 message (id `{messageId}`) deleted in **{channel.name}** (no more info available due to restart)",
"VOICE_CHANNEL_JOIN": "🔸 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) joined **{channel.name}**",
"VOICE_CHANNEL_MOVE": "🔹 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) moved **{oldChannel.name}** ➞ **{newChannel.name}**",
"VOICE_CHANNEL_LEAVE": "♦ **{member.user.username}#{member.user.discriminator}** (`{member.id}`) left **{channel.name}**",
"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}`) triggered spam filter: **{filterName}**",
"CENSOR": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) censored message in **{channel.name}**:\n`{messageText}`",
"CENSOR": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) censored message in **#{channel.name}** (`{channel.id}`) {reason}:\n```{messageText}```",
"CASE_CREATE": "✏ **{member.user.username}#{member.user.discriminator}** (`{member.id}`) manually created new **{caseType}** case (#{caseNum})"
}

View file

@ -21,6 +21,7 @@ export class GuildLogs extends EventEmitter {
super();
this.guildId = guildId;
this.ignoredLogs = [];
// Store the instance for this guild so it can be returned later if a new instance for this guild is requested
guildInstances.set(guildId, this);

View file

@ -24,6 +24,7 @@ export enum LogType {
MESSAGE_EDIT,
MESSAGE_DELETE,
MESSAGE_DELETE_BULK,
MESSAGE_DELETE_BARE,
VOICE_CHANNEL_JOIN,
VOICE_CHANNEL_LEAVE,

View file

@ -19,6 +19,7 @@ import { UtilityPlugin } from "./plugins/Utility";
import { LogsPlugin } from "./plugins/Logs";
import { PostPlugin } from "./plugins/Post";
import { ReactionRolesPlugin } from "./plugins/ReactionRoles";
import { CensorPlugin } from "./plugins/Censor";
import knex from "./knex";
// Run latest database migrations
@ -32,7 +33,8 @@ knex.migrate.latest().then(() => {
mod_actions: ModActionsPlugin,
logs: LogsPlugin,
post: PostPlugin,
reaction_roles: ReactionRolesPlugin
reaction_roles: ReactionRolesPlugin,
censor: CensorPlugin
},
globalPlugins: {
bot_control: BotControlPlugin

212
src/plugins/Censor.ts Normal file
View file

@ -0,0 +1,212 @@
import { Plugin, decorators as d } from "knub";
import { Invite, Message } from "eris";
import url from "url";
import tlds from "tlds";
import escapeStringRegexp from "escape-string-regexp";
import { GuildLogs } from "../data/GuildLogs";
import { LogType } from "../data/LogType";
import { stripObjectToScalars } from "../utils";
const urlRegex = /(\S+\.\S+)/g;
const protocolRegex = /^[a-z]+:\/\//;
const getInviteCodesInString = (str: string): string[] => {
const inviteCodeRegex = /(?:discord.gg|discordapp.com\/invite)\/([a-z0-9]+)/gi;
const inviteCodes = [];
let match;
// tslint:disable-next-line
while ((match = inviteCodeRegex.exec(str)) !== null) {
inviteCodes.push(match[1]);
}
return inviteCodes;
};
const getUrlsInString = (str: string): url.URL[] => {
const matches = str.match(urlRegex) || [];
return matches.reduce((urls, match) => {
if (!protocolRegex.test(match)) {
match = `https://${match}`;
}
let matchUrl: url.URL;
try {
matchUrl = new url.URL(match);
} catch (e) {
return urls;
}
const hostnameParts = matchUrl.hostname.split(".");
const tld = hostnameParts[hostnameParts.length - 1];
if (tlds.includes(tld)) {
urls.push(matchUrl);
}
return urls;
}, []);
};
export class CensorPlugin extends Plugin {
protected serverLogs: GuildLogs;
getDefaultOptions() {
return {
config: {
filter_invites: false,
invite_guild_whitelist: null,
invite_guild_blacklist: null,
invite_code_whitelist: null,
invite_code_blacklist: null,
filter_domains: false,
domain_whitelist: null,
domain_blacklist: null,
blocked_tokens: null,
blocked_words: null,
blocked_regex: null
},
overrides: [
{
level: ">=50",
config: {
filter_invites: false,
filter_domains: false,
blocked_tokens: null,
blocked_words: null,
blocked_regex: null
}
}
]
};
}
onLoad() {
this.serverLogs = new GuildLogs(this.guildId);
}
async censorMessage(msg: Message, reason: string) {
this.serverLogs.ignoreLog(LogType.MESSAGE_DELETE, msg.id);
try {
await msg.delete("Censored");
} catch (e) {
return;
}
this.serverLogs.log(LogType.CENSOR, {
member: stripObjectToScalars(msg.member, ["user"]),
channel: stripObjectToScalars(msg.channel),
reason,
messageText: msg.cleanContent
});
}
async applyFiltersToMsg(msg: Message) {
if (msg.author.bot) return;
if (msg.type !== 0) return;
if (!msg.content) return;
// Filter invites
if (this.configValueForMsg(msg, "filter_invites")) {
const inviteGuildWhitelist: string[] = this.configValueForMsg(msg, "invite_guild_whitelist");
const inviteGuildBlacklist: string[] = this.configValueForMsg(msg, "invite_guild_blacklist");
const inviteCodeWhitelist: string[] = this.configValueForMsg(msg, "invite_code_whitelist");
const inviteCodeBlacklist: string[] = this.configValueForMsg(msg, "invite_code_blacklist");
const inviteCodes = getInviteCodesInString(msg.content);
const invites: Invite[] = await Promise.all(
inviteCodes.map(code => this.bot.getInvite(code))
);
for (const invite of invites) {
if (inviteGuildWhitelist && !inviteGuildWhitelist.includes(invite.guild.id)) {
this.censorMessage(
msg,
`invite guild (**${invite.guild.name}** \`${invite.guild.id}\`) not found in whitelist`
);
return;
}
if (inviteGuildBlacklist && inviteGuildBlacklist.includes(invite.guild.id)) {
this.censorMessage(
msg,
`invite guild (**${invite.guild.name}** \`${invite.guild.id}\`) found in blacklist`
);
return;
}
if (inviteCodeWhitelist && !inviteCodeWhitelist.includes(invite.code)) {
this.censorMessage(msg, `invite code (\`${invite.code}\`) not found in whitelist`);
return;
}
if (inviteCodeBlacklist && inviteCodeBlacklist.includes(invite.code)) {
this.censorMessage(msg, `invite code (\`${invite.code}\`) found in blacklist`);
return;
}
}
}
// Filter domains
if (this.configValueForMsg(msg, "filter_domains")) {
const domainWhitelist: string[] = this.configValueForMsg(msg, "domain_whitelist");
const domainBlacklist: string[] = this.configValueForMsg(msg, "domain_blacklist");
const urls = getUrlsInString(msg.content);
for (const thisUrl of urls) {
if (domainWhitelist && !domainWhitelist.includes(thisUrl.hostname)) {
this.censorMessage(msg, `domain (\`${thisUrl.hostname}\`) not found in whitelist`);
return;
}
if (domainBlacklist && domainBlacklist.includes(thisUrl.hostname)) {
this.censorMessage(msg, `domain (\`${thisUrl.hostname}\`) found in blacklist`);
return;
}
}
}
// Filter tokens
const blockedTokens = this.configValueForMsg(msg, "blocked_tokens") || [];
for (const token of blockedTokens) {
if (msg.content.includes(token)) {
this.censorMessage(msg, `blocked token (\`${token}\`) found`);
return;
}
}
// Filter words
const blockedWords = this.configValueForMsg(msg, "blocked_words") || [];
for (const word of blockedWords) {
const regex = new RegExp(`\\b${escapeStringRegexp(word)}\\b`, "i");
if (regex.test(msg.content)) {
this.censorMessage(msg, `blocked word (\`${word}\`) found`);
return;
}
}
// Filter regex
const blockedRegex = this.configValueForMsg(msg, "blocked_regex") || [];
for (const regexStr of blockedRegex) {
const regex = new RegExp(regexStr);
if (regex.test(msg.content)) {
this.censorMessage(msg, `blocked regex (\`${regexStr}\`) found`);
return;
}
}
}
@d.event("messageCreate")
async onMessageCreate(msg: Message) {
this.applyFiltersToMsg(msg);
}
@d.event("messageUpdate")
async onMessageUpdate(msg: Message) {
this.applyFiltersToMsg(msg);
}
}

View file

@ -1,15 +1,7 @@
import { decorators as d, Plugin } from "knub";
import { GuildLogs } from "../data/GuildLogs";
import { LogType } from "../data/LogType";
import {
Channel,
Constants as ErisConstants,
Member,
Message,
PrivateChannel,
TextChannel,
User
} from "eris";
import { Channel, Constants as ErisConstants, Member, Message, TextChannel, User } from "eris";
import { findRelevantAuditLogEntry, formatTemplateString, stripObjectToScalars } from "../utils";
import DefaultLogMessages from "../data/DefaultLogMessages.json";
import moment from "moment-timezone";
@ -98,7 +90,7 @@ export class LogsPlugin extends Plugin {
round: true
});
this.log(LogType.MEMBER_JOIN, {
this.serverLogs.log(LogType.MEMBER_JOIN, {
member: stripObjectToScalars(member, ["user"]),
new: member.createdAt >= newThreshold ? " :new:" : "",
account_age: accountAge
@ -107,7 +99,7 @@ export class LogsPlugin extends Plugin {
@d.event("guildMemberRemove")
onMemberLeave(_, member) {
this.log(LogType.MEMBER_LEAVE, {
this.serverLogs.log(LogType.MEMBER_LEAVE, {
member: stripObjectToScalars(member, ["user"])
});
}
@ -121,7 +113,7 @@ export class LogsPlugin extends Plugin {
);
const mod = relevantAuditLogEntry ? relevantAuditLogEntry.user : unknownUser;
this.log(LogType.MEMBER_BAN, {
this.serverLogs.log(LogType.MEMBER_BAN, {
user: stripObjectToScalars(user),
mod: stripObjectToScalars(mod)
});
@ -136,7 +128,7 @@ export class LogsPlugin extends Plugin {
);
const mod = relevantAuditLogEntry ? relevantAuditLogEntry.user : unknownUser;
this.log(LogType.MEMBER_UNBAN, {
this.serverLogs.log(LogType.MEMBER_UNBAN, {
user: stripObjectToScalars(user),
mod: stripObjectToScalars(mod)
});
@ -147,7 +139,7 @@ export class LogsPlugin extends Plugin {
if (!oldMember) return;
if (member.nick !== oldMember.nick) {
this.log(LogType.MEMBER_NICK_CHANGE, {
this.serverLogs.log(LogType.MEMBER_NICK_CHANGE, {
member,
oldNick: oldMember.nick,
newNick: member.nick
@ -166,13 +158,13 @@ export class LogsPlugin extends Plugin {
const mod = relevantAuditLogEntry ? relevantAuditLogEntry.user : unknownUser;
if (addedRoles.length) {
this.log(LogType.MEMBER_ROLE_ADD, {
this.serverLogs.log(LogType.MEMBER_ROLE_ADD, {
member,
role: this.guild.roles.get(addedRoles[0]),
mod: stripObjectToScalars(mod)
});
} else if (removedRoles.length) {
this.log(LogType.MEMBER_ROLE_REMOVE, {
this.serverLogs.log(LogType.MEMBER_ROLE_REMOVE, {
member,
role: this.guild.roles.get(removedRoles[0]),
mod: stripObjectToScalars(mod)
@ -187,7 +179,7 @@ export class LogsPlugin extends Plugin {
if (user.username !== oldUser.username || user.discriminator !== oldUser.discriminator) {
const member = this.guild.members.get(user.id) || { id: user.id, user };
this.log(LogType.MEMBER_USERNAME_CHANGE, {
this.serverLogs.log(LogType.MEMBER_USERNAME_CHANGE, {
member: stripObjectToScalars(member, ["user"]),
oldName: `${oldUser.username}#${oldUser.discriminator}`,
newName: `${user.username}#${user.discriminator}`
@ -197,28 +189,28 @@ export class LogsPlugin extends Plugin {
@d.event("channelCreate")
onChannelCreate(channel) {
this.log(LogType.CHANNEL_CREATE, {
this.serverLogs.log(LogType.CHANNEL_CREATE, {
channel: stripObjectToScalars(channel)
});
}
@d.event("channelDelete")
onChannelDelete(channel) {
this.log(LogType.CHANNEL_DELETE, {
this.serverLogs.log(LogType.CHANNEL_DELETE, {
channel: stripObjectToScalars(channel)
});
}
@d.event("guildRoleCreate")
onRoleCreate(_, role) {
this.log(LogType.ROLE_CREATE, {
this.serverLogs.log(LogType.ROLE_CREATE, {
role: stripObjectToScalars(role)
});
}
@d.event("guildRoleDelete")
onRoleDelete(_, role) {
this.log(LogType.ROLE_DELETE, {
this.serverLogs.log(LogType.ROLE_DELETE, {
role: stripObjectToScalars(role)
});
}
@ -226,8 +218,9 @@ export class LogsPlugin extends Plugin {
@d.event("messageUpdate")
onMessageUpdate(msg: Message, oldMsg: Message) {
if (oldMsg && msg.content === oldMsg.content) return;
if (msg.type !== 0) return;
this.log(LogType.MESSAGE_EDIT, {
this.serverLogs.log(LogType.MESSAGE_EDIT, {
member: stripObjectToScalars(msg.member, ["user"]),
channel: stripObjectToScalars(msg.channel),
before: oldMsg ? oldMsg.content || "" : "Unavailable due to restart",
@ -237,16 +230,33 @@ export class LogsPlugin extends Plugin {
@d.event("messageDelete")
onMessageDelete(msg: Message) {
this.log(LogType.MESSAGE_DELETE, {
member: stripObjectToScalars(msg.member, ["user"]),
channel: stripObjectToScalars(msg.channel),
messageText: msg.cleanContent || ""
});
if (msg.type !== 0) return;
if (msg.member) {
this.serverLogs.log(
LogType.MESSAGE_DELETE,
{
member: stripObjectToScalars(msg.member, ["user"]),
channel: stripObjectToScalars(msg.channel),
messageText: msg.cleanContent || ""
},
msg.id
);
} else {
this.serverLogs.log(
LogType.MESSAGE_DELETE_BARE,
{
messageId: msg.id,
channel: stripObjectToScalars(msg.channel)
},
msg.id
);
}
}
@d.event("messageDeleteBulk")
onMessageDeleteBulk(messages: Message[]) {
this.log(LogType.MESSAGE_DELETE_BULK, {
this.serverLogs.log(LogType.MESSAGE_DELETE_BULK, {
count: messages.length,
channel: messages[0] ? messages[0].channel : null
});
@ -254,7 +264,7 @@ export class LogsPlugin extends Plugin {
@d.event("voiceChannelJoin")
onVoiceChannelJoin(member: Member, channel: Channel) {
this.log(LogType.VOICE_CHANNEL_JOIN, {
this.serverLogs.log(LogType.VOICE_CHANNEL_JOIN, {
member: stripObjectToScalars(member, ["user"]),
channel: stripObjectToScalars(channel)
});
@ -262,7 +272,7 @@ export class LogsPlugin extends Plugin {
@d.event("voiceChannelLeave")
onVoiceChannelLeave(member: Member, channel: Channel) {
this.log(LogType.VOICE_CHANNEL_LEAVE, {
this.serverLogs.log(LogType.VOICE_CHANNEL_LEAVE, {
member: stripObjectToScalars(member, ["user"]),
channel: stripObjectToScalars(channel)
});
@ -270,7 +280,7 @@ export class LogsPlugin extends Plugin {
@d.event("voiceChannelSwitch")
onVoiceChannelSwitch(member: Member, newChannel: Channel, oldChannel: Channel) {
this.log(LogType.VOICE_CHANNEL_MOVE, {
this.serverLogs.log(LogType.VOICE_CHANNEL_MOVE, {
member: stripObjectToScalars(member, ["user"]),
oldChannel: stripObjectToScalars(oldChannel),
newChannel: stripObjectToScalars(newChannel)

View file

@ -220,6 +220,12 @@ export class ModActionsPlugin extends Plugin {
@d.command("warn", "<member:Member> <reason:string$>")
@d.permission("warn")
async warnCmd(msg: Message, args: any) {
// Make sure we're allowed to warn this member
if (!this.canActOn(msg.member, args.member)) {
msg.channel.createMessage(errorMessage("Cannot warn: insufficient permissions"));
return;
}
const warnMessage = this.configValue("warn_message")
.replace("{guildName}", this.guild.name)
.replace("{reason}", args.reason);
@ -235,7 +241,7 @@ export class ModActionsPlugin extends Plugin {
const failedMsg = await msg.channel.createMessage(
"Failed to message the user. Log the warning anyway?"
);
const reply = await waitForReaction(this.bot, failedMsg, ["✅", "❌"]);
const reply = await waitForReaction(this.bot, failedMsg, ["✅", "❌"], msg.author.id);
failedMsg.delete();
if (!reply || reply.name === "❌") {
return;
@ -508,11 +514,11 @@ export class ModActionsPlugin extends Plugin {
});
}
@d.command("addcase", "<type:string> <target:userId> [reason:string$]")
@d.command("addcase", "<type:string> <target:string> [reason:string$]")
@d.permission("addcase")
async addcaseCmd(msg: Message, args: any) {
// Verify the user id is a valid snowflake-ish
if (!args.type.match(/^[0-9]{17,20}$/)) {
if (!args.target.match(/^[0-9]{17,20}$/)) {
msg.channel.createMessage(errorMessage("Cannot add case: invalid user id"));
return;
}
@ -535,7 +541,7 @@ export class ModActionsPlugin extends Plugin {
// Create the case
const caseId = await this.createCase(
args.userId,
args.target,
msg.author.id,
CaseType[type],
null,