This commit is contained in:
nolan 2021-06-15 23:50:09 -07:00
commit fdb31aa0f4
16 changed files with 222 additions and 45 deletions

View File

@ -36,6 +36,7 @@
"request": "^2.88.2",
"similarity": "^1.2.1",
"timestring": "^6.0.0",
"twemoji-parser": "^13.1.0",
"winston": "^3.2.1",
"winston-transport": "^4.3.0"
},

View File

@ -16,9 +16,10 @@ class RateLimiter {
this.deleteTimeouts = {}; //same as above
this.lastSend = {}; //used by limitSend
this.lastDelete = {};
this.sendInterval = 7.5; //How frequently sending is allowed in seconds
this.deleteInterval = 1.5; //How frequently delete queues should be executed
this.deleteInterval = 2.5; //How frequently delete queues should be executed
}
@ -41,7 +42,8 @@ class RateLimiter {
if(!this.deleteQueue[channel.id]) this.deleteQueue[channel.id] = [];
this.deleteQueue[channel.id].push({ message, resolve, reject });
if(!this.deleteTimeouts[channel.id] || this.deleteTimeouts[channel.id]._destroyed) this.deleteTimeouts[channel.id] = setTimeout(this.delete.bind(this), this.deleteInterval*1000, channel);
//if(!this.deleteTimeouts[channel.id] || this.deleteTimeouts[channel.id]._destroyed) this.deleteTimeouts[channel.id] = setTimeout(this.delete.bind(this), this.deleteInterval*1000, channel);
this.delete(channel);
});
@ -56,14 +58,23 @@ class RateLimiter {
queue = [...this.deleteQueue[channel.id]],
deleteThese = [];
for(const item of queue) {
const lastDelete = this.lastDelete[channel.id];
const now = Math.floor(Date.now() / 1000);
if (now - lastDelete < this.deleteInterval) {
const timeout = this.deleteTimeouts[channel.id];
if (!timeout || timeout._destroyed) this.deleteTimeouts[channel.id] = setTimeout(this.delete.bind(this), this.deleteInterval*1000, channel);
return;
}
this.lastDelete[channel.id] = now;
for(const item of queue) { // Organise into arrays
const { message, resolve, reject } = item;
if(deleteThese.length <= 100) {
deleteThese.push(message);
resolves.push(resolve);
rejects.push(reject);
this.deleteQueue[channel.id].shift();
} else {
} else { // left over messages go in next batch
this.deleteTimeouts[channel.id] = setTimeout(this.delete.bind(this), this.deleteInterval*1000, channel);
break;
}

View File

@ -1,6 +1,7 @@
const timestring = require('timestring');
const moment = require('moment');
const dns = require('dns');
const { parse: resolveTwemoji } = require('twemoji-parser');
const { InfractionResolves } = require('../../util/Constants.js');
// eslint-disable-next-line no-unused-vars
@ -593,9 +594,9 @@ class Resolver {
const { roles } = guild;
const resolved = [];
for(const resolveable of resolveables) {
const id = /^(<@&)?([0-9]{16,22})>?/iu;
const id = /^(<@&)?([0-9]{16,22})>?/iu;
for(const resolveable of resolveables) {
if(id.test(resolveable)) {
@ -710,6 +711,74 @@ class Resolver {
return { parsed, parameters };
}
/**
* Resolves input into an emoji, either a custom or a unicode emoji. Returned object will have a type property indicating whether it's custom or unicode.
*
* @param {Array<String>} [resolveables=[]]
* @param {boolean} strict
* @param {Guild} guild
* @return {Object}
* @memberof Resolver
*/
async resolveEmojis(resolveables = [], strict, guild) {
if (typeof resolveables === 'string') resolveables = [resolveables];
if (resolveables.length === 0) return false;
//const { emojis: guildEmojis } = guild;
const { emojis: clientEmojis } = this.client;
const resolved = [];
const emojiMention = /<(?:a)?:?([\w-]{2,32}):(\d{17,32})>/iu;
for (const resolveable of resolveables) {
if (emojiMention.test(resolveable)) {
const match = resolveable.match(emojiMention);
const [, , eId] = match;
const emoji = clientEmojis.resolve(eId); //.catch(this.client.logger.error); // use .fetch(eId) once v13 rolls out
if (emoji) resolved.push({ type: 'custom', emoji });
} else {
const sorter = (a, b) => a.name.length - b.name.length;
const filter = (e) => {
if (!strict) return e.name.toLowerCase().includes(resolveable.toLowerCase());
return e.name.toLowerCase() === resolveable.toLowerCase();
};
let emoji = null;
if (guild) emoji = guild.emojis.cache.sort(sorter).filter(filter).first();
if (!emoji) emoji = clientEmojis.cache.sort(sorter).filter(filter).first();
if (emoji) {
resolved.push({ type: 'custom', emoji });
continue;
} else { // twemoji parse
const [result] = resolveTwemoji(resolveable);
if (result) {
({ text: emoji } = result);
resolved.push({ type: 'unicode', emoji });
}
}
}
}
return resolved.length > 0 ? resolved : false;
}
async resolveEmoji(resolveable, strict, guild) {
if (!resolveable) return false;
if (resolveable instanceof Array) throw new Error('Resolveable cannot be of type Array, use resolveEmojis for resolving arrays of emojis');
const result = await this.resolveEmojis([resolveable], strict, guild);
return result ? result[0] : false;
}
}

View File

@ -82,7 +82,7 @@ class StatsCommand extends Command {
});
return acc;
}, {});
totalValues.uptime = this.client.resolver.timeAgo(Math.floor(totalValues.uptime / 1000), true, true, true);
totalValues.uptime = this.client.resolver.timeAgo(Math.floor(totalValues.uptime/ shard.count / 1000), true, true, true);
totalValues.avgMem = Math.floor(totalValues.memory / shard.count);
//System information

View File

@ -182,12 +182,23 @@ module.exports = class AutoModeration extends Observer {
for (const reg of regex) {
const match = content.match(new RegExp(reg, 'iu'));
const match = content.match(new RegExp(`(?:^|\\s)(${reg})`, 'iu')); // (?:^|\\s) |un
if (match) {
log += `\nMessage matched with "${reg}" in the regex list.\nMatch: ${match[0]}\nFull content: ${content}`;
//log += `\next reg: ${tmp}`;
const fullWord = words.find((word) => word.includes(match[1]));
let inWL = false;
try { // This is for debugging only
inWL = this.whitelist.find(fullWord);
} catch (err) {
this.client.logger.debug(fullWord, match[0], words);
}
if (inWL || whitelist.some((word) => word === fullWord)) continue;
log += `\nMessage matched with "${reg}" in the regex list.\nMatch: ${match[0]}, Full word: ${fullWord}\nFull content: ${content}`;
filterResult = {
match: match[0],
match: fullWord,
matched: true,
_matcher: match[0].toLowerCase(),
matcher: `Regex: __${reg}__`,
@ -310,7 +321,7 @@ module.exports = class AutoModeration extends Observer {
for (const reg of words) {
match = content.match(new RegExp(reg, 'iu'));
match = content.match(new RegExp(`(?:^|\\s)(${reg})`, 'iu'));
if (match) break;
@ -326,7 +337,7 @@ module.exports = class AutoModeration extends Observer {
`, // ** User:** <@${ author.id }>
color: 15120384,
fields: context.reverse().reduce((acc, val) => {
const text = val.content.length ? val.content.replace(match, '**__$&__**') : '**NO CONTENT**';
const text = val.content.length ? Util.escapeMarkdown(val.content).replace(match[1], '**__$&__**') : '**NO CONTENT**';
acc.push({
name: `${val.author.tag} (${val.author.id}) - ${val.id}`,
value: text.length < 1024 ? text : text.substring(0, 1013) + '...'
@ -339,7 +350,9 @@ module.exports = class AutoModeration extends Observer {
}, [])
};
logChannel.send({ embed });
const sent = await logChannel.send({ embed }).catch((err) => {
this.client.logger.error('Error in message flag:\n' + err.stack);
});
}

View File

@ -389,15 +389,16 @@ class CommandHandler extends Observer {
const silent = inhibitors.filter((i) => i.inhibitor.silent);
const nonsilent = inhibitors.filter((i) => !i.inhibitor.silent);
if(nonsilent.length === 0 && silent.length > 0) return undefined;
if(nonsilent.length > 0) return this.handleError(message, { type: 'inhibitor', ...nonsilent[0] });
if (silent.length && silent.some((result) => result.inhibitor.id === 'channelIgnore')) return;
if (nonsilent.length === 0 && silent.length > 0) return undefined;
if (nonsilent.length > 0) return this.handleError(message, { type: 'inhibitor', ...nonsilent[0] });
const resolved = await message.resolve();
if(resolved.error) {
if (resolved.error) {
this.client.logger.error(`Command Error | ${message.command.resolveable} | Message ID: ${message.id}\n${resolved.message.stack || resolved.message}`);
if(resolved.message.code === 50013) {
if (resolved.message.code === 50013) {
const missing = message.channel.permissionsFor(message.guild.me).missing(['EMBED_LINKS']);
if(missing.length > 0) {
if (missing.length > 0) {
return message.respond(message.format('COMMANDHANDLER_COMMAND_MISSINGPERMISSIONS'), {
emoji: 'failure'
});
@ -428,7 +429,7 @@ class CommandHandler extends Observer {
const reasons = (await Promise.all(promises)).filter((p) => p.error); // Filters out inhibitors with only errors.
if(reasons.length === 0) return [];
reasons.sort((a, b) => b.inhibitor.priority - a.inhibitor.priority); // Sorts inhibitor errors by most important.
reasons.sort((a, b) => a.inhibitor.priority - b.inhibitor.priority); // Sorts inhibitor errors by most important.
return reasons;
}

View File

@ -72,7 +72,11 @@ class GuildLogger extends Observer {
await message.guild.settings();
if (!message.member) message.member = await message.guild.members.fetch(message.author.id).catch();
if (!message.member) try {
message.member = await message.guild.members.fetch(message.author.id);
} catch (_) {
// Member not found, do nothing
}
const { messageLog } = message.guild._settings;
if(!messageLog.channel) return undefined;
@ -99,30 +103,42 @@ class GuildLogger extends Observer {
return;
}
const { reference, channel, author, content, id } = message;
const embed = {
// author: {
// name: message.format('MSGLOG_DELETE_TITLE', { channel: message.channel.name, author: Util.escapeMarkdown(message.author.tag) }), //`${message.author.tag} (${message.author.id})`,
// icon_url: message.author.displayAvatarURL({ size: 32 }) //eslint-disable-line camelcase
// },
title: message.format('MSGLOG_DELETE_TITLE', { channel: message.channel.name, author: Util.escapeMarkdown(message.author.tag) }),
description: Util.escapeMarkdown(message.content)?.replace(/\\n/gu, ' ') || message.format('MSGLOG_NOCONTENT'),
title: message.format('MSGLOG_DELETE_TITLE', { channel: channel.name, author: Util.escapeMarkdown(author.tag) }),
description: Util.escapeMarkdown(content)?.replace(/\\n/gu, ' ') || message.format('MSGLOG_NOCONTENT'),
color: CONSTANTS.COLORS.RED,
footer: {
text: message.format('MSGLOG_DELETE_FOOTER', { msgID: message.id, userID: message.author.id })
text: message.format('MSGLOG_DELETE_FOOTER', { msgID: id, userID: author.id })
},
timestamp: message.createdAt
timestamp: message.createdAt,
fields: []
};
if (reference && reference.channelID === channel.id) {
const referenced = await channel.messages.fetch(reference.messageID);
embed.fields.push({
name: message.format('MSGLOG_REPLY', { tag: referenced.author.tag, id: referenced.author.id }),
value: message.format('MSGLOG_REPLY_VALUE', {
content: referenced.content.length > 900 ? referenced.content.substring(0, 900) + '...' : referenced.content,
link: referenced.url
})
});
}
if (message.filtered) {
embed.fields = [
{
name: message.format('MSGLOG_FILTERED'),
value: stripIndents`
embed.fields.push({
name: message.format('MSGLOG_FILTERED'),
value: stripIndents`
${message.format(message.filtered.preset ? 'MSGLOG_FILTERED_PRESET' : 'MSGLOG_FILTERED_VALUE', { ...message.filtered })}
${message.filtered.sanctioned ? message.format('MSGLOG_FILTERED_SANCTIONED') : ''}
`// + ()
}
];
});
}
const uploadedFiles = [];
@ -203,7 +219,9 @@ class GuildLogger extends Observer {
}
hook.send({ embeds: [embed], files: uploadedFiles });
await hook.send({ embeds: [embed], files: uploadedFiles }).catch((err) => {
this.client.logger.error('Error in message delete:\n' + err.stack);
});
/*
if(message.attachments.size > 0) {
@ -337,7 +355,7 @@ class GuildLogger extends Observer {
if (!guild) return;
if (!oldMessage.member) oldMessage.member = await guild.members.fetch(oldMessage.author);
const { member, channel, author } = oldMessage;
const { member, channel, author, reference } = oldMessage;
const settings = await guild.settings();
const chatlogs = settings.messageLog;
@ -386,16 +404,7 @@ class GuildLogger extends Observer {
description: oldMessage.format('MSGLOG_EDIT_JUMP', { guild: guild.id, channel: channel.id, message: oldMessage.id }),
color: CONSTANTS.COLORS.YELLOW,
timestamp: oldMessage.createdAt,
fields: [
// {
// name: oldMessage.format('MSGLOG_EDIT_OLD'),
// value: oldMessage.content.length > 1024 ? oldMessage.content.substring(0, 1021) + '...' : oldMessage.content
// },
// {
// name: oldMessage.format('MSGLOG_EDIT_NEW'),
// value: newMessage.content.length > 1024 ? newMessage.content.substring(0, 1021) + '...' : newMessage.content
// }
]
fields: [ ]
};
const oldCon = oldMessage.content,
@ -419,6 +428,19 @@ class GuildLogger extends Observer {
name: '\u200b',
value: '...' + newCon.substring(1021)
});
if (reference && reference.channelID === channel.id) {
const referenced = await channel.messages.fetch(reference.messageID);
// eslint-disable-next-line no-nested-ternary
const content = referenced.content ? referenced.content.length > 900 ? referenced.content.substring(0, 900) + '...' : referenced.content : oldMessage.format('MSGLOG_REPLY_NOCONTENT');
embed.fields.push({
name: oldMessage.format('MSGLOG_REPLY', { tag: referenced.author.tag, id: referenced.author.id }),
value: oldMessage.format('MSGLOG_REPLY_VALUE', {
content,
link: referenced.url
})
});
}
//if(oldMessage.content.length > 1024) embed.description += '\n' + oldMessage.format('MSGLOG_EDIT_OLD_CUTOFF');
//if(newMessage.content.length > 1024) embed.description += '\n' + oldMessage.format('MSGLOG_EDIT_NEW_CUTOFF');

View File

@ -120,6 +120,11 @@ module.exports = class InviteFilter extends FilterSetting {
}
if (langParams.error) return {
error: true,
msg: langParams.msg
};
await message.guild._updateSettings({ [this.index]: setting });
return {
error: false,

View File

@ -202,6 +202,11 @@ module.exports = class LinkFilter extends FilterSetting {
msg: message.format('ERR_INVALID_METHOD', { method })
};
if (langParams.error) return {
error: true,
msg: langParams.msg
};
await message.guild._updateSettings({ [this.index]: setting });
return {
error: false,

View File

@ -149,6 +149,11 @@ module.exports = class MentionFilter extends FilterSetting {
msg: message.format('ERR_INVALID_METHOD', { method })
};
if (langParams.error) return {
error: true,
msg: langParams.msg
};
await message.guild._updateSettings({ [this.index]: setting });
return {
error: false,

View File

@ -53,6 +53,7 @@ module.exports = class WordFilter extends FilterSetting {
async handle(message, params) {
// eslint-disable-next-line prefer-const
let [method, ...args] = params;
method = method.toLowerCase();
@ -254,6 +255,11 @@ module.exports = class WordFilter extends FilterSetting {
};
}
if (langParams.error) return {
error: true,
msg: langParams.msg
};
await message.guild._updateSettings({ [this.index]: setting });
return {
error: false,
@ -263,6 +269,7 @@ module.exports = class WordFilter extends FilterSetting {
}
async _createTrigger(message, action, actionObject, setting) {
const response = await message.prompt(message.format('S_WORDFILTER_ACTION_ADD_TRIGGERS'), { time: 60 * 1000 });
if (!response) {
if (setting.actions.find((ac) => ac.trigger === 'generic')) return {

View File

@ -64,7 +64,7 @@ class BinaryTree {
*/
find(val) {
val.toLowerCase();
val = val.toLowerCase();
if(this.isEmpty()) return;

View File

@ -72,6 +72,16 @@ Link filter violation.
[MSGLOG_NOCONTENT]
**__NO TEXT CONTENT__**
[MSGLOG_REPLY]
Message was in reply to user {tag} ({id}):
[MSGLOG_REPLY_VALUE]
**[Jump to message]({link})**
{content}
[MSGLOG_REPLY_NOCONTENT]
**__Missing content.__**
[MSGLOG_FILTERED]
The message was filtered:

View File

@ -52,6 +52,8 @@ Can be one of `{valid}`.
> You can cancel this series of prompts by responding with `cancel`.
{wordwatcher}
[S_FILTER_ACTION_ADD_TIMER]
Would you like the **{action}** to have a timer?
Not assigning a timer will use defaults for the corresponding action.
@ -219,6 +221,11 @@ Successfully removed all presets from the regex filter.
[S_WORDWATCHER_DESCRIPTION]
Configure the behaviour of the word watcher.
Wordwatcher is a moderation utility that flags messages for manual review based on keywords.
Keywords can be regex expressions.
Wordwatcher also supports having 5 reactions for quick actions.
[S_WORDWATCHER_TOGGLE]
Successfully toggled the word watcher **{toggle}**.
@ -261,6 +268,18 @@ Successfully removed **{changes}** from the watch list.
[S_WORDWATCHER_CHANNEL]
Will log flagged messages to <#{channel}>.
[S_WORDWATCHER_ACTION_LIMIT]
You've hit the limit of quick actions. Either modify or remove existing ones.
[S_WORDWATCHER_ACTION_ADD_START]
You can only define up to 5 quick actions, you currently have {amount} existing actions.
[S_WORDWATCHER_ACTION_ADD_TRIGGERS]
Which emoji should represent this action?
Make sure it is one that the bot has access to (i.e. from this server or one you know the bot is in).
The bot will use defaults if no emoji is given.
// Wordfilter
[S_WORDFILTER_DESCRIPTION]
Configure the word filtering behaviour for your server.

View File

@ -1,8 +1,7 @@
{
"regex": {
"slurs": [
"n(ae|ji|j|y|i|x|!|1|\\||l)(gg?|qq|99?|bb)(?!(ht|el|un))((e|3)r|let|ur|\\s?nog|y|ah?|or)?s?",
"(?<!u)niqa?",
"n(ae|ji|j|y|i|x|!|1|\\||l)(gg?|qq?|99?|bb)((e|3)r|let|ur|\\s?nog|y|ah?|or)?s?",
"nick\\s?(gurr?|ger|ga)",
"(fur\\s?)?f(e|a|4|x)(gg?|qq|99?)(otry|ots|ot|y|s)?",
"(fur\\s?)?fgg?ts?",
@ -500,8 +499,12 @@
"blobsob",
"poggers",
"night",
"nightmare",
"nightmares",
"nightcore",
"pingers",
"nigeria",
"nigel",
"negative",
"neglect",
"initiative",
@ -560,6 +563,7 @@
"determination",
"tenis",
"nights",
"nights?",
"poin",
"alteration",
"mock",

View File

@ -2350,6 +2350,11 @@ tweetnacl@^1.0.3:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
twemoji-parser@^13.1.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4"
integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"