Add user-specific command rule 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:10:25 +01:00
parent 52a5ce895f
commit a48cd5bc22
+58
View File
@@ -170,6 +170,11 @@
let commandWebUrl = '';
let commandWebBody = '';
let commandResponseFields = '';
let commandRules: Array<{ user_id: string; response_template: string | null }> = [];
let commandRuleUserId = '';
let commandRuleResponse = '';
let commandRuleBusy = false;
let commandRuleError = '';
let commandFormBusy = false;
let commandSimulationBusy = false;
let commandSimulationError = '';
@@ -838,6 +843,10 @@
commandWebUrl = '';
commandWebBody = '';
commandResponseFields = '';
commandRules = [];
commandRuleUserId = '';
commandRuleResponse = '';
commandRuleError = '';
}
const commandVariables = ['user_id', 'user_name', 'message', 'args', ...Array.from({ length: 16 }, (_, index) => `arg_${index + 1}`)];
@@ -873,6 +882,54 @@
commandWebUrl = command.web_request?.url ?? '';
commandWebBody = command.web_request?.body ?? '';
commandResponseFields = (command.response_fields ?? []).map(([name, path]) => `${name}=${path}`).join('\n');
void loadCommandRules(command.id);
}
async function loadCommandRules(commandId: string) {
commandRuleError = '';
try {
const response = await fetch(`${serverAddress}/api/v1/bot/commands/${encodeURIComponent(commandId)}/user-rules`, { headers: { authorization: `Bearer ${sessionToken}` } });
if (!response.ok) throw new Error('User rules could not be loaded.');
commandRules = await response.json();
} catch (error) {
commandRuleError = error instanceof Error ? error.message : 'User rules could not be loaded.';
}
}
async function saveCommandRule() {
if (!commandFormId || !/^\d{1,64}$/.test(commandRuleUserId)) {
commandRuleError = 'Enter a valid Twitch user ID.';
return;
}
commandRuleBusy = true;
commandRuleError = '';
try {
const response = await fetch(`${serverAddress}/api/v1/bot/commands/${encodeURIComponent(commandFormId)}/user-rules`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` }, body: JSON.stringify({ user_id: commandRuleUserId, response_template: commandRuleResponse || null }) });
const result = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(result.error || 'The user rule could not be saved.');
commandRuleUserId = '';
commandRuleResponse = '';
await loadCommandRules(commandFormId);
} catch (error) {
commandRuleError = error instanceof Error ? error.message : 'The user rule could not be saved.';
} finally {
commandRuleBusy = false;
}
}
async function deleteCommandRule(userId: string) {
if (!commandFormId) return;
commandRuleBusy = true;
commandRuleError = '';
try {
const response = await fetch(`${serverAddress}/api/v1/bot/commands/${encodeURIComponent(commandFormId)}/user-rules/${encodeURIComponent(userId)}`, { method: 'DELETE', headers: { authorization: `Bearer ${sessionToken}` } });
if (!response.ok) throw new Error('The user rule could not be deleted.');
await loadCommandRules(commandFormId);
} catch (error) {
commandRuleError = error instanceof Error ? error.message : 'The user rule could not be deleted.';
} finally {
commandRuleBusy = false;
}
}
async function saveCommand() {
@@ -1392,6 +1449,7 @@
<section class="bot-page panel"><div class="panel-header"><span>BOT COMMANDS</span><button class="secondary" onclick={loadBotCommands}>REFRESH</button></div><div class="bot-page-body"><div class="micro">PUBLIC COMMAND CATALOG</div>{#if botCommandError}<div class="auth-error">{botCommandError}</div>{:else if botCommands.length}<div class="bot-command-list">{#each botCommands as command}<article class="bot-command-row"><div><strong>!{command.name}</strong><span class="bot-command-description">{command.description}</span>{#if command.tags.length}<div class="micro">{command.tags.join(' · ')}</div>{/if}</div><div class="bot-command-meta"><span>{command.permission_level.toUpperCase()}</span>{#if command.cooldown_seconds}<span>{command.cooldown_seconds}s / {command.cooldown_scope}</span>{/if}</div></article>{/each}</div>{:else}<div class="empty-state"><strong>No enabled commands</strong><span>Create commands on the server to make them available here.</span></div>{/if}<div class="bot-secret-section"><div class="micro">SECRETS MANAGEMENT</div><div class="bot-secret-note">Values are encrypted and cannot be read after saving.</div><div class="secret-form"><input class="control" bind:value={secretName} maxlength="100" placeholder="Name" /><input class="control" bind:value={secretDescription} maxlength="500" placeholder="Description" /><input class="control" type="password" bind:value={secretValue} maxlength="65536" placeholder="Value" /><button class="primary" onclick={createSecret} disabled={!secretName || !secretValue}>ADD SECRET</button></div>{#if botSecrets.length}<div class="bot-secret-list">{#each botSecrets as secret}<div class="bot-secret-row"><div><strong>{secret.name}</strong><span>{secret.description || 'No description'}</span>{#if secret.used_by.length}<div class="micro">USED BY: {secret.used_by.join(', ')}</div>{/if}</div><button class="secondary-action" onclick={() => deleteSecret(secret.id)}>DELETE</button></div>{/each}</div>{:else}<div class="empty-row">There are no secrets yet.</div>{/if}{#if secretError}<div class="auth-error">{secretError}</div>{/if}</div></div></section>
<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>
{:else if activeView === 'Collaboration'}
{#if collaborationSession}
<section class="collaboration-page panel">