#!/usr/bin/env node /** * Export transcript channel messages with embed "Users in transcript" to JSONL. * Each line: { message_id, created, ticket_name, ticket_owner_id, users: [{ id, count }], total } * Usage: node scripts/export-transcript-embeds.js [maxMessages] [outputPath] * If outputPath is omitted, writes to stdout (redirect: node ... > transcript_embeds.jsonl). * If outputPath is given, writes JSONL to that file (avoids dotenv/logs mixing with JSON). */ const path = require('path'); const { Client, GatewayIntentBits } = require('discord.js'); require('dotenv').config({ path: path.join(__dirname, '../.env') }); require('dotenv').config({ path: path.join(__dirname, '../../.env') }); const fs = require('fs'); const TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; const channelId = process.argv[2]; const maxMessages = parseInt(process.argv[3], 10) || 10000; const outputPath = process.argv[4]; const PAGE = 100; // Parse "Users in transcript" value: "5 - <@123> - name#0\n 4 - <@456> - ..." function parseUsersInTranscript(value) { const users = []; let total = 0; const lines = (value || '').split(/\n/).map((s) => s.trim()).filter(Boolean); for (const line of lines) { const match = line.match(/^(\d+)\s+-\s+<@!?(\d+)>/); if (match) { const count = parseInt(match[1], 10); users.push({ id: match[2], count }); total += count; } } return { users, total }; } if (!TOKEN || !channelId) { console.error('Usage: node scripts/export-transcript-embeds.js [maxMessages]'); process.exit(1); } const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], }); client.once('ready', async () => { try { const channel = await client.channels.fetch(channelId).catch(() => null); if (!channel) { console.error('Channel not found or bot cannot access it.'); process.exit(1); } if (outputPath) { fs.writeFileSync(outputPath, ''); } let totalScanned = 0; let before = undefined; while (totalScanned < maxMessages) { const limit = Math.min(PAGE, maxMessages - totalScanned); const options = before ? { limit, before } : { limit }; const messages = await channel.messages.fetch(options); if (messages.size === 0) break; totalScanned += messages.size; for (const [, m] of messages.sort((a, b) => b.createdTimestamp - a.createdTimestamp)) { if (!m.embeds?.length) continue; for (const emb of m.embeds) { const usersField = emb.fields?.find((f) => f.name && f.name.toLowerCase().includes('users in transcript')); if (!usersField?.value) continue; const ticketNameField = emb.fields?.find((f) => f.name && f.name.toLowerCase().includes('ticket name')); const ticketName = ticketNameField?.value?.trim() || ''; const ownerField = emb.fields?.find((f) => f.name && f.name.toLowerCase().includes('ticket owner')); const ownerMatch = ownerField?.value?.match(/<@!?(\d+)>/); const ticket_owner_id = ownerMatch ? ownerMatch[1] : null; const { users, total } = parseUsersInTranscript(usersField.value); if (users.length === 0 && !ticket_owner_id) continue; const out = { message_id: m.id, created: m.createdAt.toISOString(), ticket_name: ticketName, ticket_owner_id: ticket_owner_id || undefined, users, total, }; const line = JSON.stringify(out) + '\n'; if (outputPath) { fs.appendFileSync(outputPath, line); } else { process.stdout.write(line); } } } const oldestMsg = messages.reduce((a, m) => (m.createdTimestamp < (a?.createdTimestamp ?? Infinity) ? m : a), null); before = oldestMsg?.id; if (messages.size < PAGE) break; } process.stderr.write('Scanned ' + totalScanned + ' messages\n'); } catch (e) { console.error(e.message || e); } finally { client.destroy(); process.exit(0); } }); client.login(TOKEN).catch((e) => { console.error('Login failed:', e.message); process.exit(1); });