more wip work yippeee

This commit is contained in:
Jill 2024-03-18 22:19:14 +03:00
parent 72b835f475
commit ad48cd516e
Signed by: oat
GPG Key ID: 33489AA58A955108
6 changed files with 386 additions and 73 deletions

View File

@ -16,6 +16,7 @@
"@discordjs/rest": "^2.2.0",
"chalk": "^4.1.2",
"d3-array": "^2.12.1",
"diff": "^5.2.0",
"discord.js": "^14.14.1",
"express": "^4.18.3",
"express-handlebars": "^7.1.2",
@ -31,6 +32,7 @@
},
"devDependencies": {
"@types/d3-array": "^3.2.1",
"@types/diff": "^5.0.9",
"@types/express": "^4.17.21",
"@types/parse-color": "^1.0.3",
"@types/tough-cookie": "^4.0.5",

View File

@ -17,6 +17,9 @@ dependencies:
d3-array:
specifier: ^2.12.1
version: 2.12.1
diff:
specifier: ^5.2.0
version: 5.2.0
discord.js:
specifier: ^14.14.1
version: 14.14.1
@ -58,6 +61,9 @@ devDependencies:
'@types/d3-array':
specifier: ^3.2.1
version: 3.2.1
'@types/diff':
specifier: ^5.0.9
version: 5.0.9
'@types/express':
specifier: ^4.17.21
version: 4.17.21
@ -372,6 +378,10 @@ packages:
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
dev: true
/@types/diff@5.0.9:
resolution: {integrity: sha512-RWVEhh/zGXpAVF/ZChwNnv7r4rvqzJ7lYNSmZSVTxjV0PBLf6Qu7RNg+SUtkpzxmiNkjCx0Xn2tPp7FIkshJwQ==}
dev: true
/@types/express-serve-static-core@4.17.43:
resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==}
dependencies:
@ -1043,6 +1053,11 @@ packages:
engines: {node: '>=8'}
dev: false
/diff@5.2.0:
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
engines: {node: '>=0.3.1'}
dev: false
/dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}

View File

@ -0,0 +1,123 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, ComponentType, SlashCommandBuilder, StringSelectMenuBuilder } from 'discord.js';
import { Command } from '../types/index';
import { AuditLog, db } from '../lib/db';
import { EventType } from '../lib/events';
import { chunks } from '../lib/util';
export default {
data: new SlashCommandBuilder()
.setName('auditlog')
.setDescription('[ADMIN] Set up an audit logger, or edit an existing one')
.setDefaultMemberPermissions('0'),
execute: async (interaction: CommandInteraction) => {
if (!interaction.isChatInputCommand()) return;
const channel = interaction.channelId;
if (!channel) return;
const log = await db<AuditLog>('auditLogs')
.select('eventTypes')
.where('channel', channel)
.first();
let types = log ? log.eventTypes.split(',') : [];
const options = Object.values(EventType)
.map(event => ({
label: event,
value: event,
}));
const chunked = [...chunks(options, 25)];
const selectRows =
chunked.map(
(opt, i) => new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
new StringSelectMenuBuilder()
.addOptions(
...opt
)
.setMinValues(0)
.setMaxValues(opt.length)
.setCustomId(`auditlog-select-events-${i}`)
)
);
const components = [
...selectRows,
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('auditlog-select-events-done')
.setLabel('Done')
.setStyle(ButtonStyle.Primary)
.setDisabled(true)
),
];
const formatContent = () =>
`${log ? `Editing audit log in <#${channel}>` : `Creating audit log in <#${channel}>`}\n` +
'Select the types of events to be reported on' + ((types.length > 0) ? `\n**Current events**: ${types.map(s => '`' + s + '`').join(', ')}` : '');
const msg = await interaction.reply({
ephemeral: true,
content: formatContent(),
components,
});
const selectCollector = msg.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
time: 60_000 * 5,
});
selectCollector.on('collect', async selectInteraction => {
const possibleTypes = selectInteraction.component.options.map(opt => opt.value);
const selectedTypes = selectInteraction.values;
types = types.filter(t => !possibleTypes.includes(t));
types = [...types, ...selectedTypes];
components[components.length - 1].components[0].setDisabled(types.length === 0);
await selectInteraction.reply({
content: 'Hit "Done" when finished',
ephemeral: true,
});
await msg.edit({
content: formatContent(),
components,
});
});
selectCollector.on('end', async () => {
await msg.edit({
content: formatContent(),
components: [],
});
});
const buttonInteraction = await msg.awaitMessageComponent({ componentType: ComponentType.Button, time: 60_000 * 5 });
selectCollector.stop();
if (log) {
await db<AuditLog>('auditLogs')
.where('channel', channel)
.update({
eventTypes: types.join(','),
});
} else {
await db<AuditLog>('auditLogs')
.insert({
guild: interaction.guildId!,
channel: channel,
eventTypes: types.join(','),
});
}
await buttonInteraction.reply({
content: 'Audit log successfully created.',
components: [],
ephemeral: true,
});
}
} satisfies Command;

View File

@ -1,12 +1,10 @@
import { Sticker, ThreadChannel, Message, User, Invite, GuildMember, GuildScheduledEvent, GuildEmoji, Channel, Client, Events, GuildAuditLogsEntry, AuditLogEvent } from 'discord.js';
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 {
ChannelCreate = 'CHANNEL_CREATE',
ChannelRename = 'CHANNEL_RENAME',
ChannelDelete = 'CHANNEL_DELETE',
EmojiCreate = 'EMOJI_CREATE',
EmojiRename = 'EMOJI_RENAME',
EmojiDelete = 'EMOJI_DELETE',
@ -16,7 +14,6 @@ export enum EventType {
EventDelete = 'EVENT_DELETE',
InviteCreate = 'INVITE_CREATE',
InviteUpdate = 'INVITE_UPDATE',
InviteDelete = 'INVITE_DELETE',
MemberBan = 'MEMBER_BAN',
@ -24,7 +21,8 @@ export enum EventType {
MemberKick = 'MEMBER_KICK',
MemberDisconnect = 'MEMBER_DISCONNECT',
MemberNickname = 'MEMBER_NICKNAME',
MemberChangeRoles = 'MEMBER_CHANGE_ROLES',
MemberJoin = 'MEMBER_JOIN',
MemberLeave = 'MEMBER_LEAVE',
MessageDelete = 'MESSAGE_DELETE',
MessageEdit = 'MESSAGE_EDIT',
@ -39,129 +37,107 @@ export enum EventType {
}
export type Event = {
type: EventType.ChannelCreate,
causer: User,
channel: Channel,
} | {
type: EventType.ChannelRename,
causer: User,
channel: Channel,
oldName: string, newName: string,
} | {
type: EventType.ChannelDelete,
causer: User,
channel: Channel,
} | {
type: EventType.EmojiCreate,
causer: User,
causer: User | null,
emoji: GuildEmoji,
} | {
type: EventType.EmojiRename,
causer: User,
causer: User | null,
emoji: GuildEmoji,
oldName: string, newName: string,
} | {
type: EventType.EmojiDelete,
causer: User,
causer: User | null,
emoji: GuildEmoji,
} | {
type: EventType.EventCreate,
causer: User,
causer: User | null,
event: GuildScheduledEvent,
} | {
type: EventType.EventEdit,
causer: User,
causer: User | null,
oldEvent: GuildScheduledEvent,
event: GuildScheduledEvent,
} | {
type: EventType.EventDelete,
causer: User,
causer: User | null,
event: GuildScheduledEvent,
} | {
type: EventType.InviteCreate,
causer: User,
invite: Invite,
} | {
type: EventType.InviteUpdate,
causer: User,
oldInvite: Invite,
causer: User | null,
invite: Invite,
} | {
type: EventType.InviteDelete,
causer: User,
causer: User | null,
invite: Invite,
} | {
type: EventType.MemberBan,
causer: User,
causer: User | null,
member: GuildMember,
reason: string,
} | {
type: EventType.MemberUnban,
causer: User,
causer: User | null,
member: GuildMember,
reason: string,
} | {
type: EventType.MemberKick,
causer: User,
causer: User | null,
member: GuildMember,
reason: string,
} | {
type: EventType.MemberDisconnect,
causer: User,
causer: User | null,
member: GuildMember,
} | {
type: EventType.MemberNickname,
causer: User,
causer: User | null,
member: GuildMember,
oldNickname: string, newNickname: string,
} | {
type: EventType.MemberChangeRoles,
causer: User,
member: GuildMember,
// TODO: huh
} | {
type: EventType.MessageDelete,
causer: User,
message: Message,
causer: User | null,
message: Message | PartialMessage,
} | {
type: EventType.MessageEdit,
causer: User,
causer: User | null,
oldMessage: Message,
message: Message,
} | {
type: EventType.StickerCreate,
causer: User,
causer: User | null,
sticker: Sticker,
} | {
type: EventType.StickerRename,
causer: User,
causer: User | null,
oldName: string, newName: string,
} | {
type: EventType.StickerDelete,
causer: User,
causer: User | null,
sticker: Sticker,
} | {
type: EventType.ThreadCreate,
causer: User,
causer: User | null,
thread: ThreadChannel,
} | {
type: EventType.ThreadEdit,
causer: User,
causer: User | null,
oldThread: ThreadChannel,
thread: ThreadChannel,
} | {
type: EventType.ThreadDelete,
causer: User,
causer: User | null,
thread: ThreadChannel,
};
export async function triggerEvent(bot: Client, event: Event) {
export async function triggerEvent(bot: Client, guildId: string, event: Event) {
const type = event.type;
log.info(`Got event ${event.type}: ${JSON.stringify(event)}`);
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()}%`);
@ -173,31 +149,144 @@ export async function triggerEvent(bot: Client, event: Event) {
log.warn(err);
}
if (channel && channel.isText()) {
channel.send(`${JSON.stringify(event)}`);
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.GuildAuditLogEntryCreate, async (auditLog: GuildAuditLogsEntry) => {
const { action, extra: channel, executorId, targetId } = auditLog;
bot.on(Events.MessageDelete, async message => {
if (!message.guild) return;
if (!(message.type === MessageType.Default || message.type === MessageType.Reply || message.type === MessageType.ThreadStarterMessage)) return;
// Check only for deleted messages.
if (action !== AuditLogEvent.MessageDelete) return;
let causer: User | null = message.author;
// Ensure the executor is cached.
const executor = await bot.users.fetch(executorId);
const audit = await message.guild.fetchAuditLogs({
type: AuditLogEvent.MessageDelete,
limit: 1,
});
// Ensure the author whose message was deleted is cached.
const target = await bot.users.fetch(targetId);
const entry = audit.entries.first();
// Log the output.
console.log(`A message by ${target.tag} was deleted by ${executor.tag} in ${channel}.`);
});*/
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) {
}
});
}

View File

@ -1,5 +1,6 @@
import { Interaction, Message, TextBasedChannel, User } from 'discord.js';
import * as log from '../lib/log';
import { chunks } from './util';
export const RANDOM_WORDS = [
'tarsorado', 'aboba', 'radiation', 'extreme', 'glogging', 'glogged', 'penis', 'easy', 'glue', 'contaminated water',
@ -140,12 +141,6 @@ export async function getTextResponsePrettyPlease(user: User, prompt: string, fi
return randomWord();
}
function* chunks(arr: unknown[], n: number) {
for (let i = 0; i < arr.length; i += n) {
yield arr.slice(i, i + n);
}
}
export async function sendSegments(segments: string[], channel: TextBasedChannel) {
const content = [];
let contentBuffer = '';

View File

@ -2,6 +2,7 @@ import * as fsp from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { randomBytes } from 'crypto';
import { Attachment, EmbedBuilder, Guild, GuildPremiumTier, Message, PartialMessage } from 'discord.js';
export async function exists(file: string) {
try {
@ -41,3 +42,91 @@ export class Right<R> {
constructor(private readonly value: R) {}
public getValue() { return this.value; }
}
export function* chunks<T>(arr: T[], n: number) {
for (let i = 0; i < arr.length; i += n) {
yield arr.slice(i, i + n);
}
}
export function shortenStr(str: string, chars: number) {
if (str.length > chars)
return str.slice(0, chars - 1) + '…';
return str;
}
export const MAX_MESSAGE_SIZE = 2000;
export const defaultUploadLimit = 25 * 1024 * 1024;
export const getUploadLimitForGuild = (guild: Guild) => {
switch (guild.premiumTier) {
case GuildPremiumTier.Tier3: return 100 * 1024 * 1024;
case GuildPremiumTier.Tier2: return 50 * 1024 * 1024;
default: return defaultUploadLimit;
}
};
export function formatMessageAsEmbed(message: Message | PartialMessage, uploadLimit: number): { embed: EmbedBuilder, attach: Attachment | null } {
const embed = new EmbedBuilder()
.setTimestamp(message.createdTimestamp);
if (message.author) {
embed.setAuthor({
name: message.author.tag,
iconURL: message.author.displayAvatarURL(),
});
}
let attach = null;
message.embeds.forEach(em => {
if (em.image) {
embed.setImage(em.image.url);
} else if (em.thumbnail) {
embed.setImage(em.thumbnail.url);
}
if (em.provider) {
embed.addFields({
name: shortenStr(`${em.provider.name}:`, 256),
value: shortenStr(`${em.author}: **${em.title}**\n${em.description || ''}`, 2048),
});
} else if (!em.image) {
embed.addFields({
name: 'Embed:',
value: shortenStr(em.description || em.fields[0]?.value || em.title || 'Unknown content', 2048),
});
}
});
let msgEnd = null;
const first = message.attachments.first();
if (
message.attachments.size === 1 && first && first.contentType
&& !first.spoiler
&& (first.contentType?.startsWith('image/')
|| first.contentType?.startsWith('video/'))
&& (first.size <= uploadLimit)
) {
attach = first;
} else {
for (const attach of message.attachments.values()) {
msgEnd = (msgEnd || '\n') + `\n[${attach.name}](${attach.url})`;
}
}
if (message.content) {
if (!msgEnd) {
embed.setDescription(shortenStr(message.content, 4096));
} else {
embed.setDescription(shortenStr(message.content, 4096 - msgEnd.length) + msgEnd);
}
} else if (msgEnd) {
embed.setDescription('_Content unknown_' + msgEnd);
}
return { embed, attach };
}