292 lines
8.2 KiB
TypeScript
292 lines
8.2 KiB
TypeScript
import { Sticker, ThreadChannel, Message, User, Invite, GuildMember, GuildScheduledEvent, GuildEmoji, Client, Events, AuditLogEvent, ChannelType, PartialMessage, EmbedBuilder, MessageType } from 'discord.js';
|
|
import { AuditLog, db } from './db';
|
|
import * as log from './log';
|
|
import { formatMessageAsEmbed, getUploadLimitForGuild, shortenStr } from './util';
|
|
import { Change, diffWords } from 'diff';
|
|
|
|
export enum EventType {
|
|
EmojiCreate = 'EMOJI_CREATE',
|
|
EmojiRename = 'EMOJI_RENAME',
|
|
EmojiDelete = 'EMOJI_DELETE',
|
|
|
|
EventCreate = 'EVENT_CREATE',
|
|
EventEdit = 'EVENT_EDIT',
|
|
EventDelete = 'EVENT_DELETE',
|
|
|
|
InviteCreate = 'INVITE_CREATE',
|
|
InviteDelete = 'INVITE_DELETE',
|
|
|
|
MemberBan = 'MEMBER_BAN',
|
|
MemberUnban = 'MEMBER_UNBAN',
|
|
MemberKick = 'MEMBER_KICK',
|
|
MemberDisconnect = 'MEMBER_DISCONNECT',
|
|
MemberNickname = 'MEMBER_NICKNAME',
|
|
MemberJoin = 'MEMBER_JOIN',
|
|
MemberLeave = 'MEMBER_LEAVE',
|
|
|
|
MessageDelete = 'MESSAGE_DELETE',
|
|
MessageEdit = 'MESSAGE_EDIT',
|
|
|
|
StickerCreate = 'STICKER_CREATE',
|
|
StickerRename = 'STICKER_RENAME',
|
|
StickerDelete = 'STICKER_DELETE',
|
|
|
|
ThreadCreate = 'THREAD_CREATE',
|
|
ThreadEdit = 'THREAD_EDIT',
|
|
ThreadDelete = 'THREAD_DELETE',
|
|
}
|
|
|
|
export type Event = {
|
|
type: EventType.EmojiCreate,
|
|
causer: User | null,
|
|
emoji: GuildEmoji,
|
|
} | {
|
|
type: EventType.EmojiRename,
|
|
causer: User | null,
|
|
emoji: GuildEmoji,
|
|
oldName: string, newName: string,
|
|
} | {
|
|
type: EventType.EmojiDelete,
|
|
causer: User | null,
|
|
emoji: GuildEmoji,
|
|
} | {
|
|
type: EventType.EventCreate,
|
|
causer: User | null,
|
|
event: GuildScheduledEvent,
|
|
} | {
|
|
type: EventType.EventEdit,
|
|
causer: User | null,
|
|
oldEvent: GuildScheduledEvent,
|
|
event: GuildScheduledEvent,
|
|
} | {
|
|
type: EventType.EventDelete,
|
|
causer: User | null,
|
|
event: GuildScheduledEvent,
|
|
} | {
|
|
type: EventType.InviteCreate,
|
|
causer: User | null,
|
|
invite: Invite,
|
|
} | {
|
|
type: EventType.InviteDelete,
|
|
causer: User | null,
|
|
invite: Invite,
|
|
} | {
|
|
type: EventType.MemberBan,
|
|
causer: User | null,
|
|
member: GuildMember,
|
|
reason: string,
|
|
} | {
|
|
type: EventType.MemberUnban,
|
|
causer: User | null,
|
|
member: GuildMember,
|
|
reason: string,
|
|
} | {
|
|
type: EventType.MemberKick,
|
|
causer: User | null,
|
|
member: GuildMember,
|
|
reason: string,
|
|
} | {
|
|
type: EventType.MemberDisconnect,
|
|
causer: User | null,
|
|
member: GuildMember,
|
|
} | {
|
|
type: EventType.MemberNickname,
|
|
causer: User | null,
|
|
member: GuildMember,
|
|
oldNickname: string, newNickname: string,
|
|
} | {
|
|
type: EventType.MessageDelete,
|
|
causer: User | null,
|
|
message: Message | PartialMessage,
|
|
} | {
|
|
type: EventType.MessageEdit,
|
|
causer: User | null,
|
|
oldMessage: Message,
|
|
message: Message,
|
|
} | {
|
|
type: EventType.StickerCreate,
|
|
causer: User | null,
|
|
sticker: Sticker,
|
|
} | {
|
|
type: EventType.StickerRename,
|
|
causer: User | null,
|
|
oldName: string, newName: string,
|
|
} | {
|
|
type: EventType.StickerDelete,
|
|
causer: User | null,
|
|
sticker: Sticker,
|
|
} | {
|
|
type: EventType.ThreadCreate,
|
|
causer: User | null,
|
|
thread: ThreadChannel,
|
|
} | {
|
|
type: EventType.ThreadEdit,
|
|
causer: User | null,
|
|
oldThread: ThreadChannel,
|
|
thread: ThreadChannel,
|
|
} | {
|
|
type: EventType.ThreadDelete,
|
|
causer: User | null,
|
|
thread: ThreadChannel,
|
|
};
|
|
|
|
export async function triggerEvent(bot: Client, guildId: string, event: Event) {
|
|
const type = event.type;
|
|
|
|
log.info(`got event ${event.type}`);
|
|
|
|
const logs = await db<AuditLog>('auditLogs')
|
|
.select('guild', 'channel')
|
|
.where('guild', guildId)
|
|
// @ts-expect-error this LITERALLY works
|
|
.whereLike(db.raw('UPPER(eventTypes)'), `%${type.toUpperCase()}%`);
|
|
|
|
for (const auditLog of logs) {
|
|
let channel;
|
|
try {
|
|
channel = await bot.channels.fetch(auditLog.channel);
|
|
} catch (err) {
|
|
log.warn(err);
|
|
}
|
|
|
|
if (channel && (channel.type === ChannelType.GuildText)) {
|
|
switch(event.type) {
|
|
case EventType.MessageDelete: {
|
|
const limit = getUploadLimitForGuild(channel.guild);
|
|
const { embed, attach } = formatMessageAsEmbed(event.message, limit);
|
|
|
|
const logEmbed = new EmbedBuilder()
|
|
.setDescription(`Message sent by ${event.message.author} in <#${event.message.channelId}> deleted by **${event.causer}**:`)
|
|
.setTimestamp();
|
|
|
|
channel.send({
|
|
embeds: [ logEmbed, embed ],
|
|
files: attach ? [{
|
|
attachment: attach.url,
|
|
name: attach.name,
|
|
description: attach.url,
|
|
}] : [],
|
|
});
|
|
|
|
break;
|
|
}
|
|
case EventType.MessageEdit: {
|
|
const logEmbed = new EmbedBuilder()
|
|
.setDescription(`Message sent by ${event.message.author} in <#${event.message.channelId}> edited:`)
|
|
.setTimestamp();
|
|
|
|
const diff = diffWords(event.oldMessage.content.replace(/`/g, ''), event.message.content.replace(/`/g, ''));
|
|
let output = '';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
diff.forEach((part: Change) => {
|
|
if (part.added) output += `\x1B[2;32m\x1B[1;32m${part.value}\x1B[0m`;
|
|
if (part.removed) output += `\x1B[2;41m\x1B[1;2m${part.value}\x1B[0m`;
|
|
if (!part.added && !part.removed) output += part.value;
|
|
});
|
|
|
|
const diffMsg = `\`\`\`ansi\n${shortenStr(output, 4000)}\`\`\``;
|
|
|
|
const editEmbed = new EmbedBuilder()
|
|
.setAuthor({ name: event.message.author.tag, iconURL: event.message.author.displayAvatarURL() })
|
|
.setDescription(diffMsg);
|
|
|
|
channel.send({
|
|
embeds: [ logEmbed, editEmbed ],
|
|
});
|
|
|
|
break;
|
|
}
|
|
case EventType.InviteCreate: {
|
|
const logEmbed = new EmbedBuilder()
|
|
.setDescription(`Invite ${event.invite.code} created by ${event.causer} in <#${event.invite.channelId}>`)
|
|
.setTimestamp();
|
|
|
|
channel.send({
|
|
embeds: [ logEmbed ],
|
|
});
|
|
|
|
break;
|
|
}
|
|
case EventType.InviteDelete: {
|
|
const logEmbed = new EmbedBuilder()
|
|
.setDescription(`Invite ${event.invite.code} made by ${event.invite.inviter} in <#${event.invite.channelId}> deleted by ${event.causer}`)
|
|
.setTimestamp();
|
|
|
|
channel.send({
|
|
embeds: [ logEmbed ],
|
|
});
|
|
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
log.warn(`Channel ${auditLog.channel} from guild ${auditLog.guild} not found! Deleting audit log`);
|
|
await db<AuditLog>('auditLogs')
|
|
.where('guild', auditLog.guild)
|
|
.where('channel', auditLog.channel)
|
|
.delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
const DELETE_FETCH_TIMEOUT = 2_000;
|
|
|
|
export function setupListeners(bot: Client) {
|
|
bot.on(Events.MessageDelete, async message => {
|
|
if (!message.guild) return;
|
|
if (!(message.type === MessageType.Default || message.type === MessageType.Reply || message.type === MessageType.ThreadStarterMessage)) return;
|
|
|
|
let causer: User | null = message.author;
|
|
|
|
const audit = await message.guild.fetchAuditLogs({
|
|
type: AuditLogEvent.MessageDelete,
|
|
limit: 1,
|
|
});
|
|
|
|
const entry = audit.entries.first();
|
|
|
|
if (entry && ((Date.now() - entry.createdTimestamp) < DELETE_FETCH_TIMEOUT || (entry.extra.count > 1)) && entry.targetId === message.author?.id) {
|
|
causer = entry.executor;
|
|
}
|
|
|
|
triggerEvent(bot, message.guild.id, {
|
|
type: EventType.MessageDelete,
|
|
causer, message,
|
|
});
|
|
});
|
|
bot.on(Events.MessageUpdate, async (old, message) => {
|
|
if (!message.guild) return;
|
|
if (message.content === old.content) return;
|
|
if (message.partial || old.partial) return;
|
|
|
|
triggerEvent(bot, message.guild.id, {
|
|
type: EventType.MessageEdit,
|
|
causer: message.author,
|
|
oldMessage: old, message: message,
|
|
});
|
|
});
|
|
bot.on(Events.InviteCreate, async invite => {
|
|
if (!invite.guild || !invite.inviter) return;
|
|
|
|
triggerEvent(bot, invite.guild.id, {
|
|
type: EventType.InviteCreate,
|
|
causer: invite.inviter, invite,
|
|
});
|
|
});
|
|
|
|
bot.on(Events.GuildAuditLogEntryCreate, async (entry, guild) => {
|
|
if (entry.action === AuditLogEvent.InviteDelete) {
|
|
if (!entry.target) return;
|
|
|
|
triggerEvent(bot, guild.id, {
|
|
type: EventType.InviteDelete,
|
|
causer: entry.executor,
|
|
// @ts-expect-error shut up
|
|
invite: entry.target
|
|
});
|
|
} else if (entry.action === AuditLogEvent.EmojiCreate) {
|
|
|
|
}
|
|
});
|
|
} |