mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-06-07 15:39:30 +00:00
feat: add giveaway system
Signed-off-by: Ahmad <103906421+ahmadk953@users.noreply.github.com>
This commit is contained in:
parent
e898a9238d
commit
d9d5f087e7
23 changed files with 2811 additions and 168 deletions
292
src/db/db.ts
292
src/db/db.ts
|
@ -4,14 +4,15 @@ import { Client, Collection, GuildMember } from 'discord.js';
|
|||
import { and, desc, eq, isNull, sql } 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';
|
||||
import { calculateLevelFromXp } from '../util/levelingSystem.js';
|
||||
import { calculateLevelFromXp } from '@/util/levelingSystem.js';
|
||||
import { selectGiveawayWinners } from '@/util/giveaways/giveawayManager.js';
|
||||
import {
|
||||
logManagerNotification,
|
||||
NotificationType,
|
||||
notifyManagers,
|
||||
} from '../util/notificationHandler.js';
|
||||
} from '@/util/notificationHandler.js';
|
||||
|
||||
const { Pool } = pkg;
|
||||
const config = loadConfig();
|
||||
|
@ -192,7 +193,7 @@ export async function ensureDatabaseConnection(): Promise<boolean> {
|
|||
* @param error - Original error object
|
||||
*/
|
||||
export const handleDbError = (errorMessage: string, error: Error): never => {
|
||||
console.error(`${errorMessage}: `, error);
|
||||
console.error(`${errorMessage}:`, error);
|
||||
|
||||
if (
|
||||
error.message.includes('connection') ||
|
||||
|
@ -448,6 +449,7 @@ export async function getUserLevel(
|
|||
xp: 0,
|
||||
level: 0,
|
||||
lastMessageTimestamp: new Date(),
|
||||
messagesSent: 0,
|
||||
};
|
||||
|
||||
await db.insert(schema.levelTable).values(newLevel);
|
||||
|
@ -470,6 +472,7 @@ export async function addXpToUser(
|
|||
leveledUp: boolean;
|
||||
newLevel: number;
|
||||
oldLevel: number;
|
||||
messagesSent: number;
|
||||
}> {
|
||||
try {
|
||||
await ensureDbInitialized();
|
||||
|
@ -485,6 +488,7 @@ export async function addXpToUser(
|
|||
userData.xp += amount;
|
||||
userData.lastMessageTimestamp = new Date();
|
||||
userData.level = calculateLevelFromXp(userData.xp);
|
||||
userData.messagesSent += 1;
|
||||
|
||||
await invalidateLeaderboardCache();
|
||||
await invalidateCache(cacheKey);
|
||||
|
@ -497,6 +501,7 @@ export async function addXpToUser(
|
|||
xp: userData.xp,
|
||||
level: userData.level,
|
||||
lastMessageTimestamp: userData.lastMessageTimestamp,
|
||||
messagesSent: userData.messagesSent,
|
||||
})
|
||||
.where(eq(schema.levelTable.discordId, discordId))
|
||||
.returning();
|
||||
|
@ -510,6 +515,7 @@ export async function addXpToUser(
|
|||
leveledUp: userData.level > currentLevel,
|
||||
newLevel: userData.level,
|
||||
oldLevel: currentLevel,
|
||||
messagesSent: userData.messagesSent,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleDbError('Error adding XP to user', error as Error);
|
||||
|
@ -551,7 +557,7 @@ export async function getUserRank(discordId: string): Promise<number> {
|
|||
* Clear leaderboard cache
|
||||
*/
|
||||
export async function invalidateLeaderboardCache(): Promise<void> {
|
||||
await invalidateCache('xp-leaderboard-cache');
|
||||
await invalidateCache('xp-leaderboard');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -571,7 +577,7 @@ async function getLeaderboardData(): Promise<
|
|||
console.error('Database not initialized, cannot get leaderboard data');
|
||||
}
|
||||
|
||||
const cacheKey = 'xp-leaderboard-cache';
|
||||
const cacheKey = 'xp-leaderboard';
|
||||
return withCache<Array<{ discordId: string; xp: number }>>(
|
||||
cacheKey,
|
||||
async () => {
|
||||
|
@ -911,3 +917,277 @@ export async function deleteFact(id: number): Promise<void> {
|
|||
return handleDbError('Failed to delete fact', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Giveaway Functions
|
||||
// ========================
|
||||
|
||||
/**
|
||||
* Create a giveaway in the database
|
||||
* @param giveawayData - Data for the giveaway
|
||||
* @returns Created giveaway object
|
||||
*/
|
||||
export async function createGiveaway(giveawayData: {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
endAt: Date;
|
||||
prize: string;
|
||||
winnerCount: number;
|
||||
hostId: string;
|
||||
requirements?: {
|
||||
level?: number;
|
||||
roleId?: string;
|
||||
messageCount?: number;
|
||||
requireAll?: boolean;
|
||||
};
|
||||
bonuses?: {
|
||||
roles?: Array<{ id: string; entries: number }>;
|
||||
levels?: Array<{ threshold: number; entries: number }>;
|
||||
messages?: Array<{ threshold: number; entries: number }>;
|
||||
};
|
||||
}): Promise<schema.giveawayTableTypes> {
|
||||
try {
|
||||
await ensureDbInitialized();
|
||||
|
||||
if (!db) {
|
||||
console.error('Database not initialized, cannot create giveaway');
|
||||
}
|
||||
|
||||
const [giveaway] = await db
|
||||
.insert(schema.giveawayTable)
|
||||
.values({
|
||||
channelId: giveawayData.channelId,
|
||||
messageId: giveawayData.messageId,
|
||||
endAt: giveawayData.endAt,
|
||||
prize: giveawayData.prize,
|
||||
winnerCount: giveawayData.winnerCount,
|
||||
hostId: giveawayData.hostId,
|
||||
requiredLevel: giveawayData.requirements?.level,
|
||||
requiredRoleId: giveawayData.requirements?.roleId,
|
||||
requiredMessageCount: giveawayData.requirements?.messageCount,
|
||||
requireAllCriteria: giveawayData.requirements?.requireAll ?? true,
|
||||
bonusEntries:
|
||||
giveawayData.bonuses as schema.giveawayTableTypes['bonusEntries'],
|
||||
})
|
||||
.returning();
|
||||
|
||||
return giveaway as schema.giveawayTableTypes;
|
||||
} catch (error) {
|
||||
return handleDbError('Failed to create giveaway', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a giveaway by ID or message ID
|
||||
* @param id - ID of the giveaway
|
||||
* @param isDbId - Whether the ID is a database ID
|
||||
* @returns Giveaway object or undefined if not found
|
||||
*/
|
||||
export async function getGiveaway(
|
||||
id: string | number,
|
||||
isDbId = false,
|
||||
): Promise<schema.giveawayTableTypes | undefined> {
|
||||
try {
|
||||
await ensureDbInitialized();
|
||||
|
||||
if (!db) {
|
||||
console.error('Database not initialized, cannot get giveaway');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isDbId) {
|
||||
const numId = typeof id === 'string' ? parseInt(id) : id;
|
||||
const [giveaway] = await db
|
||||
.select()
|
||||
.from(schema.giveawayTable)
|
||||
.where(eq(schema.giveawayTable.id, numId))
|
||||
.limit(1);
|
||||
|
||||
return giveaway as schema.giveawayTableTypes;
|
||||
} else {
|
||||
const [giveaway] = await db
|
||||
.select()
|
||||
.from(schema.giveawayTable)
|
||||
.where(eq(schema.giveawayTable.messageId, id as string))
|
||||
.limit(1);
|
||||
|
||||
return giveaway as schema.giveawayTableTypes;
|
||||
}
|
||||
} catch (error) {
|
||||
return handleDbError('Failed to get giveaway', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active giveaways
|
||||
* @returns Array of active giveaway objects
|
||||
*/
|
||||
export async function getActiveGiveaways(): Promise<
|
||||
schema.giveawayTableTypes[]
|
||||
> {
|
||||
try {
|
||||
await ensureDbInitialized();
|
||||
|
||||
if (!db) {
|
||||
console.error('Database not initialized, cannot get active giveaways');
|
||||
}
|
||||
|
||||
return (await db
|
||||
.select()
|
||||
.from(schema.giveawayTable)
|
||||
.where(
|
||||
eq(schema.giveawayTable.status, 'active'),
|
||||
)) as schema.giveawayTableTypes[];
|
||||
} catch (error) {
|
||||
return handleDbError('Failed to get active giveaways', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update giveaway participants
|
||||
* @param messageId - ID of the giveaway message
|
||||
* @param userId - ID of the user to add
|
||||
* @param entries - Number of entries to add
|
||||
* @return 'success' | 'already_entered' | 'inactive' | 'error'
|
||||
*/
|
||||
export async function addGiveawayParticipant(
|
||||
messageId: string,
|
||||
userId: string,
|
||||
entries = 1,
|
||||
): Promise<'success' | 'already_entered' | 'inactive' | 'error'> {
|
||||
try {
|
||||
await ensureDbInitialized();
|
||||
|
||||
if (!db) {
|
||||
console.error('Database not initialized, cannot add participant');
|
||||
return 'error';
|
||||
}
|
||||
|
||||
const giveaway = await getGiveaway(messageId);
|
||||
if (!giveaway || giveaway.status !== 'active') {
|
||||
return 'inactive';
|
||||
}
|
||||
|
||||
if (giveaway.participants?.includes(userId)) {
|
||||
return 'already_entered';
|
||||
}
|
||||
|
||||
const participants = [...(giveaway.participants || [])];
|
||||
for (let i = 0; i < entries; i++) {
|
||||
participants.push(userId);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(schema.giveawayTable)
|
||||
.set({ participants: participants })
|
||||
.where(eq(schema.giveawayTable.messageId, messageId));
|
||||
|
||||
return 'success';
|
||||
} catch (error) {
|
||||
handleDbError('Failed to add giveaway participant', error as Error);
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End a giveaway
|
||||
* @param id - ID of the giveaway
|
||||
* @param isDbId - Whether the ID is a database ID
|
||||
* @param forceWinners - Array of user IDs to force as winners
|
||||
* @return Updated giveaway object
|
||||
*/
|
||||
export async function endGiveaway(
|
||||
id: string | number,
|
||||
isDbId = false,
|
||||
forceWinners?: string[],
|
||||
): Promise<schema.giveawayTableTypes | undefined> {
|
||||
try {
|
||||
await ensureDbInitialized();
|
||||
|
||||
if (!db) {
|
||||
console.error('Database not initialized, cannot end giveaway');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const giveaway = await getGiveaway(id, isDbId);
|
||||
if (!giveaway || giveaway.status !== 'active' || !giveaway.participants) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const winners = selectGiveawayWinners(
|
||||
giveaway.participants,
|
||||
giveaway.winnerCount,
|
||||
forceWinners,
|
||||
);
|
||||
|
||||
const [updatedGiveaway] = await db
|
||||
.update(schema.giveawayTable)
|
||||
.set({
|
||||
status: 'ended',
|
||||
winnersIds: winners,
|
||||
})
|
||||
.where(eq(schema.giveawayTable.id, giveaway.id))
|
||||
.returning();
|
||||
|
||||
return updatedGiveaway as schema.giveawayTableTypes;
|
||||
} catch (error) {
|
||||
return handleDbError('Failed to end giveaway', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reroll winners for a giveaway
|
||||
* @param id - ID of the giveaway
|
||||
* @return Updated giveaway object
|
||||
*/
|
||||
export async function rerollGiveaway(
|
||||
id: string,
|
||||
): Promise<schema.giveawayTableTypes | undefined> {
|
||||
try {
|
||||
await ensureDbInitialized();
|
||||
|
||||
if (!db) {
|
||||
console.error('Database not initialized, cannot reroll giveaway');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const giveaway = await getGiveaway(id, true);
|
||||
if (
|
||||
!giveaway ||
|
||||
!giveaway.participants ||
|
||||
giveaway.participants.length === 0 ||
|
||||
giveaway.status !== 'ended'
|
||||
) {
|
||||
console.warn(
|
||||
`Cannot reroll giveaway ${id}: Not found, no participants, or not ended.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newWinners = selectGiveawayWinners(
|
||||
giveaway.participants,
|
||||
giveaway.winnerCount,
|
||||
undefined,
|
||||
giveaway.winnersIds ?? [],
|
||||
);
|
||||
|
||||
if (newWinners.length === 0) {
|
||||
console.warn(
|
||||
`Cannot reroll giveaway ${id}: No eligible participants left after excluding previous winners.`,
|
||||
);
|
||||
return giveaway;
|
||||
}
|
||||
|
||||
const [updatedGiveaway] = await db
|
||||
.update(schema.giveawayTable)
|
||||
.set({
|
||||
winnersIds: newWinners,
|
||||
})
|
||||
.where(eq(schema.giveawayTable.id, giveaway.id))
|
||||
.returning();
|
||||
|
||||
return updatedGiveaway as schema.giveawayTableTypes;
|
||||
} catch (error) {
|
||||
return handleDbError('Failed to reroll giveaway', error as Error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
timestamp,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
import { InferSelectModel, relations } from 'drizzle-orm';
|
||||
|
||||
export interface memberTableTypes {
|
||||
id?: number;
|
||||
|
@ -30,6 +31,7 @@ export interface levelTableTypes {
|
|||
discordId: string;
|
||||
xp: number;
|
||||
level: number;
|
||||
messagesSent: number;
|
||||
lastMessageTimestamp?: Date;
|
||||
}
|
||||
|
||||
|
@ -40,6 +42,7 @@ export const levelTable = pgTable('levels', {
|
|||
.references(() => memberTable.discordId, { onDelete: 'cascade' }),
|
||||
xp: integer('xp').notNull().default(0),
|
||||
level: integer('level').notNull().default(0),
|
||||
messagesSent: integer('messages_sent').notNull().default(0),
|
||||
lastMessageTimestamp: timestamp('last_message_timestamp'),
|
||||
});
|
||||
|
||||
|
@ -111,3 +114,32 @@ export const factTable = pgTable('facts', {
|
|||
approved: boolean('approved').default(false).notNull(),
|
||||
usedOn: timestamp('used_on'),
|
||||
});
|
||||
|
||||
export type giveawayTableTypes = InferSelectModel<typeof giveawayTable> & {
|
||||
bonusEntries: {
|
||||
roles?: Array<{ id: string; entries: number }>;
|
||||
levels?: Array<{ threshold: number; entries: number }>;
|
||||
messages?: Array<{ threshold: number; entries: number }>;
|
||||
};
|
||||
};
|
||||
|
||||
export const giveawayTable = pgTable('giveaways', {
|
||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
channelId: varchar('channel_id').notNull(),
|
||||
messageId: varchar('message_id').notNull().unique(),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
endAt: timestamp('end_at').notNull(),
|
||||
prize: varchar('prize').notNull(),
|
||||
winnerCount: integer('winner_count').notNull().default(1),
|
||||
hostId: varchar('host_id')
|
||||
.references(() => memberTable.discordId)
|
||||
.notNull(),
|
||||
status: varchar('status').notNull().default('active'),
|
||||
participants: varchar('participants').array().default([]),
|
||||
winnersIds: varchar('winners_ids').array().default([]),
|
||||
requiredLevel: integer('required_level'),
|
||||
requiredRoleId: varchar('required_role_id'),
|
||||
requiredMessageCount: integer('required_message_count'),
|
||||
requireAllCriteria: boolean('require_all_criteria').default(true),
|
||||
bonusEntries: jsonb('bonus_entries').default({}),
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue