Added Basic Leveling System and QoL Updates

This commit is contained in:
Ahmad 2025-03-09 15:52:10 -04:00
parent 7af6d5914d
commit b5ce514397
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
15 changed files with 970 additions and 39 deletions

View file

@ -1,10 +1,11 @@
import pkg from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import { and, eq, isNull, sql } from 'drizzle-orm';
import { and, desc, eq, isNull, sql } from 'drizzle-orm';
import * as schema from './schema.js';
import { loadConfig } from '../util/configLoader.js';
import { del, exists, getJson, setJson } from './redis.js';
import { calculateLevelFromXp } from '../util/levelingSystem.js';
const { Pool } = pkg;
const config = loadConfig();
@ -162,6 +163,179 @@ export async function updateMember({
}
}
export async function getUserLevel(
discordId: string,
): Promise<schema.levelTableTypes> {
try {
if (await exists(`level-${discordId}`)) {
const cachedLevel = await getJson<schema.levelTableTypes>(
`level-${discordId}`,
);
if (cachedLevel !== null) {
return cachedLevel;
}
await del(`level-${discordId}`);
}
const level = await db
.select()
.from(schema.levelTable)
.where(eq(schema.levelTable.discordId, discordId))
.then((rows) => rows[0]);
if (level) {
const typedLevel: schema.levelTableTypes = {
...level,
lastMessageTimestamp: level.lastMessageTimestamp ?? undefined,
};
await setJson(`level-${discordId}`, typedLevel);
return typedLevel;
}
const newLevel = {
discordId,
xp: 0,
level: 0,
lastMessageTimestamp: new Date(),
};
await db.insert(schema.levelTable).values(newLevel);
await setJson(`level-${discordId}`, newLevel);
return newLevel;
} catch (error) {
console.error('Error getting user level:', error);
throw error;
}
}
export async function addXpToUser(discordId: string, amount: number) {
try {
const userData = await getUserLevel(discordId);
const currentLevel = userData.level;
userData.xp += amount;
userData.lastMessageTimestamp = new Date();
const newLevel = calculateLevelFromXp(userData.xp);
userData.level = newLevel;
await db
.update(schema.levelTable)
.set({
xp: userData.xp,
level: newLevel,
lastMessageTimestamp: userData.lastMessageTimestamp,
})
.where(eq(schema.levelTable.discordId, discordId));
await setJson(`level-${discordId}`, userData);
await invalidateLeaderboardCache();
return {
leveledUp: newLevel > currentLevel,
newLevel,
oldLevel: currentLevel,
};
} catch (error) {
console.error('Error adding XP to user:', error);
throw error;
}
}
export async function getUserRank(discordId: string): Promise<number> {
try {
if (await exists('xp-leaderboard-cache')) {
const leaderboardCache = await getJson<
Array<{ discordId: string; xp: number }>
>('xp-leaderboard-cache');
if (leaderboardCache) {
const userIndex = leaderboardCache.findIndex(
(member) => member.discordId === discordId,
);
if (userIndex !== -1) {
return userIndex + 1;
}
}
}
const allMembers = await db
.select({
discordId: schema.levelTable.discordId,
xp: schema.levelTable.xp,
})
.from(schema.levelTable)
.orderBy(desc(schema.levelTable.xp));
await setJson('xp-leaderboard-cache', allMembers, 300);
const userIndex = allMembers.findIndex(
(member) => member.discordId === discordId,
);
return userIndex !== -1 ? userIndex + 1 : 1;
} catch (error) {
console.error('Error getting user rank:', error);
return 1;
}
}
export async function invalidateLeaderboardCache() {
try {
if (await exists('xp-leaderboard-cache')) {
await del('xp-leaderboard-cache');
}
} catch (error) {
console.error('Error invalidating leaderboard cache:', error);
}
}
export async function getLevelLeaderboard(limit = 10) {
try {
if (await exists('xp-leaderboard-cache')) {
const leaderboardCache = await getJson<
Array<{ discordId: string; xp: number }>
>('xp-leaderboard-cache');
if (leaderboardCache) {
const limitedCache = leaderboardCache.slice(0, limit);
const fullLeaderboard = await Promise.all(
limitedCache.map(async (entry) => {
const userData = await getUserLevel(entry.discordId);
return userData;
}),
);
return fullLeaderboard;
}
}
const leaderboard = await db
.select()
.from(schema.levelTable)
.orderBy(desc(schema.levelTable.xp))
.limit(limit);
const allMembers = await db
.select({
discordId: schema.levelTable.discordId,
xp: schema.levelTable.xp,
})
.from(schema.levelTable)
.orderBy(desc(schema.levelTable.xp));
await setJson('xp-leaderboard-cache', allMembers, 300);
return leaderboard;
} catch (error) {
console.error('Error getting leaderboard:', error);
throw error;
}
}
export async function updateMemberModerationHistory({
discordId,
moderatorDiscordId,

View file

@ -25,6 +25,24 @@ export const memberTable = pgTable('members', {
currentlyMuted: boolean('currently_muted').notNull().default(false),
});
export interface levelTableTypes {
id?: number;
discordId: string;
xp: number;
level: number;
lastMessageTimestamp?: Date;
}
export const levelTable = pgTable('levels', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
discordId: varchar('discord_id')
.notNull()
.references(() => memberTable.discordId, { onDelete: 'cascade' }),
xp: integer('xp').notNull().default(0),
level: integer('level').notNull().default(1),
lastMessageTimestamp: timestamp('last_message_timestamp'),
});
export interface moderationTableTypes {
id?: number;
discordId: string;
@ -51,8 +69,19 @@ export const moderationTable = pgTable('moderations', {
active: boolean('active').notNull().default(true),
});
export const memberRelations = relations(memberTable, ({ many }) => ({
export const memberRelations = relations(memberTable, ({ many, one }) => ({
moderations: many(moderationTable),
levels: one(levelTable, {
fields: [memberTable.discordId],
references: [levelTable.discordId],
}),
}));
export const levelRelations = relations(levelTable, ({ one }) => ({
member: one(memberTable, {
fields: [levelTable.discordId],
references: [memberTable.discordId],
}),
}));
export const moderationRelations = relations(moderationTable, ({ one }) => ({