mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-04-02 09:44:14 +00:00
Merge pull request #294 from ahmadk953/redis-caching
Redis Caching and General Improvements
This commit is contained in:
commit
de599534f0
26 changed files with 787 additions and 186 deletions
|
@ -3,6 +3,7 @@
|
||||||
"clientId": "DISCORD_BOT_ID",
|
"clientId": "DISCORD_BOT_ID",
|
||||||
"guildId": "DISCORD_SERVER_ID",
|
"guildId": "DISCORD_SERVER_ID",
|
||||||
"dbConnectionString": "POSTGRESQL_CONNECTION_STRING",
|
"dbConnectionString": "POSTGRESQL_CONNECTION_STRING",
|
||||||
|
"redisConnectionString": "REDIS_CONNECTION_STRING",
|
||||||
"channels": {
|
"channels": {
|
||||||
"welcome": "WELCOME_CHANNEL_ID",
|
"welcome": "WELCOME_CHANNEL_ID",
|
||||||
"logs": "LOG_CHAANNEL_ID"
|
"logs": "LOG_CHAANNEL_ID"
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"@napi-rs/canvas": "^0.1.67",
|
"@napi-rs/canvas": "^0.1.67",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.18.0",
|
||||||
"drizzle-orm": "^0.40.0",
|
"drizzle-orm": "^0.40.0",
|
||||||
|
"ioredis": "^5.5.0",
|
||||||
"pg": "^8.13.3"
|
"pg": "^8.13.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
import {
|
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
|
||||||
CommandInteraction,
|
|
||||||
PermissionsBitField,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
SlashCommandOptionsOnlyBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { updateMember, updateMemberModerationHistory } from '../../db/db.js';
|
import { updateMember, updateMemberModerationHistory } from '../../db/db.js';
|
||||||
import { parseDuration, scheduleUnban } from '../../util/helpers.js';
|
import { parseDuration, scheduleUnban } from '../../util/helpers.js';
|
||||||
|
import { OptionsCommand } from '../../types/CommandTypes.js';
|
||||||
import logAction from '../../util/logging/logAction.js';
|
import logAction from '../../util/logging/logAction.js';
|
||||||
|
|
||||||
interface Command {
|
const command: OptionsCommand = {
|
||||||
data: SlashCommandOptionsOnlyBuilder;
|
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('ban')
|
.setName('ban')
|
||||||
.setDescription('Ban a member from the server')
|
.setDescription('Ban a member from the server')
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
import {
|
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
|
||||||
CommandInteraction,
|
|
||||||
PermissionsBitField,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
SlashCommandOptionsOnlyBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { executeUnban } from '../../util/helpers.js';
|
import { executeUnban } from '../../util/helpers.js';
|
||||||
|
import { OptionsCommand } from '../../types/CommandTypes.js';
|
||||||
|
|
||||||
interface Command {
|
const command: OptionsCommand = {
|
||||||
data: SlashCommandOptionsOnlyBuilder;
|
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('unban')
|
.setName('unban')
|
||||||
.setDescription('Unban a user from the server')
|
.setDescription('Unban a user from the server')
|
||||||
|
|
|
@ -1,18 +1,10 @@
|
||||||
import {
|
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
|
||||||
CommandInteraction,
|
|
||||||
PermissionsBitField,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
SlashCommandOptionsOnlyBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { updateMemberModerationHistory } from '../../db/db.js';
|
import { updateMemberModerationHistory } from '../../db/db.js';
|
||||||
|
import { OptionsCommand } from '../../types/CommandTypes.js';
|
||||||
import logAction from '../../util/logging/logAction.js';
|
import logAction from '../../util/logging/logAction.js';
|
||||||
|
|
||||||
interface Command {
|
const command: OptionsCommand = {
|
||||||
data: SlashCommandOptionsOnlyBuilder;
|
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('warn')
|
.setName('warn')
|
||||||
.setDescription('Warn a member')
|
.setDescription('Warn a member')
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import {
|
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
|
||||||
CommandInteraction,
|
|
||||||
PermissionsBitField,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
SlashCommandOptionsOnlyBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
|
|
||||||
interface Command {
|
import { Command } from '../../types/CommandTypes.js';
|
||||||
data: SlashCommandOptionsOnlyBuilder;
|
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
import {
|
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
|
||||||
CommandInteraction,
|
|
||||||
PermissionsBitField,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
SlashCommandOptionsOnlyBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { updateMember } from '../../db/db.js';
|
|
||||||
|
|
||||||
interface Command {
|
import { updateMember } from '../../db/db.js';
|
||||||
data: SlashCommandOptionsOnlyBuilder;
|
import { Command } from '../../types/CommandTypes.js';
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
CommandInteraction,
|
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
|
@ -9,12 +8,9 @@ import {
|
||||||
APIEmbed,
|
APIEmbed,
|
||||||
JSONEncodable,
|
JSONEncodable,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { getAllMembers } from '../../db/db.js';
|
|
||||||
|
|
||||||
interface Command {
|
import { getAllMembers } from '../../db/db.js';
|
||||||
data: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
import { Command } from '../../types/CommandTypes.js';
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { SlashCommandBuilder, CommandInteraction } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
|
||||||
interface Command {
|
import { Command } from '../../types/CommandTypes.js';
|
||||||
data: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import {
|
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||||
SlashCommandBuilder,
|
|
||||||
CommandInteraction,
|
|
||||||
EmbedBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
|
|
||||||
interface Command {
|
import { Command } from '../../types/CommandTypes.js';
|
||||||
data: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rulesEmbed = new EmbedBuilder()
|
const rulesEmbed = new EmbedBuilder()
|
||||||
.setColor(0x0099ff)
|
.setColor(0x0099ff)
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { SlashCommandBuilder, CommandInteraction } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
|
||||||
interface Command {
|
import { Command } from '../../types/CommandTypes.js';
|
||||||
data: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
import {
|
import {
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
CommandInteraction,
|
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
SlashCommandOptionsOnlyBuilder,
|
|
||||||
GuildMember,
|
GuildMember,
|
||||||
PermissionsBitField,
|
PermissionsBitField,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
|
|
||||||
import { getMember } from '../../db/db.js';
|
import { getMember } from '../../db/db.js';
|
||||||
|
import { OptionsCommand } from '../../types/CommandTypes.js';
|
||||||
|
|
||||||
interface Command {
|
const command: OptionsCommand = {
|
||||||
data: SlashCommandOptionsOnlyBuilder;
|
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: Command = {
|
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('userinfo')
|
.setName('userinfo')
|
||||||
.setDescription('Provides information about the specified user.')
|
.setDescription('Provides information about the specified user.')
|
||||||
|
|
237
src/db/db.ts
237
src/db/db.ts
|
@ -1,8 +1,10 @@
|
||||||
import pkg from 'pg';
|
import pkg from 'pg';
|
||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import * as schema from './schema.js';
|
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import * as schema from './schema.js';
|
||||||
import { loadConfig } from '../util/configLoader.js';
|
import { loadConfig } from '../util/configLoader.js';
|
||||||
|
import { del, exists, getJson, setJson } from './redis.js';
|
||||||
|
|
||||||
const { Pool } = pkg;
|
const { Pool } = pkg;
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
@ -13,41 +15,121 @@ const dbPool = new Pool({
|
||||||
});
|
});
|
||||||
export const db = drizzle({ client: dbPool, schema });
|
export const db = drizzle({ client: dbPool, schema });
|
||||||
|
|
||||||
|
class DatabaseError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public originalError?: Error,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'DatabaseError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAllMembers() {
|
export async function getAllMembers() {
|
||||||
return await db
|
try {
|
||||||
.select()
|
if (await exists('nonBotMembers')) {
|
||||||
.from(schema.memberTable)
|
const memberData =
|
||||||
.where(eq(schema.memberTable.currentlyInServer, true));
|
await getJson<(typeof schema.memberTable.$inferSelect)[]>(
|
||||||
|
'nonBotMembers',
|
||||||
|
);
|
||||||
|
if (memberData && memberData.length > 0) {
|
||||||
|
return memberData;
|
||||||
|
} else {
|
||||||
|
await del('nonBotMembers');
|
||||||
|
return await getAllMembers();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const nonBotMembers = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.memberTable)
|
||||||
|
.where(eq(schema.memberTable.currentlyInServer, true));
|
||||||
|
await setJson<(typeof schema.memberTable.$inferSelect)[]>(
|
||||||
|
'nonBotMembers',
|
||||||
|
nonBotMembers,
|
||||||
|
);
|
||||||
|
return nonBotMembers;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting all members: ', error);
|
||||||
|
throw new DatabaseError('Failed to get all members: ', error as Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setMembers(nonBotMembers: any) {
|
export async function setMembers(nonBotMembers: any) {
|
||||||
nonBotMembers.forEach(async (member: any) => {
|
try {
|
||||||
const memberExists = await db
|
nonBotMembers.forEach(async (member: any) => {
|
||||||
.select()
|
const memberInfo = await db
|
||||||
.from(schema.memberTable)
|
.select()
|
||||||
.where(eq(schema.memberTable.discordId, member.user.id));
|
.from(schema.memberTable)
|
||||||
if (memberExists.length > 0) {
|
|
||||||
await db
|
|
||||||
.update(schema.memberTable)
|
|
||||||
.set({ discordUsername: member.user.username })
|
|
||||||
.where(eq(schema.memberTable.discordId, member.user.id));
|
.where(eq(schema.memberTable.discordId, member.user.id));
|
||||||
} else {
|
if (memberInfo.length > 0) {
|
||||||
const members: typeof schema.memberTable.$inferInsert = {
|
await updateMember({
|
||||||
discordId: member.user.id,
|
discordId: member.user.id,
|
||||||
discordUsername: member.user.username,
|
discordUsername: member.user.username,
|
||||||
};
|
currentlyInServer: true,
|
||||||
await db.insert(schema.memberTable).values(members);
|
});
|
||||||
}
|
} else {
|
||||||
});
|
const members: typeof schema.memberTable.$inferInsert = {
|
||||||
|
discordId: member.user.id,
|
||||||
|
discordUsername: member.user.username,
|
||||||
|
};
|
||||||
|
await db.insert(schema.memberTable).values(members);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting members: ', error);
|
||||||
|
throw new DatabaseError('Failed to set members: ', error as Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMember(discordId: string) {
|
export async function getMember(discordId: string) {
|
||||||
return await db.query.memberTable.findFirst({
|
try {
|
||||||
where: eq(schema.memberTable.discordId, discordId),
|
if (await exists(`${discordId}-memberInfo`)) {
|
||||||
with: {
|
const cachedMember = await getJson<
|
||||||
moderations: true,
|
typeof schema.memberTable.$inferSelect
|
||||||
},
|
>(`${discordId}-memberInfo`);
|
||||||
});
|
const cachedModerationHistory = await getJson<
|
||||||
|
(typeof schema.moderationTable.$inferSelect)[]
|
||||||
|
>(`${discordId}-moderationHistory`);
|
||||||
|
|
||||||
|
if (
|
||||||
|
cachedMember &&
|
||||||
|
'discordId' in cachedMember &&
|
||||||
|
cachedModerationHistory &&
|
||||||
|
cachedModerationHistory.length > 0
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...cachedMember,
|
||||||
|
moderations: cachedModerationHistory,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
await del(`${discordId}-memberInfo`);
|
||||||
|
await del(`${discordId}-moderationHistory`);
|
||||||
|
return await getMember(discordId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const member = await db.query.memberTable.findFirst({
|
||||||
|
where: eq(schema.memberTable.discordId, discordId),
|
||||||
|
with: {
|
||||||
|
moderations: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await setJson<typeof schema.memberTable.$inferSelect>(
|
||||||
|
`${discordId}-memberInfo`,
|
||||||
|
member!,
|
||||||
|
);
|
||||||
|
await setJson<(typeof schema.moderationTable.$inferSelect)[]>(
|
||||||
|
`${discordId}-moderationHistory`,
|
||||||
|
member!.moderations,
|
||||||
|
);
|
||||||
|
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting member: ', error);
|
||||||
|
throw new DatabaseError('Failed to get member: ', error as Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMember({
|
export async function updateMember({
|
||||||
|
@ -56,14 +138,28 @@ export async function updateMember({
|
||||||
currentlyInServer,
|
currentlyInServer,
|
||||||
currentlyBanned,
|
currentlyBanned,
|
||||||
}: schema.memberTableTypes) {
|
}: schema.memberTableTypes) {
|
||||||
return await db
|
try {
|
||||||
.update(schema.memberTable)
|
const result = await db
|
||||||
.set({
|
.update(schema.memberTable)
|
||||||
discordUsername,
|
.set({
|
||||||
currentlyInServer,
|
discordUsername,
|
||||||
currentlyBanned,
|
currentlyInServer,
|
||||||
})
|
currentlyBanned,
|
||||||
.where(eq(schema.memberTable.discordId, discordId));
|
})
|
||||||
|
.where(eq(schema.memberTable.discordId, discordId));
|
||||||
|
|
||||||
|
if (await exists(`${discordId}-memberInfo`)) {
|
||||||
|
await del(`${discordId}-memberInfo`);
|
||||||
|
}
|
||||||
|
if (await exists('nonBotMembers')) {
|
||||||
|
await del('nonBotMembers');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating member: ', error);
|
||||||
|
throw new DatabaseError('Failed to update member: ', error as Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMemberModerationHistory({
|
export async function updateMemberModerationHistory({
|
||||||
|
@ -76,22 +172,61 @@ export async function updateMemberModerationHistory({
|
||||||
expiresAt,
|
expiresAt,
|
||||||
active,
|
active,
|
||||||
}: schema.moderationTableTypes) {
|
}: schema.moderationTableTypes) {
|
||||||
const moderationEntry = {
|
try {
|
||||||
discordId,
|
const moderationEntry = {
|
||||||
moderatorDiscordId,
|
discordId,
|
||||||
action,
|
moderatorDiscordId,
|
||||||
reason,
|
action,
|
||||||
duration,
|
reason,
|
||||||
createdAt,
|
duration,
|
||||||
expiresAt,
|
createdAt,
|
||||||
active,
|
expiresAt,
|
||||||
};
|
active,
|
||||||
return await db.insert(schema.moderationTable).values(moderationEntry);
|
};
|
||||||
|
const result = await db
|
||||||
|
.insert(schema.moderationTable)
|
||||||
|
.values(moderationEntry);
|
||||||
|
|
||||||
|
if (await exists(`${discordId}-moderationHistory`)) {
|
||||||
|
await del(`${discordId}-moderationHistory`);
|
||||||
|
}
|
||||||
|
if (await exists(`${discordId}-memberInfo`)) {
|
||||||
|
await del(`${discordId}-memberInfo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating moderation history: ', error);
|
||||||
|
throw new DatabaseError(
|
||||||
|
'Failed to update moderation history: ',
|
||||||
|
error as Error,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMemberModerationHistory(discordId: string) {
|
export async function getMemberModerationHistory(discordId: string) {
|
||||||
return await db
|
try {
|
||||||
.select()
|
if (await exists(`${discordId}-moderationHistory`)) {
|
||||||
.from(schema.moderationTable)
|
return await getJson<(typeof schema.moderationTable.$inferSelect)[]>(
|
||||||
.where(eq(schema.moderationTable.discordId, discordId));
|
`${discordId}-moderationHistory`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const moderationHistory = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.moderationTable)
|
||||||
|
.where(eq(schema.moderationTable.discordId, discordId));
|
||||||
|
|
||||||
|
await setJson<(typeof schema.moderationTable.$inferSelect)[]>(
|
||||||
|
`${discordId}-moderationHistory`,
|
||||||
|
moderationHistory,
|
||||||
|
);
|
||||||
|
return moderationHistory;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting moderation history: ', error);
|
||||||
|
throw new DatabaseError(
|
||||||
|
'Failed to get moderation history: ',
|
||||||
|
error as Error,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
105
src/db/redis.ts
Normal file
105
src/db/redis.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import { loadConfig } from '../util/configLoader.js';
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const redis = new Redis(config.redisConnectionString);
|
||||||
|
|
||||||
|
class RedisError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public originalError?: Error,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'RedisError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redis.on('error', (error) => {
|
||||||
|
console.error('Redis connection error:', error);
|
||||||
|
throw new RedisError('Failed to connect to Redis instance: ', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
redis.on('connect', () => {
|
||||||
|
console.log('Successfully connected to Redis');
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function set(
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
ttl?: number,
|
||||||
|
): Promise<'OK'> {
|
||||||
|
try {
|
||||||
|
await redis.set(key, value);
|
||||||
|
if (ttl) await redis.expire(key, ttl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redis set error: ', error);
|
||||||
|
throw new RedisError(`Failed to set key: ${key}, `, error as Error);
|
||||||
|
}
|
||||||
|
return Promise.resolve('OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setJson<T>(
|
||||||
|
key: string,
|
||||||
|
value: T,
|
||||||
|
ttl?: number,
|
||||||
|
): Promise<'OK'> {
|
||||||
|
return await set(key, JSON.stringify(value), ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function incr(key: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
return await redis.incr(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redis increment error: ', error);
|
||||||
|
throw new RedisError(`Failed to increment key: ${key}, `, error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exists(key: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return (await redis.exists(key)) === 1;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redis exists error: ', error);
|
||||||
|
throw new RedisError(
|
||||||
|
`Failed to check if key exists: ${key}, `,
|
||||||
|
error as Error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(key: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
return await redis.get(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redis get error: ', error);
|
||||||
|
throw new RedisError(`Failed to get key: ${key}, `, error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function mget(...keys: string[]): Promise<(string | null)[]> {
|
||||||
|
try {
|
||||||
|
return await redis.mget(keys);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redis mget error: ', error);
|
||||||
|
throw new RedisError(`Failed to get keys: ${keys}, `, error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getJson<T>(key: string): Promise<T | null> {
|
||||||
|
const value = await get(key);
|
||||||
|
if (!value) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del(key: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
return await redis.del(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redis del error: ', error);
|
||||||
|
throw new RedisError(`Failed to delete key: ${key}, `, error as Error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,101 @@
|
||||||
import { AuditLogEvent, Events, GuildChannel } from 'discord.js';
|
import {
|
||||||
import logAction from '../util/logging/logAction.js';
|
AuditLogEvent,
|
||||||
import { Event } from '../types/EventTypes.js';
|
ChannelType,
|
||||||
|
DMChannel,
|
||||||
|
Events,
|
||||||
|
GuildChannel,
|
||||||
|
PermissionOverwrites,
|
||||||
|
} from 'discord.js';
|
||||||
|
|
||||||
export const channelCreate = {
|
import { ChannelLogAction } from '../util/logging/types.js';
|
||||||
|
import { Event } from '../types/EventTypes.js';
|
||||||
|
import logAction from '../util/logging/logAction.js';
|
||||||
|
|
||||||
|
function arePermissionsEqual(
|
||||||
|
oldPerms: Map<string, PermissionOverwrites>,
|
||||||
|
newPerms: Map<string, PermissionOverwrites>,
|
||||||
|
): boolean {
|
||||||
|
if (oldPerms.size !== newPerms.size) return false;
|
||||||
|
|
||||||
|
for (const [id, oldPerm] of oldPerms.entries()) {
|
||||||
|
const newPerm = newPerms.get(id);
|
||||||
|
if (!newPerm) return false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!oldPerm.allow.equals(newPerm.allow) ||
|
||||||
|
!oldPerm.deny.equals(newPerm.deny)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function getPermissionChanges(
|
||||||
|
oldChannel: GuildChannel,
|
||||||
|
newChannel: GuildChannel,
|
||||||
|
): ChannelLogAction['permissionChanges'] {
|
||||||
|
const changes: ChannelLogAction['permissionChanges'] = [];
|
||||||
|
const newPerms = newChannel.permissionOverwrites.cache;
|
||||||
|
const oldPerms = oldChannel.permissionOverwrites.cache;
|
||||||
|
|
||||||
|
for (const [id, newPerm] of newPerms.entries()) {
|
||||||
|
const oldPerm = oldPerms.get(id);
|
||||||
|
const targetType = newPerm.type === 0 ? 'role' : 'member';
|
||||||
|
const targetName =
|
||||||
|
newPerm.type === 0
|
||||||
|
? newChannel.guild.roles.cache.get(id)?.name || id
|
||||||
|
: newChannel.guild.members.cache.get(id)?.user.username || id;
|
||||||
|
|
||||||
|
if (!oldPerm) {
|
||||||
|
changes.push({
|
||||||
|
action: 'added',
|
||||||
|
targetId: id,
|
||||||
|
targetType,
|
||||||
|
targetName,
|
||||||
|
allow: newPerm.allow,
|
||||||
|
deny: newPerm.deny,
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
!oldPerm.allow.equals(newPerm.allow) ||
|
||||||
|
!oldPerm.deny.equals(newPerm.deny)
|
||||||
|
) {
|
||||||
|
changes.push({
|
||||||
|
action: 'modified',
|
||||||
|
targetId: id,
|
||||||
|
targetType,
|
||||||
|
targetName,
|
||||||
|
oldAllow: oldPerm.allow,
|
||||||
|
oldDeny: oldPerm.deny,
|
||||||
|
newAllow: newPerm.allow,
|
||||||
|
newDeny: newPerm.deny,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, oldPerm] of oldPerms.entries()) {
|
||||||
|
if (!newPerms.has(id)) {
|
||||||
|
const targetType = oldPerm.type === 0 ? 'role' : 'member';
|
||||||
|
const targetName =
|
||||||
|
oldPerm.type === 0
|
||||||
|
? oldChannel.guild.roles.cache.get(id)?.name || id
|
||||||
|
: oldChannel.guild.members.cache.get(id)?.user.username || id;
|
||||||
|
|
||||||
|
changes.push({
|
||||||
|
action: 'removed',
|
||||||
|
targetId: id,
|
||||||
|
targetType,
|
||||||
|
targetName,
|
||||||
|
allow: oldPerm.allow,
|
||||||
|
deny: oldPerm.deny,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const channelCreate: Event<typeof Events.ChannelCreate> = {
|
||||||
name: Events.ChannelCreate,
|
name: Events.ChannelCreate,
|
||||||
execute: async (channel: GuildChannel) => {
|
execute: async (channel: GuildChannel) => {
|
||||||
try {
|
try {
|
||||||
|
@ -28,10 +121,12 @@ export const channelCreate = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const channelDelete = {
|
export const channelDelete: Event<typeof Events.ChannelDelete> = {
|
||||||
name: Events.ChannelDelete,
|
name: Events.ChannelDelete,
|
||||||
execute: async (channel: GuildChannel) => {
|
execute: async (channel: GuildChannel | DMChannel) => {
|
||||||
try {
|
try {
|
||||||
|
if (channel.type === ChannelType.DM) return;
|
||||||
|
|
||||||
const { guild } = channel;
|
const { guild } = channel;
|
||||||
const auditLogs = await guild.fetchAuditLogs({
|
const auditLogs = await guild.fetchAuditLogs({
|
||||||
type: AuditLogEvent.ChannelDelete,
|
type: AuditLogEvent.ChannelDelete,
|
||||||
|
@ -54,20 +149,46 @@ export const channelDelete = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const channelUpdate = {
|
export const channelUpdate: Event<typeof Events.ChannelUpdate> = {
|
||||||
name: Events.ChannelUpdate,
|
name: Events.ChannelUpdate,
|
||||||
execute: async (oldChannel: GuildChannel, newChannel: GuildChannel) => {
|
execute: async (
|
||||||
|
oldChannel: GuildChannel | DMChannel,
|
||||||
|
newChannel: GuildChannel | DMChannel,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
|
if (
|
||||||
|
oldChannel.type === ChannelType.DM ||
|
||||||
|
newChannel.type === ChannelType.DM
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
oldChannel.name === newChannel.name &&
|
||||||
|
oldChannel.type === newChannel.type &&
|
||||||
|
oldChannel.permissionOverwrites.cache.size ===
|
||||||
|
newChannel.permissionOverwrites.cache.size &&
|
||||||
|
arePermissionsEqual(
|
||||||
|
oldChannel.permissionOverwrites.cache,
|
||||||
|
newChannel.permissionOverwrites.cache,
|
||||||
|
) &&
|
||||||
|
oldChannel.position !== newChannel.position
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { guild } = newChannel;
|
const { guild } = newChannel;
|
||||||
const auditLogs = await guild.fetchAuditLogs({
|
const auditLogs = await guild.fetchAuditLogs({
|
||||||
type: AuditLogEvent.ChannelUpdate,
|
type: AuditLogEvent.ChannelUpdate,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
const executor = auditLogs.entries.first()?.executor;
|
const log = auditLogs.entries.first();
|
||||||
|
const executor = log?.executor;
|
||||||
const moderator = executor
|
const moderator = executor
|
||||||
? await guild.members.fetch(executor.id)
|
? await guild.members.fetch(executor.id)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const permissionChanges = getPermissionChanges(oldChannel, newChannel);
|
||||||
|
|
||||||
await logAction({
|
await logAction({
|
||||||
guild,
|
guild,
|
||||||
action: 'channelUpdate',
|
action: 'channelUpdate',
|
||||||
|
@ -75,8 +196,8 @@ export const channelUpdate = {
|
||||||
moderator,
|
moderator,
|
||||||
oldName: oldChannel.name,
|
oldName: oldChannel.name,
|
||||||
newName: newChannel.name,
|
newName: newChannel.name,
|
||||||
oldPermissions: oldChannel.permissionOverwrites.cache.first()?.allow,
|
permissionChanges:
|
||||||
newPermissions: newChannel.permissionOverwrites.cache.first()?.allow,
|
(permissionChanges ?? []).length > 0 ? permissionChanges : undefined,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling channel update:', error);
|
console.error('Error handling channel update:', error);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { Events, Interaction } from 'discord.js';
|
import { Events, Interaction } from 'discord.js';
|
||||||
|
|
||||||
import { ExtendedClient } from '../structures/ExtendedClient.js';
|
import { ExtendedClient } from '../structures/ExtendedClient.js';
|
||||||
|
import { Event } from '../types/EventTypes.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.InteractionCreate,
|
name: Events.InteractionCreate,
|
||||||
|
@ -35,4 +37,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
} as Event<typeof Events.InteractionCreate>;
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { Events, GuildMember } from 'discord.js';
|
import { Events, Guild, GuildMember, PartialGuildMember } from 'discord.js';
|
||||||
|
|
||||||
import { updateMember, setMembers } from '../db/db.js';
|
import { updateMember, setMembers } from '../db/db.js';
|
||||||
import { generateMemberBanner } from '../util/helpers.js';
|
import { generateMemberBanner } from '../util/helpers.js';
|
||||||
import { loadConfig } from '../util/configLoader.js';
|
import { loadConfig } from '../util/configLoader.js';
|
||||||
|
import { Event } from '../types/EventTypes.js';
|
||||||
import logAction from '../util/logging/logAction.js';
|
import logAction from '../util/logging/logAction.js';
|
||||||
|
|
||||||
export const memberJoin = {
|
export const memberJoin: Event<typeof Events.GuildMemberAdd> = {
|
||||||
name: Events.GuildMemberAdd,
|
name: Events.GuildMemberAdd,
|
||||||
execute: async (member: GuildMember) => {
|
execute: async (member: GuildMember) => {
|
||||||
const { guild } = member;
|
const { guild } = member;
|
||||||
|
@ -17,9 +19,12 @@ export const memberJoin = {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const members = await guild.members.fetch();
|
await setMembers([
|
||||||
const nonBotMembers = members.filter((m) => !m.user.bot);
|
{
|
||||||
await setMembers(nonBotMembers);
|
discordId: member.user.id,
|
||||||
|
discordUsername: member.user.username,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
if (!member.user.bot) {
|
if (!member.user.bot) {
|
||||||
const attachment = await generateMemberBanner({
|
const attachment = await generateMemberBanner({
|
||||||
|
@ -37,10 +42,6 @@ export const memberJoin = {
|
||||||
content: `Welcome to ${guild.name}, we hope you enjoy your stay!`,
|
content: `Welcome to ${guild.name}, we hope you enjoy your stay!`,
|
||||||
files: [attachment],
|
files: [attachment],
|
||||||
}),
|
}),
|
||||||
updateMember({
|
|
||||||
discordId: member.user.id,
|
|
||||||
currentlyInServer: true,
|
|
||||||
}),
|
|
||||||
member.roles.add(config.roles.joinRoles),
|
member.roles.add(config.roles.joinRoles),
|
||||||
logAction({
|
logAction({
|
||||||
guild,
|
guild,
|
||||||
|
@ -55,9 +56,9 @@ export const memberJoin = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const memberLeave = {
|
export const memberLeave: Event<typeof Events.GuildMemberRemove> = {
|
||||||
name: Events.GuildMemberRemove,
|
name: Events.GuildMemberRemove,
|
||||||
execute: async (member: GuildMember) => {
|
execute: async (member: GuildMember | PartialGuildMember) => {
|
||||||
const { guild } = member;
|
const { guild } = member;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -69,7 +70,7 @@ export const memberLeave = {
|
||||||
logAction({
|
logAction({
|
||||||
guild,
|
guild,
|
||||||
action: 'memberLeave',
|
action: 'memberLeave',
|
||||||
member,
|
member: member as GuildMember,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -78,9 +79,12 @@ export const memberLeave = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const memberUpdate = {
|
export const memberUpdate: Event<typeof Events.GuildMemberUpdate> = {
|
||||||
name: Events.GuildMemberUpdate,
|
name: Events.GuildMemberUpdate,
|
||||||
execute: async (oldMember: GuildMember, newMember: GuildMember) => {
|
execute: async (
|
||||||
|
oldMember: GuildMember | PartialGuildMember,
|
||||||
|
newMember: GuildMember,
|
||||||
|
) => {
|
||||||
const { guild } = newMember;
|
const { guild } = newMember;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { AuditLogEvent, Events, Message } from 'discord.js';
|
import { AuditLogEvent, Events, Message, PartialMessage } from 'discord.js';
|
||||||
|
|
||||||
|
import { Event } from '../types/EventTypes.js';
|
||||||
import logAction from '../util/logging/logAction.js';
|
import logAction from '../util/logging/logAction.js';
|
||||||
|
|
||||||
export const messageDelete = {
|
export const messageDelete: Event<typeof Events.MessageDelete> = {
|
||||||
name: Events.MessageDelete,
|
name: Events.MessageDelete,
|
||||||
execute: async (message: Message) => {
|
execute: async (
|
||||||
|
message: Omit<Partial<Message<boolean> | PartialMessage>, 'channel'>,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!message.guild || message.author?.bot) return;
|
if (!message.guild || message.author?.bot) return;
|
||||||
|
|
||||||
|
@ -30,9 +34,12 @@ export const messageDelete = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messageUpdate = {
|
export const messageUpdate: Event<typeof Events.MessageUpdate> = {
|
||||||
name: Events.MessageUpdate,
|
name: Events.MessageUpdate,
|
||||||
execute: async (oldMessage: Message, newMessage: Message) => {
|
execute: async (
|
||||||
|
oldMessage: Omit<Partial<Message<boolean> | PartialMessage>, 'channel'>,
|
||||||
|
newMessage: Message,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
!oldMessage.guild ||
|
!oldMessage.guild ||
|
||||||
|
|
|
@ -1,9 +1,30 @@
|
||||||
import { Client, Events } from 'discord.js';
|
import { Client, Events } from 'discord.js';
|
||||||
|
|
||||||
|
import { setMembers } from '../db/db.js';
|
||||||
|
import { loadConfig } from '../util/configLoader.js';
|
||||||
|
import { Event } from '../types/EventTypes.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.ClientReady,
|
name: Events.ClientReady,
|
||||||
once: true,
|
once: true,
|
||||||
execute: async (client: Client) => {
|
execute: async (client: Client) => {
|
||||||
|
const config = loadConfig();
|
||||||
|
try {
|
||||||
|
const guild = client.guilds.cache.find(
|
||||||
|
(guilds) => guilds.id === config.guildId,
|
||||||
|
);
|
||||||
|
if (!guild) {
|
||||||
|
console.error(`Guild with ID ${config.guildId} not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = await guild.members.fetch();
|
||||||
|
const nonBotMembers = members.filter((m) => !m.user.bot);
|
||||||
|
await setMembers(nonBotMembers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize members in database:', error);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Ready! Logged in as ${client.user?.tag}`);
|
console.log(`Ready! Logged in as ${client.user?.tag}`);
|
||||||
},
|
},
|
||||||
};
|
} as Event<typeof Events.ClientReady>;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { AuditLogEvent, Events, Role } from 'discord.js';
|
import { AuditLogEvent, Events, Role } from 'discord.js';
|
||||||
|
|
||||||
|
import { Event } from '../types/EventTypes.js';
|
||||||
import logAction from '../util/logging/logAction.js';
|
import logAction from '../util/logging/logAction.js';
|
||||||
|
|
||||||
const convertRoleProperties = (role: Role) => ({
|
const convertRoleProperties = (role: Role) => ({
|
||||||
|
@ -8,7 +10,7 @@ const convertRoleProperties = (role: Role) => ({
|
||||||
mentionable: role.mentionable,
|
mentionable: role.mentionable,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roleCreate = {
|
export const roleCreate: Event<typeof Events.GuildRoleCreate> = {
|
||||||
name: Events.GuildRoleCreate,
|
name: Events.GuildRoleCreate,
|
||||||
execute: async (role: Role) => {
|
execute: async (role: Role) => {
|
||||||
try {
|
try {
|
||||||
|
@ -34,7 +36,7 @@ export const roleCreate = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const roleDelete = {
|
export const roleDelete: Event<typeof Events.GuildRoleDelete> = {
|
||||||
name: Events.GuildRoleDelete,
|
name: Events.GuildRoleDelete,
|
||||||
execute: async (role: Role) => {
|
execute: async (role: Role) => {
|
||||||
try {
|
try {
|
||||||
|
@ -60,7 +62,7 @@ export const roleDelete = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const roleUpdate = {
|
export const roleUpdate: Event<typeof Events.GuildRoleUpdate> = {
|
||||||
name: Events.GuildRoleUpdate,
|
name: Events.GuildRoleUpdate,
|
||||||
execute: async (oldRole: Role, newRole: Role) => {
|
execute: async (oldRole: Role, newRole: Role) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
import { CommandInteraction, SlashCommandBuilder } from 'discord.js';
|
import {
|
||||||
|
CommandInteraction,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
SlashCommandOptionsOnlyBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
|
||||||
export interface Command {
|
export interface Command {
|
||||||
data: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
data: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
execute: (interaction: CommandInteraction) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OptionsCommand {
|
||||||
|
data: SlashCommandOptionsOnlyBuilder;
|
||||||
|
execute: (interaction: CommandInteraction) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ export interface Config {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
guildId: string;
|
guildId: string;
|
||||||
dbConnectionString: string;
|
dbConnectionString: string;
|
||||||
|
redisConnectionString: string;
|
||||||
channels: {
|
channels: {
|
||||||
welcome: string;
|
welcome: string;
|
||||||
logs: string;
|
logs: string;
|
||||||
|
|
|
@ -19,6 +19,8 @@ import {
|
||||||
createRoleChangeFields,
|
createRoleChangeFields,
|
||||||
getLogItemId,
|
getLogItemId,
|
||||||
getEmojiForAction,
|
getEmojiForAction,
|
||||||
|
getPermissionDifference,
|
||||||
|
getPermissionNames,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
export default async function logAction(payload: LogActionPayload) {
|
export default async function logAction(payload: LogActionPayload) {
|
||||||
|
@ -209,6 +211,12 @@ export default async function logAction(payload: LogActionPayload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'channelUpdate': {
|
case 'channelUpdate': {
|
||||||
|
const changesExist =
|
||||||
|
payload.oldName !== payload.newName ||
|
||||||
|
(payload.permissionChanges && payload.permissionChanges.length > 0);
|
||||||
|
|
||||||
|
if (!changesExist) return;
|
||||||
|
|
||||||
fields.push({
|
fields.push({
|
||||||
name: '📝 Channel Information',
|
name: '📝 Channel Information',
|
||||||
value: [
|
value: [
|
||||||
|
@ -223,12 +231,138 @@ export default async function logAction(payload: LogActionPayload) {
|
||||||
inline: false,
|
inline: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payload.oldPermissions && payload.newPermissions) {
|
if (payload.permissionChanges && payload.permissionChanges.length > 0) {
|
||||||
const permissionChanges = createPermissionChangeFields(
|
const changes = {
|
||||||
payload.oldPermissions,
|
added: payload.permissionChanges.filter((c) => c.action === 'added'),
|
||||||
payload.newPermissions,
|
modified: payload.permissionChanges.filter(
|
||||||
);
|
(c) => c.action === 'modified',
|
||||||
fields.push(...permissionChanges);
|
),
|
||||||
|
removed: payload.permissionChanges.filter(
|
||||||
|
(c) => c.action === 'removed',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (changes.added.length > 0) {
|
||||||
|
fields.push({
|
||||||
|
name: '➕ Added Permissions',
|
||||||
|
value: changes.added
|
||||||
|
.map((c) => {
|
||||||
|
const targetMention =
|
||||||
|
c.targetType === 'role'
|
||||||
|
? `<@&${c.targetId}>`
|
||||||
|
: `<@${c.targetId}>`;
|
||||||
|
return `For ${c.targetType} ${targetMention} (${c.targetName})`;
|
||||||
|
})
|
||||||
|
.join('\n'),
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
changes.added.forEach((c) => {
|
||||||
|
if (c.allow?.bitfield || c.deny?.bitfield) {
|
||||||
|
const permList = [];
|
||||||
|
if (c.allow?.bitfield) {
|
||||||
|
const allowedPerms = getPermissionNames(c.allow);
|
||||||
|
if (allowedPerms.length) {
|
||||||
|
permList.push(`✅ **Allowed:** ${allowedPerms.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.deny?.bitfield) {
|
||||||
|
const deniedPerms = getPermissionNames(c.deny);
|
||||||
|
if (deniedPerms.length) {
|
||||||
|
permList.push(`❌ **Denied:** ${deniedPerms.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permList.length > 0) {
|
||||||
|
fields.push({
|
||||||
|
name: `Permissions for ${c.targetType} ${c.targetName}`,
|
||||||
|
value: permList.join('\n'),
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.modified.length > 0) {
|
||||||
|
fields.push({
|
||||||
|
name: '🔄 Modified Permissions',
|
||||||
|
value: changes.modified
|
||||||
|
.map((c) => {
|
||||||
|
const targetMention =
|
||||||
|
c.targetType === 'role'
|
||||||
|
? `<@&${c.targetId}>`
|
||||||
|
: `<@${c.targetId}>`;
|
||||||
|
return `For ${c.targetType} ${targetMention} (${c.targetName})`;
|
||||||
|
})
|
||||||
|
.join('\n'),
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
changes.modified.forEach((c) => {
|
||||||
|
if (c.oldAllow && c.newAllow && c.oldDeny && c.newDeny) {
|
||||||
|
const addedPerms = getPermissionDifference(
|
||||||
|
c.newAllow,
|
||||||
|
c.oldAllow,
|
||||||
|
);
|
||||||
|
const removedPerms = getPermissionDifference(
|
||||||
|
c.oldAllow,
|
||||||
|
c.newAllow,
|
||||||
|
);
|
||||||
|
const addedDenies = getPermissionDifference(c.newDeny, c.oldDeny);
|
||||||
|
const removedDenies = getPermissionDifference(
|
||||||
|
c.oldDeny,
|
||||||
|
c.newDeny,
|
||||||
|
);
|
||||||
|
|
||||||
|
const permissionChanges = [];
|
||||||
|
if (addedPerms.length) {
|
||||||
|
permissionChanges.push(
|
||||||
|
`✅ **Newly Allowed:** ${addedPerms.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (removedPerms.length) {
|
||||||
|
permissionChanges.push(
|
||||||
|
`⬇️ **No Longer Allowed:** ${removedPerms.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (addedDenies.length) {
|
||||||
|
permissionChanges.push(
|
||||||
|
`❌ **Newly Denied:** ${addedDenies.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (removedDenies.length) {
|
||||||
|
permissionChanges.push(
|
||||||
|
`⬆️ **No Longer Denied:** ${removedDenies.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionChanges.length > 0) {
|
||||||
|
fields.push({
|
||||||
|
name: `Changes for ${c.targetType} ${c.targetName}`,
|
||||||
|
value: permissionChanges.join('\n'),
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.removed.length > 0) {
|
||||||
|
fields.push({
|
||||||
|
name: '➖ Removed Permissions',
|
||||||
|
value: changes.removed
|
||||||
|
.map((c) => {
|
||||||
|
const targetMention =
|
||||||
|
c.targetType === 'role'
|
||||||
|
? `<@&${c.targetId}>`
|
||||||
|
: `<@${c.targetId}>`;
|
||||||
|
return `For ${c.targetType} ${targetMention} (${c.targetName})`;
|
||||||
|
})
|
||||||
|
.join('\n'),
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const moderatorField = createModeratorField(
|
const moderatorField = createModeratorField(
|
||||||
|
@ -242,7 +376,11 @@ export default async function logAction(payload: LogActionPayload) {
|
||||||
case 'channelCreate':
|
case 'channelCreate':
|
||||||
case 'channelDelete': {
|
case 'channelDelete': {
|
||||||
fields.push(
|
fields.push(
|
||||||
{ name: 'Channel', value: `<#${payload.channel.id}>`, inline: true },
|
{
|
||||||
|
name: 'Channel',
|
||||||
|
value: `<#${payload.channel.id}> (#${payload.channel.name})`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Type',
|
name: 'Type',
|
||||||
value:
|
value:
|
||||||
|
|
|
@ -108,8 +108,18 @@ export interface ChannelLogAction extends BaseLogAction {
|
||||||
channel: GuildChannel;
|
channel: GuildChannel;
|
||||||
oldName?: string;
|
oldName?: string;
|
||||||
newName?: string;
|
newName?: string;
|
||||||
oldPermissions?: Readonly<PermissionsBitField>;
|
permissionChanges?: Array<{
|
||||||
newPermissions?: Readonly<PermissionsBitField>;
|
action: 'added' | 'modified' | 'removed';
|
||||||
|
targetId: string;
|
||||||
|
targetType: 'role' | 'member';
|
||||||
|
targetName: string;
|
||||||
|
allow?: Readonly<PermissionsBitField>;
|
||||||
|
deny?: Readonly<PermissionsBitField>;
|
||||||
|
oldAllow?: Readonly<PermissionsBitField>;
|
||||||
|
oldDeny?: Readonly<PermissionsBitField>;
|
||||||
|
newAllow?: Readonly<PermissionsBitField>;
|
||||||
|
newDeny?: Readonly<PermissionsBitField>;
|
||||||
|
}>;
|
||||||
moderator?: GuildMember;
|
moderator?: GuildMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,36 @@ export const createPermissionChangeFields = (
|
||||||
return fields;
|
return fields;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPermissionNames = (
|
||||||
|
permissions: Readonly<PermissionsBitField>,
|
||||||
|
): string[] => {
|
||||||
|
const names: string[] = [];
|
||||||
|
|
||||||
|
Object.keys(PermissionsBitField.Flags).forEach((perm) => {
|
||||||
|
if (permissions.has(perm as keyof typeof PermissionsBitField.Flags)) {
|
||||||
|
names.push(formatPermissionName(perm));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return names;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPermissionDifference = (
|
||||||
|
a: Readonly<PermissionsBitField>,
|
||||||
|
b: Readonly<PermissionsBitField>,
|
||||||
|
): string[] => {
|
||||||
|
const names: string[] = [];
|
||||||
|
|
||||||
|
Object.keys(PermissionsBitField.Flags).forEach((perm) => {
|
||||||
|
const permKey = perm as keyof typeof PermissionsBitField.Flags;
|
||||||
|
if (a.has(permKey) && !b.has(permKey)) {
|
||||||
|
names.push(formatPermissionName(perm));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return names;
|
||||||
|
};
|
||||||
|
|
||||||
export const createRoleChangeFields = (
|
export const createRoleChangeFields = (
|
||||||
oldRole: Partial<RoleProperties>,
|
oldRole: Partial<RoleProperties>,
|
||||||
newRole: Partial<RoleProperties>,
|
newRole: Partial<RoleProperties>,
|
||||||
|
|
76
yarn.lock
76
yarn.lock
|
@ -780,6 +780,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@ioredis/commands@npm:^1.1.1":
|
||||||
|
version: 1.2.0
|
||||||
|
resolution: "@ioredis/commands@npm:1.2.0"
|
||||||
|
checksum: 10c0/a5d3c29dd84d8a28b7c67a441ac1715cbd7337a7b88649c0f17c345d89aa218578d2b360760017c48149ef8a70f44b051af9ac0921a0622c2b479614c4f65b36
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@isaacs/cliui@npm:^8.0.2":
|
"@isaacs/cliui@npm:^8.0.2":
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
resolution: "@isaacs/cliui@npm:8.0.2"
|
resolution: "@isaacs/cliui@npm:8.0.2"
|
||||||
|
@ -1443,6 +1450,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"cluster-key-slot@npm:^1.1.0":
|
||||||
|
version: 1.1.2
|
||||||
|
resolution: "cluster-key-slot@npm:1.1.2"
|
||||||
|
checksum: 10c0/d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"color-convert@npm:^2.0.1":
|
"color-convert@npm:^2.0.1":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "color-convert@npm:2.0.1"
|
resolution: "color-convert@npm:2.0.1"
|
||||||
|
@ -1515,6 +1529,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"denque@npm:^2.1.0":
|
||||||
|
version: 2.1.0
|
||||||
|
resolution: "denque@npm:2.1.0"
|
||||||
|
checksum: 10c0/f9ef81aa0af9c6c614a727cb3bd13c5d7db2af1abf9e6352045b86e85873e629690f6222f4edd49d10e4ccf8f078bbeec0794fafaf61b659c0589d0c511ec363
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"diff@npm:^4.0.1":
|
"diff@npm:^4.0.1":
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
resolution: "diff@npm:4.0.2"
|
resolution: "diff@npm:4.0.2"
|
||||||
|
@ -2568,6 +2589,23 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ioredis@npm:^5.5.0":
|
||||||
|
version: 5.5.0
|
||||||
|
resolution: "ioredis@npm:5.5.0"
|
||||||
|
dependencies:
|
||||||
|
"@ioredis/commands": "npm:^1.1.1"
|
||||||
|
cluster-key-slot: "npm:^1.1.0"
|
||||||
|
debug: "npm:^4.3.4"
|
||||||
|
denque: "npm:^2.1.0"
|
||||||
|
lodash.defaults: "npm:^4.2.0"
|
||||||
|
lodash.isarguments: "npm:^3.1.0"
|
||||||
|
redis-errors: "npm:^1.2.0"
|
||||||
|
redis-parser: "npm:^3.0.0"
|
||||||
|
standard-as-callback: "npm:^2.1.0"
|
||||||
|
checksum: 10c0/ba64502fc92d9e05465793fafcd0568cb668af6e2350462b61daadfd499e3a48239d9a723d3ce08b08c93f3f745d05dda91136cdc597d4d485604e6730305305
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ip-address@npm:^9.0.5":
|
"ip-address@npm:^9.0.5":
|
||||||
version: 9.0.5
|
version: 9.0.5
|
||||||
resolution: "ip-address@npm:9.0.5"
|
resolution: "ip-address@npm:9.0.5"
|
||||||
|
@ -2723,6 +2761,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.defaults@npm:^4.2.0":
|
||||||
|
version: 4.2.0
|
||||||
|
resolution: "lodash.defaults@npm:4.2.0"
|
||||||
|
checksum: 10c0/d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isarguments@npm:^3.1.0":
|
||||||
|
version: 3.1.0
|
||||||
|
resolution: "lodash.isarguments@npm:3.1.0"
|
||||||
|
checksum: 10c0/5e8f95ba10975900a3920fb039a3f89a5a79359a1b5565e4e5b4310ed6ebe64011e31d402e34f577eca983a1fc01ff86c926e3cbe602e1ddfc858fdd353e62d8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.merge@npm:^4.6.2":
|
"lodash.merge@npm:^4.6.2":
|
||||||
version: 4.6.2
|
version: 4.6.2
|
||||||
resolution: "lodash.merge@npm:4.6.2"
|
resolution: "lodash.merge@npm:4.6.2"
|
||||||
|
@ -3211,6 +3263,7 @@ __metadata:
|
||||||
eslint: "npm:^9.21.0"
|
eslint: "npm:^9.21.0"
|
||||||
eslint-config-prettier: "npm:^10.0.2"
|
eslint-config-prettier: "npm:^10.0.2"
|
||||||
globals: "npm:^16.0.0"
|
globals: "npm:^16.0.0"
|
||||||
|
ioredis: "npm:^5.5.0"
|
||||||
pg: "npm:^8.13.3"
|
pg: "npm:^8.13.3"
|
||||||
prettier: "npm:3.5.2"
|
prettier: "npm:3.5.2"
|
||||||
ts-node: "npm:^10.9.2"
|
ts-node: "npm:^10.9.2"
|
||||||
|
@ -3333,6 +3386,22 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0":
|
||||||
|
version: 1.2.0
|
||||||
|
resolution: "redis-errors@npm:1.2.0"
|
||||||
|
checksum: 10c0/5b316736e9f532d91a35bff631335137a4f974927bb2fb42bf8c2f18879173a211787db8ac4c3fde8f75ed6233eb0888e55d52510b5620e30d69d7d719c8b8a7
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"redis-parser@npm:^3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "redis-parser@npm:3.0.0"
|
||||||
|
dependencies:
|
||||||
|
redis-errors: "npm:^1.0.0"
|
||||||
|
checksum: 10c0/ee16ac4c7b2a60b1f42a2cdaee22b005bd4453eb2d0588b8a4939718997ae269da717434da5d570fe0b05030466eeb3f902a58cf2e8e1ca058bf6c9c596f632f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"resolve-from@npm:^4.0.0":
|
"resolve-from@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "resolve-from@npm:4.0.0"
|
resolution: "resolve-from@npm:4.0.0"
|
||||||
|
@ -3504,6 +3573,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"standard-as-callback@npm:^2.1.0":
|
||||||
|
version: 2.1.0
|
||||||
|
resolution: "standard-as-callback@npm:2.1.0"
|
||||||
|
checksum: 10c0/012677236e3d3fdc5689d29e64ea8a599331c4babe86956bf92fc5e127d53f85411c5536ee0079c52c43beb0026b5ce7aa1d834dd35dd026e82a15d1bcaead1f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0":
|
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0":
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
resolution: "string-width@npm:4.2.3"
|
resolution: "string-width@npm:4.2.3"
|
||||||
|
|
Loading…
Add table
Reference in a new issue