Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ main.db
main.db-shm
main.db-wal
migrations

.idea
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"module": "src/index.ts",
"type": "module",
"devDependencies": {
"better-sqlite3": "^8.5.1",
"bun-types": "^0.7.3",
"better-sqlite3": "^8.5.2",
"bun-types": "^0.8.0",
"drizzle-kit": "^0.19.13",
"rome": "^12.1.3"
},
Expand All @@ -15,7 +15,7 @@
"bufferutil": "^4.0.7",
"discord.js": "^14.13.0",
"djs-fsrouter": "^0.0.4",
"drizzle-orm": "^0.28.3",
"drizzle-orm": "^0.28.5",
"erlpack": "^0.1.4",
"utf-8-validate": "^6.0.3",
"zlib-sync": "^0.1.8"
Expand Down
10 changes: 10 additions & 0 deletions src/Exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {Client, GatewayIntentBits} from "discord.js";

export const client: Client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
],
});
7 changes: 3 additions & 4 deletions src/commands/info.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// noinspection JSUnusedGlobalSymbols

import {
type ChatInputCommandInteraction,
type APIEmbed,
ApplicationCommandType,
channelMention,
} from "discord.js";
import type { Command } from "djs-fsrouter";

export const type = ApplicationCommandType.ChatInput;
const Info: Command = {
description: "Get info about the bot and server",
defaultMemberPermissions: "0",
async run(interaction) {
if (!interaction.guild) {
interaction.reply({
await interaction.reply({
content: "Run this command in a server to get server info",
ephemeral: true,
});
Expand Down
49 changes: 49 additions & 0 deletions src/commands/qotw-date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// noinspection JSUnusedGlobalSymbols

import type {Command} from "djs-fsrouter";
import type {
ChatInputCommandInteraction,
ApplicationCommandOptionData,
InteractionReplyOptions
} from "discord.js";
import {EmbedBuilder, GuildMemberRoleManager, Role, SlashCommandStringOption,} from "discord.js";
import {type QotwDate, QotwReminderManager} from "../types/qotw.js";
import type {IntRange} from "../types/IntRange.js";

export default {
dmPermission: false,
description: "lets you change the next QOTW reminder date.",
options: [new SlashCommandStringOption().setName("time").setDescription("the time formatted as dd:hh:mm").setRequired(true)] as ApplicationCommandOptionData[],

async run(interaction: ChatInputCommandInteraction): Promise<void> {
function errMsg(title: string, desc: string): InteractionReplyOptions {
return {embeds: [new EmbedBuilder().setTitle(title).setColor(0xFF0000).setDescription(desc)], ephemeral: true};
}

if (!(<GuildMemberRoleManager>interaction.member!.roles).cache.find(r => r.id === "1140826891599757382" || r.id === "722933309633462274")) {
await interaction.reply(errMsg("Insufficient permissions", "You require the following roles: `Owner` | `QOTW Manger`"));
return;
}

const timeRegex: RegExpExecArray | null = /^(\d+):(\d+):(\d+)$/gm.exec(interaction.options.getString("time")!);

if (!timeRegex || timeRegex.length !== 4) {
await interaction.reply(errMsg("Invalid time", "The time format should follow the `1-7:1-24:0-59` format."));
return;
}

const newDate: QotwDate = {
weekDay: parseInt(timeRegex[1]) as IntRange<1, 7>,
hour: parseInt(timeRegex[2]) as IntRange<1, 24>,
minute: parseInt(timeRegex[3]) as IntRange<0, 59>
}

QotwReminderManager.uploadDate(newDate);

await interaction.reply({embeds: [new EmbedBuilder()
.setTitle("Success")
.setColor(0x00FF00)
.setDescription(`Set the new date for reminder to:\`\`\`\nday of the week: ${newDate.weekDay}\nhour of the day: ${newDate.hour}:${newDate.minute}\`\`\``)
]});
}
} as Command;
14 changes: 2 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import loadCommands from "djs-fsrouter";
import {
Client,
GatewayIntentBits,
Events,
type ClientEvents,
type Awaitable,
Expand All @@ -10,15 +8,7 @@ import { join } from "path";
import { readdir } from "fs/promises";
import { castArray } from "./utils.ts";
import type { Listener } from "./types/listener.ts";

const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
],
});
import { client } from "./Exports.js";

client.once(Events.ClientReady, async (bot) => {
try {
Expand Down Expand Up @@ -57,4 +47,4 @@ client.once(Events.ClientReady, async (bot) => {
console.log(`Bot ${bot.user.username} ready!`);
});

client.login(process.env.TOKEN);
await client.login(process.env.TOKEN);
14 changes: 14 additions & 0 deletions src/listeners/qotwReminder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// noinspection JSUnusedGlobalSymbols

import type { Listener } from "../types/listener.ts";
import {QotwReminderManager} from "../types/qotw.js";

export default [
{
event: "ready",

handler(): void {
QotwReminderManager.reloadInterval();
}
}
] as Listener[]
2 changes: 1 addition & 1 deletion src/schemas/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export const Config = sqliteTable("guildConfig", {
gatewayLeaveContent: text("gatewayLeaveContent"),
});
export type ConfigSelect = InferSelectModel<typeof Config>;
export type ConfigInsert = InferInsertModel<typeof Config>;
export type ConfigInsert = InferInsertModel<typeof Config>;
11 changes: 11 additions & 0 deletions src/schemas/keyvaluepair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core";

export const Keyvaluepair = sqliteTable("keyvaluepair", {
key: text("key").notNull().primaryKey(),
value: text("value")
});

export interface KeyValuePairDisposition {
key: string;
value: string | null;
}
6 changes: 6 additions & 0 deletions src/types/IntRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc['length']]>;

export type IntRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>> | F | T;
81 changes: 81 additions & 0 deletions src/types/qotw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type {IntRange} from "./IntRange.js";
import {client} from "../Exports.js";
import {EmbedBuilder} from "discord.js";
import db from "../db.js";
import {Keyvaluepair, type KeyValuePairDisposition} from "../schemas/keyvaluepair.js";
import {eq} from "drizzle-orm";

export interface QotwDate {
weekDay: IntRange<1, 7>;
hour: IntRange<1, 24>;
minute: IntRange<0, 59>;
}

export class QotwReminderManager {
private static currentInterval: NodeJS.Timeout | undefined;
private static currentTimeout: NodeJS.Timeout | undefined;

public static uploadDate(date: QotwDate) {
(!QotwReminderManager.getDate()
? db.insert(Keyvaluepair).values({key: "qotwdate", value: JSON.stringify(date)})
: db.update(Keyvaluepair).set({value: JSON.stringify(date)}).where(eq(Keyvaluepair.key, "qotwdate")))
.execute().then()

QotwReminderManager.reloadInterval();
}

public static getDate(): QotwDate | undefined {
const value: KeyValuePairDisposition[] = db.select().from(Keyvaluepair).where(eq(Keyvaluepair.key, "qotwdate")).all()
return value.length === 0 ? undefined : JSON.parse(value[0].value!);
}

public static reloadInterval(): void {
if (QotwReminderManager.currentInterval) {
clearInterval(QotwReminderManager.currentInterval);
QotwReminderManager.currentInterval = undefined;
}

if (QotwReminderManager.currentTimeout) {
clearInterval(QotwReminderManager.currentTimeout);
QotwReminderManager.currentTimeout = undefined;
}

const qotwRemindDate: QotwDate | undefined = QotwReminderManager.getDate();

if (!qotwRemindDate) {
QotwReminderManager.currentInterval = undefined;
QotwReminderManager.currentTimeout = undefined;
return;
}

async function searchAndSendReminder() {
for (const [_, member] of (await (await client.guilds.fetch("779474636780863488")).roles.fetch("1140826891599757382"))!.members)
await member.send({embeds: [new EmbedBuilder()
.setColor(0x9b59b6)
.setTitle("QOTW reminder")
.setDescription("The time for a new QOTW is here, go to <#779826597729271828> and create a new question thread.")]});
}

QotwReminderManager.currentTimeout = setTimeout(async () => {
await searchAndSendReminder();
QotwReminderManager.currentInterval = setInterval(async () => {
await searchAndSendReminder();
}, 604800000);
QotwReminderManager.currentTimeout = undefined;
}, QotwReminderManager.getRemainingMilliseconds(qotwRemindDate as QotwDate))
}

private static getRemainingMilliseconds(qotw: QotwDate): number {
const now: Date = new Date(),
currentDay: number = now.getDay() || 7,
targetDay: number = qotw.weekDay === 7 ? 0 : qotw.weekDay,
cd: boolean = currentDay < targetDay || (currentDay === targetDay && now.getHours() < qotw.hour) || (currentDay === targetDay && now.getHours() === qotw.hour && now.getMinutes() < qotw.minute),
targetDate: Date = cd
? new Date(now.getFullYear(), now.getMonth(), now.getDate() + (targetDay - currentDay))
: new Date(now.getFullYear(), now.getMonth(), now.getDate() + (7 - currentDay + (targetDay === 0 ? 7 : targetDay)));

cd ? targetDate.setHours(qotw.hour, qotw.minute, 0, 0) : targetDate.setHours(qotw.hour, qotw.minute, 0, 0);

return targetDate.getTime() - now.getTime();
}
}
6 changes: 5 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import db from "./db.ts";
import { Config } from "./schemas/config";
import { Config } from "./schemas/config.ts";
import { sql, eq } from "drizzle-orm";
/**
* Casts a value into an array.
Expand All @@ -19,3 +19,7 @@ export const getConfig = db
.from(Config)
.where(eq(Config.id, sql.placeholder("guildId")))
.prepare();

export function isEmptyObject<T extends object>(obj: T): boolean {
return typeof obj === "object" && Object.keys(obj).length === 0;
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"allowImportingTsExtensions": true,
"moduleDetection": "force",
// drizzle orm errors with nodenext
"moduleResolution": "Bundler",
"moduleResolution": "NodeNext",
"esModuleInterop": true,

"strict": true,
Expand Down