Add typed argument command editor
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s

This commit is contained in:
2026-10-25 06:12:47 +01:00
parent a48cd5bc22
commit 7682abdc9f
+15 -2
View File
@@ -152,7 +152,7 @@
let oauthIdentity: 'streamer' | 'bot' = 'streamer';
let oauthBusy = false;
let oauthError = '';
let botCommands: Array<{ id: string; name: string; description: string; permission_level: string; aliases: string[]; tags: string[]; cooldown_seconds: number; cooldown_scope: string; response_template: string; error_template: string | null; enabled: boolean; web_request?: { method: string; url: string; body?: string | null } | null; response_fields?: Array<[string, string]> }> = [];
let botCommands: Array<{ id: string; name: string; description: string; permission_level: string; aliases: string[]; tags: string[]; cooldown_seconds: number; cooldown_scope: string; response_template: string; error_template: string | null; enabled: boolean; web_request?: { method: string; url: string; body?: string | null } | null; response_fields?: Array<[string, string]>; argument_definitions?: unknown[] }> = [];
let publicBotCommands: Array<{ id: string; name: string; description: string; permission_level: string; aliases: string[]; tags: string[] }> = [];
let botCommandError = '';
let commandFormId = '';
@@ -170,6 +170,7 @@
let commandWebUrl = '';
let commandWebBody = '';
let commandResponseFields = '';
let commandArgumentDefinitions = '[]';
let commandRules: Array<{ user_id: string; response_template: string | null }> = [];
let commandRuleUserId = '';
let commandRuleResponse = '';
@@ -843,6 +844,7 @@
commandWebUrl = '';
commandWebBody = '';
commandResponseFields = '';
commandArgumentDefinitions = '[]';
commandRules = [];
commandRuleUserId = '';
commandRuleResponse = '';
@@ -882,6 +884,7 @@
commandWebUrl = command.web_request?.url ?? '';
commandWebBody = command.web_request?.body ?? '';
commandResponseFields = (command.response_fields ?? []).map(([name, path]) => `${name}=${path}`).join('\n');
commandArgumentDefinitions = JSON.stringify(command.argument_definitions ?? [], null, 2);
void loadCommandRules(command.id);
}
@@ -938,11 +941,20 @@
const aliases = commandFormAliases.split(',').map((value) => value.trim()).filter(Boolean);
const tags = commandFormTags.split(',').map((value) => value.trim()).filter(Boolean);
const responseFields = commandResponseFields.split('\n').map((value) => value.trim()).filter(Boolean).map((value) => value.split('=').map((part) => part.trim())).filter((parts): parts is [string, string] => parts.length === 2 && Boolean(parts[0]) && Boolean(parts[1]));
let argumentDefinitions: unknown[] = [];
try {
argumentDefinitions = JSON.parse(commandArgumentDefinitions);
if (!Array.isArray(argumentDefinitions)) throw new Error('Argument definitions must be a JSON array.');
} catch (error) {
botCommandError = error instanceof Error ? error.message : 'Argument definitions must be valid JSON.';
commandFormBusy = false;
return;
}
try {
const response = await fetch(commandFormId ? `${serverAddress}/api/v1/bot/commands/${encodeURIComponent(commandFormId)}` : `${serverAddress}/api/v1/bot/commands`, {
method: commandFormId ? 'PATCH' : 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` },
body: JSON.stringify({ name: commandFormName, description: commandFormDescription, permission_level: commandFormPermission, aliases, tags, cooldown_seconds: Number(commandFormCooldown), cooldown_scope: commandFormCooldownScope, response_template: commandFormResponse, error_template: commandFormErrorResponse || null, enabled: commandFormEnabled, web_request: commandWebUrl ? { method: commandWebMethod, url: commandWebUrl, body: commandWebBody || null } : null, response_fields: responseFields }),
body: JSON.stringify({ name: commandFormName, description: commandFormDescription, permission_level: commandFormPermission, aliases, tags, cooldown_seconds: Number(commandFormCooldown), cooldown_scope: commandFormCooldownScope, response_template: commandFormResponse, error_template: commandFormErrorResponse || null, enabled: commandFormEnabled, web_request: commandWebUrl ? { method: commandWebMethod, url: commandWebUrl, body: commandWebBody || null } : null, response_fields: responseFields, argument_definitions: argumentDefinitions }),
});
if (!response.ok) throw new Error('The command could not be saved. Check its name, aliases, and fields.');
resetCommandForm();
@@ -1450,6 +1462,7 @@
<section class="panel command-management-panel"><div class="panel-header"><span>COMMAND MANAGEMENT</span></div><div class="settings-body">{#each botCommands as command}<div class="bot-command-row"><div><strong>!{command.name}</strong><span class="bot-command-description">{command.enabled ? 'ENABLED' : 'DISABLED'} · {command.permission_level.toUpperCase()}</span></div><div class="bot-command-meta"><button class="secondary-action" onclick={() => editCommand(command)}>EDIT</button><button class="secondary-action danger" onclick={() => deleteCommand(command.id)}>DELETE</button></div></div>{/each}{#if !botCommands.length}<div class="empty-row">No commands are configured yet.</div>{/if}</div></section>
<section class="panel command-web-action-panel"><div class="panel-header"><span>WEB ACTION CONFIGURATION</span></div><div class="settings-body command-form"><p class="bot-secret-note">Optional HTTPS request and response variables. The server validates the URL and never executes template values as code.</p><div class="command-form-grid"><label class="micro" for="command-web-method">METHOD</label><select id="command-web-method" class="control" bind:value={commandWebMethod}><option value="GET">GET</option><option value="POST">POST</option></select><label class="micro" for="command-web-url">HTTPS URL</label><input id="command-web-url" class="control" bind:value={commandWebUrl} maxlength="2048" placeholder="https://example.com/status" /><label class="micro" for="command-web-body">JSON BODY (OPTIONAL)</label><textarea id="command-web-body" class="control command-template" bind:value={commandWebBody} maxlength="65536" placeholder="JSON body"></textarea><label class="micro" for="command-response-fields">RESPONSE VARIABLES (one name=path per line)</label><textarea id="command-response-fields" class="control command-template" bind:value={commandResponseFields} maxlength="4096" placeholder="status=$.status"></textarea></div></div></section>
<section class="panel command-rules-panel"><div class="panel-header"><span>USER-SPECIFIC RULES</span><span class="micro">{commandFormId ? `COMMAND ${commandFormName}` : 'SELECT A COMMAND'}</span></div><div class="settings-body command-form"><p class="bot-secret-note">A command with rules is limited to the named Twitch users. Add an optional response for a personalized variant.</p><div class="chat-send-row"><input class="control" bind:value={commandRuleUserId} inputmode="numeric" maxlength="64" placeholder="Twitch user ID" disabled={!commandFormId} /><input class="control" bind:value={commandRuleResponse} maxlength="2000" placeholder="Optional personalized response" disabled={!commandFormId} /><button class="primary" onclick={saveCommandRule} disabled={commandRuleBusy || !commandFormId}>ADD / UPDATE</button></div>{#if commandRuleError}<div class="auth-error">{commandRuleError}</div>{/if}{#if commandRules.length}<div class="bot-command-list">{#each commandRules as rule}<div class="bot-command-row"><div><strong>{rule.user_id}</strong><span class="bot-command-description">{rule.response_template || 'Uses the command response template'}</span></div><button class="secondary-action danger" onclick={() => deleteCommandRule(rule.user_id)} disabled={commandRuleBusy}>DELETE</button></div>{/each}</div>{:else}<div class="empty-row">No user-specific rules configured.</div>{/if}</div></section>
<section class="panel command-arguments-panel"><div class="panel-header"><span>TYPED ARGUMENTS</span><span class="micro">JSON DEFINITIONS</span></div><div class="settings-body command-form"><p class="bot-secret-note">Declare text, number, choice, username, or duration arguments. Values are validated as data and never executed.</p><textarea class="control command-template" bind:value={commandArgumentDefinitions} maxlength="12000" placeholder="[]" aria-label="Typed argument definitions"></textarea><div class="micro">Example: a JSON array containing count, number, required, minimum, and maximum fields.</div></div></section>
{:else if activeView === 'Collaboration'}
{#if collaborationSession}
<section class="collaboration-page panel">