mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-05-10 10:43:06 +00:00
Added Basic Redis Caching for DB Queries
This commit is contained in:
parent
e1003ee214
commit
0d04adf4fd
8 changed files with 386 additions and 58 deletions
237
src/db/db.ts
237
src/db/db.ts
|
@ -1,8 +1,10 @@
|
|||
import pkg from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from './schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import * as schema from './schema.js';
|
||||
import { loadConfig } from '../util/configLoader.js';
|
||||
import { del, exists, getJson, setJson } from './redis.js';
|
||||
|
||||
const { Pool } = pkg;
|
||||
const config = loadConfig();
|
||||
|
@ -13,41 +15,121 @@ const dbPool = new Pool({
|
|||
});
|
||||
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() {
|
||||
return await db
|
||||
.select()
|
||||
.from(schema.memberTable)
|
||||
.where(eq(schema.memberTable.currentlyInServer, true));
|
||||
try {
|
||||
if (await exists('nonBotMembers')) {
|
||||
const memberData =
|
||||
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) {
|
||||
nonBotMembers.forEach(async (member: any) => {
|
||||
const memberExists = await db
|
||||
.select()
|
||||
.from(schema.memberTable)
|
||||
.where(eq(schema.memberTable.discordId, member.user.id));
|
||||
if (memberExists.length > 0) {
|
||||
await db
|
||||
.update(schema.memberTable)
|
||||
.set({ discordUsername: member.user.username })
|
||||
try {
|
||||
nonBotMembers.forEach(async (member: any) => {
|
||||
const memberInfo = await db
|
||||
.select()
|
||||
.from(schema.memberTable)
|
||||
.where(eq(schema.memberTable.discordId, member.user.id));
|
||||
} else {
|
||||
const members: typeof schema.memberTable.$inferInsert = {
|
||||
discordId: member.user.id,
|
||||
discordUsername: member.user.username,
|
||||
};
|
||||
await db.insert(schema.memberTable).values(members);
|
||||
}
|
||||
});
|
||||
if (memberInfo.length > 0) {
|
||||
await updateMember({
|
||||
discordId: member.user.id,
|
||||
discordUsername: member.user.username,
|
||||
currentlyInServer: true,
|
||||
});
|
||||
} 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) {
|
||||
return await db.query.memberTable.findFirst({
|
||||
where: eq(schema.memberTable.discordId, discordId),
|
||||
with: {
|
||||
moderations: true,
|
||||
},
|
||||
});
|
||||
try {
|
||||
if (await exists(`${discordId}-memberInfo`)) {
|
||||
const cachedMember = await getJson<
|
||||
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({
|
||||
|
@ -56,14 +138,28 @@ export async function updateMember({
|
|||
currentlyInServer,
|
||||
currentlyBanned,
|
||||
}: schema.memberTableTypes) {
|
||||
return await db
|
||||
.update(schema.memberTable)
|
||||
.set({
|
||||
discordUsername,
|
||||
currentlyInServer,
|
||||
currentlyBanned,
|
||||
})
|
||||
.where(eq(schema.memberTable.discordId, discordId));
|
||||
try {
|
||||
const result = await db
|
||||
.update(schema.memberTable)
|
||||
.set({
|
||||
discordUsername,
|
||||
currentlyInServer,
|
||||
currentlyBanned,
|
||||
})
|
||||
.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({
|
||||
|
@ -76,22 +172,61 @@ export async function updateMemberModerationHistory({
|
|||
expiresAt,
|
||||
active,
|
||||
}: schema.moderationTableTypes) {
|
||||
const moderationEntry = {
|
||||
discordId,
|
||||
moderatorDiscordId,
|
||||
action,
|
||||
reason,
|
||||
duration,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
active,
|
||||
};
|
||||
return await db.insert(schema.moderationTable).values(moderationEntry);
|
||||
try {
|
||||
const moderationEntry = {
|
||||
discordId,
|
||||
moderatorDiscordId,
|
||||
action,
|
||||
reason,
|
||||
duration,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
active,
|
||||
};
|
||||
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) {
|
||||
return await db
|
||||
.select()
|
||||
.from(schema.moderationTable)
|
||||
.where(eq(schema.moderationTable.discordId, discordId));
|
||||
try {
|
||||
if (await exists(`${discordId}-moderationHistory`)) {
|
||||
return await getJson<(typeof schema.moderationTable.$inferSelect)[]>(
|
||||
`${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);
|
||||
}
|
||||
}
|
|
@ -17,9 +17,12 @@ export const memberJoin = {
|
|||
}
|
||||
|
||||
try {
|
||||
const members = await guild.members.fetch();
|
||||
const nonBotMembers = members.filter((m) => !m.user.bot);
|
||||
await setMembers(nonBotMembers);
|
||||
await setMembers([
|
||||
{
|
||||
discordId: member.user.id,
|
||||
discordUsername: member.user.username,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!member.user.bot) {
|
||||
const attachment = await generateMemberBanner({
|
||||
|
@ -37,10 +40,6 @@ export const memberJoin = {
|
|||
content: `Welcome to ${guild.name}, we hope you enjoy your stay!`,
|
||||
files: [attachment],
|
||||
}),
|
||||
updateMember({
|
||||
discordId: member.user.id,
|
||||
currentlyInServer: true,
|
||||
}),
|
||||
member.roles.add(config.roles.joinRoles),
|
||||
logAction({
|
||||
guild,
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import { Client, Events } from 'discord.js';
|
||||
|
||||
import { setMembers } from '../db/db.js';
|
||||
import { loadConfig } from '../util/configLoader.js';
|
||||
|
||||
export default {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
execute: async (client: Client) => {
|
||||
const config = loadConfig();
|
||||
const members = await client.guilds.cache
|
||||
.find((guild) => guild.id === config.guildId)
|
||||
?.members.fetch();
|
||||
const nonBotMembers = members!.filter((m) => !m.user.bot);
|
||||
await setMembers(nonBotMembers);
|
||||
|
||||
console.log(`Ready! Logged in as ${client.user?.tag}`);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ export interface Config {
|
|||
clientId: string;
|
||||
guildId: string;
|
||||
dbConnectionString: string;
|
||||
redisConnectionString: string;
|
||||
channels: {
|
||||
welcome: string;
|
||||
logs: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue