Phase 4: Bots & Extensibility
Backend: - internal/bot/auth.go: bot token generation and verification - internal/bot/handlers.go: bot CRUD (create, list, get, update, delete, server management, token regen) - internal/bot/commands.go: slash command registration and management - internal/webhook/handlers.go: webhook CRUD and execution endpoint - internal/webhook/token.go: webhook token generation - internal/db/db.go: bots, bot_servers, slash_commands, webhooks tables - internal/gateway/events.go: BOT_JOIN, BOT_LEAVE event constants - cmd/server/main.go: wired bot, webhook, invite routes Frontend: - stores/bot.ts: Zustand store for bot management - BotManager.tsx: bot list, create, edit, delete, add to server, token display - CommandManager.tsx: slash command CRUD per bot - SlashCommandPopup.tsx: / command autocomplete popup - App.tsx: /bots and /bots/:id/commands routes Examples: - examples/modbot/: auto-delete banned words, /kick, /ban, /purge commands - examples/welcome/: welcome message on member join
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useBotStore, type Bot, type SlashCommand } from '../stores/bot.ts';
|
||||
import { useServerStore } from '../stores/server.ts';
|
||||
|
||||
export function CommandManager() {
|
||||
const { id: botId } = useParams<{ id: string }>();
|
||||
|
||||
const fetchCommands = useBotStore((s) => s.fetchCommands);
|
||||
const createCommand = useBotStore((s) => s.createCommand);
|
||||
const deleteCommand = useBotStore((s) => s.deleteCommand);
|
||||
const bots = useBotStore((s) => s.bots);
|
||||
const fetchBots = useBotStore((s) => s.fetchBots);
|
||||
const error = useBotStore((s) => s.error);
|
||||
|
||||
const servers = useServerStore((s) => s.servers);
|
||||
const fetchServers = useServerStore((s) => s.fetchServers);
|
||||
|
||||
const [commands, setCommands] = useState<SlashCommand[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [bot, setBot] = useState<Bot | null>(null);
|
||||
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [cmdName, setCmdName] = useState('');
|
||||
const [cmdDesc, setCmdDesc] = useState('');
|
||||
const [cmdServer, setCmdServer] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers();
|
||||
fetchBots();
|
||||
}, [fetchServers, fetchBots]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!botId) return;
|
||||
const found = bots.find((b) => b.id === botId);
|
||||
if (found) setBot(found);
|
||||
}, [bots, botId]);
|
||||
|
||||
const loadCommands = async () => {
|
||||
if (!botId) return;
|
||||
setLoading(true);
|
||||
const cmds = await fetchCommands(botId);
|
||||
setCommands(cmds);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCommands();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [botId]);
|
||||
|
||||
const handleAdd = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!botId || !cmdName.trim() || !cmdServer) return;
|
||||
try {
|
||||
const cmd = await createCommand(botId, cmdServer, cmdName.trim(), cmdDesc.trim());
|
||||
setCommands((prev) => [...prev, cmd]);
|
||||
setCmdName('');
|
||||
setCmdDesc('');
|
||||
setCmdServer('');
|
||||
setShowAdd(false);
|
||||
} catch {
|
||||
// error in store
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (cmdId: string) => {
|
||||
if (!botId) return;
|
||||
if (!window.confirm('Delete this command?')) return;
|
||||
try {
|
||||
await deleteCommand(botId, cmdId);
|
||||
setCommands((prev) => prev.filter((c) => c.id !== cmdId));
|
||||
} catch {
|
||||
// error in store
|
||||
}
|
||||
};
|
||||
|
||||
const getServerName = (serverId: string) => {
|
||||
const s = servers.find((sv) => sv.id === serverId);
|
||||
return s ? s.name : serverId.slice(0, 8);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-gb-bg p-4 md:p-8 overflow-y-auto">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="border border-gb-bg-t p-6">
|
||||
<pre className="text-gb-orange font-mono text-center mb-6">
|
||||
{'┌──────────────────────────────────┐\n'}
|
||||
{'│ === SLASH COMMANDS === │\n'}
|
||||
{'└──────────────────────────────────┘'}
|
||||
</pre>
|
||||
|
||||
{bot && (
|
||||
<p className="text-gb-fg-s font-mono text-sm mb-4">
|
||||
BOT: <span className="text-gb-green">[{bot.name}]</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-gb-red text-sm font-mono mb-4">ERR: {error}</p>
|
||||
)}
|
||||
|
||||
{/* Add command form */}
|
||||
<div className="mb-6">
|
||||
{!showAdd ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdd(true)}
|
||||
className="terminal-button"
|
||||
>
|
||||
[ADD COMMAND]
|
||||
</button>
|
||||
) : (
|
||||
<form onSubmit={handleAdd} className="border border-gb-bg-t p-4 space-y-3">
|
||||
<p className="text-gb-aqua font-mono text-sm">{'>'} NEW SLASH COMMAND</p>
|
||||
<div>
|
||||
<label className="block text-gb-fg-f mb-1 font-mono text-xs">SERVER:</label>
|
||||
<select
|
||||
value={cmdServer}
|
||||
onChange={(e) => setCmdServer(e.target.value)}
|
||||
className="terminal-input w-full"
|
||||
>
|
||||
<option value="">-- select server --</option>
|
||||
{servers.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gb-fg-f mb-1 font-mono text-xs">COMMAND NAME:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cmdName}
|
||||
onChange={(e) => setCmdName(e.target.value.replace(/\s/g, '').slice(0, 32))}
|
||||
maxLength={32}
|
||||
className="terminal-input w-full"
|
||||
placeholder="mycommand"
|
||||
autoFocus
|
||||
/>
|
||||
<span className="text-gb-fg-f text-xs font-mono mt-1 block">
|
||||
/{cmdName || 'command'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gb-fg-f mb-1 font-mono text-xs">DESCRIPTION:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cmdDesc}
|
||||
onChange={(e) => setCmdDesc(e.target.value.slice(0, 256))}
|
||||
maxLength={256}
|
||||
className="terminal-input w-full"
|
||||
placeholder="what does this command do?"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="terminal-button text-xs"
|
||||
disabled={!cmdName.trim() || !cmdServer}
|
||||
>
|
||||
[SAVE]
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setShowAdd(false); setCmdName(''); setCmdDesc(''); setCmdServer(''); }}
|
||||
className="text-gb-fg-f hover:text-gb-aqua font-mono text-xs"
|
||||
>
|
||||
[CANCEL]
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Command list */}
|
||||
{loading && (
|
||||
<p className="text-gb-fg-f font-mono text-sm">[loading commands...]</p>
|
||||
)}
|
||||
{!loading && commands.length === 0 && (
|
||||
<p className="text-gb-fg-f font-mono text-sm">[no commands registered]</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{commands.map((cmd) => (
|
||||
<div
|
||||
key={cmd.id}
|
||||
className="border border-gb-bg-t p-3 flex items-start justify-between gap-3"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-gb-green font-mono text-sm">
|
||||
/{cmd.name}
|
||||
</p>
|
||||
{cmd.description && (
|
||||
<p className="text-gb-fg-f font-mono text-xs mt-1">{cmd.description}</p>
|
||||
)}
|
||||
<p className="text-gb-fg-f font-mono text-xs mt-1">
|
||||
server: <span className="text-gb-aqua">{getServerName(cmd.server_id)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(cmd.id)}
|
||||
className="terminal-button text-xs hover:!text-gb-red shrink-0"
|
||||
>
|
||||
[DELETE]
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-6 pt-4 border-t border-gb-bg-t flex justify-between">
|
||||
<Link to="/bots" className="text-gb-fg-f hover:text-gb-aqua font-mono text-sm">
|
||||
{'<'} [BACK TO BOTS]
|
||||
</Link>
|
||||
<Link to="/" className="text-gb-fg-f hover:text-gb-aqua font-mono text-sm">
|
||||
[CHAT]
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user