fix(migrate-v2): correct JID parsing, Discord guildId lookup, silent failures
- shared.ts: parseJid now recognizes raw Baileys WhatsApp JIDs (`<id>@s.whatsapp.net`, `@g.us`, etc.); v2PlatformId returns the raw JID for whatsapp to match what the runtime adapter emits. Without this, every WhatsApp group in a v1 install was silently skipped. - discord-resolver.ts: new helper that uses DISCORD_BOT_TOKEN to look up channelId → guildId via the Discord API, since v1 stored only the channel id but v2 needs `discord:<guildId>:<channelId>`. Best-effort: on missing/invalid token or network error, returns empty resolver and the affected groups are skipped with the reason surfaced per channel. - db.ts, tasks.ts: route Discord groups through the resolver; other channels go through v2PlatformId unchanged. Resolver only built when at least one Discord group exists, so non-Discord installs incur no network. - db.ts: when every v1 group is skipped, exit non-zero with a FAIL line instead of `OK:groups=N,...,skipped=N`, so the wrapper doesn't hide total failure under a successful-looking summary. - migrate-v2.sh: run_step now surfaces ERROR: lines from successful steps (with count + first 3 + raw log path); phase 2c install loop populates STEP_RESULTS so install failures show in handoff.json instead of silently passing. - sessions.ts: copyTree skips dangling symlinks (e.g. v1's `.claude/debug/latest`) instead of crashing the entire step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
120
setup/migrate-v2/discord-resolver.ts
Normal file
120
setup/migrate-v2/discord-resolver.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Discord channel → guild resolver for the v1 → v2 migration.
|
||||
*
|
||||
* v1 stored Discord groups as `dc:<channelId>` — only the channel id, not
|
||||
* the guild id. v2's `@chat-adapter/discord` encodes `platform_id` as
|
||||
* `discord:<guildId>:<channelId>`, so we can't reconstruct it from v1 data
|
||||
* alone. Instead, we use the v1 bot token (carried forward by 1a-env) to
|
||||
* query the Discord API and build a channelId → guildId map.
|
||||
*
|
||||
* Network calls are best-effort: on auth failure or network error, the
|
||||
* resolver returns null for every channel and the caller falls back to
|
||||
* skipping with a clear warning.
|
||||
*/
|
||||
|
||||
const DISCORD_API = 'https://discord.com/api/v10';
|
||||
|
||||
interface Guild {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Channel {
|
||||
id: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface DiscordResolver {
|
||||
/** Returns `discord:<guildId>:<channelId>` or null if the channel isn't visible to the bot. */
|
||||
resolve(channelId: string): string | null;
|
||||
/** Diagnostic info — guild count and total channel count discovered. */
|
||||
stats(): { guilds: number; channels: number; reason?: string };
|
||||
}
|
||||
|
||||
/** A no-op resolver that returns null for every lookup with a stored reason. */
|
||||
function emptyResolver(reason: string): DiscordResolver {
|
||||
return {
|
||||
resolve: () => null,
|
||||
stats: () => ({ guilds: 0, channels: 0, reason }),
|
||||
};
|
||||
}
|
||||
|
||||
type FetchFn = typeof fetch;
|
||||
|
||||
async function getJson<T>(url: string, token: string, fetchImpl: FetchFn): Promise<T> {
|
||||
const res = await fetchImpl(url, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`,
|
||||
'User-Agent': 'NanoClaw-Migration (https://github.com/qwibitai/nanoclaw, 2.x)',
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '');
|
||||
throw new Error(`Discord API ${res.status} ${res.statusText}: ${body.slice(0, 200)}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Discord resolver by enumerating every guild the bot is in and
|
||||
* every channel in those guilds. Returns an empty resolver on any error.
|
||||
*
|
||||
* Costs: 1 + N HTTP calls (N = guild count). Discord's global rate limit
|
||||
* is 50 req/s; even installs with hundreds of guilds finish in under a
|
||||
* second of network time.
|
||||
*/
|
||||
export async function buildDiscordResolver(
|
||||
token: string,
|
||||
fetchImpl: FetchFn = fetch,
|
||||
): Promise<DiscordResolver> {
|
||||
if (!token) return emptyResolver('no DISCORD_BOT_TOKEN in .env');
|
||||
|
||||
// Page through guilds. Default page size is 200; loop until short page.
|
||||
const guilds: Guild[] = [];
|
||||
let after: string | null = null;
|
||||
try {
|
||||
while (true) {
|
||||
const url = new URL(`${DISCORD_API}/users/@me/guilds`);
|
||||
url.searchParams.set('limit', '200');
|
||||
if (after) url.searchParams.set('after', after);
|
||||
const page = await getJson<Guild[]>(url.toString(), token, fetchImpl);
|
||||
guilds.push(...page);
|
||||
if (page.length < 200) break;
|
||||
after = page[page.length - 1].id;
|
||||
}
|
||||
} catch (err) {
|
||||
return emptyResolver(`failed to list guilds: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
// Per-guild channel enumeration.
|
||||
const channelToGuild = new Map<string, string>();
|
||||
for (const guild of guilds) {
|
||||
try {
|
||||
const channels = await getJson<Channel[]>(
|
||||
`${DISCORD_API}/guilds/${guild.id}/channels`,
|
||||
token,
|
||||
fetchImpl,
|
||||
);
|
||||
for (const ch of channels) {
|
||||
channelToGuild.set(ch.id, guild.id);
|
||||
}
|
||||
} catch (err) {
|
||||
// Skip this guild but keep going — partial results are still useful.
|
||||
// The caller logs which channels couldn't be resolved.
|
||||
console.error(
|
||||
`WARN:discord-resolver: failed to enumerate guild ${guild.id} (${guild.name}): ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
resolve(channelId: string): string | null {
|
||||
const guildId = channelToGuild.get(channelId);
|
||||
if (!guildId) return null;
|
||||
return `discord:${guildId}:${channelId}`;
|
||||
},
|
||||
stats: () => ({ guilds: guilds.length, channels: channelToGuild.size }),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user