ANSI color codeblocks, ephemeral message flag update, etc.
This commit is contained in:
parent
b497423ba7
commit
4f5a90b3bb
119
_opt/ansi.js
Normal file
119
_opt/ansi.js
Normal file
@ -0,0 +1,119 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
||||
import { MessageFlags } from 'discord-api-types/v10';
|
||||
import { CODES } from '../_src/ansiColors.js';
|
||||
|
||||
/**
|
||||
* Combined ANSI utilities module
|
||||
* - /ansi: preview nested [tag]…[/] ANSI coloring
|
||||
* - /ansitheme: display full BG×FG theme chart
|
||||
* Both commands are Admin-only.
|
||||
*/
|
||||
export const commands = [
|
||||
// Preview arbitrary ANSI tags
|
||||
{
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ansi')
|
||||
.setDescription('Preview an ANSI-colored code block')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.setDMPermission(false)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('text')
|
||||
.setDescription('Use [red]…[/], [bold,blue]…[/], escape \\[/]')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt
|
||||
.setName('ephemeral')
|
||||
.setDescription('Reply ephemerally?')
|
||||
.setRequired(false)
|
||||
),
|
||||
async execute(interaction, client) {
|
||||
const raw = interaction.options.getString('text', true);
|
||||
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
||||
const colored = client.ansi`${raw}`;
|
||||
const block = client.wrapAnsi(colored);
|
||||
const opts = { content: block };
|
||||
if (ephemeral) opts.flags = MessageFlags.Ephemeral;
|
||||
await interaction.reply(opts);
|
||||
}
|
||||
},
|
||||
|
||||
// Show complete ANSI theme chart
|
||||
{
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ansitheme')
|
||||
.setDescription('Show ANSI color theme chart')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.setDMPermission(false)
|
||||
.addBooleanOption(opt =>
|
||||
opt
|
||||
.setName('ephemeral')
|
||||
.setDescription('Reply ephemerally?')
|
||||
.setRequired(false)
|
||||
),
|
||||
async execute(interaction, client) {
|
||||
const fgs = ['gray', 'red', 'green', 'yellow', 'blue', 'pink', 'cyan', 'white'];
|
||||
const bgs = ['bgGray', 'bgOrange', 'bgBlue', 'bgTurquoise', 'bgFirefly', 'bgIndigo', 'bgLightGray', 'bgWhite'];
|
||||
const pad = 8;
|
||||
// Column header with padded labels (no colors) - shifted right by 1
|
||||
const header = ' ' + fgs.map(f => f.padEnd(pad, ' ')).join('');
|
||||
// Sample row with no background (padded cells)
|
||||
let defaultRow = '';
|
||||
for (const fg of fgs) {
|
||||
const fgCode = CODES[fg];
|
||||
const openNormal = `\u001b[${fgCode}m`;
|
||||
const openBold = `\u001b[${fgCode};${CODES.bold}m`;
|
||||
const openUnder = `\u001b[${fgCode};${CODES.underline}m`;
|
||||
const cell = `${openNormal}sa\u001b[0m${openBold}mp\u001b[0m${openUnder}le\u001b[0m`;
|
||||
defaultRow += ' ' + cell + ' ';
|
||||
}
|
||||
// Append default row label after one pad
|
||||
defaultRow += 'default';
|
||||
// Colored rows per background
|
||||
const rows = [];
|
||||
for (const bg of bgs) {
|
||||
let row = '';
|
||||
const bgCode = CODES[bg];
|
||||
for (const fg of fgs) {
|
||||
const fgCode = CODES[fg];
|
||||
const openNormal = `\u001b[${bgCode};${fgCode}m`;
|
||||
const openBold = `\u001b[${bgCode};${fgCode};${CODES.bold}m`;
|
||||
const openUnder = `\u001b[${bgCode};${fgCode};${CODES.underline}m`;
|
||||
const cell = `${openNormal}sa\u001b[0m${openBold}mp\u001b[0m${openUnder}le\u001b[0m`;
|
||||
row += ' ' + cell + ' ';
|
||||
}
|
||||
// Append uncolored row label immediately after cell padding
|
||||
row += bg;
|
||||
rows.push(row);
|
||||
}
|
||||
// Determine ephemeral setting
|
||||
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
||||
// Initial sample table (header + default row)
|
||||
const sampleContent = [header, defaultRow].join('\n');
|
||||
const optsSample = { content: client.wrapAnsi(sampleContent) };
|
||||
if (ephemeral) optsSample.flags = MessageFlags.Ephemeral;
|
||||
await interaction.reply(optsSample);
|
||||
// Split colored rows into two tables
|
||||
const half = Math.ceil(rows.length / 2);
|
||||
const firstRows = rows.slice(0, half);
|
||||
const secondRows = rows.slice(half);
|
||||
// First colored table
|
||||
const table1 = [header, ...firstRows].join('\n');
|
||||
const opts1 = { content: client.wrapAnsi(table1) };
|
||||
if (ephemeral) opts1.flags = MessageFlags.Ephemeral;
|
||||
await interaction.followUp(opts1);
|
||||
// Second colored table
|
||||
if (secondRows.length > 0) {
|
||||
const table2 = [header, ...secondRows].join('\n');
|
||||
const opts2 = { content: client.wrapAnsi(table2) };
|
||||
if (ephemeral) opts2.flags = MessageFlags.Ephemeral;
|
||||
await interaction.followUp(opts2);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export async function init(client) {
|
||||
client.logger.info('[module:ansi] Loaded ANSI utilities');
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||||
import { MessageFlags } from 'discord-api-types/v10';
|
||||
|
||||
/**
|
||||
* botUtils module - provides administrative bot control commands
|
||||
@ -28,16 +29,16 @@ export const commands = [
|
||||
const ownerId = client.config.owner;
|
||||
// Check invoking user is the bot owner
|
||||
if (interaction.user.id !== String(ownerId)) {
|
||||
return interaction.reply({ content: 'Only the bot owner can shutdown the bot.', ephemeral: true });
|
||||
return interaction.reply({ content: 'Only the bot owner can shutdown the bot.', flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
// Determine desired exit code (default 0)
|
||||
const exitCode = interaction.options.getInteger('code') ?? 0;
|
||||
// Validate exit code bounds
|
||||
if (exitCode < 0 || exitCode > 254) {
|
||||
return interaction.reply({ content: 'Exit code must be between 0 and 254 inclusive.', ephemeral: true });
|
||||
return interaction.reply({ content: 'Exit code must be between 0 and 254 inclusive.', flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
// Acknowledge before shutting down
|
||||
await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, ephemeral: true });
|
||||
await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, flags: MessageFlags.Ephemeral });
|
||||
client.logger.info(
|
||||
`[cmd:exit] Shutdown initiated by owner ${interaction.user.tag} (${interaction.user.id}), exit code ${exitCode}`
|
||||
);
|
||||
@ -72,9 +73,8 @@ export const commands = [
|
||||
.setRequired(false)
|
||||
),
|
||||
async execute(interaction, client) {
|
||||
// Determine if response should be ephemeral (default true)
|
||||
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
||||
await interaction.deferReply({ ephemeral });
|
||||
await interaction.deferReply({ flags: ephemeral ? MessageFlags.Ephemeral : undefined });
|
||||
// Process metrics
|
||||
const uptimeSec = process.uptime();
|
||||
const hours = Math.floor(uptimeSec / 3600);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { MessageFlags } from 'discord-api-types/v10';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
const execAsync = promisify(exec);
|
||||
@ -72,12 +73,12 @@ export const commands = [
|
||||
async execute(interaction, client) {
|
||||
const ownerId = client.config.owner;
|
||||
if (interaction.user.id !== ownerId) {
|
||||
return interaction.reply({ content: 'Only the bot owner can run git commands.', ephemeral: true });
|
||||
return interaction.reply({ content: 'Only the bot owner can run git commands.', flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
const raw = interaction.options.getString('args');
|
||||
// Disallow semicolons to prevent command chaining
|
||||
if (raw.includes(';')) {
|
||||
return interaction.reply({ content: 'Semicolons are not allowed in git arguments.', ephemeral: true });
|
||||
return interaction.reply({ content: 'Semicolons are not allowed in git arguments.', flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
||||
const args = raw.match(/(?:[^\s"]+|"[^"]*")+/g)
|
||||
@ -98,14 +99,18 @@ export const commands = [
|
||||
const outputChunks = chunkString(output, firstChunkSize);
|
||||
// Send first block with header + first output chunk
|
||||
const firstBlock = header + (outputChunks[0] || '');
|
||||
await interaction.reply({ content: formatCodeBlock(firstBlock), ephemeral });
|
||||
const replyOpts = { content: formatCodeBlock(firstBlock) };
|
||||
if (ephemeral) replyOpts.flags = MessageFlags.Ephemeral;
|
||||
await interaction.reply(replyOpts);
|
||||
// Send any remaining blocks without the header
|
||||
for (let i = 1; i < outputChunks.length; i++) {
|
||||
await interaction.followUp({ content: formatCodeBlock(outputChunks[i]), ephemeral });
|
||||
const fuOpts = { content: formatCodeBlock(outputChunks[i]) };
|
||||
if (ephemeral) fuOpts.flags = MessageFlags.Ephemeral;
|
||||
await interaction.followUp(fuOpts);
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof GitError ? err.message : String(err);
|
||||
await interaction.reply({ content: `Error: ${msg}`, ephemeral: true });
|
||||
await interaction.reply({ content: `Error: ${msg}`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { MessageFlags } from 'discord-api-types/v10';
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@ -30,10 +31,10 @@ export const commands = [
|
||||
// URL-based update
|
||||
if (url) {
|
||||
client.logger.info(`[cmd:prompt] URL update requested for client ${clientId}: ${url}`);
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral});
|
||||
if (!url.toLowerCase().endsWith('.txt')) {
|
||||
client.logger.warn(`[cmd:prompt] Invalid URL extension, must end .txt: ${url}`);
|
||||
return interaction.editReply({ content: 'URL must point to a .txt file.', ephemeral: true });
|
||||
return interaction.editReply({ content: 'URL must point to a .txt file.', flags: MessageFlags.Ephemeral});
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
@ -48,7 +49,7 @@ export const commands = [
|
||||
updatedBy: interaction.user.id
|
||||
});
|
||||
client.responsesPrompt = text;
|
||||
return interaction.editReply({ content: 'Prompt updated from URL.', ephemeral: true });
|
||||
return interaction.editReply({ content: 'Prompt updated from URL.', flags: MessageFlags.Ephemeral});
|
||||
} catch (err) {
|
||||
client.logger.error(`[cmd:prompt] URL update failed: ${err.message}`);
|
||||
return interaction.editReply({ content: `Error fetching URL: ${err.message}`, ephemeral: true });
|
||||
@ -151,7 +152,7 @@ export async function init(client, clientConfig) {
|
||||
updatedBy: interaction.user.id
|
||||
});
|
||||
client.responsesPrompt = newPrompt;
|
||||
await interaction.reply({ content: 'Prompt updated!', ephemeral: true });
|
||||
await interaction.reply({ content: 'Prompt updated!', flags: MessageFlags.Ephemeral});
|
||||
} catch (err) {
|
||||
client.logger.error(`responsesPrompt modal submit error: ${err.message}`);
|
||||
await interaction.reply({ content: `Error saving prompt: ${err.message}`, ephemeral: true });
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { MessageFlags } from 'discord-api-types/v10';
|
||||
/**
|
||||
* Slash command module for '/query'.
|
||||
* Defines and handles the /query command via the OpenAI Responses API,
|
||||
@ -182,7 +183,7 @@ export const commands = [
|
||||
}
|
||||
} catch (err) {
|
||||
client.logger.error(`[cmd:query] Error checking score: ${err.message}`);
|
||||
return interaction.reply({ content: 'Error verifying your score. Please try again later.', ephemeral: true });
|
||||
return interaction.reply({ content: 'Error verifying your score. Please try again later.', flags: MessageFlags.Ephemeral});
|
||||
}
|
||||
}
|
||||
const prompt = interaction.options.getString('prompt');
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { MessageFlags } from 'discord-api-types/v10';
|
||||
// _opt/schangar.js
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { MessageFlags } from 'discord-api-types/v10';
|
||||
// opt/scorekeeper.js
|
||||
import cron from 'node-cron';
|
||||
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
|
||||
@ -846,7 +847,7 @@ export const commands = [
|
||||
await interaction.reply({ content: `Category '${name}' created.`, ephemeral: true });
|
||||
} catch (err) {
|
||||
client.logger.error(`Error in addcategory: ${err.message}`);
|
||||
await interaction.reply({ content: 'Failed to create category.', ephemeral: true });
|
||||
await interaction.reply({ content: 'Failed to create category.', flags: MessageFlags.Ephemeral});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -875,7 +876,7 @@ export const commands = [
|
||||
await interaction.reply({ content: `Category '${name}' removed.`, ephemeral: true });
|
||||
} catch (err) {
|
||||
client.logger.error(`Error in removecategory: ${err.message}`);
|
||||
await interaction.reply({ content: 'Failed to remove category.', ephemeral: true });
|
||||
await interaction.reply({ content: 'Failed to remove category.', flags: MessageFlags.Ephemeral});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
_src/ansiColors.js
Normal file
90
_src/ansiColors.js
Normal file
@ -0,0 +1,90 @@
|
||||
// ANSI Colors helper - provides nested [tag]…[/] parsing and code-block wrapping.
|
||||
|
||||
// ANSI color/style codes
|
||||
const CODES = {
|
||||
// text colors
|
||||
gray: 30, red: 31, green: 32, yellow: 33,
|
||||
blue: 34, pink: 35, cyan: 36, white: 37,
|
||||
// background colors
|
||||
bgGray: 40, bgOrange: 41, bgBlue: 42,
|
||||
bgTurquoise: 43, bgFirefly: 44, bgIndigo: 45,
|
||||
bgLightGray: 46, bgWhite: 47,
|
||||
// styles
|
||||
bold: 1, underline: 4,
|
||||
// reset
|
||||
reset: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape literal brackets so users can write \[ and \] without triggering tags.
|
||||
*/
|
||||
export function escapeBrackets(str) {
|
||||
return str
|
||||
.replace(/\\\[/g, '__ESC_LB__')
|
||||
.replace(/\\\]/g, '__ESC_RB__');
|
||||
}
|
||||
|
||||
/** Restore any escaped brackets after formatting. */
|
||||
export function restoreBrackets(str) {
|
||||
return str
|
||||
.replace(/__ESC_LB__/g, '[')
|
||||
.replace(/__ESC_RB__/g, ']');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse nested [tag1,tag2]…[/] patterns into ANSI codes (stack-based).
|
||||
*/
|
||||
export function formatAnsi(input) {
|
||||
const stack = [];
|
||||
let output = '';
|
||||
const pattern = /\[\/\]|\[([^\]]+)\]/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(input)) !== null) {
|
||||
output += input.slice(lastIndex, match.index);
|
||||
|
||||
if (match[0] === '[/]') {
|
||||
if (stack.length) stack.pop();
|
||||
output += `\u001b[${CODES.reset}m`;
|
||||
for (const tag of stack) {
|
||||
const code = CODES[tag] ?? CODES.gray;
|
||||
output += `\u001b[${code}m`;
|
||||
}
|
||||
} else {
|
||||
const tags = match[1].split(/[,;\s]+/).filter(Boolean);
|
||||
for (const tag of tags) {
|
||||
stack.push(tag);
|
||||
const code = CODES[tag] ?? CODES.gray;
|
||||
output += `\u001b[${code}m`;
|
||||
}
|
||||
}
|
||||
|
||||
lastIndex = pattern.lastIndex;
|
||||
}
|
||||
|
||||
output += input.slice(lastIndex);
|
||||
if (stack.length) output += `\u001b[${CODES.reset}m`;
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template-tag: ansi`[red]…[/] text`
|
||||
* Escapes brackets, parses ANSI, and restores literals.
|
||||
*/
|
||||
export function ansi(strings, ...values) {
|
||||
let built = '';
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
built += strings[i];
|
||||
if (i < values.length) built += values[i];
|
||||
}
|
||||
return restoreBrackets(formatAnsi(escapeBrackets(built)));
|
||||
}
|
||||
|
||||
/** Wrap text in a ```ansi code block for Discord. */
|
||||
export function wrapAnsi(text) {
|
||||
return '```ansi\n' + text + '\n```';
|
||||
}
|
||||
|
||||
// Export raw codes for advanced use (e.g., ansitheme module)
|
||||
export { CODES };
|
||||
@ -66,6 +66,7 @@ export default {
|
||||
},
|
||||
|
||||
modules: [
|
||||
'ansi',
|
||||
'botUtils',
|
||||
'pbUtils',
|
||||
'gitUtils',
|
||||
@ -194,6 +195,7 @@ export default {
|
||||
},
|
||||
|
||||
modules: [
|
||||
'ansi',
|
||||
'botUtils',
|
||||
'pbUtils',
|
||||
'gitUtils',
|
||||
|
||||
10
index.js
10
index.js
@ -3,6 +3,8 @@ import { createLogger } from './_src/logger.js';
|
||||
import { initializePocketbase } from './_src/pocketbase.js';
|
||||
import { loadModules } from './_src/loader.js';
|
||||
import config from './config.js';
|
||||
// For deprecated ephemeral option: convert to flags
|
||||
import { ansi, wrapAnsi } from './_src/ansiColors.js';
|
||||
|
||||
// Initialize Discord client
|
||||
const initializeClient = async (clientConfig) => {
|
||||
@ -27,8 +29,11 @@ const initializeClient = async (clientConfig) => {
|
||||
// Set up Pocketbase
|
||||
client.pb = await initializePocketbase(clientConfig, client.logger);
|
||||
|
||||
// Commands collection
|
||||
client.commands = new Collection();
|
||||
// Commands collection
|
||||
client.commands = new Collection();
|
||||
// ANSI helper attached to client
|
||||
client.ansi = ansi;
|
||||
client.wrapAnsi = wrapAnsi;
|
||||
|
||||
// Load optional modules
|
||||
await loadModules(clientConfig, client);
|
||||
@ -39,6 +44,7 @@ const initializeClient = async (clientConfig) => {
|
||||
client.on('interactionCreate', async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
|
||||
const commandName = interaction.commandName;
|
||||
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user