This commit is contained in:
Ahmad 2025-03-08 18:39:30 +00:00 committed by GitHub
commit 3efa7e960b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 890 additions and 30 deletions

View file

@ -3,6 +3,9 @@
> [!WARNING]
> This Discord bot is not production ready and everything is subject to change
> [!TIP]
> Want to see the bot in action? [Join our Discord server](https://discord.gg/KRTGjxx7gY).
## Development Commands
Install Dependencies: ``yarn install``

View file

@ -6,11 +6,15 @@
"redisConnectionString": "REDIS_CONNECTION_STRING",
"channels": {
"welcome": "WELCOME_CHANNEL_ID",
"logs": "LOG_CHAANNEL_ID"
"logs": "LOG_CHANNEL_ID",
"counting": "COUNTING_CHANNEL_ID",
"factOfTheDay": "FACT_OF_THE_DAY_CHANNEL_ID",
"factApproval": "FACT_APPROVAL_CHANNEL_ID"
},
"roles": {
"joinRoles": [
"JOIN_ROLE_IDS"
]
],
"factPingRole": "FACT_OF_THE_DAY_ROLE_ID"
}
}

View file

@ -38,5 +38,5 @@
"tsx": "^4.19.3",
"typescript": "^5.8.2"
},
"packageManager": "yarn@4.6.0"
"packageManager": "yarn@4.7.0"
}

View file

@ -0,0 +1,117 @@
import {
SlashCommandBuilder,
EmbedBuilder,
PermissionsBitField,
} from 'discord.js';
import { SubcommandCommand } from '../../types/CommandTypes.js';
import { getCountingData, setCount } from '../../util/countingManager.js';
import { loadConfig } from '../../util/configLoader.js';
const command: SubcommandCommand = {
data: new SlashCommandBuilder()
.setName('counting')
.setDescription('Commands related to the counting channel')
.addSubcommand((subcommand) =>
subcommand
.setName('status')
.setDescription('Check the current counting status'),
)
.addSubcommand((subcommand) =>
subcommand
.setName('setcount')
.setDescription(
'Set the current count to a specific number (Admin only)',
)
.addIntegerOption((option) =>
option
.setName('count')
.setDescription('The number to set as the current count')
.setRequired(true)
.setMinValue(0),
),
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'status') {
const countingData = await getCountingData();
const countingChannelId = loadConfig().channels.counting;
const embed = new EmbedBuilder()
.setTitle('Counting Channel Status')
.setColor(0x0099ff)
.addFields(
{
name: 'Current Count',
value: countingData.currentCount.toString(),
inline: true,
},
{
name: 'Next Number',
value: (countingData.currentCount + 1).toString(),
inline: true,
},
{
name: 'Highest Count',
value: countingData.highestCount.toString(),
inline: true,
},
{
name: 'Total Correct Counts',
value: countingData.totalCorrect.toString(),
inline: true,
},
{
name: 'Counting Channel',
value: `<#${countingChannelId}>`,
inline: true,
},
)
.setFooter({ text: 'Remember: No user can count twice in a row!' })
.setTimestamp();
if (countingData.lastUserId) {
embed.addFields({
name: 'Last Counter',
value: `<@${countingData.lastUserId}>`,
inline: true,
});
}
await interaction.reply({ embeds: [embed] });
} else if (subcommand === 'setcount') {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
content: 'You need administrator permissions to use this command.',
flags: ['Ephemeral'],
});
return;
}
const count = interaction.options.getInteger('count');
if (count === null) {
await interaction.reply({
content: 'Invalid count specified.',
flags: ['Ephemeral'],
});
return;
}
await setCount(count);
await interaction.reply({
content: `Count has been set to **${count}**. The next number should be **${count + 1}**.`,
flags: ['Ephemeral'],
});
}
},
};
export default command;

247
src/commands/fun/fact.ts Normal file
View file

@ -0,0 +1,247 @@
import {
SlashCommandBuilder,
PermissionsBitField,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
} from 'discord.js';
import {
addFact,
getPendingFacts,
approveFact,
deleteFact,
getLastInsertedFactId,
} from '../../db/db.js';
import { postFactOfTheDay } from '../../util/factManager.js';
import { loadConfig } from '../../util/configLoader.js';
import { SubcommandCommand } from '../../types/CommandTypes.js';
const command: SubcommandCommand = {
data: new SlashCommandBuilder()
.setName('fact')
.setDescription('Manage facts of the day')
.addSubcommand((subcommand) =>
subcommand
.setName('submit')
.setDescription('Submit a new fact for approval')
.addStringOption((option) =>
option
.setName('content')
.setDescription('The fact content')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('source')
.setDescription('Source of the fact (optional)')
.setRequired(false),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName('approve')
.setDescription('Approve a pending fact (Mod only)')
.addIntegerOption((option) =>
option
.setName('id')
.setDescription('The ID of the fact to approve')
.setRequired(true),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName('delete')
.setDescription('Delete a fact (Mod only)')
.addIntegerOption((option) =>
option
.setName('id')
.setDescription('The ID of the fact to delete')
.setRequired(true),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName('pending')
.setDescription('List all pending facts (Mod only)'),
)
.addSubcommand((subcommand) =>
subcommand
.setName('post')
.setDescription('Post a fact of the day manually (Admin only)'),
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const config = loadConfig();
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'submit') {
const content = interaction.options.getString('content', true);
const source = interaction.options.getString('source') || undefined;
const isAdmin = interaction.memberPermissions?.has(
PermissionsBitField.Flags.Administrator,
);
await addFact({
content,
source,
addedBy: interaction.user.id,
approved: isAdmin ? true : false,
});
if (!isAdmin) {
const approvalChannel = interaction.guild?.channels.cache.get(
config.channels.factApproval,
);
if (approvalChannel?.isTextBased()) {
const embed = new EmbedBuilder()
.setTitle('New Fact Submission')
.setDescription(content)
.setColor(0x0099ff)
.addFields(
{
name: 'Submitted By',
value: `<@${interaction.user.id}>`,
inline: true,
},
{ name: 'Source', value: source || 'Not provided', inline: true },
)
.setTimestamp();
const approveButton = new ButtonBuilder()
.setCustomId(`approve_fact_${await getLastInsertedFactId()}`)
.setLabel('Approve')
.setStyle(ButtonStyle.Success);
const rejectButton = new ButtonBuilder()
.setCustomId(`reject_fact_${await getLastInsertedFactId()}`)
.setLabel('Reject')
.setStyle(ButtonStyle.Danger);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
approveButton,
rejectButton,
);
await approvalChannel.send({
embeds: [embed],
components: [row],
});
} else {
console.error('Approval channel not found or is not a text channel');
}
}
await interaction.reply({
content: isAdmin
? 'Your fact has been automatically approved and added to the database!'
: 'Your fact has been submitted for approval!',
flags: ['Ephemeral'],
});
} else if (subcommand === 'approve') {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.ModerateMembers,
)
) {
await interaction.reply({
content: 'You do not have permission to approve facts.',
flags: ['Ephemeral'],
});
return;
}
const id = interaction.options.getInteger('id', true);
await approveFact(id);
await interaction.reply({
content: `Fact #${id} has been approved!`,
flags: ['Ephemeral'],
});
} else if (subcommand === 'delete') {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.ModerateMembers,
)
) {
await interaction.reply({
content: 'You do not have permission to delete facts.',
flags: ['Ephemeral'],
});
return;
}
const id = interaction.options.getInteger('id', true);
await deleteFact(id);
await interaction.reply({
content: `Fact #${id} has been deleted!`,
flags: ['Ephemeral'],
});
} else if (subcommand === 'pending') {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.ModerateMembers,
)
) {
await interaction.reply({
content: 'You do not have permission to view pending facts.',
flags: ['Ephemeral'],
});
return;
}
const pendingFacts = await getPendingFacts();
if (pendingFacts.length === 0) {
await interaction.reply({
content: 'There are no pending facts.',
flags: ['Ephemeral'],
});
return;
}
const embed = new EmbedBuilder()
.setTitle('Pending Facts')
.setColor(0x0099ff)
.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.reply({
embeds: [embed],
flags: ['Ephemeral'],
});
} else if (subcommand === 'post') {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
content: 'You do not have permission to manually post facts.',
flags: ['Ephemeral'],
});
return;
}
await postFactOfTheDay(interaction.client);
await interaction.reply({
content: 'Fact of the day has been posted!',
flags: ['Ephemeral'],
});
}
},
};
export default command;

View file

@ -1,6 +1,6 @@
import pkg from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import { eq } from 'drizzle-orm';
import { and, eq, isNull, sql } from 'drizzle-orm';
import * as schema from './schema.js';
import { loadConfig } from '../util/configLoader.js';
@ -230,3 +230,132 @@ export async function getMemberModerationHistory(discordId: string) {
);
}
}
export async function addFact({
content,
source,
addedBy,
approved = false,
}: schema.factTableTypes) {
try {
const result = await db.insert(schema.factTable).values({
content,
source,
addedBy,
approved,
});
await del('unusedFacts');
return result;
} catch (error) {
console.error('Error adding fact:', error);
throw new DatabaseError('Failed to add fact:', error as Error);
}
}
export async function getLastInsertedFactId(): Promise<number> {
try {
const result = await db
.select({ id: sql<number>`MAX(${schema.factTable.id})` })
.from(schema.factTable);
return result[0]?.id ?? 0;
} catch (error) {
console.error('Error getting last inserted fact ID:', error);
throw new DatabaseError(
'Failed to get last inserted fact ID:',
error as Error,
);
}
}
export async function getRandomUnusedFact() {
try {
if (await exists('unusedFacts')) {
const facts =
await getJson<(typeof schema.factTable.$inferSelect)[]>('unusedFacts');
if (facts && facts.length > 0) {
return facts[Math.floor(Math.random() * facts.length)];
}
}
const facts = await db
.select()
.from(schema.factTable)
.where(
and(
eq(schema.factTable.approved, true),
isNull(schema.factTable.usedOn),
),
);
if (facts.length === 0) {
await db
.update(schema.factTable)
.set({ usedOn: null })
.where(eq(schema.factTable.approved, true));
return await getRandomUnusedFact();
}
await setJson<(typeof schema.factTable.$inferSelect)[]>(
'unusedFacts',
facts,
);
return facts[Math.floor(Math.random() * facts.length)];
} catch (error) {
console.error('Error getting random fact:', error);
throw new DatabaseError('Failed to get random fact:', error as Error);
}
}
export async function markFactAsUsed(id: number) {
try {
await db
.update(schema.factTable)
.set({ usedOn: new Date() })
.where(eq(schema.factTable.id, id));
await del('unusedFacts');
} catch (error) {
console.error('Error marking fact as used:', error);
throw new DatabaseError('Failed to mark fact as used:', error as Error);
}
}
export async function getPendingFacts() {
try {
return await db
.select()
.from(schema.factTable)
.where(eq(schema.factTable.approved, false));
} catch (error) {
console.error('Error getting pending facts:', error);
throw new DatabaseError('Failed to get pending facts:', error as Error);
}
}
export async function approveFact(id: number) {
try {
await db
.update(schema.factTable)
.set({ approved: true })
.where(eq(schema.factTable.id, id));
await del('unusedFacts');
} catch (error) {
console.error('Error approving fact:', error);
throw new DatabaseError('Failed to approve fact:', error as Error);
}
}
export async function deleteFact(id: number) {
try {
await db.delete(schema.factTable).where(eq(schema.factTable.id, id));
await del('unusedFacts');
} catch (error) {
console.error('Error deleting fact:', error);
throw new DatabaseError('Failed to delete fact:', error as Error);
}
}

View file

@ -14,7 +14,7 @@ class RedisError extends Error {
}
}
redis.on('error', (error) => {
redis.on('error', (error: Error) => {
console.error('Redis connection error:', error);
throw new RedisError('Failed to connect to Redis instance: ', error);
});

View file

@ -61,3 +61,23 @@ export const moderationRelations = relations(moderationTable, ({ one }) => ({
references: [memberTable.discordId],
}),
}));
export type factTableTypes = {
id?: number;
content: string;
source?: string;
addedBy: string;
addedAt?: Date;
approved?: boolean;
usedOn?: Date;
};
export const factTable = pgTable('facts', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
content: varchar('content').notNull(),
source: varchar('source'),
addedBy: varchar('added_by').notNull(),
addedAt: timestamp('added_at').defaultNow().notNull(),
approved: boolean('approved').default(false).notNull(),
usedOn: timestamp('used_on'),
});

View file

@ -2,39 +2,79 @@ import { Events, Interaction } from 'discord.js';
import { ExtendedClient } from '../structures/ExtendedClient.js';
import { Event } from '../types/EventTypes.js';
import { approveFact, deleteFact } from '../db/db.js';
export default {
name: Events.InteractionCreate,
execute: async (interaction: Interaction) => {
if (!interaction.isCommand()) return;
if (interaction.isCommand()) {
const client = interaction.client as ExtendedClient;
const command = client.commands.get(interaction.commandName);
const client = interaction.client as ExtendedClient;
const command = client.commands.get(interaction.commandName);
if (!command) {
console.error(
`No command matching ${interaction.commandName} was found.`,
);
return;
}
if (!command) {
console.error(
`No command matching ${interaction.commandName} was found.`,
);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(`Error executing ${interaction.commandName}`);
console.error(error);
try {
await command.execute(interaction);
} catch (error) {
console.error(`Error executing ${interaction.commandName}`);
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: 'There was an error while executing this command!',
flags: ['Ephemeral'],
});
} else {
await interaction.reply({
content: 'There was an error while executing this command!',
flags: ['Ephemeral'],
});
}
}
} else if (interaction.isButton()) {
const { customId } = interaction;
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: 'There was an error while executing this command!',
flags: ['Ephemeral'],
if (customId.startsWith('approve_fact_')) {
if (!interaction.memberPermissions?.has('ModerateMembers')) {
await interaction.reply({
content: 'You do not have permission to approve facts.',
ephemeral: true,
});
return;
}
const factId = parseInt(customId.replace('approve_fact_', ''), 10);
await approveFact(factId);
await interaction.update({
content: `✅ Fact #${factId} has been approved by <@${interaction.user.id}>`,
components: [],
});
} else {
await interaction.reply({
content: 'There was an error while executing this command!',
flags: ['Ephemeral'],
} else if (customId.startsWith('reject_fact_')) {
if (!interaction.memberPermissions?.has('ModerateMembers')) {
await interaction.reply({
content: 'You do not have permission to reject facts.',
ephemeral: true,
});
return;
}
const factId = parseInt(customId.replace('reject_fact_', ''), 10);
await deleteFact(factId);
await interaction.update({
content: `❌ Fact #${factId} has been rejected by <@${interaction.user.id}>`,
components: [],
});
}
} else {
console.log('Unhandled interaction type:', interaction);
return;
}
},
} as Event<typeof Events.InteractionCreate>;

View file

@ -1,6 +1,12 @@
import { AuditLogEvent, Events, Message, PartialMessage } from 'discord.js';
import { Event } from '../types/EventTypes.js';
import { loadConfig } from '../util/configLoader.js';
import {
addCountingReactions,
processCountingMessage,
resetCounting,
} from '../util/countingManager.js';
import logAction from '../util/logging/logAction.js';
export const messageDelete: Event<typeof Events.MessageDelete> = {
@ -62,4 +68,56 @@ export const messageUpdate: Event<typeof Events.MessageUpdate> = {
},
};
export default [messageDelete, messageUpdate];
export const messageCreate: Event<typeof Events.MessageCreate> = {
name: Events.MessageCreate,
execute: async (message: Message) => {
try {
if (message.author.bot) return;
const countingChannelId = loadConfig().channels.counting;
const countingChannel =
message.guild?.channels.cache.get(countingChannelId);
if (!countingChannel || message.channel.id !== countingChannelId) return;
if (!countingChannel.isTextBased()) {
console.error('Counting channel not found or is not a text channel');
return;
}
const result = await processCountingMessage(message);
if (result.isValid) {
await addCountingReactions(message, result.milestoneType || 'normal');
} else {
let errorMessage: string;
switch (result.reason) {
case 'not_a_number':
errorMessage = `${message.author}, that's not a valid number! The count has been reset. The next number should be **1**.`;
break;
case 'too_high':
errorMessage = `${message.author}, too high! The count was **${(result?.expectedCount ?? 0) - 1}** and the next number should have been **${result.expectedCount}**. The count has been reset.`;
break;
case 'too_low':
errorMessage = `${message.author}, too low! The count was **${(result?.expectedCount ?? 0) - 1}** and the next number should have been **${result.expectedCount}**. The count has been reset.`;
break;
case 'same_user':
errorMessage = `${message.author}, you can't count twice in a row! The count has been reset. The next number should be **1**.`;
break;
default:
errorMessage = `${message.author}, something went wrong with the count. The count has been reset. The next number should be **1**.`;
}
await resetCounting();
await countingChannel.send(errorMessage);
await message.react('❌');
}
} catch (error) {
console.error('Error handling message create:', error);
}
},
};
export default [messageCreate, messageDelete, messageUpdate];

View file

@ -3,6 +3,7 @@ import { Client, Events } from 'discord.js';
import { setMembers } from '../db/db.js';
import { loadConfig } from '../util/configLoader.js';
import { Event } from '../types/EventTypes.js';
import { scheduleFactOfTheDay } from '../util/factManager.js';
export default {
name: Events.ClientReady,
@ -21,6 +22,8 @@ export default {
const members = await guild.members.fetch();
const nonBotMembers = members.filter((m) => !m.user.bot);
await setMembers(nonBotMembers);
await scheduleFactOfTheDay(client);
} catch (error) {
console.error('Failed to initialize members in database:', error);
}

View file

@ -2,6 +2,7 @@ import {
CommandInteraction,
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
export interface Command {
@ -13,3 +14,8 @@ export interface OptionsCommand {
data: SlashCommandOptionsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}
export interface SubcommandCommand {
data: SlashCommandSubcommandsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}

View file

@ -7,8 +7,12 @@ export interface Config {
channels: {
welcome: string;
logs: string;
counting: string;
factOfTheDay: string;
factApproval: string;
};
roles: {
joinRoles: string[];
factPingRole: string;
};
}

157
src/util/countingManager.ts Normal file
View file

@ -0,0 +1,157 @@
import { Message } from 'discord.js';
import { getJson, setJson } from '../db/redis.js';
interface CountingData {
currentCount: number;
lastUserId: string | null;
highestCount: number;
totalCorrect: number;
}
const MILESTONE_REACTIONS = {
normal: '✅',
multiples25: '✨',
multiples50: '⭐',
multiples100: '🎉',
};
export async function initializeCountingData(): Promise<CountingData> {
const exists = await getJson<CountingData>('counting');
if (exists) return exists;
const initialData: CountingData = {
currentCount: 0,
lastUserId: null,
highestCount: 0,
totalCorrect: 0,
};
await setJson<CountingData>('counting', initialData);
return initialData;
}
export async function getCountingData(): Promise<CountingData> {
const data = await getJson<CountingData>('counting');
if (!data) {
return initializeCountingData();
}
return data;
}
export async function updateCountingData(
data: Partial<CountingData>,
): Promise<void> {
const currentData = await getCountingData();
const updatedData = { ...currentData, ...data };
await setJson<CountingData>('counting', updatedData);
}
export async function resetCounting(): Promise<void> {
await updateCountingData({
currentCount: 0,
lastUserId: null,
});
return;
}
export async function processCountingMessage(message: Message): Promise<{
isValid: boolean;
expectedCount?: number;
isMilestone?: boolean;
milestoneType?: keyof typeof MILESTONE_REACTIONS;
reason?: string;
}> {
try {
const countingData = await getCountingData();
const content = message.content.trim();
const count = Number(content);
if (isNaN(count) || !Number.isInteger(count)) {
return {
isValid: false,
expectedCount: countingData.currentCount + 1,
reason: 'not_a_number',
};
}
const expectedCount = countingData.currentCount + 1;
if (count !== expectedCount) {
return {
isValid: false,
expectedCount,
reason: count > expectedCount ? 'too_high' : 'too_low',
};
}
if (countingData.lastUserId === message.author.id) {
return { isValid: false, expectedCount, reason: 'same_user' };
}
const newCount = countingData.currentCount + 1;
const newHighestCount = Math.max(newCount, countingData.highestCount);
await updateCountingData({
currentCount: newCount,
lastUserId: message.author.id,
highestCount: newHighestCount,
totalCorrect: countingData.totalCorrect + 1,
});
let isMilestone = false;
let milestoneType: keyof typeof MILESTONE_REACTIONS = 'normal';
if (newCount % 100 === 0) {
isMilestone = true;
milestoneType = 'multiples100';
} else if (newCount % 50 === 0) {
isMilestone = true;
milestoneType = 'multiples50';
} else if (newCount % 25 === 0) {
isMilestone = true;
milestoneType = 'multiples25';
}
return {
isValid: true,
expectedCount: newCount + 1,
isMilestone,
milestoneType,
};
} catch (error) {
console.error('Error processing counting message:', error);
return { isValid: false, reason: 'error' };
}
}
export async function addCountingReactions(
message: Message,
milestoneType: keyof typeof MILESTONE_REACTIONS,
): Promise<void> {
try {
await message.react(MILESTONE_REACTIONS[milestoneType]);
if (milestoneType === 'multiples100') {
await message.react('💯');
}
} catch (error) {
console.error('Error adding counting reactions:', error);
}
}
export async function getCountingStatus(): Promise<string> {
const data = await getCountingData();
return `Current count: ${data.currentCount}\nHighest count ever: ${data.highestCount}\nTotal correct counts: ${data.totalCorrect}`;
}
export async function setCount(count: number): Promise<void> {
if (!Number.isInteger(count) || count < 0) {
throw new Error('Count must be a non-negative integer.');
}
await updateCountingData({
currentCount: count,
lastUserId: null,
});
}

69
src/util/factManager.ts Normal file
View file

@ -0,0 +1,69 @@
import { EmbedBuilder, Client } from 'discord.js';
import { getRandomUnusedFact, markFactAsUsed } from '../db/db.js';
import { loadConfig } from './configLoader.js';
export async function scheduleFactOfTheDay(client: Client) {
try {
const now = new Date();
const tomorrow = new Date();
tomorrow.setDate(now.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
const timeUntilMidnight = tomorrow.getTime() - now.getTime();
setTimeout(() => {
postFactOfTheDay(client);
scheduleFactOfTheDay(client);
}, timeUntilMidnight);
console.log(
`Next fact of the day scheduled in ${Math.floor(timeUntilMidnight / 1000 / 60)} minutes`,
);
} catch (error) {
console.error('Error scheduling fact of the day:', error);
setTimeout(() => scheduleFactOfTheDay(client), 60 * 60 * 1000);
}
}
export async function postFactOfTheDay(client: Client) {
try {
const config = loadConfig();
const guild = client.guilds.cache.get(config.guildId);
if (!guild) {
console.error('Guild not found');
return;
}
const factChannel = guild.channels.cache.get(config.channels.factOfTheDay);
if (!factChannel?.isTextBased()) {
console.error('Fact channel not found or is not a text channel');
return;
}
const fact = await getRandomUnusedFact();
if (!fact) {
console.error('No facts available');
return;
}
const embed = new EmbedBuilder()
.setTitle('🌟 Fact of the Day 🌟')
.setDescription(fact.content)
.setColor(0xffaa00)
.setTimestamp();
if (fact.source) {
embed.setFooter({ text: `Source: ${fact.source}` });
}
await factChannel.send({
content: `<@&${config.roles.factPingRole}>`,
embeds: [embed],
});
await markFactAsUsed(fact.id!);
} catch (error) {
console.error('Error posting fact of the day:', error);
}
}

View file

@ -5,6 +5,7 @@ import {
ActionRowBuilder,
GuildChannel,
} from 'discord.js';
import {
LogActionPayload,
ModerationLogAction,
@ -22,10 +23,12 @@ import {
getPermissionDifference,
getPermissionNames,
} from './utils.js';
import { loadConfig } from '../configLoader.js';
export default async function logAction(payload: LogActionPayload) {
const logChannel = payload.guild.channels.cache.get('1007787977432383611');
if (!logChannel || !(logChannel instanceof TextChannel)) {
const config = loadConfig();
const logChannel = payload.guild.channels.cache.get(config.channels.logs);
if (!logChannel?.isTextBased()) {
console.error('Log channel not found or is not a Text Channel.');
return;
}