feat: nested Config system with section/key access pattern, Discord API abstraction start
This commit is contained in:
parent
1911cbe225
commit
17ff1d932f
61 changed files with 591 additions and 359 deletions
|
|
@ -4,7 +4,7 @@ import {
|
||||||
REST,
|
REST,
|
||||||
Routes,
|
Routes,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { cfg } from "../systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { hasOfficerRole } from "../systems/users";
|
import { hasOfficerRole } from "../systems/users";
|
||||||
|
|
||||||
// Poll subcommands
|
// Poll subcommands
|
||||||
|
|
@ -281,7 +281,7 @@ export async function handleTgCommand(interaction: ChatInputCommandInteraction):
|
||||||
const group = interaction.options.getSubcommandGroup(false);
|
const group = interaction.options.getSubcommandGroup(false);
|
||||||
const sub = interaction.options.getSubcommand();
|
const sub = interaction.options.getSubcommand();
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
|
|
||||||
// Officer-only commands
|
// Officer-only commands
|
||||||
const officerOnlyGroups = ["poll", "result", "bringer"];
|
const officerOnlyGroups = ["poll", "result", "bringer"];
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||||
import { cfg, setCfg, resetCfg } from "../systems/config";
|
import { Config, SectionMap } from "../systems/config";
|
||||||
import { hasOfficerRole } from "../systems/users";
|
import { hasOfficerRole } from "../systems/users";
|
||||||
import { replyAndDelete } from "../utils";
|
import { replyAndDelete } from "../utils";
|
||||||
import { Nation } from "@types";
|
import { Nation } from "@types";
|
||||||
import { handleSetLayout } from "@subcommands/tg-config/set-layout";
|
import { handleSetLayout } from "@subcommands/tg-config/set-layout";
|
||||||
|
|
||||||
|
const ROLE_KEY_MAP: Record<"officerRoles" | "configRoles" | "tagRoles", keyof SectionMap["roles"]> = {
|
||||||
|
officerRoles: "officer",
|
||||||
|
configRoles: "config",
|
||||||
|
tagRoles: "tag",
|
||||||
|
};
|
||||||
|
|
||||||
export function buildTgConfigCommand(): SlashCommandBuilder {
|
export function buildTgConfigCommand(): SlashCommandBuilder {
|
||||||
const cmd = new SlashCommandBuilder()
|
const cmd = new SlashCommandBuilder()
|
||||||
.setName("tg-config")
|
.setName("tg-config")
|
||||||
|
|
@ -121,50 +127,58 @@ export function buildTgConfigCommand(): SlashCommandBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleTgConfigCommand(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleTgConfigCommand(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
|
// discord.js types CommandInteractionOptionResolver with Omit<> which hides
|
||||||
|
// methods like getString/getInteger at the type level despite them existing at runtime
|
||||||
|
// Needs to be cast as any, since Discord.js has issues with the type
|
||||||
|
const options = interaction.options as any;
|
||||||
|
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
if (!hasOfficerRole(member, cfg("configRoles"))) {
|
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "config" }))) {
|
||||||
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = interaction.options.getSubcommandGroup(true);
|
const group = options.getSubcommandGroup(true);
|
||||||
const sub = interaction.options.getSubcommand();
|
const sub = options.getSubcommand();
|
||||||
|
|
||||||
const roleSubcommand = (cfgKey: "officerRoles" | "configRoles" | "tagRoles", action: string) => {
|
const roleSubcommand = (cfgKey: "officerRoles" | "configRoles" | "tagRoles", action: string) => {
|
||||||
|
const key = ROLE_KEY_MAP[cfgKey];
|
||||||
if (action === "set") {
|
if (action === "set") {
|
||||||
const roles = interaction.options.getString("roles", true).split(",").map((r) => r.trim()).filter(Boolean);
|
const roles = options.getString("roles", true).split(",").map((r: string) => r.trim()).filter(Boolean);
|
||||||
setCfg(cfgKey, roles);
|
Config.set({ section: "roles", key, value: roles });
|
||||||
return void replyAndDelete(interaction, `✅ Roles updated: ${roles.join(", ")}`);
|
return void replyAndDelete(interaction, `✅ Roles updated: ${roles.join(", ")}`);
|
||||||
}
|
}
|
||||||
if (action === "add") {
|
if (action === "add") {
|
||||||
const role = interaction.options.getString("role", true).trim();
|
const role = options.getString("role", true).trim();
|
||||||
const roles = [...new Set([...cfg(cfgKey), role])];
|
const roles = [...new Set([...Config.get({ section: "roles", key }), role])];
|
||||||
setCfg(cfgKey, roles);
|
Config.set({ section: "roles", key, value: roles });
|
||||||
return void replyAndDelete(interaction, `✅ Added **${role}**. Current: ${roles.join(", ")}`);
|
return void replyAndDelete(interaction, `✅ Added **${role}**. Current: ${roles.join(", ")}`);
|
||||||
}
|
}
|
||||||
if (action === "remove") {
|
if (action === "remove") {
|
||||||
const role = interaction.options.getString("role", true).trim();
|
const role = options.getString("role", true).trim();
|
||||||
const roles = cfg(cfgKey).filter((r: string) => r !== role);
|
const roles = Config.get({ section: "roles", key }).filter((r: string) => r !== role);
|
||||||
setCfg(cfgKey, roles);
|
Config.set({ section: "roles", key, value: roles });
|
||||||
return void replyAndDelete(interaction, `✅ Removed **${role}**. Current: ${roles.join(", ")}`);
|
return void replyAndDelete(interaction, `✅ Removed **${role}**. Current: ${roles.join(", ")}`);
|
||||||
}
|
}
|
||||||
if (action === "reset") {
|
if (action === "reset") {
|
||||||
resetCfg(cfgKey);
|
Config.reset({ section: "roles", key });
|
||||||
return void replyAndDelete(interaction, `✅ Roles reset to default.`);
|
return void replyAndDelete(interaction, `✅ Roles reset to default.`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── message ────────────────────────────────────────────────────────────────
|
// ── message ────────────────────────────────────────────────────────────────
|
||||||
if (group === "message") {
|
if (group === "message") {
|
||||||
if (sub === "set-lock") { setCfg("lockMessage", interaction.options.getString("message", true)); return void replyAndDelete(interaction, "✅ Lock message updated."); }
|
if (sub === "set-lock") { Config.set({ section: "poll", key: "lockMessage", value: options.getString("message", true) }); return void replyAndDelete(interaction, "✅ Lock message updated."); }
|
||||||
if (sub === "reset-lock") { resetCfg("lockMessage"); return void replyAndDelete(interaction, "✅ Lock message reset."); }
|
if (sub === "reset-lock") { Config.reset({ section: "poll", key: "lockMessage" }); return void replyAndDelete(interaction, "✅ Lock message reset."); }
|
||||||
if (sub === "set-confirm") {
|
if (sub === "set-confirm") {
|
||||||
const d = interaction.options.getString("decision", true);
|
const d = options.getString("decision", true);
|
||||||
setCfg(d === "yes" ? "confirmYesMessage" : "confirmNoMessage", interaction.options.getString("message", true));
|
const key = d === "yes" ? "confirmYes" : "confirmNo";
|
||||||
|
Config.set({ section: "poll", key, value: options.getString("message", true) });
|
||||||
return void replyAndDelete(interaction, `✅ Confirm ${d} message updated.`);
|
return void replyAndDelete(interaction, `✅ Confirm ${d} message updated.`);
|
||||||
}
|
}
|
||||||
if (sub === "reset-confirm") {
|
if (sub === "reset-confirm") {
|
||||||
const d = interaction.options.getString("decision", true);
|
const d = options.getString("decision", true);
|
||||||
resetCfg(d === "yes" ? "confirmYesMessage" : "confirmNoMessage");
|
const key = d === "yes" ? "confirmYes" : "confirmNo";
|
||||||
|
Config.reset({ section: "poll", key });
|
||||||
return void replyAndDelete(interaction, `✅ Confirm ${d} message reset.`);
|
return void replyAndDelete(interaction, `✅ Confirm ${d} message reset.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,42 +201,42 @@ export async function handleTgConfigCommand(interaction: ChatInputCommandInterac
|
||||||
|
|
||||||
// ── channel ────────────────────────────────────────────────────────────────
|
// ── channel ────────────────────────────────────────────────────────────────
|
||||||
if (group === "channel") {
|
if (group === "channel") {
|
||||||
if (sub === "set-poll") { setCfg("pollChannelId", interaction.options.getChannel("channel", true).id); return void replyAndDelete(interaction, "✅ Poll channel updated."); }
|
if (sub === "set-poll") { Config.set({ section: "channels", key: "poll", value: options.getChannel().id }); return void replyAndDelete(interaction, "✅ Poll channel updated."); }
|
||||||
if (sub === "set-results") { setCfg("resultsChannelId", interaction.options.getChannel("channel", true).id); return void replyAndDelete(interaction, "✅ Results channel updated."); }
|
if (sub === "set-results") { Config.set({ section: "channels", key: "results", value: options.getChannel().id }); return void replyAndDelete(interaction, "✅ Results channel updated."); }
|
||||||
if (sub === "set-score") { setCfg("scoreChannelId", interaction.options.getChannel("channel", true).id); return void replyAndDelete(interaction, "✅ Score channel updated."); }
|
if (sub === "set-score") { Config.set({ section: "channels", key: "score", value: options.getChannel().id }); return void replyAndDelete(interaction, "✅ Score channel updated."); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── slot ───────────────────────────────────────────────────────────────────
|
// ── slot ───────────────────────────────────────────────────────────────────
|
||||||
if (group === "slot") {
|
if (group === "slot") {
|
||||||
if (sub === "add") {
|
if (sub === "add") {
|
||||||
const hour = interaction.options.getInteger("hour", true);
|
const hour = options.getInteger("hour", true);
|
||||||
const pollOpens = interaction.options.getString("poll_opens", true);
|
const pollOpens = options.getString("poll_opens", true);
|
||||||
const slots = cfg("slots");
|
const slots = Config.get({ section: "poll", key: "slots" });
|
||||||
if (slots.some((s) => s.tgHour === hour)) return void replyAndDelete(interaction, `❌ Slot ${hour}:00 already exists.`);
|
if (slots.some((s) => s.tgHour === hour)) return void replyAndDelete(interaction, `❌ Slot ${hour}:00 already exists.`);
|
||||||
slots.push({ tgHour: hour, pollOpens, closesAfter: cfg("tgDurationMinutes"), active: true });
|
slots.push({ tgHour: hour, pollOpens, closesAfter: Config.get({ section: "tg", key: "durationMinutes" }), active: true });
|
||||||
setCfg("slots", slots);
|
Config.set({ section: "poll", key: "slots", value: slots });
|
||||||
return void replyAndDelete(interaction, `✅ Slot ${hour}:00 added (poll opens at ${pollOpens}).`);
|
return void replyAndDelete(interaction, `✅ Slot ${hour}:00 added (poll opens at ${pollOpens}).`);
|
||||||
}
|
}
|
||||||
if (sub === "remove") {
|
if (sub === "remove") {
|
||||||
const hour = interaction.options.getInteger("hour", true);
|
const hour = options.getInteger("hour", true);
|
||||||
const slots = cfg("slots").filter((s) => s.tgHour !== hour);
|
const slots = Config.get({ section: "poll", key: "slots" }).filter((s) => s.tgHour !== hour);
|
||||||
setCfg("slots", slots);
|
Config.set({ section: "poll", key: "slots", value: slots });
|
||||||
return void replyAndDelete(interaction, `✅ Slot ${hour}:00 removed.`);
|
return void replyAndDelete(interaction, `✅ Slot ${hour}:00 removed.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── wrank ──────────────────────────────────────────────────────────────────
|
// ── wrank ──────────────────────────────────────────────────────────────────
|
||||||
if (group === "wrank") {
|
if (group === "wrank") {
|
||||||
if (sub === "set-goal") { setCfg("wRankGoal", interaction.options.getInteger("goal", true)); return void replyAndDelete(interaction, "✅ W.Rank goal updated."); }
|
if (sub === "set-goal") { Config.set({ section: "wrank", key: "goal", value: options.getInteger("goal", true)! }); return void replyAndDelete(interaction, "✅ W.Rank goal updated."); }
|
||||||
if (sub === "set-post-on-reset") { setCfg("wRankPostOnReset", interaction.options.getBoolean("enabled", true)); return void replyAndDelete(interaction, "✅ W.Rank post on reset updated."); }
|
if (sub === "set-post-on-reset") { Config.set({ section: "wrank", key: "postOnReset", value: options.getBoolean("enabled", true)! }); return void replyAndDelete(interaction, "✅ W.Rank post on reset updated."); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── tg ─────────────────────────────────────────────────────────────────────
|
// ── tg ─────────────────────────────────────────────────────────────────────
|
||||||
if (group === "tg") {
|
if (group === "tg") {
|
||||||
if (sub === "set-score-window") { setCfg("scoreWindowHours", interaction.options.getNumber("hours", true)); return void replyAndDelete(interaction, "✅ Score window updated."); }
|
if (sub === "set-score-window") { Config.set({ section: "tg", key: "scoreWindowHours", value: options.getNumber("hours", true)! }); return void replyAndDelete(interaction, "✅ Score window updated."); }
|
||||||
if (sub === "set-duration") { setCfg("tgDurationMinutes", interaction.options.getInteger("minutes", true)); return void replyAndDelete(interaction, "✅ TG duration updated."); }
|
if (sub === "set-duration") { Config.set({ section: "tg", key: "durationMinutes", value: options.getInteger("minutes", true)! }); return void replyAndDelete(interaction, "✅ TG duration updated."); }
|
||||||
if (sub === "set-no-display") { setCfg("showNoInNationField" as any, interaction.options.getString("mode", true) === "inline"); return void replyAndDelete(interaction, "✅ No voter display updated."); }
|
if (sub === "set-no-display") { Config.set({ section: "poll", key: "showNoInNationField", value: options.getString("mode", true) === "inline" }); return void replyAndDelete(interaction, "✅ No voter display updated."); }
|
||||||
if (sub === "set-nation-source"){ setCfg("nationSource", interaction.options.getString("nation", true) as Nation); return void replyAndDelete(interaction, "✅ Nation source updated."); }
|
if (sub === "set-nation-source"){ Config.set({ section: "nation", key: "source", value: options.getString("nation", true) as Nation }); return void replyAndDelete(interaction, "✅ Nation source updated."); }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group === "poll") {
|
if (group === "poll") {
|
||||||
|
|
|
||||||
40
src/discord/channel.ts
Normal file
40
src/discord/channel.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* Discord.Channel — channel fetching and messaging.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Client,
|
||||||
|
TextChannel,
|
||||||
|
MessageCreateOptions,
|
||||||
|
MessageEditOptions,
|
||||||
|
} from "discord.js";
|
||||||
|
|
||||||
|
async function fetch({ client, id }: { client: Client; id: string }): Promise<TextChannel> {
|
||||||
|
return client.channels.fetch(id) as Promise<TextChannel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send({ channel, content, embeds, components }: {
|
||||||
|
channel: TextChannel;
|
||||||
|
content?: string;
|
||||||
|
embeds?: any[];
|
||||||
|
components?: any[];
|
||||||
|
}): Promise<void> {
|
||||||
|
await channel.send({ content, embeds, components } as MessageCreateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function edit({ channel, messageId, content, embeds, components }: {
|
||||||
|
channel: TextChannel;
|
||||||
|
messageId: string;
|
||||||
|
content?: string;
|
||||||
|
embeds?: any[];
|
||||||
|
components?: any[];
|
||||||
|
}): Promise<void> {
|
||||||
|
const msg = await channel.messages.fetch(messageId);
|
||||||
|
await msg.edit({ content, embeds, components } as MessageEditOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Channel = {
|
||||||
|
fetch,
|
||||||
|
send,
|
||||||
|
edit,
|
||||||
|
};
|
||||||
29
src/discord/guild.ts
Normal file
29
src/discord/guild.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Discord.Guild — guild and member operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
ButtonInteraction,
|
||||||
|
GuildMember,
|
||||||
|
} from "discord.js";
|
||||||
|
|
||||||
|
type AnyInteraction = ChatInputCommandInteraction | ButtonInteraction;
|
||||||
|
|
||||||
|
async function member({ interaction }: { interaction: AnyInteraction }): Promise<GuildMember> {
|
||||||
|
return interaction.guild!.members.fetch(interaction.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMember({ interaction, userId }: { interaction: AnyInteraction; userId: string }): Promise<GuildMember> {
|
||||||
|
return interaction.guild!.members.fetch(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRole({ member, roles }: { member: GuildMember; roles: string[] }): boolean {
|
||||||
|
return member.roles.cache.some((r) => roles.includes(r.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Guild = {
|
||||||
|
member,
|
||||||
|
fetchMember,
|
||||||
|
hasRole,
|
||||||
|
};
|
||||||
25
src/discord/index.ts
Normal file
25
src/discord/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Discord — abstraction layer over Discord.js API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { Discord } from "@discord";
|
||||||
|
*
|
||||||
|
* Discord.Interaction.options<ChatInputCommandInteraction>(i).string({ key: "name" })
|
||||||
|
* Discord.Guild.member({ interaction })
|
||||||
|
* Discord.Channel.fetch({ client, id })
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { Interaction } from "./interaction";
|
||||||
|
export { Guild } from "./guild";
|
||||||
|
export { Channel } from "./channel";
|
||||||
|
|
||||||
|
// Top-level namespace for convenience
|
||||||
|
import { Interaction } from "./interaction";
|
||||||
|
import { Guild } from "./guild";
|
||||||
|
import { Channel } from "./channel";
|
||||||
|
|
||||||
|
export const Discord = {
|
||||||
|
Interaction,
|
||||||
|
Guild,
|
||||||
|
Channel,
|
||||||
|
};
|
||||||
89
src/discord/interaction.ts
Normal file
89
src/discord/interaction.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* Discord.Interaction — abstracts interaction option reading and replies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
ButtonInteraction,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
GuildMember,
|
||||||
|
InteractionReplyOptions,
|
||||||
|
MessageFlags,
|
||||||
|
} from "discord.js";
|
||||||
|
|
||||||
|
type AnyInteraction =
|
||||||
|
| ChatInputCommandInteraction
|
||||||
|
| ButtonInteraction
|
||||||
|
| ModalSubmitInteraction;
|
||||||
|
|
||||||
|
// ─── Options resolver ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface OptionParams {
|
||||||
|
key: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionsResolver {
|
||||||
|
string(params: OptionParams): string | null;
|
||||||
|
integer(params: OptionParams): number | null;
|
||||||
|
number(params: OptionParams): number | null;
|
||||||
|
boolean(params: OptionParams): boolean | null;
|
||||||
|
channel(params: OptionParams): any | null;
|
||||||
|
user(params: OptionParams): any | null;
|
||||||
|
subcommand(): string | null;
|
||||||
|
subcommandGroup(): string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function options<T extends ChatInputCommandInteraction>(interaction: T): OptionsResolver {
|
||||||
|
const opts = interaction.options as any;
|
||||||
|
return {
|
||||||
|
string: ({ key, required = false }) => opts.getString(key, required),
|
||||||
|
integer: ({ key, required = false }) => opts.getInteger(key, required),
|
||||||
|
number: ({ key, required = false }) => opts.getNumber(key, required),
|
||||||
|
boolean: ({ key, required = false }) => opts.getBoolean(key, required),
|
||||||
|
channel: ({ key, required = false }) => opts.getChannel(key, required),
|
||||||
|
user: ({ key, required = false }) => opts.getUser(key, required),
|
||||||
|
subcommand: () => opts.getSubcommand(false),
|
||||||
|
subcommandGroup: () => opts.getSubcommandGroup(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Reply helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface ReplyParams {
|
||||||
|
content: string;
|
||||||
|
ephemeral?: boolean;
|
||||||
|
components?: any[];
|
||||||
|
embeds?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reply(interaction: AnyInteraction, params: ReplyParams): Promise<void> {
|
||||||
|
const opts: InteractionReplyOptions = {
|
||||||
|
content: params.content,
|
||||||
|
components: params.components,
|
||||||
|
embeds: params.embeds,
|
||||||
|
flags: params.ephemeral ? MessageFlags.Ephemeral : undefined,
|
||||||
|
};
|
||||||
|
if (interaction.replied || interaction.deferred) {
|
||||||
|
await interaction.followUp(opts);
|
||||||
|
} else {
|
||||||
|
await interaction.reply(opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function followUp(interaction: AnyInteraction, params: ReplyParams): Promise<void> {
|
||||||
|
await interaction.followUp({
|
||||||
|
content: params.content,
|
||||||
|
components: params.components,
|
||||||
|
embeds: params.embeds,
|
||||||
|
flags: params.ephemeral ? MessageFlags.Ephemeral : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Namespace ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const Interaction = {
|
||||||
|
options,
|
||||||
|
reply,
|
||||||
|
followUp,
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { AutocompleteInteraction } from "discord.js";
|
import { AutocompleteInteraction } from "discord.js";
|
||||||
import { resolveUser } from "@systems/users";
|
import { resolveUser } from "@systems/users";
|
||||||
import { getCharacters } from "@systems/characters";
|
import { getCharacters } from "@systems/characters";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
import { CharacterRegistry } from "@registry/character-registry";
|
import { CharacterRegistry } from "@registry/character-registry";
|
||||||
import { UserRegistry } from "@registry/user-registry";
|
import { UserRegistry } from "@registry/user-registry";
|
||||||
|
|
@ -99,7 +99,7 @@ async function autocompleteSlots(
|
||||||
interaction: AutocompleteInteraction,
|
interaction: AutocompleteInteraction,
|
||||||
focused: string
|
focused: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const slots = cfg("slots")
|
const slots = Config.get({ section: "poll", key: "slots" })
|
||||||
.filter((s) => s.active)
|
.filter((s) => s.active)
|
||||||
.map((s) => ({ name: `${s.tgHour}:00`, value: String(s.tgHour) }))
|
.map((s) => ({ name: `${s.tgHour}:00`, value: String(s.tgHour) }))
|
||||||
.filter((s) => s.name.includes(focused));
|
.filter((s) => s.name.includes(focused));
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
TextChannel
|
TextChannel
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
|
||||||
import { pollReplyAndDelete } from "../utils";
|
import { pollReplyAndDelete } from "../utils";
|
||||||
import { resolveUser } from "@systems/users";
|
import { resolveUser } from "@systems/users";
|
||||||
import { resolveMessage, nowFormatted } from "@systems/messages";
|
import { resolveMessage, nowFormatted } from "@systems/messages";
|
||||||
|
|
@ -27,7 +26,7 @@ import { Benchmark } from "@systems/benchmark";
|
||||||
import { Config } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
|
|
||||||
|
|
||||||
const LOCK_AT = Config.get("lockAt");
|
const LOCK_AT = Config.get({ section: "poll", key: "lockAt" });
|
||||||
|
|
||||||
const clickCounts = new Map<string, { yes: number; no: number }>();
|
const clickCounts = new Map<string, { yes: number; no: number }>();
|
||||||
const _processingVotes = new Set<string>();
|
const _processingVotes = new Set<string>();
|
||||||
|
|
@ -78,7 +77,7 @@ async function handleCharacterConflict(
|
||||||
}
|
}
|
||||||
|
|
||||||
const slot = [...polls.keys()][0];
|
const slot = [...polls.keys()][0];
|
||||||
const slotHour = slot !== undefined ? polls.get(slot)?.slot : cfg("slots")[0]?.tgHour ?? 20;
|
const slotHour = slot !== undefined ? polls.get(slot)?.slot : Config.get({ section: "poll", key: "slots" })[0]?.tgHour ?? 20;
|
||||||
|
|
||||||
const { buildCharSelectButtons } = require("@systems/charSelect");
|
const { buildCharSelectButtons } = require("@systems/charSelect");
|
||||||
const buttons = buildCharSelectButtons(userKey ?? "", {
|
const buttons = buildCharSelectButtons(userKey ?? "", {
|
||||||
|
|
@ -245,7 +244,7 @@ export async function showActiveCharSwitching(interaction: ButtonInteraction): P
|
||||||
const { char, borrowedFrom } = getEffectiveCharacter(user.userKey);
|
const { char, borrowedFrom } = getEffectiveCharacter(user.userKey);
|
||||||
bench.mark("getEffectiveCharacter");
|
bench.mark("getEffectiveCharacter");
|
||||||
if (char) {
|
if (char) {
|
||||||
const starEmoji = Config.get("activeCharEmoji");
|
const starEmoji = Config.get({ section: "emoji", key: "activeChar" });
|
||||||
const borrowNote = borrowedFrom ? ` 🔗` : "";
|
const borrowNote = borrowedFrom ? ` 🔗` : "";
|
||||||
const buttons = buildCharSelectButtons(user.userKey, {
|
const buttons = buildCharSelectButtons(user.userKey, {
|
||||||
customIdPrefix: `companion_switch:${user.userKey}`,
|
customIdPrefix: `companion_switch:${user.userKey}`,
|
||||||
|
|
@ -316,8 +315,8 @@ export async function handleScoreSubmitButton(interaction: ButtonInteraction): P
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // Build slot selector — all valid slots, with the active TG pre-selected
|
// // Build slot selector — all valid slots, with the active TG pre-selected
|
||||||
// const validSlots = cfg("slots").map((s) => s.tgHour) as number[];
|
// const validSlots = Config.get({ section: "poll", key: "slots" }).map((s) => s.tgHour) as number[];
|
||||||
// const activeSlot = slot ?? cfg("slots").find((s) => s.active)?.tgHour ?? 20;
|
// const activeSlot = slot ?? Config.get({ section: "poll", key: "slots" }).find((s) => s.active)?.tgHour ?? 20;
|
||||||
|
|
||||||
// const select = new StringSelectMenuBuilder()
|
// const select = new StringSelectMenuBuilder()
|
||||||
// .setCustomId(`score_slot_select:${user.userKey}`)
|
// .setCustomId(`score_slot_select:${user.userKey}`)
|
||||||
|
|
|
||||||
18
src/index.ts
18
src/index.ts
|
|
@ -1,9 +1,5 @@
|
||||||
import { Client, GatewayIntentBits, TextChannel, REST, Routes } from "discord.js";
|
import { Client, GatewayIntentBits, TextChannel, REST, Routes } from "discord.js";
|
||||||
import { loadConfig, cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { loadMessages } from "@systems/messages";
|
|
||||||
import { Emoji } from "@systems/emojis";
|
|
||||||
import { Char } from "@systems/characters";
|
|
||||||
import { WRank } from "@systems/wrank";
|
|
||||||
import { postPoll, polls, lockPoll, updatePollMessage } from "@systems/poll";
|
import { postPoll, polls, lockPoll, updatePollMessage } from "@systems/poll";
|
||||||
import { handleInteraction } from "@handlers/interactions";
|
import { handleInteraction } from "@handlers/interactions";
|
||||||
import { buildTgCommand } from "@commands/tg";
|
import { buildTgCommand } from "@commands/tg";
|
||||||
|
|
@ -35,7 +31,8 @@ async function registerCommands(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPollOpen(slot: TGSlot): Promise<void> {
|
async function onPollOpen(slot: TGSlot): Promise<void> {
|
||||||
const channel = await client.channels.fetch(cfg("pollChannelId")) as any;
|
const channelId = Config.get({ section: "channels", key: "poll" });
|
||||||
|
const channel = await client.channels.fetch(channelId) as any;
|
||||||
if (!channel) return console.error("Poll channel not found.");
|
if (!channel) return console.error("Poll channel not found.");
|
||||||
await postPoll(channel, slot);
|
await postPoll(channel, slot);
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +44,8 @@ async function onPollLock(slot: TGSlot): Promise<void> {
|
||||||
|
|
||||||
lockPoll(slot.tgHour);
|
lockPoll(slot.tgHour);
|
||||||
|
|
||||||
const channel = await client.channels.fetch(cfg("pollChannelId")) as any;
|
const channelId = Config.get({ section: "channels", key: "poll" });
|
||||||
|
const channel = await client.channels.fetch(channelId) as any;
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
|
|
||||||
// Buttons disabled, no submit button yet — that comes at close
|
// Buttons disabled, no submit button yet — that comes at close
|
||||||
|
|
@ -60,7 +58,8 @@ async function onPollClose(slot: TGSlot): Promise<void> {
|
||||||
const state = polls.get(slot.tgHour);
|
const state = polls.get(slot.tgHour);
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
const channel = await client.channels.fetch(cfg("pollChannelId")) as any;
|
const channelId = Config.get({ section: "channels", key: "poll" });
|
||||||
|
const channel = await client.channels.fetch(channelId) as any;
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
|
|
||||||
await updatePollMessage(channel, slot.tgHour, undefined, true); // showSubmit = true
|
await updatePollMessage(channel, slot.tgHour, undefined, true); // showSubmit = true
|
||||||
|
|
@ -79,7 +78,8 @@ client.once("clientReady", async () => {
|
||||||
for (const [slot, state] of restored) polls.set(slot, state);
|
for (const [slot, state] of restored) polls.set(slot, state);
|
||||||
|
|
||||||
// Re-render all restored poll messages
|
// Re-render all restored poll messages
|
||||||
const channel = await client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channelId = Config.get({ section: "channels", key: "poll" });
|
||||||
|
const channel = await client.channels.fetch(channelId) as any;
|
||||||
for (const slot of polls.keys()) {
|
for (const slot of polls.keys()) {
|
||||||
const state = polls.get(slot)!;
|
const state = polls.get(slot)!;
|
||||||
await updatePollMessage(channel, slot, undefined, state.locked && state.confirmed === null);
|
await updatePollMessage(channel, slot, undefined, state.locked && state.confirmed === null);
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ cron.schedule("0 20 * * *", async () => {
|
||||||
|
|
||||||
lockPoll(slot);
|
lockPoll(slot);
|
||||||
|
|
||||||
const channel = await client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot, cfg("lockMessage"));
|
await updatePollMessage(channel, slot, Config.get({ section: "poll", key: "lockMessage" }));
|
||||||
console.log(`[${new Date().toISOString()}] Poll locked for ${slot}:00.`);
|
console.log(`[${new Date().toISOString()}] Poll locked for ${slot}:00.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ cron.schedule("35 20 * * *", async () => {
|
||||||
const state = polls.get(slot);
|
const state = polls.get(slot);
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
const channel = await client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot, undefined, true); // showSubmit = true
|
await updatePollMessage(channel, slot, undefined, true); // showSubmit = true
|
||||||
console.log(`[${new Date().toISOString()}] Submit Score button shown for ${slot}:00 TG.`);
|
console.log(`[${new Date().toISOString()}] Submit Score button shown for ${slot}:00 TG.`);
|
||||||
});
|
});
|
||||||
|
|
@ -29,5 +29,5 @@ cron.schedule("35 20 * * *", async () => {
|
||||||
// ─── NOTE on future slots ─────────────────────────────────────────────────────
|
// ─── NOTE on future slots ─────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Right now only slot 20 has an active poll. When we add more votable slots,
|
// Right now only slot 20 has an active poll. When we add more votable slots,
|
||||||
// pull the active slot from cfg("slots").filter(s => s.active) and schedule
|
// pull the active slot from Config.get({ section: "poll", key: "slots" }).filter(s => s.active) and schedule
|
||||||
// dynamically, or make the cron time configurable in config.json.
|
// dynamically, or make the cron time configurable in config.json.
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { hasOfficerRole } from "@systems/users";
|
import { hasOfficerRole } from "@systems/users";
|
||||||
import { getUsermapEntryById, setUsermapEntry, removeUsermapEntry } from "@systems/messages";
|
import { getUsermapEntryById, setUsermapEntry, removeUsermapEntry } from "@systems/messages";
|
||||||
import { replyAndDelete } from "@utils";
|
import { replyAndDelete } from "@utils";
|
||||||
|
|
@ -9,7 +9,7 @@ import { getEffectiveCharacter } from "@systems/borrow";
|
||||||
|
|
||||||
export async function handleAdminUserMap(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleAdminUserMap(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
if (!hasOfficerRole(member, cfg("officerRoles"))) {
|
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||||||
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ export async function handleAdminUserMap(interaction: ChatInputCommandInteractio
|
||||||
|
|
||||||
export async function handleAdminPollFixVoter(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleAdminPollFixVoter(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
if (!hasOfficerRole(member, cfg("officerRoles"))) {
|
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||||||
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +138,7 @@ export async function handleAdminPollFixVoter(interaction: ChatInputCommandInter
|
||||||
updateEntry(state.yes);
|
updateEntry(state.yes);
|
||||||
updateEntry(state.no);
|
updateEntry(state.no);
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as any;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as any;
|
||||||
await updatePollMessage(channel, slot);
|
await updatePollMessage(channel, slot);
|
||||||
|
|
||||||
const { format } = require("@format");
|
const { format } = require("@format");
|
||||||
|
|
@ -147,7 +147,7 @@ export async function handleAdminPollFixVoter(interaction: ChatInputCommandInter
|
||||||
|
|
||||||
export async function handleAdminPollShowEntry(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleAdminPollShowEntry(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
if (!hasOfficerRole(member, cfg("officerRoles"))) {
|
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||||||
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, ButtonInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, ButtonInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { getCharacterByName } from "@systems/characters";
|
import { getCharacterByName } from "@systems/characters";
|
||||||
import { getPendingRequest, removePendingRequest, setSessionBorrow, updateBorrowDM } from "@systems/borrow";
|
import { getPendingRequest, removePendingRequest, setSessionBorrow, updateBorrowDM } from "@systems/borrow";
|
||||||
import { polls, updatePollMessage } from "@systems/poll";
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
|
|
@ -58,7 +58,7 @@ async function acceptBorrow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot);
|
await updatePollMessage(channel, slot);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { getCharacterByName, getActiveCharacter } from "../../systems/characters";
|
import { getCharacterByName, getActiveCharacter } from "../../systems/characters";
|
||||||
import { addPendingRequest, setSessionBorrow, sendBorrowRequestDM, canUseCharacter } from "../../systems/borrow";
|
import { addPendingRequest, setSessionBorrow, sendBorrowRequestDM, canUseCharacter } from "../../systems/borrow";
|
||||||
|
|
@ -11,7 +11,7 @@ export async function handleCharActive(interaction: ChatInputCommandInteraction)
|
||||||
const group = interaction.options.getSubcommandGroup(false);
|
const group = interaction.options.getSubcommandGroup(false);
|
||||||
const sub = interaction.options.getSubcommand();
|
const sub = interaction.options.getSubcommand();
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
if (nameArg && !isOfficer) {
|
if (nameArg && !isOfficer) {
|
||||||
return void replyAndDelete(interaction, "❌ Only officers can check other players' active character.");
|
return void replyAndDelete(interaction, "❌ Only officers can check other players' active character.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { addCharacter } from "../../systems/characters";
|
import { addCharacter } from "../../systems/characters";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
@ -7,7 +7,7 @@ import { ClassKey, Nation } from "../../types";
|
||||||
|
|
||||||
export async function handleCharAdd(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleCharAdd(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const nameArg = interaction.options.getString("name");
|
const nameArg = interaction.options.getString("name");
|
||||||
const charName = interaction.options.getString("char_name", true);
|
const charName = interaction.options.getString("char_name", true);
|
||||||
const cls = interaction.options.getString("class", true) as ClassKey;
|
const cls = interaction.options.getString("class", true) as ClassKey;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { getCharacterByName, getActiveCharacter } from "../../systems/characters";
|
import { getCharacterByName, getActiveCharacter } from "../../systems/characters";
|
||||||
import { addPendingRequest, setSessionBorrow, sendBorrowRequestDM, canUseCharacter } from "../../systems/borrow";
|
import { addPendingRequest, setSessionBorrow, sendBorrowRequestDM, canUseCharacter } from "../../systems/borrow";
|
||||||
|
|
@ -9,7 +9,7 @@ import { getUsermapEntry, getUsermapEntryById } from "@src/systems/messages";
|
||||||
|
|
||||||
export async function handleCharBorrow(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleCharBorrow(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const requester = await resolveUser(member);
|
const requester = await resolveUser(member);
|
||||||
|
|
||||||
// Args: owner, charname, [username] (officer only — grants directly)
|
// Args: owner, charname, [username] (officer only — grants directly)
|
||||||
|
|
@ -40,7 +40,7 @@ export async function handleCharBorrow(interaction: ChatInputCommandInteraction)
|
||||||
const slot = [...polls.keys()][0];
|
const slot = [...polls.keys()][0];
|
||||||
if (slot !== undefined) {
|
if (slot !== undefined) {
|
||||||
const state = polls.get(slot)!;
|
const state = polls.get(slot)!;
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
// Find the voter entry and update their character
|
// Find the voter entry and update their character
|
||||||
for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) {
|
for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) {
|
||||||
if (entry.userKey === requesterKey) {
|
if (entry.userKey === requesterKey) {
|
||||||
|
|
@ -78,7 +78,7 @@ export async function handleCharBorrow(interaction: ChatInputCommandInteraction)
|
||||||
return void replyAndDelete(interaction, `✅ Borrow request sent — but **${ownerArg}** is not currently in the server to be notified.`);
|
return void replyAndDelete(interaction, `✅ Borrow request sent — but **${ownerArg}** is not currently in the server to be notified.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackChannel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const fallbackChannel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await sendBorrowRequestDM(
|
await sendBorrowRequestDM(
|
||||||
interaction.client,
|
interaction.client,
|
||||||
ownerMember.user.id,
|
ownerMember.user.id,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { removeCharacter } from "../../systems/characters";
|
import { removeCharacter } from "../../systems/characters";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
||||||
export async function handleCharRemove(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleCharRemove(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const nameArg = interaction.options.getString("name");
|
const nameArg = interaction.options.getString("name");
|
||||||
const charName = interaction.options.getString("char_name", true);
|
const charName = interaction.options.getString("char_name", true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { setActiveCharacter } from "../../systems/characters";
|
import { setActiveCharacter } from "../../systems/characters";
|
||||||
import { setSessionBorrow } from "../../systems/borrow";
|
import { setSessionBorrow } from "../../systems/borrow";
|
||||||
|
|
@ -25,7 +25,7 @@ function findSharedChar(userKey: string, charName: string): { ownerKey: string;
|
||||||
|
|
||||||
export async function handleCharSetActive(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleCharSetActive(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const nameArg = interaction.options.getString("name");
|
const nameArg = interaction.options.getString("name");
|
||||||
const charName = interaction.options.getString("char_name", true);
|
const charName = interaction.options.getString("char_name", true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { setCharacterNation, getActiveCharacter } from "../../systems/characters";
|
import { setCharacterNation, getActiveCharacter } from "../../systems/characters";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
@ -7,7 +7,7 @@ import { Nation } from "../../types";
|
||||||
|
|
||||||
export async function handleCharSetNation(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleCharSetNation(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const nameArg = interaction.options.getString("name");
|
const nameArg = interaction.options.getString("name");
|
||||||
const nation = interaction.options.getString("nation", true) as Nation;
|
const nation = interaction.options.getString("nation", true) as Nation;
|
||||||
const charName = interaction.options.getString("char_name"); // optional, defaults to active
|
const charName = interaction.options.getString("char_name"); // optional, defaults to active
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { setCharacterStats, getActiveCharacter } from "../../systems/characters";
|
import { setCharacterStats, getActiveCharacter } from "../../systems/characters";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
@ -8,7 +8,7 @@ export async function handleCharSetStats(interaction: ChatInputCommandInteractio
|
||||||
return void replyAndDelete(interaction, "⚠️ Character stats system is being redesigned. Coming soon.", true);
|
return void replyAndDelete(interaction, "⚠️ Character stats system is being redesigned. Coming soon.", true);
|
||||||
|
|
||||||
// const member = await interaction.guild!.members.fetch(interaction.user.id);
|
// const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
// const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
// const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
// const nameArg = interaction.options.getString("name");
|
// const nameArg = interaction.options.getString("name");
|
||||||
// const charName = interaction.options.getString("char_name");
|
// const charName = interaction.options.getString("char_name");
|
||||||
// const atk = interaction.options.getInteger("atk") ?? undefined;
|
// const atk = interaction.options.getInteger("atk") ?? undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { getCharacterByName } from "../../systems/characters";
|
import { getCharacterByName } from "../../systems/characters";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
@ -19,7 +19,7 @@ function loadRawChars(): any {
|
||||||
|
|
||||||
export async function handleCharShare(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleCharShare(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const user = await resolveUser(member);
|
const user = await resolveUser(member);
|
||||||
|
|
||||||
const ownerArg = interaction.options.getString("owner");
|
const ownerArg = interaction.options.getString("owner");
|
||||||
|
|
@ -54,7 +54,7 @@ export async function handleCharShare(interaction: ChatInputCommandInteraction):
|
||||||
|
|
||||||
export async function handleCharUnshare(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleCharUnshare(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const user = await resolveUser(member);
|
const user = await resolveUser(member);
|
||||||
|
|
||||||
const ownerArg = interaction.options.getString("owner");
|
const ownerArg = interaction.options.getString("owner");
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { hasOfficerRole } from "@systems/users";
|
import { hasOfficerRole } from "@systems/users";
|
||||||
import { getRegisteredUsers, setImpersonation, clearImpersonation, getImpersonation } from "@systems/impersonate";
|
import { getRegisteredUsers, setImpersonation, clearImpersonation, getImpersonation } from "@systems/impersonate";
|
||||||
import { replyAndDelete } from "@utils";
|
import { replyAndDelete } from "@utils";
|
||||||
|
|
@ -70,7 +70,7 @@ function buildImpersonateButtons(
|
||||||
// Slash command handler
|
// Slash command handler
|
||||||
export async function handleImpersonate(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleImpersonate(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
if (!hasOfficerRole(member, cfg("officerRoles"))) {
|
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||||||
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
return void replyAndDelete(interaction, "❌ You don't have permission to use this command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg, setCfg, resetCfg } from "../../systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { polls, updatePollMessage } from "../../systems/poll";
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
import { Nation } from "../../types";
|
import { Nation } from "../../types";
|
||||||
|
|
||||||
|
|
@ -16,15 +16,16 @@ export async function handleConfirm(interaction: ChatInputCommandInteraction): P
|
||||||
state.confirmed = decision;
|
state.confirmed = decision;
|
||||||
state.locked = true;
|
state.locked = true;
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
|
|
||||||
if (oneTimeMsg) {
|
if (oneTimeMsg) {
|
||||||
const cfgKey = decision === "yes" ? "confirmYesMessage" : "confirmNoMessage";
|
const isYes = decision === "yes";
|
||||||
const saved = cfg(cfgKey);
|
const key = isYes ? "confirmYes" : "confirmNo";
|
||||||
setCfg(cfgKey, oneTimeMsg);
|
const saved = Config.get({ section: "poll", key });
|
||||||
|
|
||||||
|
Config.set({ section: "poll", key, value: oneTimeMsg });
|
||||||
await updatePollMessage(channel, slot);
|
await updatePollMessage(channel, slot);
|
||||||
if (saved !== undefined) setCfg(cfgKey, saved);
|
Config.set({ section: "poll", key, value: saved });
|
||||||
else resetCfg(cfgKey);
|
|
||||||
} else {
|
} else {
|
||||||
await updatePollMessage(channel, slot);
|
await updatePollMessage(channel, slot);
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +35,7 @@ export async function handleConfirm(interaction: ChatInputCommandInteraction): P
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tagRoles(channel: TextChannel, interaction: ChatInputCommandInteraction): Promise<void> {
|
async function tagRoles(channel: TextChannel, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const roles = cfg("tagRoles");
|
const roles = Config.get({ section: "roles", key: "tag" });
|
||||||
const guild = interaction.guild!;
|
const guild = interaction.guild!;
|
||||||
await guild.roles.fetch();
|
await guild.roles.fetch();
|
||||||
const mentions = roles
|
const mentions = roles
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { polls, updatePollMessage } from "../../systems/poll";
|
import { polls, updatePollMessage } from "../../systems/poll";
|
||||||
import { getEffectiveCharacter } from "../../systems/borrow";
|
import { getEffectiveCharacter } from "../../systems/borrow";
|
||||||
import { nowFormatted, resolveMessage } from "../../systems/messages";
|
import { nowFormatted, resolveMessage } from "../../systems/messages";
|
||||||
|
|
@ -50,7 +50,7 @@ const entry: VoteEntry = {
|
||||||
state.no.set(syntheticId, entry);
|
state.no.set(syntheticId, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot);
|
await updatePollMessage(channel, slot);
|
||||||
return void replyAndDelete(interaction, `✅ Injected **${userKey}** as **${voteType}**.`);
|
return void replyAndDelete(interaction, `✅ Injected **${userKey}** as **${voteType}**.`);
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +76,7 @@ export async function handleRemoveVote(interaction: ChatInputCommandInteraction)
|
||||||
|
|
||||||
if (!removed) return void replyAndDelete(interaction, `❌ No vote found for **${userKey}**.`);
|
if (!removed) return void replyAndDelete(interaction, `❌ No vote found for **${userKey}**.`);
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot);
|
await updatePollMessage(channel, slot);
|
||||||
return void replyAndDelete(interaction, `✅ Vote removed for **${userKey}**.`);
|
return void replyAndDelete(interaction, `✅ Vote removed for **${userKey}**.`);
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { polls, lockPoll, updatePollMessage } from "@systems/poll";
|
import { polls, lockPoll, updatePollMessage } from "@systems/poll";
|
||||||
import { replyAndDelete } from "@utils";
|
import { replyAndDelete } from "@utils";
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ export async function handleLock(interaction: ChatInputCommandInteraction): Prom
|
||||||
// Use lockPoll() so lockedYesKeys gets snapshotted — same path as the cron
|
// Use lockPoll() so lockedYesKeys gets snapshotted — same path as the cron
|
||||||
lockPoll(slot);
|
lockPoll(slot);
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
|
|
||||||
if (simulateClose) {
|
if (simulateClose) {
|
||||||
// Simulate TG end: show Submit Score button (same path as onPollClose cron)
|
// Simulate TG end: show Submit Score button (same path as onPollClose cron)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel, Collection, Message } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel, Collection, Message } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { polls } from "../../systems/poll";
|
import { polls } from "../../systems/poll";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
||||||
export async function handlePurge(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handlePurge(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const channelId = cfg("pollChannelId");
|
const channelId = Config.get({ section: "channels", key: "poll" });
|
||||||
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
||||||
if (!channel) return void replyAndDelete(interaction, "❌ Poll channel not found.");
|
if (!channel) return void replyAndDelete(interaction, "❌ Poll channel not found.");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ import { loadMessages } from "@systems/messages";
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
import { loadCharacters } from "@systems/characters";
|
import { loadCharacters } from "@systems/characters";
|
||||||
import { loadWRank } from "@systems/wrank";
|
import { loadWRank } from "@systems/wrank";
|
||||||
import { loadConfig, cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { polls, updatePollMessage } from "@systems/poll";
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
import { persist } from "@systems/pollPersistence";
|
import { persist } from "@systems/pollPersistence";
|
||||||
import { replyAndDelete } from "@utils";
|
import { replyAndDelete } from "@utils";
|
||||||
import { CharacterRegistry } from "@root/src/systems/registry/character-registry";
|
import { CharacterRegistry } from "@root/src/systems/registry/character-registry";
|
||||||
import { invalidateUsermapCache } from "@root/src/handlers/autocomplete";
|
|
||||||
import { UserRegistry } from "@root/src/systems/registry/user-registry";
|
import { UserRegistry } from "@root/src/systems/registry/user-registry";
|
||||||
|
|
||||||
const RELOADABLE = ["all", "messages", "emojis", "characters", "wrank", "config", "poll"] as const;
|
const RELOADABLE = ["all", "messages", "emojis", "characters", "wrank", "config", "poll"] as const;
|
||||||
|
|
@ -21,7 +20,7 @@ export async function handleReload(interaction: ChatInputCommandInteraction): Pr
|
||||||
|
|
||||||
const should = (k: Reloadable) => target === "all" || target === k;
|
const should = (k: Reloadable) => target === "all" || target === k;
|
||||||
|
|
||||||
if (should("config")) { loadConfig(); reloaded.push("config"); }
|
if (should("config")) { Config.load(); reloaded.push("config"); }
|
||||||
if (should("messages")) { loadMessages(); reloaded.push("messages"); }
|
if (should("messages")) { loadMessages(); reloaded.push("messages"); }
|
||||||
if (should("emojis")) { Emoji.load(); reloaded.push("emojis"); }
|
if (should("emojis")) { Emoji.load(); reloaded.push("emojis"); }
|
||||||
if (should("characters")) { loadCharacters(); reloaded.push("characters"); }
|
if (should("characters")) { loadCharacters(); reloaded.push("characters"); }
|
||||||
|
|
@ -29,7 +28,7 @@ export async function handleReload(interaction: ChatInputCommandInteraction): Pr
|
||||||
|
|
||||||
// Re-render active poll message(s) so embed reflects reloaded data
|
// Re-render active poll message(s) so embed reflects reloaded data
|
||||||
if (should("poll") || should("emojis") || should("all")) {
|
if (should("poll") || should("emojis") || should("all")) {
|
||||||
const channelId = cfg("pollChannelId");
|
const channelId = Config.get({ section: "channels", key: "poll" });
|
||||||
if (channelId) {
|
if (channelId) {
|
||||||
try {
|
try {
|
||||||
// Restore from disk first if reloading poll specifically
|
// Restore from disk first if reloading poll specifically
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { polls, updatePollMessage } from "@systems/poll";
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
import { nowFormatted, resolveMessage } from "@systems/messages";
|
import { nowFormatted, resolveMessage } from "@systems/messages";
|
||||||
import { getEffectiveCharacter } from "@systems/borrow";
|
import { getEffectiveCharacter } from "@systems/borrow";
|
||||||
|
|
@ -53,7 +53,7 @@ export async function handleSeed(interaction: ChatInputCommandInteraction): Prom
|
||||||
injected++;
|
injected++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot);
|
await updatePollMessage(channel, slot);
|
||||||
return void replyAndDelete(interaction, `✅ Seeded **${injected}** player(s)${skipped > 0 ? `, skipped **${skipped}** (no active character)` : ""}.`);
|
return void replyAndDelete(interaction, `✅ Seeded **${injected}** player(s)${skipped > 0 ? `, skipped **${skipped}** (no active character)` : ""}.`);
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { setPublicOverride, clearPublicOverride, setEphemeralOverride, clearEphemeralOverride } from "../../systems/poll";
|
import { setPublicOverride, clearPublicOverride, setEphemeralOverride, clearEphemeralOverride } from "../../systems/poll";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
import { hasOfficerRole } from "../../systems/users";
|
import { hasOfficerRole } from "../../systems/users";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
|
||||||
import { postPoll } from "../../systems/poll";
|
import { postPoll } from "../../systems/poll";
|
||||||
import { resetClickCounts } from "../../handlers/buttons";
|
import { resetClickCounts } from "../../handlers/buttons";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
@ -8,7 +7,7 @@ import { Config } from "@systems/config";
|
||||||
|
|
||||||
export async function handleStart(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleStart(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const slotArg = interaction.options.getString("slot");
|
const slotArg = interaction.options.getString("slot");
|
||||||
const slots = cfg("slots").filter((s) => s.active);
|
const slots = Config.get({ section: "poll", key: "slots" }).filter((s) => s.active);
|
||||||
|
|
||||||
let slot: TGSlot | undefined;
|
let slot: TGSlot | undefined;
|
||||||
if (slotArg) {
|
if (slotArg) {
|
||||||
|
|
@ -20,7 +19,7 @@ export async function handleStart(interaction: ChatInputCommandInteraction): Pro
|
||||||
}
|
}
|
||||||
if (!slot) return void replyAndDelete(interaction, "❌ No active TG slots configured.");
|
if (!slot) return void replyAndDelete(interaction, "❌ No active TG slots configured.");
|
||||||
|
|
||||||
const channelId = Config.get("pollChannelId");
|
const channelId = Config.get({ section: "channels", key: "poll" });
|
||||||
console.log("pollChannelId:", channelId);
|
console.log("pollChannelId:", channelId);
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { polls } from "../../systems/poll";
|
import { polls } from "../../systems/poll";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
||||||
|
|
@ -9,19 +9,19 @@ export async function handleStatus(interaction: ChatInputCommandInteraction): Pr
|
||||||
).join("\n") || "None";
|
).join("\n") || "None";
|
||||||
|
|
||||||
const status = [
|
const status = [
|
||||||
`**Officer roles:** ${cfg("officerRoles").join(", ")}`,
|
`**Officer roles:** ${Config.get({ section: "roles", key: "officer" }).join(", ")}`,
|
||||||
`**Config roles:** ${cfg("configRoles").join(", ")}`,
|
`**Config roles:** ${Config.get({ section: "roles", key: "config" }).join(", ")}`,
|
||||||
`**Tag roles:** ${cfg("tagRoles").join(", ")}`,
|
`**Tag roles:** ${Config.get({ section: "roles", key: "tag" }).join(", ")}`,
|
||||||
`**Lock message:** ${cfg("lockMessage")}`,
|
`**Lock message:** ${Config.get({ section: "poll", key: "lockMessage" })}`,
|
||||||
`**Confirm yes:** ${cfg("confirmYesMessage")}`,
|
`**Confirm yes:** ${Config.get({ section: "poll", key: "confirmYes" })}`,
|
||||||
`**Confirm no:** ${cfg("confirmNoMessage")}`,
|
`**Confirm no:** ${Config.get({ section: "poll", key: "confirmNo" })}`,
|
||||||
`**Poll channel:** ${cfg("pollChannelId") || "not set"}`,
|
`**Poll channel:** ${Config.get({ section: "channels", key: "poll" }) || "not set"}`,
|
||||||
`**Results channel:** ${cfg("resultsChannelId") || "not set"}`,
|
`**Results channel:** ${Config.get({ section: "channels", key: "results" }) || "not set"}`,
|
||||||
`**Score channel:** ${cfg("scoreChannelId") || "not set"}`,
|
`**Score channel:** ${Config.get({ section: "channels", key: "score" }) || "not set"}`,
|
||||||
`**Score window:** ${cfg("scoreWindowHours")}h`,
|
`**Score window:** ${Config.get({ section: "tg", key: "scoreWindowHours" })}h`,
|
||||||
`**TG duration:** ${cfg("tgDurationMinutes")}min`,
|
`**TG duration:** ${Config.get({ section: "tg", key: "durationMinutes" })}min`,
|
||||||
`**Nation source:** ${cfg("nationSource")}`,
|
`**Nation source:** ${Config.get({ section: "nation", key: "source" })}`,
|
||||||
`**W.Rank goal:** ${cfg("wRankGoal")} TGs`,
|
`**W.Rank goal:** ${Config.get({ section: "wrank", key: "goal" })} TGs`,
|
||||||
`**Timezone:** ${process.env.TZ ?? "Etc/GMT-2"}`,
|
`**Timezone:** ${process.env.TZ ?? "Etc/GMT-2"}`,
|
||||||
`**Active polls:**\n${activePolls}`,
|
`**Active polls:**\n${activePolls}`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { polls, updatePollMessage } from "../../systems/poll";
|
import { polls, updatePollMessage } from "../../systems/poll";
|
||||||
import { loadMessages } from "../../systems/messages";
|
import { loadMessages } from "../../systems/messages";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
@ -14,7 +14,7 @@ export async function handleUnlock(interaction: ChatInputCommandInteraction): Pr
|
||||||
state.locked = false;
|
state.locked = false;
|
||||||
loadMessages();
|
loadMessages();
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot);
|
await updatePollMessage(channel, slot);
|
||||||
return void replyAndDelete(interaction, "🔓 Poll unlocked!");
|
return void replyAndDelete(interaction, "🔓 Poll unlocked!");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "@systems/users";
|
import { resolveUser, hasOfficerRole } from "@systems/users";
|
||||||
import { WRank } from "@systems/wrank";
|
import { WRank } from "@systems/wrank";
|
||||||
import { Bringer } from "@systems/bringer";
|
import { Bringer } from "@systems/bringer";
|
||||||
|
|
@ -9,7 +9,7 @@ import { TG } from "@systems/tg";
|
||||||
|
|
||||||
export async function handleRankGet(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleRankGet(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const nameArg = interaction.options.getString("name");
|
const nameArg = interaction.options.getString("name");
|
||||||
|
|
||||||
if (nameArg && !isOfficer) {
|
if (nameArg && !isOfficer) {
|
||||||
|
|
@ -27,7 +27,7 @@ export async function handleRankGet(interaction: ChatInputCommandInteraction): P
|
||||||
if (!userKey) return void replyAndDelete(interaction, "❌ You are not registered in the system.");
|
if (!userKey) return void replyAndDelete(interaction, "❌ You are not registered in the system.");
|
||||||
|
|
||||||
const week = TG.currentWeek();
|
const week = TG.currentWeek();
|
||||||
const goal = cfg("wRankGoal");
|
const goal = Config.get({ section: "wrank", key: "goal" });
|
||||||
const weekKey = WRank.weekKey();
|
const weekKey = WRank.weekKey();
|
||||||
|
|
||||||
for (const nation of ["capella", "procyon"] as const) {
|
for (const nation of ["capella", "procyon"] as const) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel, EmbedBuilder } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel, EmbedBuilder } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { WRank } from "@systems/wrank";
|
import { WRank } from "@systems/wrank";
|
||||||
import { getEmoji } from "@systems/emojis";
|
import { getEmoji } from "@systems/emojis";
|
||||||
import { replyAndDelete } from "@utils";
|
import { replyAndDelete } from "@utils";
|
||||||
|
|
@ -12,7 +12,7 @@ import { TG } from "@systems/tg";
|
||||||
|
|
||||||
export async function handleRankPost(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleRankPost(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const week = TG.currentWeek();
|
const week = TG.currentWeek();
|
||||||
const goal = cfg("wRankGoal");
|
const goal = Config.get({ section: "wrank", key: "goal" });
|
||||||
const weekKey = WRank.weekKey();
|
const weekKey = WRank.weekKey();
|
||||||
|
|
||||||
const formatNation = (nation: Nation): string => {
|
const formatNation = (nation: Nation): string => {
|
||||||
|
|
@ -57,7 +57,7 @@ export async function handleRankPost(interaction: ChatInputCommandInteraction):
|
||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
const channelId = cfg("resultsChannelId") || cfg("pollChannelId");
|
const channelId = Config.get({ section: "channels", key: "results" }) || Config.get({ section: "channels", key: "poll" });
|
||||||
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
||||||
await channel.send({ embeds: [embed] });
|
await channel.send({ embeds: [embed] });
|
||||||
return void replyAndDelete(interaction, "✅ Leaderboard posted.");
|
return void replyAndDelete(interaction, "✅ Leaderboard posted.");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction, TextChannel, EmbedBuilder } from "discord.js";
|
import { ChatInputCommandInteraction, TextChannel, EmbedBuilder } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { loadResult, todayString } from "@systems/history";
|
import { loadResult, todayString } from "@systems/history";
|
||||||
import { normalizeSlot, detectSlot } from "@systems/scores";
|
import { normalizeSlot, detectSlot } from "@systems/scores";
|
||||||
import { replyAndDelete } from "@src/utils";
|
import { replyAndDelete } from "@src/utils";
|
||||||
|
|
@ -13,7 +13,7 @@ export async function handleResultPost(interaction: ChatInputCommandInteraction)
|
||||||
slot = normalizeSlot(slotArg);
|
slot = normalizeSlot(slotArg);
|
||||||
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`);
|
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`);
|
||||||
} else {
|
} else {
|
||||||
slot = detectSlot() ?? cfg("slots")[0]?.tgHour ?? 20;
|
slot = detectSlot() ?? Config.get({ section: "poll", key: "slots" })[0]?.tgHour ?? 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = loadResult(todayString(), slot);
|
const result = loadResult(todayString(), slot);
|
||||||
|
|
@ -40,7 +40,7 @@ export async function handleResultPost(interaction: ChatInputCommandInteraction)
|
||||||
.setFooter({ text: `Source of truth: ${kd.source}` })
|
.setFooter({ text: `Source of truth: ${kd.source}` })
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
const channelId = cfg("resultsChannelId") || cfg("pollChannelId");
|
const channelId = Config.get({ section: "channels", key: "results" }) || Config.get({ section: "channels", key: "poll" });
|
||||||
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
const channel = await interaction.client.channels.fetch(channelId) as TextChannel;
|
||||||
await channel.send({ embeds: [embed] });
|
await channel.send({ embeds: [embed] });
|
||||||
return void replyAndDelete(interaction, "✅ Results posted.");
|
return void replyAndDelete(interaction, "✅ Results posted.");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { setNationKD } from "../../systems/history";
|
import { setNationKD } from "../../systems/history";
|
||||||
import { normalizeSlot, detectSlot } from "../../systems/scores";
|
import { normalizeSlot, detectSlot } from "../../systems/scores";
|
||||||
import { todayString } from "../../systems/history";
|
import { todayString } from "../../systems/history";
|
||||||
|
|
@ -17,7 +17,7 @@ export async function handleResultSet(interaction: ChatInputCommandInteraction):
|
||||||
slot = normalizeSlot(slotArg);
|
slot = normalizeSlot(slotArg);
|
||||||
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`);
|
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`);
|
||||||
} else {
|
} else {
|
||||||
slot = detectSlot() ?? cfg("slots")[0]?.tgHour ?? 20;
|
slot = detectSlot() ?? Config.get({ section: "poll", key: "slots" })[0]?.tgHour ?? 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = setNationKD(todayString(), slot, nation, kills, deaths);
|
const result = setNationKD(todayString(), slot, nation, kills, deaths);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { loadResult, todayString } from "../../systems/history";
|
import { loadResult, todayString } from "../../systems/history";
|
||||||
import { normalizeSlot, detectSlot } from "../../systems/scores";
|
import { normalizeSlot, detectSlot } from "../../systems/scores";
|
||||||
import { replyAndDelete } from "../../utils";
|
import { replyAndDelete } from "../../utils";
|
||||||
|
|
@ -12,7 +12,7 @@ export async function handleResultView(interaction: ChatInputCommandInteraction)
|
||||||
slot = normalizeSlot(slotArg);
|
slot = normalizeSlot(slotArg);
|
||||||
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`);
|
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`);
|
||||||
} else {
|
} else {
|
||||||
slot = detectSlot() ?? cfg("slots")[0]?.tgHour ?? 20;
|
slot = detectSlot() ?? Config.get({ section: "poll", key: "slots" })[0]?.tgHour ?? 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = loadResult(todayString(), slot);
|
const result = loadResult(todayString(), slot);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "../../systems/config";
|
import { Config } from "../../systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
||||||
import { normalizeSlot, detectSlot } from "../../systems/scores";
|
import { normalizeSlot, detectSlot } from "../../systems/scores";
|
||||||
import { loadResult, todayString } from "../../systems/history";
|
import { loadResult, todayString } from "../../systems/history";
|
||||||
|
|
@ -8,7 +8,7 @@ import { replyAndDelete } from "../../utils";
|
||||||
|
|
||||||
export async function handleScoreGet(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleScoreGet(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const nameArg = interaction.options.getString("name");
|
const nameArg = interaction.options.getString("name");
|
||||||
const slotArg = interaction.options.getString("slot");
|
const slotArg = interaction.options.getString("slot");
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ export async function handleScoreGet(interaction: ChatInputCommandInteraction):
|
||||||
slot = normalizeSlot(slotArg);
|
slot = normalizeSlot(slotArg);
|
||||||
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`, true);
|
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`, true);
|
||||||
} else {
|
} else {
|
||||||
slot = detectSlot() ?? cfg("slots").find((s) => s.active)?.tgHour ?? 20;
|
slot = detectSlot() ?? Config.get({ section: "poll", key: "slots" }).find((s) => s.active)?.tgHour ?? 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = loadResult(todayString(), slot);
|
const result = loadResult(todayString(), slot);
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,29 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "@systems/users";
|
import { resolveUser, hasOfficerRole } from "@systems/users";
|
||||||
import { submitScore, detectSlot, normalizeSlot } from "@systems/scores";
|
import { submitScore, detectSlot, normalizeSlot } from "@systems/scores";
|
||||||
import { getEffectiveCharacter } from "@systems/borrow";
|
import { getEffectiveCharacter } from "@systems/borrow";
|
||||||
import { replyAndDelete } from "@utils";
|
import { replyAndDelete } from "@utils";
|
||||||
import { getEmoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
|
import { Discord } from "@discord";
|
||||||
|
import { User } from "@systems/users";
|
||||||
|
|
||||||
export async function handleScoreSet(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleScoreSet(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
|
const options = Discord.Interaction.options<ChatInputCommandInteraction>(interaction);
|
||||||
|
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = User.hasOfficerRole({
|
||||||
const nameArg = interaction.options.getString("name");
|
member: member,
|
||||||
const ptsArg = interaction.options.getInteger("pts", true);
|
officerRoles: Config.get({ section: "roles", key: "officer"
|
||||||
const slotArg = interaction.options.getString("slot");
|
})});
|
||||||
const kills = interaction.options.getInteger("k") ?? undefined;
|
const nameArg = options.string({ key: "name" });
|
||||||
const deaths = interaction.options.getInteger("d") ?? undefined;
|
const ptsArg = options.integer({ key: "pts", required: true });
|
||||||
const k = interaction.options.getInteger("k") ?? undefined;
|
const slotArg = options.string({ key: "slot" });
|
||||||
const d = interaction.options.getInteger("d") ?? undefined;
|
const k = options.integer({ key: "k" }) ?? undefined;
|
||||||
const atk = interaction.options.getInteger("atk") ?? undefined;
|
const d = options.integer({ key: "d" }) ?? undefined;
|
||||||
const def = interaction.options.getInteger("def") ?? undefined;
|
const atk = options.integer({ key: "atk" }) ?? undefined;
|
||||||
const heal = interaction.options.getInteger("heal") ?? undefined;
|
const def = options.integer({ key: "def" }) ?? undefined;
|
||||||
|
const heal = options.integer({ key: "heal" }) ?? undefined;
|
||||||
|
|
||||||
let userKey: string | null;
|
let userKey: string | null;
|
||||||
if (nameArg) {
|
if (nameArg) {
|
||||||
|
|
@ -39,14 +44,14 @@ export async function handleScoreSet(interaction: ChatInputCommandInteraction):
|
||||||
slot = normalizeSlot(slotArg);
|
slot = normalizeSlot(slotArg);
|
||||||
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`);
|
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`);
|
||||||
} else {
|
} else {
|
||||||
slot = detectSlot() ?? cfg("slots").find((s) => s.active)?.tgHour ?? 20;
|
slot = detectSlot() ?? Config.get({ section: "poll", key: "slots" }).find((s) => s.active)?.tgHour ?? 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
await submitScore({
|
await submitScore({
|
||||||
userKey: borrowedFrom ?? userKey,
|
userKey: borrowedFrom ?? userKey,
|
||||||
playedBy: borrowedFrom ? userKey : undefined,
|
playedBy: borrowedFrom ? userKey : undefined,
|
||||||
characterName: char.name,
|
characterName: char.name,
|
||||||
cls: char.class,
|
cls: char.class.key,
|
||||||
nation: char.nation,
|
nation: char.nation,
|
||||||
pts: ptsArg,
|
pts: ptsArg,
|
||||||
k,
|
k,
|
||||||
|
|
@ -58,10 +63,10 @@ export async function handleScoreSet(interaction: ChatInputCommandInteraction):
|
||||||
submittedByOfficer: isOfficer && !!nameArg,
|
submittedByOfficer: isOfficer && !!nameArg,
|
||||||
});
|
});
|
||||||
|
|
||||||
const scoreEmoji = getEmoji("score") || "📊";
|
const scoreEmoji = Emoji.get("score") || "📊";
|
||||||
const kdEmoji = getEmoji("kd") || "⚔️";
|
const kdEmoji = Emoji.get("kd") || "⚔️";
|
||||||
const borrowNote = borrowedFrom ? ` *(borrowed from ${borrowedFrom})*` : "";
|
const borrowNote = borrowedFrom ? ` *(borrowed from ${borrowedFrom})*` : "";
|
||||||
const kdNote = kills !== undefined && deaths !== undefined ? `\n${kdEmoji} ${kills}/${deaths}` : "";
|
const kdNote = k !== undefined && d !== undefined ? `\n${kdEmoji} ${k}/${d}` : "";
|
||||||
const statsNote = [
|
const statsNote = [
|
||||||
atk !== undefined ? `ATK: ${atk}` : null,
|
atk !== undefined ? `ATK: ${atk}` : null,
|
||||||
def !== undefined ? `DEF: ${def}` : null,
|
def !== undefined ? `DEF: ${def}` : null,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { submitScore, detectSlot, normalizeSlot } from "@systems/scores";
|
import { submitScore, detectSlot, normalizeSlot } from "@systems/scores";
|
||||||
import { getEffectiveCharacter } from "@systems/borrow";
|
import { getEffectiveCharacter } from "@systems/borrow";
|
||||||
import { format } from "@format";
|
import { format } from "@format";
|
||||||
|
|
@ -47,7 +47,7 @@ export namespace score {
|
||||||
return { ok: false, message: `❌ Could not parse slot "${input.slot}".` };
|
return { ok: false, message: `❌ Could not parse slot "${input.slot}".` };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
slot = detectSlot() ?? cfg("slots").find((s) => s.active)?.tgHour ?? 20;
|
slot = detectSlot() ?? Config.get({ section: "poll", key: "slots" }).find((s) => s.active)?.tgHour ?? 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
await submitScore({
|
await submitScore({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatInputCommandInteraction } from "discord.js";
|
import { ChatInputCommandInteraction } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { resolveUser, hasOfficerRole } from "@systems/users";
|
import { resolveUser, hasOfficerRole } from "@systems/users";
|
||||||
import { getCharacterByName } from "@systems/characters";
|
import { getCharacterByName } from "@systems/characters";
|
||||||
import { Character } from "@systems/character";
|
import { Character } from "@systems/character";
|
||||||
|
|
@ -27,7 +27,7 @@ function findSharedChar(userKey: string, charName: string): { ownerKey: string;
|
||||||
|
|
||||||
export async function handleSwitch(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleSwitch(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
||||||
const nameArg = interaction.options.getString("name");
|
const nameArg = interaction.options.getString("name");
|
||||||
const charName = interaction.options.getString("char_name", true);
|
const charName = interaction.options.getString("char_name", true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,17 @@ import { replyAndDelete } from "@utils";
|
||||||
import { hasOfficerRole } from "@systems/users";
|
import { hasOfficerRole } from "@systems/users";
|
||||||
|
|
||||||
export async function handleSetLayout(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleSetLayout(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
|
// discord.js types CommandInteractionOptionResolver with Omit<> which hides
|
||||||
|
// methods like getString/getInteger at the type level despite them existing at runtime
|
||||||
|
// Needs to be cast as any, since Discord.js has issues with the type
|
||||||
|
const options = interaction.options as any;
|
||||||
|
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
if (!hasOfficerRole(member, Config.get("officerRoles"))) {
|
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||||||
return void replyAndDelete(interaction, "❌ Only officers can change the poll layout.", true);
|
return void replyAndDelete(interaction, "❌ Only officers can change the poll layout.", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = interaction.options.getString("layout", true);
|
const name = options.getString("layout", true);
|
||||||
|
|
||||||
if (!PollUI.setLayout(name)) {
|
if (!PollUI.setLayout(name)) {
|
||||||
const available = PollUI.layouts()
|
const available = PollUI.layouts()
|
||||||
|
|
@ -21,7 +26,7 @@ export async function handleSetLayout(interaction: ChatInputCommandInteraction):
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.set("pollLayout", name);
|
Config.set({ section: "poll", key: "layout", value: name });
|
||||||
|
|
||||||
return void replyAndDelete(interaction,
|
return void replyAndDelete(interaction,
|
||||||
`✅ Poll layout set to \`${name}\`. Use \`/tg poll reload\` to apply.`, true
|
`✅ Poll layout set to \`${name}\`. Use \`/tg poll reload\` to apply.`, true
|
||||||
|
|
@ -29,7 +34,12 @@ export async function handleSetLayout(interaction: ChatInputCommandInteraction):
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function autocompleteLayout(interaction: any): Promise<void> {
|
export async function autocompleteLayout(interaction: any): Promise<void> {
|
||||||
const focused = interaction.options.getFocused().toLowerCase();
|
// discord.js types CommandInteractionOptionResolver with Omit<> which hides
|
||||||
|
// methods like getString/getInteger at the type level despite them existing at runtime
|
||||||
|
// Needs to be cast as any, since Discord.js has issues with the type
|
||||||
|
const options = interaction.options as any;
|
||||||
|
|
||||||
|
const focused = options.getFocused().toLowerCase();
|
||||||
const choices = PollUI.layouts()
|
const choices = PollUI.layouts()
|
||||||
.filter((l) => l.name.includes(focused))
|
.filter((l) => l.name.includes(focused))
|
||||||
.map((l) => ({ name: `${l.name} — ${l.description}`, value: l.name }));
|
.map((l) => ({ name: `${l.name} — ${l.description}`, value: l.name }));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Client, TextChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
import { Client, TextChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { BorrowRequest } from "@src/types";
|
import { BorrowRequest } from "@src/types";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { Char } from "@systems/characters";
|
import { Char } from "@systems/characters";
|
||||||
import { Store } from "@systems/store";
|
import { Store } from "@systems/store";
|
||||||
import { Paths } from "@helpers/paths";
|
import { Paths } from "@helpers/paths";
|
||||||
|
|
@ -56,7 +56,8 @@ export function getAllPendingForOwner(ownerKey: string): BorrowRequest[] {
|
||||||
|
|
||||||
export function addPendingRequest(request: BorrowRequest): void {
|
export function addPendingRequest(request: BorrowRequest): void {
|
||||||
const key = requestKey(request.ownerKey, request.requesterKey);
|
const key = requestKey(request.ownerKey, request.requesterKey);
|
||||||
const expiry = cfg("borrowRequestExpiryMs" as any) ?? 0;
|
const expiry = Config.get({ section: "borrow", key: "requestExpiryMs" });
|
||||||
|
|
||||||
pendingRequests.set(key, request);
|
pendingRequests.set(key, request);
|
||||||
if (expiry > 0) {
|
if (expiry > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Nation, Character } from "@types";
|
import { Nation, Character } from "@types";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { WRank, WRankWeek } from "@systems/wrank";
|
import { WRank, WRankWeek } from "@systems/wrank";
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
* Called after weekly reset — the top ranked player with >= goal TGs becomes Bringer.
|
* Called after weekly reset — the top ranked player with >= goal TGs becomes Bringer.
|
||||||
*/
|
*/
|
||||||
update({ week }: { week?: WRankWeek }): void {
|
update({ week }: { week?: WRankWeek }): void {
|
||||||
const goal = cfg("wRankGoal");
|
const goal = Config.get({ section: "wrank", key: "goal" });
|
||||||
for (const nation of [Nation.Capella, Nation.Procyon]) {
|
for (const nation of [Nation.Capella, Nation.Procyon]) {
|
||||||
const key = NATION_BRINGER_KEY[nation];
|
const key = NATION_BRINGER_KEY[nation];
|
||||||
const _week = week ?? WRank.currentWeek();
|
const _week = week ?? WRank.currentWeek();
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
ActionRowBuilder
|
ActionRowBuilder
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
|
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { setActiveCharacter, getCharacters } from "@systems/characters";
|
import { setActiveCharacter, getCharacters } from "@systems/characters";
|
||||||
import {
|
import {
|
||||||
getEffectiveCharacter,
|
getEffectiveCharacter,
|
||||||
|
|
@ -158,7 +158,7 @@
|
||||||
else state.no.set(voteId, voteEntry);
|
else state.no.set(voteId, voteEntry);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const channel = await (interaction as any).client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await (interaction as any).client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot!);
|
await updatePollMessage(channel, slot!);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,171 +3,172 @@ import { Store } from "@systems/store";
|
||||||
import { Paths } from "@paths";
|
import { Paths } from "@paths";
|
||||||
import { Runtime } from "@systems/runtime";
|
import { Runtime } from "@systems/runtime";
|
||||||
|
|
||||||
// ─── Runtime ──────────────────────────────────────────────────────────────────
|
// ─── Runtime ─────────────────────────────────────────────────────────────────
|
||||||
Runtime.phase("load", () => Config.load(), { name: "Config.load", priority: -1 });
|
Runtime.phase("load", () => Config.load(), { name: "Config.load", priority: -1 });
|
||||||
|
|
||||||
// ─── Sub-interfaces ───────────────────────────────────────────────────────────
|
// ─── Section interfaces (internal) ───────────────────────────────────────────
|
||||||
|
|
||||||
interface ChannelConfig {
|
interface ChannelConfig {
|
||||||
pollChannelId: string;
|
poll: string;
|
||||||
resultsChannelId: string;
|
results: string;
|
||||||
scoreChannelId: string;
|
score: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoleConfig {
|
interface RoleConfig {
|
||||||
officerRoles: string[];
|
officer: string[];
|
||||||
configRoles: string[];
|
config: string[];
|
||||||
tagRoles: string[];
|
tag: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PollConfig {
|
interface PollConfig {
|
||||||
pollLayout: string;
|
layout: string;
|
||||||
pollEphemeralEnabled: boolean;
|
ephemeralEnabled: boolean;
|
||||||
commandEphemeralEnabled: boolean;
|
commandEphemeralEnabled: boolean;
|
||||||
ephemeralDeleteMs: number;
|
ephemeralDeleteMs: number;
|
||||||
lockAt: number;
|
lockAt: number;
|
||||||
lockMessage: string;
|
lockMessage: string;
|
||||||
confirmYesMessage: string;
|
confirmYes: string;
|
||||||
confirmNoMessage: string;
|
confirmNo: string;
|
||||||
charDisplayFormat: string;
|
charDisplayFormat: string;
|
||||||
showClassInMessages: boolean;
|
showClassInMessages: boolean;
|
||||||
showLevelInMessages: boolean;
|
showLevelInMessages: boolean;
|
||||||
showNoInNationField: boolean;
|
showNoInNationField: boolean;
|
||||||
showNationTotalsInHeader:boolean;
|
showNationTotalsInHeader:boolean;
|
||||||
autoVoteOnConflictSwitch: boolean;
|
autoVoteOnConflict: boolean;
|
||||||
reclaimNotifyBorrower: boolean;
|
reclaimNotifyBorrower: boolean;
|
||||||
conflictReclaimBehavior: string;
|
conflictReclaimBehavior: string;
|
||||||
slots: TGSlot[];
|
slots: TGSlot[];
|
||||||
scoreWindowHours: number;
|
|
||||||
tgDurationMinutes: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WRankConfig {
|
interface WRankConfig {
|
||||||
wRankGoal: number;
|
goal: number;
|
||||||
wRankPostOnReset: boolean;
|
postOnReset: boolean;
|
||||||
wRankYellowColor: string;
|
yellowColor: string;
|
||||||
wRankGrayColor: string;
|
grayColor: string;
|
||||||
deltaUpColor: string;
|
deltaUpColor: string;
|
||||||
deltaDownColor:string;
|
deltaDownColor:string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BringerConfig {
|
interface BringerConfig {
|
||||||
stormBringerColor: string;
|
stormColor: string;
|
||||||
luminousBringerColor: string;
|
luminousColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImpersonateConfig {
|
interface ImpersonateConfig {
|
||||||
impersonateResetOnPoll: boolean;
|
resetOnPoll: boolean;
|
||||||
impersonateIndicator: boolean;
|
indicator: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EmojiConfig {
|
interface EmojiConfig {
|
||||||
activeCharEmoji: string;
|
activeChar: string;
|
||||||
emojiDonorGuilds: string[];
|
donorGuilds: string[];
|
||||||
}
|
|
||||||
|
|
||||||
interface LoggingConfig {
|
|
||||||
logLevel: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NationConfig {
|
interface NationConfig {
|
||||||
nationSource: Nation;
|
source: Nation;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BorrowConfig {
|
interface BorrowConfig {
|
||||||
borrowRequestExpiryMs: number;
|
requestExpiryMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── BotConfig — merged, all fields optional ──────────────────────────────────
|
interface TGConfig {
|
||||||
|
scoreWindowHours: number;
|
||||||
|
durationMinutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type BotConfig = Partial<
|
// ─── Section map ──────────────────────────────────────────────────────────────
|
||||||
ChannelConfig &
|
|
||||||
RoleConfig &
|
export interface SectionMap {
|
||||||
PollConfig &
|
channels: ChannelConfig;
|
||||||
WRankConfig &
|
roles: RoleConfig;
|
||||||
BringerConfig &
|
poll: PollConfig;
|
||||||
ImpersonateConfig &
|
wrank: WRankConfig;
|
||||||
EmojiConfig &
|
bringer: BringerConfig;
|
||||||
LoggingConfig &
|
impersonate:ImpersonateConfig;
|
||||||
NationConfig &
|
emoji: EmojiConfig;
|
||||||
BorrowConfig
|
nation: NationConfig;
|
||||||
>;
|
borrow: BorrowConfig;
|
||||||
|
tg: TGConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfigSection = keyof SectionMap;
|
||||||
|
|
||||||
// ─── Defaults ─────────────────────────────────────────────────────────────────
|
// ─── Defaults ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function getDefaults(): Required<BotConfig> {
|
function getDefaults(): SectionMap {
|
||||||
return {
|
return {
|
||||||
// Channels
|
channels: {
|
||||||
pollChannelId: "",
|
poll: "",
|
||||||
resultsChannelId: "",
|
results: "",
|
||||||
scoreChannelId: "",
|
score: "",
|
||||||
|
},
|
||||||
// Roles
|
roles: {
|
||||||
officerRoles: ["Ice King"],
|
officer: ["Ice King"],
|
||||||
configRoles: ["Ice King"],
|
config: ["Ice King"],
|
||||||
tagRoles: ["Ice King", "Ice", "Rebellion"],
|
tag: ["Ice King", "Ice", "Rebellion"],
|
||||||
|
},
|
||||||
// Poll
|
poll: {
|
||||||
pollLayout: "default",
|
layout: "default",
|
||||||
pollEphemeralEnabled: false,
|
ephemeralEnabled: false,
|
||||||
commandEphemeralEnabled: true,
|
commandEphemeralEnabled: true,
|
||||||
ephemeralDeleteMs: 0,
|
ephemeralDeleteMs: 0,
|
||||||
lockAt: 10,
|
lockAt: 10,
|
||||||
lockMessage: "🔒 This poll has been locked.",
|
lockMessage: "🔒 This poll has been locked.",
|
||||||
confirmYesMessage: "⚔️ TG is confirmed for tonight!",
|
confirmYes: "⚔️ TG is confirmed for tonight!",
|
||||||
confirmNoMessage: "❌ TG is cancelled for tonight.",
|
confirmNo: "❌ TG is cancelled for tonight.",
|
||||||
charDisplayFormat: "{wrank} {class} {level} {name}",
|
charDisplayFormat: "{wrank} {class} {level} {name}",
|
||||||
showClassInMessages: false,
|
showClassInMessages: false,
|
||||||
showLevelInMessages: false,
|
showLevelInMessages: false,
|
||||||
showNoInNationField: false,
|
showNoInNationField: false,
|
||||||
showNationTotalsInHeader:false,
|
showNationTotalsInHeader:false,
|
||||||
autoVoteOnConflictSwitch: true,
|
autoVoteOnConflict: true,
|
||||||
reclaimNotifyBorrower: true,
|
reclaimNotifyBorrower: true,
|
||||||
conflictReclaimBehavior: "revert",
|
conflictReclaimBehavior: "revert",
|
||||||
slots: [{ tgHour: 20, pollOpens: "10:00", closesAfter: 35, active: true }],
|
slots: [{ tgHour: 20, pollOpens: "10:00", closesAfter: 35, active: true }],
|
||||||
scoreWindowHours: 2,
|
},
|
||||||
tgDurationMinutes: 35,
|
wrank: {
|
||||||
|
goal: 7,
|
||||||
// W.Rank
|
postOnReset: false,
|
||||||
wRankGoal: 7,
|
yellowColor: "#BA7517",
|
||||||
wRankPostOnReset: false,
|
grayColor: "#888888",
|
||||||
wRankYellowColor: "#BA7517",
|
|
||||||
wRankGrayColor: "#888888",
|
|
||||||
deltaUpColor: "#A32D2D",
|
deltaUpColor: "#A32D2D",
|
||||||
deltaDownColor:"#185FA5",
|
deltaDownColor:"#185FA5",
|
||||||
|
},
|
||||||
// Bringer
|
bringer: {
|
||||||
stormBringerColor: "#185FA5",
|
stormColor: "#185FA5",
|
||||||
luminousBringerColor: "#8B4CB8",
|
luminousColor: "#8B4CB8",
|
||||||
|
},
|
||||||
// Impersonate
|
impersonate: {
|
||||||
impersonateResetOnPoll: false,
|
resetOnPoll: false,
|
||||||
impersonateIndicator: true,
|
indicator: true,
|
||||||
|
},
|
||||||
// Emoji
|
emoji: {
|
||||||
activeCharEmoji: "⭐",
|
activeChar: "⭐",
|
||||||
emojiDonorGuilds: [],
|
donorGuilds: [],
|
||||||
|
},
|
||||||
// Logging
|
nation: {
|
||||||
logLevel: "info",
|
source: Nation.Procyon,
|
||||||
|
},
|
||||||
// Nation
|
borrow: {
|
||||||
nationSource: Nation.Procyon,
|
requestExpiryMs: 0,
|
||||||
|
},
|
||||||
// Borrow
|
tg: {
|
||||||
borrowRequestExpiryMs: 0,
|
scoreWindowHours: 2,
|
||||||
|
durationMinutes: 35,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── State ────────────────────────────────────────────────────────────────────
|
// ─── State ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
let _cfg: BotConfig = {};
|
let _cfg: Partial<SectionMap> = {};
|
||||||
|
|
||||||
// ─── Config namespace ─────────────────────────────────────────────────────────
|
// ─── Config namespace ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const Config = {
|
export const Config = {
|
||||||
load(): void {
|
load(): void {
|
||||||
_cfg = Store.readOrDefault(Paths.data("config.json"), {});
|
_cfg = Store.readOrDefault<Partial<SectionMap>>(Paths.data("config.json"), {});
|
||||||
},
|
},
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
|
|
@ -178,28 +179,41 @@ export const Config = {
|
||||||
Config.load();
|
Config.load();
|
||||||
},
|
},
|
||||||
|
|
||||||
get<K extends keyof BotConfig>(key: K): Required<BotConfig>[K] {
|
get<S extends ConfigSection, K extends keyof SectionMap[S]>(
|
||||||
return (_cfg[key] !== undefined ? _cfg[key] : getDefaults()[key]) as Required<BotConfig>[K];
|
{ section, key }: { section: S; key: K }
|
||||||
|
): SectionMap[S][K] {
|
||||||
|
const sectionData = _cfg[section] as Partial<SectionMap[S]> | undefined;
|
||||||
|
const value = sectionData?.[key];
|
||||||
|
const def = getDefaults()[section][key];
|
||||||
|
return (value !== undefined ? value : def) as SectionMap[S][K];
|
||||||
},
|
},
|
||||||
|
|
||||||
set<K extends keyof BotConfig>(key: K, value: BotConfig[K]): void {
|
set<S extends ConfigSection, K extends keyof SectionMap[S]>(
|
||||||
_cfg[key] = value;
|
{ section, key, value }: { section: S; key: K; value: SectionMap[S][K] }
|
||||||
|
): void {
|
||||||
|
if (!_cfg[section]) (_cfg[section] as any) = {};
|
||||||
|
(_cfg[section] as any)[key] = value;
|
||||||
Config.save();
|
Config.save();
|
||||||
},
|
},
|
||||||
|
|
||||||
reset<K extends keyof BotConfig>(key: K): void {
|
reset<S extends ConfigSection, K extends keyof SectionMap[S]>(
|
||||||
delete _cfg[key];
|
{ section, key }: { section: S; key: K }
|
||||||
|
): void {
|
||||||
|
if (_cfg[section]) delete (_cfg[section] as any)[key];
|
||||||
Config.save();
|
Config.save();
|
||||||
},
|
},
|
||||||
|
|
||||||
all(): Required<BotConfig> {
|
/** Get an entire section (merged with defaults) */
|
||||||
return { ...getDefaults(), ..._cfg };
|
section<S extends ConfigSection>(section: S): SectionMap[S] {
|
||||||
|
return { ...getDefaults()[section], ...(_cfg[section] ?? {}) } as SectionMap[S];
|
||||||
|
},
|
||||||
|
|
||||||
|
all(): SectionMap {
|
||||||
|
const defaults = getDefaults();
|
||||||
|
const result = { ...defaults } as any;
|
||||||
|
for (const section of Object.keys(defaults) as ConfigSection[]) {
|
||||||
|
result[section] = { ...defaults[section], ...(_cfg[section] ?? {}) };
|
||||||
|
}
|
||||||
|
return result as SectionMap;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Legacy aliases — remove after full migration ────────────────────────────
|
|
||||||
export const cfg = <K extends keyof BotConfig>(key: K) => Config.get(key);
|
|
||||||
export const setCfg = <K extends keyof BotConfig>(key: K, value: BotConfig[K]) => Config.set(key, value);
|
|
||||||
export const resetCfg = <K extends keyof BotConfig>(key: K) => Config.reset(key);
|
|
||||||
export const loadConfig = () => Config.load();
|
|
||||||
export const saveConfig = () => Config.save();
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
ButtonInteraction,
|
ButtonInteraction,
|
||||||
TextChannel,
|
TextChannel,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
|
||||||
import { getCharacters, setActiveCharacter } from "@systems/characters";
|
import { getCharacters, setActiveCharacter } from "@systems/characters";
|
||||||
import { clearSessionBorrowForUser, clearPersistentPreference, getEffectiveCharacter } from "@systems/borrow";
|
import { clearSessionBorrowForUser, clearPersistentPreference, getEffectiveCharacter } from "@systems/borrow";
|
||||||
import { getImpersonation } from "@systems/impersonate";
|
import { getImpersonation } from "@systems/impersonate";
|
||||||
|
|
@ -22,8 +21,8 @@ import { Config } from "@systems/config";
|
||||||
// ─── Config ───────────────────────────────────────────────────────────────────
|
// ─── Config ───────────────────────────────────────────────────────────────────
|
||||||
const RECLAIM_STYLE = ButtonStyle.Secondary;
|
const RECLAIM_STYLE = ButtonStyle.Secondary;
|
||||||
const SWITCH_STYLE = ButtonStyle.Secondary;
|
const SWITCH_STYLE = ButtonStyle.Secondary;
|
||||||
const AUTO_VOTE_ON_SWITCH = Config.get("autoVoteOnConflictSwitch");
|
const AUTO_VOTE_ON_SWITCH = Config.get({ section: "poll", key: "autoVoteOnConflict" });
|
||||||
const RECLAIM_NOTIFY_BORROWER = Config.get("reclaimNotifyBorrower");
|
const RECLAIM_NOTIFY_BORROWER = Config.get({ section: "poll", key: "reclaimNotifyBorrower" });
|
||||||
|
|
||||||
// ─── State ────────────────────────────────────────────────────────────────────
|
// ─── State ────────────────────────────────────────────────────────────────────
|
||||||
const pendingConflicts = new Map<string, {
|
const pendingConflicts = new Map<string, {
|
||||||
|
|
@ -181,7 +180,7 @@ export async function handleConflictButton(interaction: ButtonInteraction): Prom
|
||||||
publicMessage: publicMsg ?? undefined,
|
publicMessage: publicMsg ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot!);
|
await updatePollMessage(channel, slot!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,7 +206,7 @@ export async function handleConflictButton(interaction: ButtonInteraction): Prom
|
||||||
const charName = parts[3];
|
const charName = parts[3];
|
||||||
const ownerId = parts[4];
|
const ownerId = parts[4];
|
||||||
|
|
||||||
const reclaimBehavior = (cfg as any)("conflictReclaimBehavior") ?? "revert";
|
const reclaimBehavior = Config.get({ section: "poll", key: "conflictReclaimBehavior" });
|
||||||
const slot = [...polls.keys()][0];
|
const slot = [...polls.keys()][0];
|
||||||
const state = slot !== undefined ? polls.get(slot) : null;
|
const state = slot !== undefined ? polls.get(slot) : null;
|
||||||
|
|
||||||
|
|
@ -268,7 +267,7 @@ export async function handleConflictButton(interaction: ButtonInteraction): Prom
|
||||||
publicMessage: publicMsg ?? undefined,
|
publicMessage: publicMsg ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot!);
|
await updatePollMessage(channel, slot!);
|
||||||
|
|
||||||
console.log("[reclaim notify] borrowerDiscordId:", borrowerDiscordId, "notify:", RECLAIM_NOTIFY_BORROWER);
|
console.log("[reclaim notify] borrowerDiscordId:", borrowerDiscordId, "notify:", RECLAIM_NOTIFY_BORROWER);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { UserRegistry } from "@registry/user-registry";
|
||||||
import { UsermapEntry } from "@types";
|
import { UsermapEntry } from "@types";
|
||||||
import { Config } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
|
|
||||||
const IMPERSONATE_RESET_ON_POLL = Config.get("impersonateResetOnPoll");
|
const IMPERSONATE_RESET_ON_POLL = Config.get({ section: "impersonate", key: "resetOnPoll" });
|
||||||
const IMPERSONATE_INDICATOR = Config.get("impersonateIndicator");
|
const IMPERSONATE_INDICATOR = Config.get({ section: "impersonate", key: "indicator" });
|
||||||
|
|
||||||
// realDiscordId → userKey being impersonated
|
// realDiscordId → userKey being impersonated
|
||||||
const impersonations = new Map<string, string>();
|
const impersonations = new Map<string, string>();
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,12 @@ import {
|
||||||
GuildMember,
|
GuildMember,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { PollState, VoteEntry, Nation, TGSlot } from "@src/types";
|
import { PollState, VoteEntry, Nation, TGSlot } from "@src/types";
|
||||||
import { cfg } from "@systems/config";
|
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
import { Nations } from "@systems/nations";
|
import { Nations } from "@systems/nations";
|
||||||
import { WRank } from "@systems/wrank";
|
|
||||||
import { format } from "@format";
|
import { format } from "@format";
|
||||||
import { persist } from "@systems/pollPersistence"
|
import { persist } from "@systems/pollPersistence"
|
||||||
import { clearSessionBorrows, getEffectiveCharacter } from "@systems/borrow";
|
import { clearSessionBorrows, getEffectiveCharacter } from "@systems/borrow";
|
||||||
import { clearAllImpersonations } from "@systems/impersonate";
|
import { clearAllImpersonations } from "@systems/impersonate";
|
||||||
import { Bringer } from "@systems/bringer";
|
|
||||||
import { Attendance } from "@systems/attendance";
|
import { Attendance } from "@systems/attendance";
|
||||||
import { PollUI } from "@ui/poll";
|
import { PollUI } from "@ui/poll";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
import cron from "node-cron";
|
import cron from "node-cron";
|
||||||
import { Client, TextChannel } from "discord.js";
|
import { Client, TextChannel } from "discord.js";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { TGSlot } from "@types";
|
import { TGSlot } from "@types";
|
||||||
import { polls, updatePollMessage } from "@systems/poll";
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
import { WRank } from "@systems/wrank";
|
import { WRank } from "@systems/wrank";
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
stopAll();
|
stopAll();
|
||||||
|
|
||||||
const tz = process.env.TZ ?? "Etc/GMT-2";
|
const tz = process.env.TZ ?? "Etc/GMT-2";
|
||||||
const slots = cfg("slots").filter((s) => s.active);
|
const slots = Config.get({ section: "poll", key: "slots" }).filter((s) => s.active);
|
||||||
|
|
||||||
for (const slot of slots) {
|
for (const slot of slots) {
|
||||||
// Poll open
|
// Poll open
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
const state = polls.get(slot.tgHour);
|
const state = polls.get(slot.tgHour);
|
||||||
if (!state?.locked) return;
|
if (!state?.locked) return;
|
||||||
try {
|
try {
|
||||||
const channel = await client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot.tgHour, undefined, false);
|
await updatePollMessage(channel, slot.tgHour, undefined, false);
|
||||||
console.log(`[Scheduler] Submit Score button removed for ${slot.tgHour}:00`);
|
console.log(`[Scheduler] Submit Score button removed for ${slot.tgHour}:00`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
import cron from "node-cron";
|
import cron from "node-cron";
|
||||||
import { Client, TextChannel } from "discord.js";
|
import { Client, TextChannel } from "discord.js";
|
||||||
import { ScheduledJob } from "./types";
|
import { ScheduledJob } from "./types";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { TGSlot } from "@types";
|
import { TGSlot } from "@types";
|
||||||
|
|
||||||
// Import all jobs
|
// Import all jobs
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
stopAll();
|
stopAll();
|
||||||
|
|
||||||
const tz = process.env.TZ ?? "Etc/GMT-2";
|
const tz = process.env.TZ ?? "Etc/GMT-2";
|
||||||
const slots = cfg("slots").filter((s) => s.active);
|
const slots = Config.get({ section: "poll", key: "slots" }).filter((s) => s.active);
|
||||||
|
|
||||||
console.log(`[Scheduler] Weekly reset scheduled: "0 0 * * 1" in ${tz}`);
|
console.log(`[Scheduler] Weekly reset scheduled: "0 0 * * 1" in ${tz}`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Client, TextChannel } from "discord.js";
|
import { Client, TextChannel } from "discord.js";
|
||||||
import { ScheduledJob } from "@scheduler/types";
|
import { ScheduledJob } from "@scheduler/types";
|
||||||
import { polls, updatePollMessage } from "@systems/poll";
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
|
|
||||||
export const job: ScheduledJob = {
|
export const job: ScheduledJob = {
|
||||||
name: "midnight-cleanup",
|
name: "midnight-cleanup",
|
||||||
|
|
@ -10,7 +10,7 @@ export const job: ScheduledJob = {
|
||||||
for (const [slot, state] of polls.entries()) {
|
for (const [slot, state] of polls.entries()) {
|
||||||
if (!state?.locked) continue;
|
if (!state?.locked) continue;
|
||||||
try {
|
try {
|
||||||
const channel = await client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
const channel = await client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as TextChannel;
|
||||||
await updatePollMessage(channel, slot, undefined, false);
|
await updatePollMessage(channel, slot, undefined, false);
|
||||||
console.log(`[Scheduler] Submit Score button removed for ${slot}:00`);
|
console.log(`[Scheduler] Submit Score button removed for ${slot}:00`);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { TGScore, Nation, ClassKey } from "../types";
|
import { TGScore, Nation, ClassKey } from "../types";
|
||||||
import { cfg } from "./config";
|
import { Config } from "./config";
|
||||||
import { upsertScore, todayString } from "./history";
|
import { upsertScore, todayString } from "./history";
|
||||||
import { recordScore } from "./wrank";
|
import { recordScore } from "./wrank";
|
||||||
|
|
||||||
|
|
@ -33,9 +33,9 @@ export function normalizeSlot(input: string): number | null {
|
||||||
|
|
||||||
// Detect which slot a submission belongs to based on current time
|
// Detect which slot a submission belongs to based on current time
|
||||||
export function detectSlot(): number | null {
|
export function detectSlot(): number | null {
|
||||||
const slots = cfg("slots").filter((s) => s.active);
|
const slots = Config.get({ section: "poll", key: "slots" }).filter((s) => s.active);
|
||||||
const windowMs = cfg("scoreWindowHours") * 60 * 60 * 1000;
|
const windowMs = Config.get({ section: "tg", key: "scoreWindowHours" }) * 60 * 60 * 1000;
|
||||||
const durationMs = cfg("tgDurationMinutes") * 60 * 1000;
|
const durationMs = Config.get({ section: "tg", key: "durationMinutes" }) * 60 * 1000;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
for (const slot of slots) {
|
for (const slot of slots) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import cron from "node-cron";
|
import cron from "node-cron";
|
||||||
import { Client, TextChannel } from "discord.js";
|
import { Client, TextChannel } from "discord.js";
|
||||||
import { cfg } from "./config";
|
import { Config } from "./config";
|
||||||
import { TGSlot } from "../types";
|
import { TGSlot } from "../types";
|
||||||
import { polls, updatePollMessage } from "@systems/poll";
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
import { Score, TGScore, WeeklySummary } from "@systems/score";
|
import { Score, TGScore, WeeklySummary } from "@systems/score";
|
||||||
import { Attendance } from "@systems/attendance";
|
import { Attendance } from "@systems/attendance";
|
||||||
import { Nations } from "@systems/nations";
|
import { Nations } from "@systems/nations";
|
||||||
|
import { Config } from "@systems/config";
|
||||||
|
|
||||||
export const TG = {
|
export const TG = {
|
||||||
// ── Week ──────────────────────────────────────────────────────────────────
|
// ── Week ──────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -40,7 +41,7 @@
|
||||||
const newWeek = WRank.currentWeek(); // ensures new week exists
|
const newWeek = WRank.currentWeek(); // ensures new week exists
|
||||||
|
|
||||||
if (prevWeek) {
|
if (prevWeek) {
|
||||||
const goal = (require("@systems/config") as any).cfg("wRankGoal");
|
const goal = Config.get({ section: "wrank", key: "goal" });
|
||||||
for (const nation of [Nation.Capella, Nation.Procyon]) {
|
for (const nation of [Nation.Capella, Nation.Procyon]) {
|
||||||
const key = Nations.key(nation);
|
const key = Nations.key(nation);
|
||||||
const entries = prevWeek.entries[key];
|
const entries = prevWeek.entries[key];
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { getImpersonation } from "@systems/impersonate";
|
||||||
import { ResolvedUser } from "@src/types";
|
import { ResolvedUser } from "@src/types";
|
||||||
import { getUsermapEntry, getUsermapEntryById } from "@systems/messages";
|
import { getUsermapEntry, getUsermapEntryById } from "@systems/messages";
|
||||||
import { getActiveCharacter } from "@systems/characters";
|
import { getActiveCharacter } from "@systems/characters";
|
||||||
|
import { Discord } from "@discord";
|
||||||
|
|
||||||
// Resolves a full user context from a GuildMember + discord username
|
// Resolves a full user context from a GuildMember + discord username
|
||||||
export async function resolveUser(member: GuildMember): Promise<ResolvedUser> {
|
export async function resolveUser(member: GuildMember): Promise<ResolvedUser> {
|
||||||
|
|
@ -47,3 +48,10 @@ export function resolveByUsermapKey(key: string): { userKey: string; activeChara
|
||||||
export function hasOfficerRole(member: GuildMember, officerRoles: string[]): boolean {
|
export function hasOfficerRole(member: GuildMember, officerRoles: string[]): boolean {
|
||||||
return member.roles.cache.some((r) => officerRoles.includes(r.name));
|
return member.roles.cache.some((r) => officerRoles.includes(r.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const User = {
|
||||||
|
hasOfficerRole({member, officerRoles}: { member: GuildMember, officerRoles: string[] }): boolean {
|
||||||
|
return Discord.Guild.hasRole({ member: member, roles: officerRoles });
|
||||||
|
return member.roles.cache.some((r) => officerRoles.includes(r.name));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { HistoryKey, UserKey, CharName, Nation, ClassKey } from "@types";
|
import { HistoryKey, UserKey, CharName, Nation, ClassKey } from "@types";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { Bringer } from "@systems/bringer";
|
import { Bringer } from "@systems/bringer";
|
||||||
import { Nations } from "@systems/nations";
|
import { Nations } from "@systems/nations";
|
||||||
import { Store } from "@systems/store";
|
import { Store } from "@systems/store";
|
||||||
|
|
@ -147,7 +147,7 @@ function recomputeRanks(week: WRankWeek, nation: Nation): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBringer(week: WRankWeek): void {
|
function updateBringer(week: WRankWeek): void {
|
||||||
const goal = cfg("wRankGoal");
|
const goal = Config.get({ section: "wrank", key: "goal" });
|
||||||
for (const nation of ["capella", "procyon"] as const) {
|
for (const nation of ["capella", "procyon"] as const) {
|
||||||
// Don't overwrite manual override
|
// Don't overwrite manual override
|
||||||
if (nation === "capella" && week.bringer.capellaOverride) continue;
|
if (nation === "capella" && week.bringer.capellaOverride) continue;
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export function discoverLayouts(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreLayout() {
|
function restoreLayout() {
|
||||||
const savedLayout = Config.get("pollLayout");
|
const savedLayout = Config.get({ section: "poll", key: "layout" });
|
||||||
|
|
||||||
if (savedLayout && _layouts.has(savedLayout)) {
|
if (savedLayout && _layouts.has(savedLayout)) {
|
||||||
_activeLayout = _layouts.get(savedLayout)!;
|
_activeLayout = _layouts.get(savedLayout)!;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { EmbedBuilder } from "discord.js";
|
import { EmbedBuilder } from "discord.js";
|
||||||
import { PollState, VoteEntry, Nation, WRankEntry } from "@types";
|
import { PollState, VoteEntry, Nation, WRankEntry } from "@types";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { WRank } from "@systems/wrank";
|
import { WRank } from "@systems/wrank";
|
||||||
import { Bringer } from "@systems/bringer";
|
import { Bringer } from "@systems/bringer";
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
context: PollRowContext
|
context: PollRowContext
|
||||||
): string {
|
): string {
|
||||||
if (wRankEntry) {
|
if (wRankEntry) {
|
||||||
const goal = cfg("wRankGoal");
|
const goal = Config.get({ section: "wrank", key: "goal" });
|
||||||
return format.wrank.full(wRankEntry, { goal, brackets: true });
|
return format.wrank.full(wRankEntry, { goal, brackets: true });
|
||||||
}
|
}
|
||||||
if (!context.nationHasRank) return "";
|
if (!context.nationHasRank) return "";
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRow(entry: VoteEntry, context: PollRowContext): string {
|
function formatRow(entry: VoteEntry, context: PollRowContext): string {
|
||||||
const cfgFormat = cfg("charDisplayFormat");
|
const cfgFormat = Config.get({ section: "poll", key: "charDisplayFormat" });
|
||||||
const nation = entry.characterNation;
|
const nation = entry.characterNation;
|
||||||
|
|
||||||
const wRankEntry = entry.characterName && entry.characterNation
|
const wRankEntry = entry.characterName && entry.characterNation
|
||||||
|
|
@ -138,9 +138,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveFooter(state: PollState, noCount: number, overrideLockMsg?: string): string {
|
function resolveFooter(state: PollState, noCount: number, overrideLockMsg?: string): string {
|
||||||
if (state.confirmed === "yes") return cfg("confirmYesMessage");
|
if (state.confirmed === "yes") return Config.get({ section: "poll", key: "confirmYes" });
|
||||||
if (state.confirmed === "no") return cfg("confirmNoMessage");
|
if (state.confirmed === "no") return Config.get({ section: "poll", key: "confirmNo" });
|
||||||
if (state.locked) return overrideLockMsg ?? cfg("lockMessage");
|
if (state.locked) return overrideLockMsg ?? Config.get({ section: "poll", key: "lockMessage" });
|
||||||
return `❌ ${noCount} • Vote updates live • Anyone can vote • /tg switch to change character`;
|
return `❌ ${noCount} • Vote updates live • Anyone can vote • /tg switch to change character`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
};
|
};
|
||||||
const noVoters: VoteEntry[] = [];
|
const noVoters: VoteEntry[] = [];
|
||||||
const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = [];
|
const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = [];
|
||||||
const showNoInline = (cfg as any)("showNoInNationField") ?? false;
|
const showNoInline = Config.get({ section: "poll", key: "showNoInNationField" });
|
||||||
|
|
||||||
for (const entry of state.yes.values()) {
|
for (const entry of state.yes.values()) {
|
||||||
const nation = entry.characterNation ?? Nation.Capella;
|
const nation = entry.characterNation ?? Nation.Capella;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { EmbedBuilder } from "discord.js";
|
import { EmbedBuilder } from "discord.js";
|
||||||
import { PollState, VoteEntry, Nation, WRankEntry } from "@types";
|
import { PollState, VoteEntry, Nation, WRankEntry } from "@types";
|
||||||
import { cfg } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { WRank } from "@systems/wrank";
|
import { WRank } from "@systems/wrank";
|
||||||
import { Bringer } from "@systems/bringer";
|
import { Bringer } from "@systems/bringer";
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
function formatWRank(wRankEntry: WRankEntry | null, context: PollRowContext): string {
|
function formatWRank(wRankEntry: WRankEntry | null, context: PollRowContext): string {
|
||||||
if (wRankEntry) {
|
if (wRankEntry) {
|
||||||
return format.wrank.full(wRankEntry, { goal: cfg("wRankGoal"), brackets: true });
|
return format.wrank.full(wRankEntry, { goal: Config.get({ section: "wrank", key: "goal" }), brackets: true });
|
||||||
}
|
}
|
||||||
if (!context.nationHasRank) return "";
|
if (!context.nationHasRank) return "";
|
||||||
if (context.nationHasDelta) return format.wrank.noRank();
|
if (context.nationHasDelta) return format.wrank.noRank();
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRow(entry: VoteEntry, context: PollRowContext): string {
|
function formatRow(entry: VoteEntry, context: PollRowContext): string {
|
||||||
const cfgFormat = cfg("charDisplayFormat");
|
const cfgFormat = Config.get({ section: "poll", key: "charDisplayFormat" });
|
||||||
const nation = entry.characterNation;
|
const nation = entry.characterNation;
|
||||||
const wRankEntry = entry.characterName && entry.characterNation
|
const wRankEntry = entry.characterName && entry.characterNation
|
||||||
? WRank.entry(entry.characterName, entry.characterNation)
|
? WRank.entry(entry.characterName, entry.characterNation)
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
};
|
};
|
||||||
const noVoters: VoteEntry[] = [];
|
const noVoters: VoteEntry[] = [];
|
||||||
const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = [];
|
const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = [];
|
||||||
const showNoInline = (cfg as any)("showNoInNationField") ?? false;
|
const showNoInline = Config.get({ section: "poll", key: "showNoInNationField" });
|
||||||
|
|
||||||
for (const entry of state.yes.values()) {
|
for (const entry of state.yes.values()) {
|
||||||
const nation = entry.characterNation ?? Nation.Capella;
|
const nation = entry.characterNation ?? Nation.Capella;
|
||||||
|
|
@ -158,9 +158,9 @@
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
let footer: string;
|
let footer: string;
|
||||||
if (state.confirmed === "yes") footer = cfg("confirmYesMessage");
|
if (state.confirmed === "yes") footer = Config.get({ section: "poll", key: "confirmYes" });
|
||||||
else if (state.confirmed === "no") footer = cfg("confirmNoMessage");
|
else if (state.confirmed === "no") footer = Config.get({ section: "poll", key: "confirmNo" });
|
||||||
else if (state.locked) footer = options?.overrideLockMsg ?? cfg("lockMessage");
|
else if (state.locked) footer = options?.overrideLockMsg ?? Config.get({ section: "poll", key: "lockMessage" });
|
||||||
else footer = `❌ ${noVoters.length} • Vote updates live • Anyone can vote • /tg switch to change character`;
|
else footer = `❌ ${noVoters.length} • Vote updates live • Anyone can vote • /tg switch to change character`;
|
||||||
embed.setFooter({ text: footer });
|
embed.setFooter({ text: footer });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import { ChatInputCommandInteraction, ButtonInteraction } from "discord.js";
|
||||||
import { Config } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
|
|
||||||
// Poll vote confirmation messages (Yes/No button responses)
|
// Poll vote confirmation messages (Yes/No button responses)
|
||||||
const POLL_EPHEMERAL_ENABLED = Config.get("pollEphemeralEnabled");
|
const POLL_EPHEMERAL_ENABLED = Config.get({ section: "poll", key: "ephemeralEnabled" });
|
||||||
// Command output messages (score, rank, status etc.) — always on by default
|
// Command output messages (score, rank, status etc.) — always on by default
|
||||||
const COMMAND_EPHEMERAL_ENABLED = Config.get("commandEphemeralEnabled");
|
const COMMAND_EPHEMERAL_ENABLED = Config.get({ section: "poll", key: "commandEphemeralEnabled" });
|
||||||
const EPHEMERAL_DELETE_MS = Config.get("ephemeralDeleteMs");
|
const EPHEMERAL_DELETE_MS = Config.get({ section: "poll", key: "ephemeralDeleteMs" });
|
||||||
|
|
||||||
// For poll button responses
|
// For poll button responses
|
||||||
export async function pollReplyAndDelete(
|
export async function pollReplyAndDelete(
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@
|
||||||
"@ui": ["src/ui/index"],
|
"@ui": ["src/ui/index"],
|
||||||
"@ui/*": ["src/ui/*"],
|
"@ui/*": ["src/ui/*"],
|
||||||
"@ui/poll": ["src/ui/poll/index"],
|
"@ui/poll": ["src/ui/poll/index"],
|
||||||
"@ui/types": ["src/ui/types"]
|
"@ui/types": ["src/ui/types"],
|
||||||
|
"@discord": ["src/discord/index"],
|
||||||
|
"@discord/*": ["src/discord/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "scripts/**/*"],
|
"include": ["src/**/*", "scripts/**/*"],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue