Skip to content
Merged
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
8 changes: 1 addition & 7 deletions README-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
* [Adding commands](#adding-commands)
* [Adding command options](#adding-command-options)
* [Adding buttons](#adding-buttons)
* [Adding modals](#adding-modals)
* [Util files](#util-files)
* [Database changes](#database-changes)
* [Misc](#misc)
Expand Down Expand Up @@ -117,7 +116,7 @@ to the files that will need to be added/updated.

### Adding commands

1. Add a new `.command.ts` file under `src/commands/commands`, extending `EveryoneCommand` or `AdminCommand`. Subcommands are methods named `<commandName>_<subcommand>` (e.g. `queues_add` for `/queues add`). Set `deferResponse = false` if the command opens a modal.
1. Add a new `.command.ts` file under `src/commands/commands`, extending `EveryoneCommand` or `AdminCommand`. Subcommands are methods named `<commandName>_<subcommand>` (e.g. `queues_add` for `/queues add`).
2. Register it in `src/commands/commands.loader.ts`.
3. Update `README.md` and the help text in `src/commands/commands/help.command.ts`.

Expand All @@ -131,11 +130,6 @@ to the files that will need to be added/updated.
1. Create a new `.button.ts` file in the `src/buttons/buttons` directory. Buttons should extend `EveryoneButton` or `AdminButton`.
2. Update the `src/buttons/buttons.loader.ts` file.

### Adding modals

1. Create a new `.modal.ts` file in `src/modals` (see `join.modal.ts`).
2. The command opening the modal must set `deferResponse = false` — Discord rejects modals on deferred interactions.

### Util files

Non-trivial command/button logic should live in a `*.utils.ts` namespace under `src/utils`, keeping the command/button file thin.
Expand Down
53 changes: 50 additions & 3 deletions src/commands/commands/events.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { findKey, isNil, omitBy } from "lodash-es";

import { Queries } from "../../db/queries.ts";
import { type DbEvent, EVENT_TABLE, QUEUE_TABLE } from "../../db/schema.ts";
import { EventScheduleModal } from "../../modals/event-schedule.modal.ts";
import { AnnouncementChannelOption } from "../../options/options/announcement-channel.option.ts";
import { AnnouncementMessageOption } from "../../options/options/announcement-message.option.ts";
import { AutopullToggleOption } from "../../options/options/autopull-toggle.option.ts";
Expand All @@ -14,6 +13,7 @@ import { CleanupOffsetHoursOption } from "../../options/options/cleanup-offset-h
import { ColorOption } from "../../options/options/color.option.ts";
import { CreateDiscordEventToggleOption } from "../../options/options/create-discord-event-toggle.option.ts";
import { CreateOffsetHoursOption } from "../../options/options/create-offset-hours.option.ts";
import { DayOption } from "../../options/options/day.option.ts";
import { DiscordEventDescriptionOption } from "../../options/options/discord-event-description.option.ts";
import { ButtonsToggleOption } from "../../options/options/display-buttons.option.ts";
import { DisplayUpdateTypeOption } from "../../options/options/display-update-type.option.ts";
Expand All @@ -27,6 +27,7 @@ import { LockToggleOption } from "../../options/options/lock-toggle.option.ts";
import { MaxRoomsPerUserOption } from "../../options/options/max-rooms-per-user.option.ts";
import { MaxSubsPerUserOption } from "../../options/options/max-subs-per-user.option.ts";
import { MemberDisplayTypeOption } from "../../options/options/member-display-type.option.ts";
import { MonthOption } from "../../options/options/month.option.ts";
import { NameOption } from "../../options/options/name.option.ts";
import { ParentSubMutuallyExclusiveOption } from "../../options/options/parent-sub-mutually-exclusive.option.ts";
import { PullBatchSizeOption } from "../../options/options/pull-batch-size.option.ts";
Expand All @@ -51,13 +52,17 @@ import { RoomSchedulingOption } from "../../options/options/room-scheduling.opti
import { SizeOption } from "../../options/options/size.option.ts";
import { SlowmodeOption } from "../../options/options/slowmode.option.ts";
import { SlowmodeTimeOption } from "../../options/options/slowmode-time.option.ts";
import { StartTimeOption } from "../../options/options/start-time.option.ts";
import { SubQueuesChannelOption } from "../../options/options/sub-queues-channel.option.ts";
import { TimestampTypeOption } from "../../options/options/timestamp-type.option.ts";
import { TimezoneOption } from "../../options/options/timezone.option.ts";
import { VoiceDestinationChannelOption } from "../../options/options/voice-destination-channel.option.ts";
import { VoiceOnlyToggleOption } from "../../options/options/voice-only-toggle.option.ts";
import { YearOption } from "../../options/options/year.option.ts";
import { AdminCommand } from "../../types/command.types.ts";
import { Color, EventQueueRole, type RoomScheduling } from "../../types/db.types.ts";
import type { SlashInteraction } from "../../types/interaction.types.ts";
import { DateUtils } from "../../utils/date.utils.ts";
import { CustomError, EventNotFoundWarning } from "../../utils/error.utils.ts";
import { EventUtils } from "../../utils/event.utils.ts";
import { EventChannelUtils } from "../../utils/event-channel.utils.ts";
Expand Down Expand Up @@ -906,12 +911,54 @@ export class EventsCommand extends AdminCommand {

static readonly SCHEDULE_OPTIONS = {
event: new EventOption({ required: true, description: "Target event" }),
year: new YearOption({ required: true, description: "Start year" }),
month: new MonthOption({ required: true, description: "Start month" }),
day: new DayOption({ required: true, description: "Start day" }),
startTime: new StartTimeOption({ required: true, description: "Start time (12-hour, e.g. 9 AM, 9:30 PM)" }),
timezone: new TimezoneOption({ required: false, description: "IANA timezone", defaultValue: process.env.DEFAULT_SCHEDULE_TIMEZONE }),
};

static async events_schedule(inter: SlashInteraction) {
await inter.deferReply();

const event = await EventsCommand.SCHEDULE_OPTIONS.event.get(inter);
EventUtils.assertHasRoomCategory(event);
await inter.showModal(EventScheduleModal.getModal({ eventId: event.id }));

const yearStr = await EventsCommand.SCHEDULE_OPTIONS.year.get(inter);
const monthStr = await EventsCommand.SCHEDULE_OPTIONS.month.get(inter);
const dayStr = await EventsCommand.SCHEDULE_OPTIONS.day.get(inter);
const startTime = await EventsCommand.SCHEDULE_OPTIONS.startTime.get(inter);
const timezoneRaw = await EventsCommand.SCHEDULE_OPTIONS.timezone.get(inter);

const parsed = DateUtils.parseScheduledStart({
yearStr,
monthStr,
dayStr,
startTime,
timezone: timezoneRaw || process.env.DEFAULT_SCHEDULE_TIMEZONE || "UTC",
});

const startTimeMs = BigInt(parsed.valueOf());
const occurrence = await EventUtils.scheduleOccurrence(
inter.store,
event,
startTimeMs,
timezoneRaw || undefined,
);

const startDate = new Date(Number(occurrence.startTime));
const embed = new EmbedBuilder()
.setTitle(`Scheduled ${event.name}`)
.setColor(Color.Green)
.setDescription(
`Occurrence scheduled for ${time(startDate, TimestampStyles.LongDateTime)} (${time(startDate, TimestampStyles.RelativeTime)}).\n\n` +
`**Event:** ${eventMention(event)}\n` +
`**Opens:** ${time(new Date(Number(occurrence.startTime) - Number(event.createOffsetMs)), TimestampStyles.RelativeTime)}\n` +
`**Locks rooms:** ${time(new Date(Number(occurrence.startTime) + Number(event.lockOffsetMs)), TimestampStyles.RelativeTime)}\n` +
`**Cleans up:** ${time(new Date(EventUtils.getRoomsFinishMs(event, Number(occurrence.startTime)) + Number(event.cleanupOffsetMs)), TimestampStyles.RelativeTime)}`,
);

await inter.respond({ embeds: [embed] });
}

// ====================================================================
Expand Down Expand Up @@ -997,7 +1044,7 @@ export class EventsCommand extends AdminCommand {
"**Quick start:**\n" +
`1. ${commandMention("events", "add")} — create an event with N rooms (${inlineCode("room_category")} is required; one private \`room-{N}\` channel and a \`{event} Room {N}\` role are auto-created per room)\n` +
`2. ${commandMention("events", "set-room-defaults")} — configure room queue defaults (size, etc.)\n` +
`3. ${commandMention("events", "schedule")} — schedule an occurrence (opens a date/time modal)\n\n` +
`3. ${commandMention("events", "schedule")} — schedule an occurrence (\`event\`, \`year\`, \`month\`, \`day\`, \`start_time\` (12-hour, e.g. \`9:30 PM\`), optional \`timezone\`)\n\n` +
"**Lifecycle per occurrence:**\n" +
"- **T − create_offset** (default 24h before): queues unlock, displays refresh, announcement posts\n" +
"- **T + lock_offset** (default 0): room queues lock (sub queues stay open)\n" +
Expand Down
6 changes: 1 addition & 5 deletions src/handlers/modal.handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { InteractionReplyOptions } from "discord.js";

import { EventScheduleModal } from "../modals/event-schedule.modal.ts";
import { JoinModal } from "../modals/join.modal.ts";
import type { Handler } from "../types/handler.types.ts";
import type { BaseInteraction, ModalInteraction } from "../types/interaction.types.ts";
Expand All @@ -16,10 +15,7 @@ export class ModalHandler implements Handler {
async handle() {
this.inter.respond = (message: InteractionReplyOptions | string, log = false) => InteractionUtils.respond(this.inter, false, message, log);

if (this.inter.customId.startsWith(EventScheduleModal.ID)) {
await EventScheduleModal.handle(this.inter);
}
else if (this.inter.customId.startsWith(JoinModal.ID)) {
if (this.inter.customId.startsWith(JoinModal.ID)) {
await JoinModal.handle(this.inter);
}
}
Expand Down
109 changes: 0 additions & 109 deletions src/modals/event-schedule.modal.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/options/options.loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { CreateDiscordEventToggleOption } from "./options/create-discord-event-t
import { CreateOffsetHoursOption } from "./options/create-offset-hours.option.ts";
import { CronOption } from "./options/cron.option.ts";
import { CustomCronOption } from "./options/custom-cron.option.ts";
import { DayOption } from "./options/day.option.ts";
import { DiscordEventDescriptionOption } from "./options/discord-event-description.option.ts";
import { DisplayOption } from "./options/display.option.ts";
import { ButtonsToggleOption } from "./options/display-buttons.option.ts";
Expand All @@ -42,6 +43,7 @@ import { MembersOption } from "./options/members.option.ts";
import { MentionableOption } from "./options/mentionable.option.ts";
import { MessageOption } from "./options/message.option.ts";
import { MessageChannelOption } from "./options/message-channel.option.ts";
import { MonthOption } from "./options/month.option.ts";
import { NameOption } from "./options/name.option.ts";
import { NumberOption } from "./options/number.option.ts";
import { ParentSubMutuallyExclusiveOption } from "./options/parent-sub-mutually-exclusive.option.ts";
Expand Down Expand Up @@ -76,6 +78,7 @@ import { ScopeOption } from "./options/scope.option.ts";
import { SizeOption } from "./options/size.option.ts";
import { SlowmodeOption } from "./options/slowmode.option.ts";
import { SlowmodeTimeOption } from "./options/slowmode-time.option.ts";
import { StartTimeOption } from "./options/start-time.option.ts";
import { SubQueuesChannelOption } from "./options/sub-queues-channel.option.ts";
import { TimestampTypeOption } from "./options/timestamp-type.option.ts";
import { TimezoneOption } from "./options/timezone.option.ts";
Expand All @@ -86,6 +89,7 @@ import { VoiceSourceChannelOption } from "./options/voice-source-channel.option.
import { VoicesOption } from "./options/voices.option.ts";
import { WhitelistedOption } from "./options/whitelisted.option.ts";
import { WhitelistedsOption } from "./options/whitelisteds.option.ts";
import { YearOption } from "./options/year.option.ts";

export const OPTIONS = new Collection<string, BaseOption>([
[AdminOption.ID, new AdminOption()],
Expand All @@ -105,6 +109,7 @@ export const OPTIONS = new Collection<string, BaseOption>([
[CreateOffsetHoursOption.ID, new CreateOffsetHoursOption()],
[CronOption.ID, new CronOption()],
[CustomCronOption.ID, new CustomCronOption()],
[DayOption.ID, new DayOption()],
[DiscordEventDescriptionOption.ID, new DiscordEventDescriptionOption()],
[DisplayOption.ID, new DisplayOption()],
[DisplaysOption.ID, new DisplaysOption()],
Expand All @@ -129,6 +134,7 @@ export const OPTIONS = new Collection<string, BaseOption>([
[MentionableOption.ID, new MentionableOption()],
[MessageChannelOption.ID, new MessageChannelOption()],
[MessageOption.ID, new MessageOption()],
[MonthOption.ID, new MonthOption()],
[NameOption.ID, new NameOption()],
[NumberOption.ID, new NumberOption()],
[ParentSubMutuallyExclusiveOption.ID, new ParentSubMutuallyExclusiveOption()],
Expand Down Expand Up @@ -163,6 +169,7 @@ export const OPTIONS = new Collection<string, BaseOption>([
[SizeOption.ID, new SizeOption()],
[SlowmodeOption.ID, new SlowmodeOption()],
[SlowmodeTimeOption.ID, new SlowmodeTimeOption()],
[StartTimeOption.ID, new StartTimeOption()],
[SubQueuesChannelOption.ID, new SubQueuesChannelOption()],
[TimestampTypeOption.ID, new TimestampTypeOption()],
[TimezoneOption.ID, new TimezoneOption()],
Expand All @@ -174,4 +181,5 @@ export const OPTIONS = new Collection<string, BaseOption>([
[VoiceOnlyToggleOption.ID, new VoiceOnlyToggleOption()],
[WhitelistedOption.ID, new WhitelistedOption()],
[WhitelistedsOption.ID, new WhitelistedsOption()],
[YearOption.ID, new YearOption()],
]);
43 changes: 43 additions & 0 deletions src/options/options/day.option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import moment from "moment-timezone";

import type { UIOption } from "../../types/handler.types.ts";
import type { AutocompleteInteraction, SlashInteraction } from "../../types/interaction.types.ts";
import { type AutoCompleteOptions, CustomOption } from "../base-option.ts";

export class DayOption extends CustomOption {
static readonly ID = "day";
id = DayOption.ID;
required = true;

getAutocompletions = DayOption.getAutocompletions;

// force return type to be string
get(inter: AutocompleteInteraction | SlashInteraction) {
return super.get(inter) as Promise<string>;
}

protected async getUncached(inter: AutocompleteInteraction | SlashInteraction) {
return inter.options.getString(DayOption.ID);
}

static async getAutocompletions(options: AutoCompleteOptions): Promise<UIOption[]> {
const { inter, lowerSearchText } = options;
const yearStr = inter.options.getString("year");
const monthStr = inter.options.getString("month");

let daysInMonth = 31;
const year = Number(yearStr);
const month = Number(monthStr);
if (Number.isInteger(year) && Number.isInteger(month) && month >= 1 && month <= 12) {
daysInMonth = moment({ year, month: month - 1 }).daysInMonth();
}

const days: string[] = [];
for (let d = 1; d <= daysInMonth; d++) {
days.push(String(d));
}
return days
.filter(d => d.includes(lowerSearchText))
.map(d => ({ name: d, value: d }));
}
}
Loading