feat(thread-discovery): implement separate endpoint for fetching threads and update channel loading logic
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# Separate Thread Discovery Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Keep channel listing fast and move expensive active/archived thread discovery to a separate endpoint loaded asynchronously by the homepage.
|
||||
|
||||
**Architecture:** `VoiceController` exposes cache-only channels and network-backed threads separately. `webserver.ts` adds `/api/guilds/:guildId/threads`. `public/index.html` loads channels first, then appends thread options after thread endpoint returns.
|
||||
|
||||
**Tech Stack:** TypeScript, discord.js-selfbot-v13, Express, vanilla JS.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add thread discovery method
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/voiceController.ts`
|
||||
|
||||
- [ ] Add `listThreads(guildId)` that fetches active and archived threads per parent text channel.
|
||||
- [ ] Keep `listWatchableChannels` cache-only.
|
||||
- [ ] Verify typecheck.
|
||||
|
||||
### Task 2: Add thread API endpoint
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/webserver.ts`
|
||||
|
||||
- [ ] Add `GET /api/guilds/:guildId/threads`.
|
||||
- [ ] Return thread summaries.
|
||||
- [ ] Verify typecheck.
|
||||
|
||||
### Task 3: Update homepage dropdown loading
|
||||
|
||||
**Files:**
|
||||
- Modify: `public/index.html`
|
||||
|
||||
- [ ] `loadChannels` fetches `/channels` first and renders immediately.
|
||||
- [ ] Then fetches `/threads` async and appends thread options.
|
||||
- [ ] Verify typecheck/tests.
|
||||
@@ -625,7 +625,21 @@
|
||||
apiRequest(`/api/guilds/${guildId}/channels`),
|
||||
]);
|
||||
renderOptions(el.channelSelect, voiceChannels, 'Select voice channel');
|
||||
renderOptions(el.channelFilter, watchChannels, 'Select channel or thread');
|
||||
renderOptions(el.channelFilter, watchChannels, 'Select channel');
|
||||
apiRequest(`/api/guilds/${guildId}/threads`)
|
||||
.then((threads) => appendOptions(el.channelFilter, threads))
|
||||
.catch((error) => showError(`Thread discovery failed: ${error.message}`));
|
||||
}
|
||||
|
||||
function appendOptions(select, items) {
|
||||
const existing = new Set([...select.options].map((option) => option.value));
|
||||
for (const item of items) {
|
||||
if (existing.has(item.id)) continue;
|
||||
const option = document.createElement('option');
|
||||
option.value = item.id;
|
||||
option.textContent = item.name;
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
|
||||
@@ -73,33 +73,42 @@ export class VoiceController {
|
||||
const guild = this.getGuild(guildId);
|
||||
await guild.channels.fetch().catch(() => null);
|
||||
|
||||
const channels = [...guild.channels.cache.values()].filter((channel) =>
|
||||
["GUILD_TEXT", "GUILD_PUBLIC_THREAD", "GUILD_PRIVATE_THREAD"].includes(
|
||||
channel.type,
|
||||
),
|
||||
);
|
||||
return guild.channels.cache
|
||||
.filter((channel) => channel.type === "GUILD_TEXT")
|
||||
.map((channel) => ({
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
type: channel.type,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
async listThreads(guildId: string): Promise<ChannelSummary[]> {
|
||||
const guild = this.getGuild(guildId);
|
||||
await guild.channels.fetch().catch(() => null);
|
||||
|
||||
const threads: ChannelSummary[] = [];
|
||||
for (const channel of guild.channels.cache.values()) {
|
||||
const threadParent = channel as typeof channel & {
|
||||
threads?: { fetch: (options: { archived: boolean; limit: number }) => Promise<any> };
|
||||
};
|
||||
if (!threadParent.threads?.fetch) continue;
|
||||
|
||||
for (const archived of [false, true]) {
|
||||
const fetched = await threadParent.threads.fetch({ archived, limit: 100 }).catch(() => null);
|
||||
if (!fetched?.threads) continue;
|
||||
channels.push(...fetched.threads.values());
|
||||
|
||||
for (const thread of fetched.threads.values()) {
|
||||
threads.push({
|
||||
id: thread.id,
|
||||
name: `${channel.name} / ${thread.name}`,
|
||||
type: thread.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(new Map(channels.map((channel) => [channel.id, channel])).values())
|
||||
.map((channel) => {
|
||||
const parentName = channel.isThread?.() ? channel.parent?.name : null;
|
||||
return {
|
||||
id: channel.id,
|
||||
name: parentName ? `${parentName} / ${channel.name}` : channel.name,
|
||||
type: channel.type,
|
||||
};
|
||||
})
|
||||
return Array.from(new Map(threads.map((thread) => [thread.id, thread])).values())
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,14 @@ export function startWebserver(
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/guilds/:guildId/threads", async (req, res, next) => {
|
||||
try {
|
||||
res.json(await voiceController.listThreads(req.params.guildId));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/connect", async (req, res, next) => {
|
||||
try {
|
||||
const { guildId, channelId } = req.body as {
|
||||
|
||||
Reference in New Issue
Block a user