mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-05-10 10:43:06 +00:00
chore: split db file into multiple files and centralized pagination
This commit is contained in:
parent
2f5c3499e7
commit
be8df5f6a2
11 changed files with 1612 additions and 1401 deletions
|
@ -17,6 +17,7 @@ import {
|
||||||
import { postFactOfTheDay } from '@/util/factManager.js';
|
import { postFactOfTheDay } from '@/util/factManager.js';
|
||||||
import { loadConfig } from '@/util/configLoader.js';
|
import { loadConfig } from '@/util/configLoader.js';
|
||||||
import { SubcommandCommand } from '@/types/CommandTypes.js';
|
import { SubcommandCommand } from '@/types/CommandTypes.js';
|
||||||
|
import { createPaginationButtons } from '@/util/helpers.js';
|
||||||
|
|
||||||
const command: SubcommandCommand = {
|
const command: SubcommandCommand = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
@ -197,6 +198,7 @@ const command: SubcommandCommand = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FACTS_PER_PAGE = 5;
|
||||||
const pendingFacts = await getPendingFacts();
|
const pendingFacts = await getPendingFacts();
|
||||||
|
|
||||||
if (pendingFacts.length === 0) {
|
if (pendingFacts.length === 0) {
|
||||||
|
@ -206,20 +208,85 @@ const command: SubcommandCommand = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const pages: EmbedBuilder[] = [];
|
||||||
.setTitle('Pending Facts')
|
for (let i = 0; i < pendingFacts.length; i += FACTS_PER_PAGE) {
|
||||||
.setColor(0x0099ff)
|
const pageFacts = pendingFacts.slice(i, i + FACTS_PER_PAGE);
|
||||||
.setDescription(
|
|
||||||
pendingFacts
|
|
||||||
.map((fact) => {
|
|
||||||
return `**ID #${fact.id}**\n${fact.content}\nSubmitted by: <@${fact.addedBy}>\nSource: ${fact.source || 'Not provided'}`;
|
|
||||||
})
|
|
||||||
.join('\n\n'),
|
|
||||||
)
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await interaction.editReply({
|
const embed = new EmbedBuilder()
|
||||||
embeds: [embed],
|
.setTitle('Pending Facts')
|
||||||
|
.setColor(0x0099ff)
|
||||||
|
.setDescription(
|
||||||
|
pageFacts
|
||||||
|
.map((fact) => {
|
||||||
|
return `**ID #${fact.id}**\n${fact.content}\nSubmitted by: <@${fact.addedBy}>\nSource: ${fact.source || 'Not provided'}`;
|
||||||
|
})
|
||||||
|
.join('\n\n'),
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
pages.push(embed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentPage = 0;
|
||||||
|
|
||||||
|
const message = await interaction.editReply({
|
||||||
|
embeds: [pages[currentPage]],
|
||||||
|
components: [createPaginationButtons(pages.length, currentPage)],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pages.length <= 1) return;
|
||||||
|
|
||||||
|
const collector = message.createMessageComponentCollector({
|
||||||
|
time: 300000,
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('collect', async (i) => {
|
||||||
|
if (i.user.id !== interaction.user.id) {
|
||||||
|
await i.reply({
|
||||||
|
content: 'These controls are not for you!',
|
||||||
|
flags: ['Ephemeral'],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i.isButton()) {
|
||||||
|
switch (i.customId) {
|
||||||
|
case 'first':
|
||||||
|
currentPage = 0;
|
||||||
|
break;
|
||||||
|
case 'prev':
|
||||||
|
if (currentPage > 0) currentPage--;
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
if (currentPage < pages.length - 1) currentPage++;
|
||||||
|
break;
|
||||||
|
case 'last':
|
||||||
|
currentPage = pages.length - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i.isStringSelectMenu()) {
|
||||||
|
const selected = parseInt(i.values[0]);
|
||||||
|
if (!isNaN(selected) && selected >= 0 && selected < pages.length) {
|
||||||
|
currentPage = selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await i.update({
|
||||||
|
embeds: [pages[currentPage]],
|
||||||
|
components: [createPaginationButtons(pages.length, currentPage)],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('end', async () => {
|
||||||
|
if (message) {
|
||||||
|
try {
|
||||||
|
await interaction.editReply({ components: [] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing components:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (subcommand === 'post') {
|
} else if (subcommand === 'post') {
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
formatWinnerMentions,
|
formatWinnerMentions,
|
||||||
builder,
|
builder,
|
||||||
} from '@/util/giveaways/giveawayManager.js';
|
} from '@/util/giveaways/giveawayManager.js';
|
||||||
|
import { createPaginationButtons } from '@/util/helpers.js';
|
||||||
|
|
||||||
const command: SubcommandCommand = {
|
const command: SubcommandCommand = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
@ -97,37 +98,103 @@ async function handleCreateGiveaway(interaction: ChatInputCommandInteraction) {
|
||||||
*/
|
*/
|
||||||
async function handleListGiveaways(interaction: ChatInputCommandInteraction) {
|
async function handleListGiveaways(interaction: ChatInputCommandInteraction) {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
const GIVEAWAYS_PER_PAGE = 5;
|
||||||
|
|
||||||
const activeGiveaways = await getActiveGiveaways();
|
try {
|
||||||
|
const activeGiveaways = await getActiveGiveaways();
|
||||||
|
|
||||||
if (activeGiveaways.length === 0) {
|
if (activeGiveaways.length === 0) {
|
||||||
await interaction.editReply('There are no active giveaways at the moment.');
|
await interaction.editReply({
|
||||||
return;
|
content: 'There are no active giveaways at the moment.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages: EmbedBuilder[] = [];
|
||||||
|
for (let i = 0; i < activeGiveaways.length; i += GIVEAWAYS_PER_PAGE) {
|
||||||
|
const pageGiveaways = activeGiveaways.slice(i, i + GIVEAWAYS_PER_PAGE);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle('🎉 Active Giveaways')
|
||||||
|
.setColor(0x00ff00)
|
||||||
|
.setDescription('Here are the currently active giveaways:')
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
pageGiveaways.forEach((giveaway) => {
|
||||||
|
embed.addFields({
|
||||||
|
name: `${giveaway.prize} (ID: ${giveaway.id})`,
|
||||||
|
value: [
|
||||||
|
`**Hosted by:** <@${giveaway.hostId}>`,
|
||||||
|
`**Winners:** ${giveaway.winnerCount}`,
|
||||||
|
`**Ends:** <t:${Math.floor(giveaway.endAt.getTime() / 1000)}:R>`,
|
||||||
|
`**Entries:** ${giveaway.participants?.length || 0}`,
|
||||||
|
`[Jump to Giveaway](https://discord.com/channels/${interaction.guildId}/${giveaway.channelId}/${giveaway.messageId})`,
|
||||||
|
].join('\n'),
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pages.push(embed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentPage = 0;
|
||||||
|
|
||||||
|
const message = await interaction.editReply({
|
||||||
|
embeds: [pages[currentPage]],
|
||||||
|
components: [createPaginationButtons(pages.length, currentPage)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const collector = message.createMessageComponentCollector({
|
||||||
|
time: 300000,
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('collect', async (i) => {
|
||||||
|
if (i.user.id !== interaction.user.id) {
|
||||||
|
await i.reply({
|
||||||
|
content: 'You cannot use these buttons.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i.isButton()) {
|
||||||
|
switch (i.customId) {
|
||||||
|
case 'first':
|
||||||
|
currentPage = 0;
|
||||||
|
break;
|
||||||
|
case 'prev':
|
||||||
|
if (currentPage > 0) currentPage--;
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
if (currentPage < pages.length - 1) currentPage++;
|
||||||
|
break;
|
||||||
|
case 'last':
|
||||||
|
currentPage = pages.length - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await i.update({
|
||||||
|
embeds: [pages[currentPage]],
|
||||||
|
components: [createPaginationButtons(pages.length, currentPage)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('end', async () => {
|
||||||
|
try {
|
||||||
|
await interaction.editReply({
|
||||||
|
components: [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing components:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching giveaways:', error);
|
||||||
|
await interaction.editReply({
|
||||||
|
content: 'There was an error fetching the giveaways.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setTitle('🎉 Active Giveaways')
|
|
||||||
.setColor(0x00ff00)
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
const giveawayDetails = activeGiveaways.map((g) => {
|
|
||||||
const channel = interaction.guild?.channels.cache.get(g.channelId);
|
|
||||||
const channelMention = channel ? `<#${channel.id}>` : 'Unknown channel';
|
|
||||||
|
|
||||||
return [
|
|
||||||
`**Prize**: ${g.prize}`,
|
|
||||||
`**ID**: ${g.id}`,
|
|
||||||
`**Winners**: ${g.winnerCount}`,
|
|
||||||
`**Ends**: <t:${Math.floor(g.endAt.getTime() / 1000)}:R>`,
|
|
||||||
`**Channel**: ${channelMention}`,
|
|
||||||
`**Entries**: ${g.participants?.length || 0}`,
|
|
||||||
'───────────────────',
|
|
||||||
].join('\n');
|
|
||||||
});
|
|
||||||
|
|
||||||
embed.setDescription(giveawayDetails.join('\n'));
|
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import {
|
import {
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
ButtonBuilder,
|
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonStyle,
|
|
||||||
StringSelectMenuBuilder,
|
StringSelectMenuBuilder,
|
||||||
APIEmbed,
|
APIEmbed,
|
||||||
JSONEncodable,
|
JSONEncodable,
|
||||||
|
@ -11,6 +9,7 @@ import {
|
||||||
|
|
||||||
import { OptionsCommand } from '@/types/CommandTypes.js';
|
import { OptionsCommand } from '@/types/CommandTypes.js';
|
||||||
import { getLevelLeaderboard } from '@/db/db.js';
|
import { getLevelLeaderboard } from '@/db/db.js';
|
||||||
|
import { createPaginationButtons } from '@/util/helpers.js';
|
||||||
|
|
||||||
const command: OptionsCommand = {
|
const command: OptionsCommand = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
@ -78,18 +77,7 @@ const command: OptionsCommand = {
|
||||||
let currentPage = 0;
|
let currentPage = 0;
|
||||||
|
|
||||||
const getButtonActionRow = () =>
|
const getButtonActionRow = () =>
|
||||||
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
createPaginationButtons(pages.length, currentPage);
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId('previous')
|
|
||||||
.setLabel('Previous')
|
|
||||||
.setStyle(ButtonStyle.Primary)
|
|
||||||
.setDisabled(currentPage === 0),
|
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId('next')
|
|
||||||
.setLabel('Next')
|
|
||||||
.setStyle(ButtonStyle.Primary)
|
|
||||||
.setDisabled(currentPage === pages.length - 1),
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSelectMenuRow = () => {
|
const getSelectMenuRow = () => {
|
||||||
const options = pages.map((_, index) => ({
|
const options = pages.map((_, index) => ({
|
||||||
|
@ -119,7 +107,7 @@ const command: OptionsCommand = {
|
||||||
if (pages.length <= 1) return;
|
if (pages.length <= 1) return;
|
||||||
|
|
||||||
const collector = message.createMessageComponentCollector({
|
const collector = message.createMessageComponentCollector({
|
||||||
time: 60000,
|
time: 300000,
|
||||||
});
|
});
|
||||||
|
|
||||||
collector.on('collect', async (i) => {
|
collector.on('collect', async (i) => {
|
||||||
|
@ -132,10 +120,19 @@ const command: OptionsCommand = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i.isButton()) {
|
if (i.isButton()) {
|
||||||
if (i.customId === 'previous' && currentPage > 0) {
|
switch (i.customId) {
|
||||||
currentPage--;
|
case 'first':
|
||||||
} else if (i.customId === 'next' && currentPage < pages.length - 1) {
|
currentPage = 0;
|
||||||
currentPage++;
|
break;
|
||||||
|
case 'prev':
|
||||||
|
if (currentPage > 0) currentPage--;
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
if (currentPage < pages.length - 1) currentPage++;
|
||||||
|
break;
|
||||||
|
case 'last':
|
||||||
|
currentPage = pages.length - 1;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import {
|
import {
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
ButtonBuilder,
|
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonStyle,
|
|
||||||
StringSelectMenuBuilder,
|
StringSelectMenuBuilder,
|
||||||
APIEmbed,
|
APIEmbed,
|
||||||
JSONEncodable,
|
JSONEncodable,
|
||||||
|
@ -11,6 +9,7 @@ import {
|
||||||
|
|
||||||
import { getAllMembers } from '@/db/db.js';
|
import { getAllMembers } from '@/db/db.js';
|
||||||
import { Command } from '@/types/CommandTypes.js';
|
import { Command } from '@/types/CommandTypes.js';
|
||||||
|
import { createPaginationButtons } from '@/util/helpers.js';
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
|
@ -42,18 +41,7 @@ const command: Command = {
|
||||||
|
|
||||||
let currentPage = 0;
|
let currentPage = 0;
|
||||||
const getButtonActionRow = () =>
|
const getButtonActionRow = () =>
|
||||||
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
createPaginationButtons(pages.length, currentPage);
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId('previous')
|
|
||||||
.setLabel('Previous')
|
|
||||||
.setStyle(ButtonStyle.Primary)
|
|
||||||
.setDisabled(currentPage === 0),
|
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId('next')
|
|
||||||
.setLabel('Next')
|
|
||||||
.setStyle(ButtonStyle.Primary)
|
|
||||||
.setDisabled(currentPage === pages.length - 1),
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSelectMenuRow = () => {
|
const getSelectMenuRow = () => {
|
||||||
const options = pages.map((_, index) => ({
|
const options = pages.map((_, index) => ({
|
||||||
|
@ -85,7 +73,7 @@ const command: Command = {
|
||||||
if (pages.length <= 1) return;
|
if (pages.length <= 1) return;
|
||||||
|
|
||||||
const collector = message.createMessageComponentCollector({
|
const collector = message.createMessageComponentCollector({
|
||||||
time: 60000,
|
time: 300000,
|
||||||
});
|
});
|
||||||
|
|
||||||
collector.on('collect', async (i) => {
|
collector.on('collect', async (i) => {
|
||||||
|
@ -98,10 +86,19 @@ const command: Command = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i.isButton()) {
|
if (i.isButton()) {
|
||||||
if (i.customId === 'previous' && currentPage > 0) {
|
switch (i.customId) {
|
||||||
currentPage--;
|
case 'first':
|
||||||
} else if (i.customId === 'next' && currentPage < pages.length - 1) {
|
currentPage = 0;
|
||||||
currentPage++;
|
break;
|
||||||
|
case 'prev':
|
||||||
|
if (currentPage > 0) currentPage--;
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
if (currentPage < pages.length - 1) currentPage++;
|
||||||
|
break;
|
||||||
|
case 'last':
|
||||||
|
currentPage = pages.length - 1;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1387
src/db/db.ts
1387
src/db/db.ts
File diff suppressed because it is too large
Load diff
282
src/db/functions/achievementFunctions.ts
Normal file
282
src/db/functions/achievementFunctions.ts
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import { db, ensureDbInitialized, handleDbError } from '../db.js';
|
||||||
|
import * as schema from '../schema.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all achievement definitions
|
||||||
|
* @returns Array of achievement definitions
|
||||||
|
*/
|
||||||
|
export async function getAllAchievements(): Promise<
|
||||||
|
schema.achievementDefinitionsTableTypes[]
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get achievements');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(schema.achievementDefinitionsTable)
|
||||||
|
.orderBy(schema.achievementDefinitionsTable.threshold);
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get all achievements', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get achievements for a specific user
|
||||||
|
* @param userId - Discord ID of the user
|
||||||
|
* @returns Array of user achievements
|
||||||
|
*/
|
||||||
|
export async function getUserAchievements(
|
||||||
|
userId: string,
|
||||||
|
): Promise<schema.userAchievementsTableTypes[]> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get user achievements');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await db
|
||||||
|
.select({
|
||||||
|
id: schema.userAchievementsTable.id,
|
||||||
|
discordId: schema.userAchievementsTable.discordId,
|
||||||
|
achievementId: schema.userAchievementsTable.achievementId,
|
||||||
|
earnedAt: schema.userAchievementsTable.earnedAt,
|
||||||
|
progress: schema.userAchievementsTable.progress,
|
||||||
|
})
|
||||||
|
.from(schema.userAchievementsTable)
|
||||||
|
.where(eq(schema.userAchievementsTable.discordId, userId));
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get user achievements', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Award an achievement to a user
|
||||||
|
* @param userId - Discord ID of the user
|
||||||
|
* @param achievementId - ID of the achievement
|
||||||
|
* @returns Boolean indicating success
|
||||||
|
*/
|
||||||
|
export async function awardAchievement(
|
||||||
|
userId: string,
|
||||||
|
achievementId: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot award achievement');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.userAchievementsTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.userAchievementsTable.discordId, userId),
|
||||||
|
eq(schema.userAchievementsTable.achievementId, achievementId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((rows) => rows[0]);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.earnedAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(schema.userAchievementsTable)
|
||||||
|
.set({
|
||||||
|
earnedAt: new Date(),
|
||||||
|
progress: 100,
|
||||||
|
})
|
||||||
|
.where(eq(schema.userAchievementsTable.id, existing.id));
|
||||||
|
} else {
|
||||||
|
await db.insert(schema.userAchievementsTable).values({
|
||||||
|
discordId: userId,
|
||||||
|
achievementId: achievementId,
|
||||||
|
earnedAt: new Date(),
|
||||||
|
progress: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to award achievement', error as Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update achievement progress for a user
|
||||||
|
* @param userId - Discord ID of the user
|
||||||
|
* @param achievementId - ID of the achievement
|
||||||
|
* @param progress - Progress value (0-100)
|
||||||
|
* @returns Boolean indicating success
|
||||||
|
*/
|
||||||
|
export async function updateAchievementProgress(
|
||||||
|
userId: string,
|
||||||
|
achievementId: number,
|
||||||
|
progress: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error(
|
||||||
|
'Database not initialized, cannot update achievement progress',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.userAchievementsTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.userAchievementsTable.discordId, userId),
|
||||||
|
eq(schema.userAchievementsTable.achievementId, achievementId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((rows) => rows[0]);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.earnedAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(schema.userAchievementsTable)
|
||||||
|
.set({
|
||||||
|
progress: Math.floor(progress) > 100 ? 100 : Math.floor(progress),
|
||||||
|
})
|
||||||
|
.where(eq(schema.userAchievementsTable.id, existing.id));
|
||||||
|
} else {
|
||||||
|
await db.insert(schema.userAchievementsTable).values({
|
||||||
|
discordId: userId,
|
||||||
|
achievementId: achievementId,
|
||||||
|
progress: Math.floor(progress) > 100 ? 100 : Math.floor(progress),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to update achievement progress', error as Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new achievement definition
|
||||||
|
* @param achievementData - Achievement definition data
|
||||||
|
* @returns Created achievement or undefined on failure
|
||||||
|
*/
|
||||||
|
export async function createAchievement(achievementData: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
requirementType: string;
|
||||||
|
threshold: number;
|
||||||
|
requirement?: any;
|
||||||
|
rewardType?: string;
|
||||||
|
rewardValue?: string;
|
||||||
|
}): Promise<schema.achievementDefinitionsTableTypes | undefined> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot create achievement');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [achievement] = await db
|
||||||
|
.insert(schema.achievementDefinitionsTable)
|
||||||
|
.values({
|
||||||
|
name: achievementData.name,
|
||||||
|
description: achievementData.description,
|
||||||
|
imageUrl: achievementData.imageUrl || null,
|
||||||
|
requirementType: achievementData.requirementType,
|
||||||
|
threshold: achievementData.threshold,
|
||||||
|
requirement: achievementData.requirement || {},
|
||||||
|
rewardType: achievementData.rewardType || null,
|
||||||
|
rewardValue: achievementData.rewardValue || null,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return achievement;
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to create achievement', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an achievement definition
|
||||||
|
* @param achievementId - ID of the achievement to delete
|
||||||
|
* @returns Boolean indicating success
|
||||||
|
*/
|
||||||
|
export async function deleteAchievement(
|
||||||
|
achievementId: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot delete achievement');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(schema.userAchievementsTable)
|
||||||
|
.where(eq(schema.userAchievementsTable.achievementId, achievementId));
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(schema.achievementDefinitionsTable)
|
||||||
|
.where(eq(schema.achievementDefinitionsTable.id, achievementId));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to delete achievement', error as Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an achievement from a user
|
||||||
|
* @param discordId - Discord user ID
|
||||||
|
* @param achievementId - Achievement ID to remove
|
||||||
|
* @returns boolean indicating success
|
||||||
|
*/
|
||||||
|
export async function removeUserAchievement(
|
||||||
|
discordId: string,
|
||||||
|
achievementId: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot remove user achievement');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(schema.userAchievementsTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.userAchievementsTable.discordId, discordId),
|
||||||
|
eq(schema.userAchievementsTable.achievementId, achievementId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to remove user achievement', error as Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
198
src/db/functions/factFunctions.ts
Normal file
198
src/db/functions/factFunctions.ts
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
import { and, eq, isNull, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
ensureDbInitialized,
|
||||||
|
handleDbError,
|
||||||
|
invalidateCache,
|
||||||
|
withCache,
|
||||||
|
} from '../db.js';
|
||||||
|
import * as schema from '../schema.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new fact to the database
|
||||||
|
* @param content - Content of the fact
|
||||||
|
* @param source - Source of the fact
|
||||||
|
* @param addedBy - Discord ID of the user who added the fact
|
||||||
|
* @param approved - Whether the fact is approved or not
|
||||||
|
*/
|
||||||
|
export async function addFact({
|
||||||
|
content,
|
||||||
|
source,
|
||||||
|
addedBy,
|
||||||
|
approved = false,
|
||||||
|
}: schema.factTableTypes): Promise<void> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot add fact');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(schema.factTable).values({
|
||||||
|
content,
|
||||||
|
source,
|
||||||
|
addedBy,
|
||||||
|
approved,
|
||||||
|
});
|
||||||
|
|
||||||
|
await invalidateCache('unused-facts');
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to add fact', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the most recently added fact
|
||||||
|
* @returns ID of the last inserted fact
|
||||||
|
*/
|
||||||
|
export async function getLastInsertedFactId(): Promise<number> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get last inserted fact');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db
|
||||||
|
.select({ id: sql<number>`MAX(${schema.factTable.id})` })
|
||||||
|
.from(schema.factTable);
|
||||||
|
|
||||||
|
return result[0]?.id ?? 0;
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get last inserted fact ID', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random fact that hasn't been used yet
|
||||||
|
* @returns Random fact object
|
||||||
|
*/
|
||||||
|
export async function getRandomUnusedFact(): Promise<schema.factTableTypes> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get random unused fact');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = 'unused-facts';
|
||||||
|
const facts = await withCache<schema.factTableTypes[]>(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
return (await db
|
||||||
|
.select()
|
||||||
|
.from(schema.factTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.factTable.approved, true),
|
||||||
|
isNull(schema.factTable.usedOn),
|
||||||
|
),
|
||||||
|
)) as schema.factTableTypes[];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (facts.length === 0) {
|
||||||
|
await db
|
||||||
|
.update(schema.factTable)
|
||||||
|
.set({ usedOn: null })
|
||||||
|
.where(eq(schema.factTable.approved, true));
|
||||||
|
|
||||||
|
await invalidateCache(cacheKey);
|
||||||
|
return await getRandomUnusedFact();
|
||||||
|
}
|
||||||
|
|
||||||
|
return facts[
|
||||||
|
Math.floor(Math.random() * facts.length)
|
||||||
|
] as schema.factTableTypes;
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get random fact', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a fact as used
|
||||||
|
* @param id - ID of the fact to mark as used
|
||||||
|
*/
|
||||||
|
export async function markFactAsUsed(id: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot mark fact as used');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(schema.factTable)
|
||||||
|
.set({ usedOn: new Date() })
|
||||||
|
.where(eq(schema.factTable.id, id));
|
||||||
|
|
||||||
|
await invalidateCache('unused-facts');
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to mark fact as used', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pending facts that need approval
|
||||||
|
* @returns Array of pending fact objects
|
||||||
|
*/
|
||||||
|
export async function getPendingFacts(): Promise<schema.factTableTypes[]> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get pending facts');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await db
|
||||||
|
.select()
|
||||||
|
.from(schema.factTable)
|
||||||
|
.where(eq(schema.factTable.approved, false))) as schema.factTableTypes[];
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get pending facts', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve a fact
|
||||||
|
* @param id - ID of the fact to approve
|
||||||
|
*/
|
||||||
|
export async function approveFact(id: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot approve fact');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(schema.factTable)
|
||||||
|
.set({ approved: true })
|
||||||
|
.where(eq(schema.factTable.id, id));
|
||||||
|
|
||||||
|
await invalidateCache('unused-facts');
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to approve fact', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a fact
|
||||||
|
* @param id - ID of the fact to delete
|
||||||
|
*/
|
||||||
|
export async function deleteFact(id: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot delete fact');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(schema.factTable).where(eq(schema.factTable.id, id));
|
||||||
|
|
||||||
|
await invalidateCache('unused-facts');
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to delete fact', error as Error);
|
||||||
|
}
|
||||||
|
}
|
275
src/db/functions/giveawayFunctions.ts
Normal file
275
src/db/functions/giveawayFunctions.ts
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import { db, ensureDbInitialized, handleDbError } from '../db.js';
|
||||||
|
import { selectGiveawayWinners } from '@/util/giveaways/utils.js';
|
||||||
|
import * as schema from '../schema.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
329
src/db/functions/levelFunctions.ts
Normal file
329
src/db/functions/levelFunctions.ts
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
import { desc, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
ensureDbInitialized,
|
||||||
|
handleDbError,
|
||||||
|
invalidateCache,
|
||||||
|
withCache,
|
||||||
|
} from '../db.js';
|
||||||
|
import * as schema from '../schema.js';
|
||||||
|
import { calculateLevelFromXp } from '@/util/levelingSystem.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user level information or create a new entry if not found
|
||||||
|
* @param discordId - Discord ID of the user
|
||||||
|
* @returns User level object
|
||||||
|
*/
|
||||||
|
export async function getUserLevel(
|
||||||
|
discordId: string,
|
||||||
|
): Promise<schema.levelTableTypes> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get user level');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `level-${discordId}`;
|
||||||
|
|
||||||
|
return await withCache<schema.levelTableTypes>(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
const level = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.levelTable)
|
||||||
|
.where(eq(schema.levelTable.discordId, discordId))
|
||||||
|
.then((rows) => rows[0]);
|
||||||
|
|
||||||
|
if (level) {
|
||||||
|
return {
|
||||||
|
...level,
|
||||||
|
lastMessageTimestamp: level.lastMessageTimestamp ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLevel: schema.levelTableTypes = {
|
||||||
|
discordId,
|
||||||
|
xp: 0,
|
||||||
|
level: 0,
|
||||||
|
lastMessageTimestamp: new Date(),
|
||||||
|
messagesSent: 0,
|
||||||
|
reactionCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.insert(schema.levelTable).values(newLevel);
|
||||||
|
return newLevel;
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Error getting user level', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add XP to a user, updating their level if necessary
|
||||||
|
* @param discordId - Discord ID of the user
|
||||||
|
* @param amount - Amount of XP to add
|
||||||
|
*/
|
||||||
|
export async function addXpToUser(
|
||||||
|
discordId: string,
|
||||||
|
amount: number,
|
||||||
|
): Promise<{
|
||||||
|
leveledUp: boolean;
|
||||||
|
newLevel: number;
|
||||||
|
oldLevel: number;
|
||||||
|
messagesSent: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot add xp to user');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `level-${discordId}`;
|
||||||
|
const userData = await getUserLevel(discordId);
|
||||||
|
const currentLevel = userData.level;
|
||||||
|
const currentXp = Number(userData.xp);
|
||||||
|
const xpToAdd = Number(amount);
|
||||||
|
|
||||||
|
userData.xp = currentXp + xpToAdd;
|
||||||
|
|
||||||
|
userData.lastMessageTimestamp = new Date();
|
||||||
|
userData.level = calculateLevelFromXp(userData.xp);
|
||||||
|
userData.messagesSent += 1;
|
||||||
|
|
||||||
|
await invalidateLeaderboardCache();
|
||||||
|
await invalidateCache(cacheKey);
|
||||||
|
await withCache<schema.levelTableTypes>(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
const result = await db
|
||||||
|
.update(schema.levelTable)
|
||||||
|
.set({
|
||||||
|
xp: userData.xp,
|
||||||
|
level: userData.level,
|
||||||
|
lastMessageTimestamp: userData.lastMessageTimestamp,
|
||||||
|
messagesSent: userData.messagesSent,
|
||||||
|
})
|
||||||
|
.where(eq(schema.levelTable.discordId, discordId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return result[0] as schema.levelTableTypes;
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
leveledUp: userData.level > currentLevel,
|
||||||
|
newLevel: userData.level,
|
||||||
|
oldLevel: currentLevel,
|
||||||
|
messagesSent: userData.messagesSent,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Error adding XP to user', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a user's rank on the XP leaderboard
|
||||||
|
* @param discordId - Discord ID of the user
|
||||||
|
* @returns User's rank on the leaderboard
|
||||||
|
*/
|
||||||
|
export async function getUserRank(discordId: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get user rank');
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaderboardCache = await getLeaderboardData();
|
||||||
|
|
||||||
|
if (leaderboardCache) {
|
||||||
|
const userIndex = leaderboardCache.findIndex(
|
||||||
|
(member) => member.discordId === discordId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userIndex !== -1) {
|
||||||
|
return userIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get user rank', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear leaderboard cache
|
||||||
|
*/
|
||||||
|
export async function invalidateLeaderboardCache(): Promise<void> {
|
||||||
|
await invalidateCache('xp-leaderboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get or create leaderboard data
|
||||||
|
* @returns Array of leaderboard data
|
||||||
|
*/
|
||||||
|
async function getLeaderboardData(): Promise<
|
||||||
|
Array<{
|
||||||
|
discordId: string;
|
||||||
|
xp: number;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get leaderboard data');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = 'xp-leaderboard';
|
||||||
|
return withCache<Array<{ discordId: string; xp: number }>>(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
return await db
|
||||||
|
.select({
|
||||||
|
discordId: schema.levelTable.discordId,
|
||||||
|
xp: schema.levelTable.xp,
|
||||||
|
})
|
||||||
|
.from(schema.levelTable)
|
||||||
|
.orderBy(desc(schema.levelTable.xp));
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get leaderboard data', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the user's reaction count
|
||||||
|
* @param userId - Discord user ID
|
||||||
|
* @returns The updated reaction count
|
||||||
|
*/
|
||||||
|
export async function incrementUserReactionCount(
|
||||||
|
userId: string,
|
||||||
|
): Promise<number> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error(
|
||||||
|
'Database not initialized, cannot increment reaction count',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelData = await getUserLevel(userId);
|
||||||
|
|
||||||
|
const newCount = (levelData.reactionCount || 0) + 1;
|
||||||
|
await db
|
||||||
|
.update(schema.levelTable)
|
||||||
|
.set({ reactionCount: newCount })
|
||||||
|
.where(eq(schema.levelTable.discordId, userId));
|
||||||
|
await invalidateCache(`level-${userId}`);
|
||||||
|
|
||||||
|
return newCount;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error incrementing user reaction count:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements the user's reaction count (but not below zero)
|
||||||
|
* @param userId - Discord user ID
|
||||||
|
* @returns The updated reaction count
|
||||||
|
*/
|
||||||
|
export async function decrementUserReactionCount(
|
||||||
|
userId: string,
|
||||||
|
): Promise<number> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error(
|
||||||
|
'Database not initialized, cannot increment reaction count',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelData = await getUserLevel(userId);
|
||||||
|
|
||||||
|
const newCount = Math.max(0, levelData.reactionCount - 1);
|
||||||
|
await db
|
||||||
|
.update(schema.levelTable)
|
||||||
|
.set({ reactionCount: newCount < 0 ? 0 : newCount })
|
||||||
|
.where(eq(schema.levelTable.discordId, userId));
|
||||||
|
await invalidateCache(`level-${userId}`);
|
||||||
|
|
||||||
|
return newCount;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decrementing user reaction count:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the user's reaction count
|
||||||
|
* @param userId - Discord user ID
|
||||||
|
* @returns The user's reaction count
|
||||||
|
*/
|
||||||
|
export async function getUserReactionCount(userId: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get user reaction count');
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelData = await getUserLevel(userId);
|
||||||
|
return levelData.reactionCount;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting user reaction count:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the XP leaderboard
|
||||||
|
* @param limit - Number of entries to return
|
||||||
|
* @returns Array of leaderboard entries
|
||||||
|
*/
|
||||||
|
export async function getLevelLeaderboard(
|
||||||
|
limit = 10,
|
||||||
|
): Promise<schema.levelTableTypes[]> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get level leaderboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaderboardCache = await getLeaderboardData();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await db
|
||||||
|
.select()
|
||||||
|
.from(schema.levelTable)
|
||||||
|
.orderBy(desc(schema.levelTable.xp))
|
||||||
|
.limit(limit)) as schema.levelTableTypes[];
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get leaderboard', error as Error);
|
||||||
|
}
|
||||||
|
}
|
160
src/db/functions/memberFunctions.ts
Normal file
160
src/db/functions/memberFunctions.ts
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import { Collection, GuildMember } from 'discord.js';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
ensureDbInitialized,
|
||||||
|
handleDbError,
|
||||||
|
invalidateCache,
|
||||||
|
withCache,
|
||||||
|
} from '../db.js';
|
||||||
|
import * as schema from '../schema.js';
|
||||||
|
import { getMemberModerationHistory } from './moderationFunctions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all non-bot members currently in the server
|
||||||
|
* @returns Array of member objects
|
||||||
|
*/
|
||||||
|
export async function getAllMembers() {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get members');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = 'nonBotMembers';
|
||||||
|
return await withCache<schema.memberTableTypes[]>(cacheKey, async () => {
|
||||||
|
const nonBotMembers = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.memberTable)
|
||||||
|
.where(eq(schema.memberTable.currentlyInServer, true));
|
||||||
|
return nonBotMembers;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get all members', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set or update multiple members at once
|
||||||
|
* @param nonBotMembers - Array of member objects
|
||||||
|
*/
|
||||||
|
export async function setMembers(
|
||||||
|
nonBotMembers: Collection<string, GuildMember>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot set members');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
nonBotMembers.map(async (member) => {
|
||||||
|
const memberInfo = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.memberTable)
|
||||||
|
.where(eq(schema.memberTable.discordId, member.user.id));
|
||||||
|
|
||||||
|
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) {
|
||||||
|
handleDbError('Failed to set members', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed information about a specific member including moderation history
|
||||||
|
* @param discordId - Discord ID of the user
|
||||||
|
* @returns Member object with moderation history
|
||||||
|
*/
|
||||||
|
export async function getMember(
|
||||||
|
discordId: string,
|
||||||
|
): Promise<
|
||||||
|
| (schema.memberTableTypes & { moderations: schema.moderationTableTypes[] })
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot get member');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `${discordId}-memberInfo`;
|
||||||
|
|
||||||
|
const member = await withCache<schema.memberTableTypes>(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
const memberData = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.memberTable)
|
||||||
|
.where(eq(schema.memberTable.discordId, discordId))
|
||||||
|
.then((rows) => rows[0]);
|
||||||
|
|
||||||
|
return memberData as schema.memberTableTypes;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const moderations = await getMemberModerationHistory(discordId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
moderations,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get member', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a member's information in the database
|
||||||
|
* @param discordId - Discord ID of the user
|
||||||
|
* @param discordUsername - New username of the member
|
||||||
|
* @param currentlyInServer - Whether the member is currently in the server
|
||||||
|
* @param currentlyBanned - Whether the member is currently banned
|
||||||
|
*/
|
||||||
|
export async function updateMember({
|
||||||
|
discordId,
|
||||||
|
discordUsername,
|
||||||
|
currentlyInServer,
|
||||||
|
currentlyBanned,
|
||||||
|
}: schema.memberTableTypes): Promise<void> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error('Database not initialized, cannot update member');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(schema.memberTable)
|
||||||
|
.set({
|
||||||
|
discordUsername,
|
||||||
|
currentlyInServer,
|
||||||
|
currentlyBanned,
|
||||||
|
})
|
||||||
|
.where(eq(schema.memberTable.discordId, discordId));
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
invalidateCache(`${discordId}-memberInfo`),
|
||||||
|
invalidateCache('nonBotMembers'),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to update member', error as Error);
|
||||||
|
}
|
||||||
|
}
|
96
src/db/functions/moderationFunctions.ts
Normal file
96
src/db/functions/moderationFunctions.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
ensureDbInitialized,
|
||||||
|
handleDbError,
|
||||||
|
invalidateCache,
|
||||||
|
withCache,
|
||||||
|
} from '../db.js';
|
||||||
|
import * as schema from '../schema.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new moderation action to a member's history
|
||||||
|
* @param discordId - Discord ID of the user
|
||||||
|
* @param moderatorDiscordId - Discord ID of the moderator
|
||||||
|
* @param action - Type of action taken
|
||||||
|
* @param reason - Reason for the action
|
||||||
|
* @param duration - Duration of the action
|
||||||
|
* @param createdAt - Timestamp of when the action was taken
|
||||||
|
* @param expiresAt - Timestamp of when the action expires
|
||||||
|
* @param active - Wether the action is active or not
|
||||||
|
*/
|
||||||
|
export async function updateMemberModerationHistory({
|
||||||
|
discordId,
|
||||||
|
moderatorDiscordId,
|
||||||
|
action,
|
||||||
|
reason,
|
||||||
|
duration,
|
||||||
|
createdAt,
|
||||||
|
expiresAt,
|
||||||
|
active,
|
||||||
|
}: schema.moderationTableTypes): Promise<void> {
|
||||||
|
try {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error(
|
||||||
|
'Database not initialized, update member moderation history',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moderationEntry = {
|
||||||
|
discordId,
|
||||||
|
moderatorDiscordId,
|
||||||
|
action,
|
||||||
|
reason,
|
||||||
|
duration,
|
||||||
|
createdAt,
|
||||||
|
expiresAt,
|
||||||
|
active,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.insert(schema.moderationTable).values(moderationEntry);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
invalidateCache(`${discordId}-moderationHistory`),
|
||||||
|
invalidateCache(`${discordId}-memberInfo`),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to update moderation history', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a member's moderation history
|
||||||
|
* @param discordId - Discord ID of the user
|
||||||
|
* @returns Array of moderation actions
|
||||||
|
*/
|
||||||
|
export async function getMemberModerationHistory(
|
||||||
|
discordId: string,
|
||||||
|
): Promise<schema.moderationTableTypes[]> {
|
||||||
|
await ensureDbInitialized();
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.error(
|
||||||
|
'Database not initialized, cannot get member moderation history',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `${discordId}-moderationHistory`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await withCache<schema.moderationTableTypes[]>(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
const history = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.moderationTable)
|
||||||
|
.where(eq(schema.moderationTable.discordId, discordId));
|
||||||
|
return history as schema.moderationTableTypes[];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return handleDbError('Failed to get moderation history', error as Error);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue