diff --git a/scripts/bulk-lookup-users-v2.js b/scripts/bulk-lookup-users-v2.js deleted file mode 100644 index a8922ad..0000000 --- a/scripts/bulk-lookup-users-v2.js +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env node -/** - * Bulk lookup Discord user information - IMPROVED VERSION - * - * Features: - * - Saves progress incrementally (every 100 users) - * - Can resume from where it left off - * - Better error handling - * - Uses guild member cache when possible - * - * Usage: - * node scripts/bulk-lookup-users-v2.js - */ - -const fs = require('fs'); -const path = require('path'); -const { Client, GatewayIntentBits } = require('discord.js'); - -// Load environment variables -const envPath = path.join(__dirname, '../../.env'); -const result = require('dotenv').config({ path: envPath }); - -const TOKEN = process.env.DISCORD_BOT_TOKEN || process.env.DISCORD_TOKEN; - -if (!TOKEN) { - console.error('Error: DISCORD_BOT_TOKEN must be set in .env'); - process.exit(1); -} - -// Parse command line args -const args = process.argv.slice(2); -if (args.length < 2) { - console.error('Usage: node scripts/bulk-lookup-users-v2.js '); - process.exit(1); -} - -const inputFile = args[0]; -const outputFile = args[1]; - -// Read user IDs from input file -const userIds = fs.readFileSync(inputFile, 'utf-8') - .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0); - -console.log(`✅ Loaded ${userIds.length} user IDs from ${inputFile}`); - -// Load existing results if any (for resume capability) -let results = {}; -let processed = 0; -let errors = 0; - -if (fs.existsSync(outputFile)) { - try { - const existing = JSON.parse(fs.readFileSync(outputFile, 'utf-8')); - results = existing.users || {}; - processed = Object.keys(results).length; - errors = existing.errors || 0; - console.log(`📂 Found existing results: ${processed} users already processed`); - } catch (e) { - console.log(`⚠️ Could not load existing results, starting fresh`); - } -} - -const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers - ] -}); - -async function lookupUser(userId) { - // Skip if already processed - if (results[userId]) { - return results[userId]; - } - - try { - const user = await client.users.fetch(userId); - return { - success: true, - id: user.id, - username: user.username, - globalName: user.globalName || user.username, - tag: user.tag, - bot: user.bot, - avatar: user.displayAvatarURL() - }; - } catch (error) { - return { - success: false, - id: userId, - error: error.message, - username: null, - globalName: null, - tag: null, - bot: false - }; - } -} - -function saveResults() { - const output = { - timestamp: new Date().toISOString(), - total_users: userIds.length, - processed: processed, - successful: processed - errors, - errors: errors, - users: results - }; - - fs.writeFileSync(outputFile, JSON.stringify(output, null, 2)); -} - -async function processUsers() { - console.log('\n🚀 Starting bulk lookup...'); - console.log(` Progress will be saved every 100 users\n`); - - const startTime = Date.now(); - const startProcessed = processed; - - // Filter out already processed users - const toProcess = userIds.filter(id => !results[id]); - console.log(` ${toProcess.length} users remaining to process\n`); - - // Process one at a time (safer and can still be reasonably fast) - for (let i = 0; i < toProcess.length; i++) { - const userId = toProcess[i]; - - const result = await lookupUser(userId); - results[result.id] = result; - - if (!result.success) { - errors++; - } - processed++; - - // Save every 100 users - if (processed % 100 === 0) { - saveResults(); - const elapsed = ((Date.now() - startTime) / 1000); - const rate = (processed - startProcessed) / elapsed; - const remaining = (toProcess.length - i - 1) / rate; - console.log(`💾 Progress: ${processed}/${userIds.length} (${errors} errors) - saved checkpoint - ~${remaining.toFixed(0)}s remaining`); - } - - // Slower delay to avoid rate limits (500ms = 2 requests/second - more reliable) - await new Promise(resolve => setTimeout(resolve, 500)); - } - - // Final save - saveResults(); - - const totalTime = ((Date.now() - startTime) / 1000); - - console.log(`\n${'='.repeat(70)}`); - console.log(`✅ Lookup Complete!`); - console.log(`${'='.repeat(70)}`); - console.log(` Total time: ${totalTime.toFixed(1)}s`); - console.log(` Total processed: ${processed}/${userIds.length}`); - console.log(` Successful: ${processed - errors} (${((processed - errors)/userIds.length*100).toFixed(1)}%)`); - console.log(` Errors: ${errors}`); - console.log(` Rate: ${((processed - startProcessed)/totalTime).toFixed(1)} users/second`); - console.log(`\n💾 Saved to: ${outputFile}\n`); - - // Sample successful results - const sample = Object.values(results).filter(r => r.success).slice(0, 5); - if (sample.length > 0) { - console.log('📋 Sample results:'); - sample.forEach(u => console.log(` ${u.username} (${u.id})`)); - } - - process.exit(0); -} - -client.once('ready', () => { - console.log(`✅ Bot logged in as ${client.user.tag}\n`); - processUsers(); -}); - -client.on('error', (error) => { - console.error('❌ Discord client error:', error); -}); - -// Handle graceful shutdown -process.on('SIGINT', () => { - console.log('\n\n⚠️ Interrupted! Saving progress...'); - saveResults(); - console.log('✅ Progress saved. You can resume by running the same command again.\n'); - process.exit(0); -}); - -client.login(TOKEN); diff --git a/scripts/bulk-lookup-users.js b/scripts/bulk-lookup-users.js deleted file mode 100644 index ef88d2f..0000000 --- a/scripts/bulk-lookup-users.js +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env node -/** - * Bulk lookup Discord user information - * - * Usage: - * node scripts/bulk-lookup-users.js - * - * Input file: Text file with one user ID per line - * Output file: JSON file with user lookup results - */ - -const fs = require('fs'); -const path = require('path'); -const { Client, GatewayIntentBits } = require('discord.js'); - -// Load environment variables from repo root -const envPath = path.join(__dirname, '../../.env'); -console.log(`Loading .env from: ${envPath}`); -const result = require('dotenv').config({ path: envPath }); -if (result.error) { - console.error(`Error loading .env: ${result.error.message}`); - // Try broccolini-bot/.env as fallback - require('dotenv').config({ path: path.join(__dirname, '../.env') }); -} - -const TOKEN = process.env.DISCORD_BOT_TOKEN || process.env.DISCORD_TOKEN; -const GUILD_ID = process.env.GUILD_ID || process.env.SERVER_ID; - -if (!TOKEN) { - console.error('Error: DISCORD_BOT_TOKEN must be set in .env'); - console.error('Available env vars:', Object.keys(process.env).filter(k => k.includes('DISCORD'))); - process.exit(1); -} - -// Parse command line args -const args = process.argv.slice(2); -if (args.length < 2) { - console.error('Usage: node scripts/bulk-lookup-users.js '); - process.exit(1); -} - -const inputFile = args[0]; -const outputFile = args[1]; - -// Read user IDs from input file -const userIds = fs.readFileSync(inputFile, 'utf-8') - .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0); - -console.log(`Loaded ${userIds.length} user IDs from ${inputFile}`); - -const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers - ] -}); - -const results = {}; -let processed = 0; -let errors = 0; - -async function lookupUser(userId) { - try { - // Add timeout to prevent hanging - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Lookup timeout')), 10000) - ); - - const fetchPromise = client.users.fetch(userId); - const user = await Promise.race([fetchPromise, timeoutPromise]); - - return { - success: true, - id: user.id, - username: user.username, - globalName: user.globalName || user.username, - tag: user.tag, - bot: user.bot, - avatar: user.displayAvatarURL() - }; - } catch (error) { - // Handle errors (not found, timeout, rate limit) - if (error.message.includes('429')) { - console.log(` ⚠️ Rate limit hit for user ${userId}, will retry`); - } - return { - success: false, - id: userId, - error: error.message, - username: null, - globalName: null, - tag: null, - bot: false - }; - } -} - -async function processUsers() { - console.log('\nStarting bulk lookup...'); - console.log('This will take a few minutes for 2,428 users\n'); - - const startTime = Date.now(); - - // Process in batches to avoid rate limits - const BATCH_SIZE = 3; // Very small batches to avoid rate limits - const DELAY_MS = 2000; // 2 seconds between batches - - for (let i = 0; i < userIds.length; i += BATCH_SIZE) { - const batch = userIds.slice(i, i + BATCH_SIZE); - - // Lookup batch in parallel - const promises = batch.map(userId => lookupUser(userId)); - const batchResults = await Promise.all(promises); - - // Store results - batchResults.forEach(result => { - if (!result.success) { - errors++; - } - results[result.id] = result; - processed++; - }); - - // Log every batch for debugging - if (processed <= 50) { - console.log(` Batch complete: ${processed} users processed`); - } - - // Progress update every 100 users - if (processed % 100 === 0 || processed === userIds.length) { - const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); - const rate = (processed / elapsed).toFixed(1); - const remaining = ((userIds.length - processed) / rate).toFixed(0); - console.log(`Progress: ${processed}/${userIds.length} (${errors} errors) - ${elapsed}s elapsed, ~${remaining}s remaining`); - } - - // Wait before next batch to avoid rate limits - if (i + BATCH_SIZE < userIds.length) { - await new Promise(resolve => setTimeout(resolve, DELAY_MS)); - } - } - - const totalTime = ((Date.now() - startTime) / 1000).toFixed(1); - console.log(`\n✅ Completed in ${totalTime}s`); - console.log(` Successful: ${processed - errors}`); - console.log(` Errors: ${errors}`); - - // Save results - const output = { - timestamp: new Date().toISOString(), - total_users: userIds.length, - successful: processed - errors, - errors: errors, - users: results - }; - - fs.writeFileSync(outputFile, JSON.stringify(output, null, 2)); - console.log(`\n💾 Saved results to ${outputFile}`); - - process.exit(0); -} - -client.once('ready', () => { - console.log(`✅ Bot logged in as ${client.user.tag}`); - processUsers(); -}); - -client.on('error', (error) => { - console.error('Discord client error:', error); -}); - -client.login(TOKEN); diff --git a/scripts/export-transcript-embeds.js b/scripts/export-transcript-embeds.js deleted file mode 100644 index 3764384..0000000 --- a/scripts/export-transcript-embeds.js +++ /dev/null @@ -1,109 +0,0 @@ -#!/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); -}); diff --git a/scripts/fetch-channel-messages.js b/scripts/fetch-channel-messages.js deleted file mode 100644 index 2e26116..0000000 --- a/scripts/fetch-channel-messages.js +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env node -/** - * Fetch recent messages from a Discord channel. - * Usage: node scripts/fetch-channel-messages.js [limit] - * Default limit: 10 - */ -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 TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; -const channelId = process.argv[2]; -const limit = Math.min(parseInt(process.argv[3], 10) || 10, 100); - -if (!TOKEN || !channelId) { - console.error('Usage: node scripts/fetch-channel-messages.js [limit]'); - 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.log('Channel not found or bot cannot access it.'); - process.exit(0); - } - const messages = await channel.messages.fetch({ limit }); - console.log('Channel:', channel.name, '(' + channel.id + ')'); - console.log('Messages fetched:', messages.size, '(requested', limit + ')'); - if (messages.size === 0) { - console.log('No messages visible (empty channel or no Read Message History permission).'); - process.exit(0); - } - for (const [, m] of messages.sort((a, b) => b.createdTimestamp - a.createdTimestamp)) { - const preview = (m.content || '(embed/attachment only)').slice(0, 80); - console.log('---'); - console.log('ID:', m.id, '| Author:', m.author.tag, '|', m.createdAt.toISOString()); - console.log(preview + (m.content && m.content.length > 80 ? '...' : '')); - } - } 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); -}); diff --git a/scripts/fetch-channel.js b/scripts/fetch-channel.js deleted file mode 100644 index 391a939..0000000 --- a/scripts/fetch-channel.js +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env node -/** - * Fetch a Discord channel by ID and print its name and type. - * Usage: node scripts/fetch-channel.js - * Example: node scripts/fetch-channel.js 1335424071227281520 - * - * Uses DISCORD_BOT_TOKEN or MEMBER_BOT_TOKEN from .env (broccolini-bot or parent). - */ -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 TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; -const channelId = process.argv[2]; - -if (!TOKEN) { - console.error('❌ No bot token (DISCORD_BOT_TOKEN or MEMBER_BOT_TOKEN)'); - process.exit(1); -} -if (!channelId) { - console.error('Usage: node scripts/fetch-channel.js '); - process.exit(1); -} - -const client = new Client({ intents: [GatewayIntentBits.Guilds] }); - -client.once('ready', async () => { - try { - const channel = await client.channels.fetch(channelId).catch((err) => null); - if (!channel) { - console.log('Channel not found or bot cannot access it.'); - process.exit(0); - } - console.log('Channel ID:', channel.id); - console.log('Name:', channel.name); - console.log('Type:', channel.type); - if (channel.guild) console.log('Guild:', channel.guild.name, `(${channel.guild.id})`); - } 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); -}); diff --git a/scripts/fetch-message.js b/scripts/fetch-message.js deleted file mode 100644 index a0843c9..0000000 --- a/scripts/fetch-message.js +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node -/** - * Fetch a Discord message by channel ID and message ID. - * Usage: node scripts/fetch-message.js - */ -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 TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; -const [channelId, messageId] = process.argv.slice(2); - -if (!TOKEN || !channelId || !messageId) { - console.error('Usage: node scripts/fetch-message.js '); - 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.log('Channel not found or bot cannot access it.'); - process.exit(0); - } - const message = await channel.messages.fetch(messageId).catch((err) => null); - if (!message) { - console.log('Message not found (wrong channel, deleted, or no access).'); - process.exit(0); - } - console.log('Channel:', channel.name, '(' + channel.id + ')'); - console.log('Message ID:', message.id); - console.log('Author:', message.author.tag, '(' + message.author.id + ')'); - console.log('Created:', message.createdAt ? message.createdAt.toISOString() : message.createdTimestamp); - console.log('Content:', message.content || '(empty or embed only)'); - if (message.embeds && message.embeds.length) { - message.embeds.forEach((emb, i) => { - console.log('\n--- Embed', i + 1, '---'); - if (emb.title) console.log('Title:', emb.title); - if (emb.description) console.log('Description:', emb.description); - if (emb.url) console.log('URL:', emb.url); - if (emb.fields && emb.fields.length) { - emb.fields.forEach((f) => console.log('Field:', f.name, '\n', f.value)); - } - if (emb.footer?.text) console.log('Footer:', emb.footer.text); - // Ticket name for display (e.g. "indifferentketchup🍅" from "indifferentketchup🍅-claimed-7235") - const ticketNameField = emb.fields?.find((f) => f.name && f.name.toLowerCase().includes('ticket name')); - if (ticketNameField?.value) { - const full = ticketNameField.value.trim(); - const short = full.replace(/-claimed-\d+$/, '').trim(); - console.log('Ticket (short):', short || full); - } - }); - } - } 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); -}); diff --git a/scripts/find-transcript-by-member.js b/scripts/find-transcript-by-member.js deleted file mode 100644 index 1c49631..0000000 --- a/scripts/find-transcript-by-member.js +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env node -/** - * Find transcript channel messages whose embed "Users in transcript" lists a given member ID. - * Usage: node scripts/find-transcript-by-member.js [maxMessages] - * Example: node scripts/find-transcript-by-member.js 1335424071227281520 219276746153787392 500 - * Fetches in pages of 100; maxMessages limits total (e.g. 500 = 5 pages). Default 100. - */ -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 TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; -const channelId = process.argv[2]; -const memberId = process.argv[3]; -const maxMessages = parseInt(process.argv[4], 10) || 100; -const PAGE = 100; - -if (!TOKEN || !channelId || !memberId) { - console.error('Usage: node scripts/find-transcript-by-member.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.log('Channel not found or bot cannot access it.'); - process.exit(0); - } - console.log('Channel:', channel.name, '(' + channel.id + ')'); - console.log('Looking for member ID', memberId, 'in embed "Users in transcript"'); - const memberRef = `<@${memberId}>`; - let totalScanned = 0; - let found = 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 || !usersField.value.includes(memberRef)) continue; - const ticketNameField = emb.fields?.find((f) => f.name && f.name.toLowerCase().includes('ticket name')); - const ticketName = ticketNameField?.value?.trim() || '(no Ticket Name field)'; - console.log('\n--- Match ---'); - console.log('Message ID:', m.id); - console.log('Created:', m.createdAt.toISOString()); - console.log('Ticket Name:', ticketName); - console.log('Users in transcript:\n' + usersField.value); - found++; - } - } - const oldestMsg = messages.reduce((a, m) => (m.createdTimestamp < (a?.createdTimestamp ?? Infinity) ? m : a), null); - before = oldestMsg?.id; - if (messages.size < PAGE) break; - } - console.log('\nTotal messages scanned:', totalScanned); - console.log('Total messages matching member', memberId, ':', found); - } 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); -}); diff --git a/scripts/find-transcript-by-owner.js b/scripts/find-transcript-by-owner.js deleted file mode 100644 index eddc944..0000000 --- a/scripts/find-transcript-by-owner.js +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env node -/** - * Find transcript messages whose embed "Ticket Owner" is a given user ID. - * Usage: node scripts/find-transcript-by-owner.js [totalMessages] [maxMessages] - * If totalMessages is given, only show messages where "Users in transcript" sum equals that. - * Example: node scripts/find-transcript-by-owner.js 1335424071227281520 241129484483297280 5 10000 - */ -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 TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; -const channelId = process.argv[2]; -const ownerId = process.argv[3]; -const totalMessages = parseInt(process.argv[4], 10) || null; -const maxMessages = parseInt(process.argv[5], 10) || 10000; -const PAGE = 100; - -function parseUsersTotal(value) { - let total = 0; - (value || '').split(/\n/).forEach((line) => { - const m = line.trim().match(/^(\d+)\s+-\s+<@!?\d+>/); - if (m) total += parseInt(m[1], 10); - }); - return total; -} - -if (!TOKEN || !channelId || !ownerId) { - console.error('Usage: node scripts/find-transcript-by-owner.js [totalMessages] [maxMessages]'); - process.exit(1); -} - -const ownerRef = `<@${ownerId}>`; -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); - } - console.error('Channel:', channel.name, '(' + channel.id + ')'); - console.error('Looking for Ticket Owner', ownerId, totalMessages != null ? 'and total=' + totalMessages : ''); - let totalScanned = 0; - let before = undefined; - let found = 0; - 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 ownerField = emb.fields?.find((f) => f.name && f.name.toLowerCase().includes('ticket owner')); - if (!ownerField?.value || !ownerField.value.includes(ownerRef)) continue; - const usersField = emb.fields?.find((f) => f.name && f.name.toLowerCase().includes('users in transcript')); - const total = usersField?.value ? parseUsersTotal(usersField.value) : 0; - if (totalMessages != null && total !== totalMessages) continue; - const ticketNameField = emb.fields?.find((f) => f.name && f.name.toLowerCase().includes('ticket name')); - const ticketName = ticketNameField?.value?.trim() || ''; - console.log('Message ID:', m.id); - console.log('Created:', m.createdAt.toISOString()); - console.log('Ticket Name:', ticketName); - console.log('Total messages:', total); - console.log('---'); - found++; - } - } - const oldestMsg = messages.reduce((a, msg) => (msg.createdTimestamp < (a?.createdTimestamp ?? Infinity) ? msg : a), null); - before = oldestMsg?.id; - if (messages.size < PAGE) break; - } - console.error('Scanned', totalScanned, 'messages, matches:', found); - } 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); -}); diff --git a/scripts/lookup-user.js b/scripts/lookup-user.js deleted file mode 100644 index 2e4cdf1..0000000 --- a/scripts/lookup-user.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Look up a Discord user by ID. Uses repo root .env for token so it works without broccolini-bot config. - * Usage: node scripts/lookup-user.js [user_id] - * Run from broccolini-bot/ (or use full path to script). - */ -const path = require('path'); -require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); - -const token = (process.env.DISCORD_BOT_TOKEN || process.env.DISCORD_TOKEN || '').trim(); -if (!token) { - console.error('Set DISCORD_BOT_TOKEN or DISCORD_TOKEN in repo root .env (/IB-Discord-Bot/.env)'); - process.exit(1); -} - -const { Client, GatewayIntentBits } = require('discord.js'); -const userId = process.argv[2] || '140081819986034688'; - -const client = new Client({ intents: [GatewayIntentBits.Guilds] }); - -client.once('ready', async () => { - try { - const user = await client.users.fetch(userId); - console.log('User:', { - id: user.id, - username: user.username, - globalName: user.globalName ?? user.username, - tag: user.tag, - bot: user.bot - }); - } catch (err) { - console.error('Lookup failed:', err.message); - if (err.code === 10013) console.error('Unknown user, or bot does not share a server with this user.'); - } finally { - client.destroy(); - process.exit(0); - } -}); - -client.login(token); diff --git a/scripts/lookup-with-dedicated-bot.js b/scripts/lookup-with-dedicated-bot.js deleted file mode 100644 index d0a2dd9..0000000 --- a/scripts/lookup-with-dedicated-bot.js +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env node -/** - * User lookup using a dedicated minimal-permissions bot - * - * This bot: - * - Has NO server permissions - * - Only needs to be in the server - * - Uses separate token from main bot - * - Won't affect your main bot's rate limits - * - * Usage: - * LOOKUP_BOT_TOKEN=your_token node scripts/lookup-with-dedicated-bot.js - */ - -const fs = require('fs'); -const path = require('path'); -const { Client, GatewayIntentBits } = require('discord.js'); - -// Load environment -require('dotenv').config({ path: path.join(__dirname, '../../.env') }); - -// Use dedicated bot token OR fall back to main bot -const TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.LOOKUP_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; - -if (!TOKEN) { - console.error('❌ Error: No bot token found'); - console.error(' Set MEMBER_BOT_TOKEN in .env or use DISCORD_BOT_TOKEN'); - process.exit(1); -} - -const args = process.argv.slice(2); -if (args.length < 2) { - console.error('Usage: node scripts/lookup-with-dedicated-bot.js '); - process.exit(1); -} - -const inputFile = args[0]; -const outputFile = args[1]; - -// Read user IDs -const userIds = fs.readFileSync(inputFile, 'utf-8') - .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0); - -console.log(`✅ Loaded ${userIds.length} user IDs`); - -// Load existing results -let results = {}; -let processed = 0; -let errors = 0; - -if (fs.existsSync(outputFile)) { - try { - const existing = JSON.parse(fs.readFileSync(outputFile, 'utf-8')); - results = existing.users || {}; - processed = Object.keys(results).length; - errors = existing.errors || 0; - console.log(`📂 Found existing: ${processed} users`); - } catch (e) { - console.log(`⚠️ Starting fresh`); - } -} - -// Create bot with MINIMAL intents -const client = new Client({ - intents: [ - GatewayIntentBits.Guilds // Only need this to stay in server - // NO other intents needed! - ] -}); - -async function lookupUser(userId) { - if (results[userId]) return results[userId]; - - try { - const user = await client.users.fetch(userId); - return { - success: true, - id: user.id, - username: user.username, - globalName: user.globalName || user.username, - tag: user.tag, - bot: user.bot, - avatar: user.displayAvatarURL() - }; - } catch (error) { - return { - success: false, - id: userId, - error: error.message, - username: null, - globalName: null, - tag: null, - bot: false - }; - } -} - -function saveResults() { - const output = { - timestamp: new Date().toISOString(), - total_users: userIds.length, - processed: processed, - successful: processed - errors, - errors: errors, - bot_type: (process.env.MEMBER_BOT_TOKEN || process.env.LOOKUP_BOT_TOKEN) ? 'dedicated' : 'main', - users: results - }; - - fs.writeFileSync(outputFile, JSON.stringify(output, null, 2)); -} - -async function processUsers() { - console.log('\n🚀 Starting lookups...'); - const isDedicated = !!(process.env.MEMBER_BOT_TOKEN || process.env.LOOKUP_BOT_TOKEN); - console.log(` Bot type: ${isDedicated ? '✅ Dedicated lookup bot' : '⚠️ Main bot'}`); - console.log(` Rate: SLOW (1 user/second for safety)`); - console.log(); - - const startTime = Date.now(); - const toProcess = userIds.filter(id => !results[id]); - console.log(` ${toProcess.length} users remaining\n`); - - for (let i = 0; i < toProcess.length; i++) { - const userId = toProcess[i]; - - const result = await lookupUser(userId); - results[result.id] = result; - - if (!result.success) errors++; - processed++; - - // Save every 10 users for frequent updates - if (processed % 10 === 0) { - saveResults(); - const elapsed = (Date.now() - startTime) / 1000; - const rate = (processed - (userIds.length - toProcess.length)) / elapsed; - const remaining = (toProcess.length - i - 1) / rate; - console.log(`💾 ${processed}/${userIds.length} (${errors} errors) - saved - ~${remaining.toFixed(0)}s left`); - } - - // Very slow to avoid rate limits (1/second) - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - saveResults(); - - const totalTime = (Date.now() - startTime) / 1000; - console.log(`\n${'='.repeat(60)}`); - console.log(`✅ Complete!`); - console.log(`${'='.repeat(60)}`); - console.log(` Time: ${totalTime.toFixed(1)}s`); - console.log(` Processed: ${processed}/${userIds.length}`); - console.log(` Successful: ${processed - errors}`); - console.log(` Errors: ${errors}`); - console.log(`\n💾 Saved to: ${outputFile}\n`); - - process.exit(0); -} - -client.once('ready', () => { - const isDedicated = !!(process.env.MEMBER_BOT_TOKEN || process.env.LOOKUP_BOT_TOKEN); - const botType = isDedicated ? 'DEDICATED LOOKUP BOT' : 'Main Bot'; - console.log(`✅ Logged in as ${client.user.tag}`); - console.log(` Type: ${botType}`); - console.log(); - processUsers(); -}); - -client.on('error', (error) => { - console.error('❌ Error:', error.message); -}); - -process.on('SIGINT', () => { - console.log('\n\n⚠️ Interrupted! Saving...'); - saveResults(); - console.log('✅ Saved. Resume by running same command.\n'); - process.exit(0); -}); - -console.log('🔌 Connecting to Discord...'); -client.login(TOKEN); diff --git a/scripts/lookup-with-roles.js b/scripts/lookup-with-roles.js deleted file mode 100644 index 7af72ba..0000000 --- a/scripts/lookup-with-roles.js +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env node -/** - * Discord user lookup WITH ROLES - * - * Fetches: - * - User info (username, display name, avatar) - * - Guild member info (roles, join date, server nickname) - * - All Palpocalypse server roles - * - * Requires: Server Members Intent enabled in Discord Developer Portal - */ - -const fs = require('fs'); -const path = require('path'); -const { Client, GatewayIntentBits } = require('discord.js'); - -require('dotenv').config({ path: path.join(__dirname, '../../.env') }); - -const TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; -const GUILD_ID = '798321161082896395'; // Indifferent Broccoli server - -if (!TOKEN) { - console.error('❌ Error: No bot token found'); - process.exit(1); -} - -const args = process.argv.slice(2); -if (args.length < 2) { - console.error('Usage: node scripts/lookup-with-roles.js '); - process.exit(1); -} - -const inputFile = args[0]; -const outputFile = args[1]; - -const userIds = fs.readFileSync(inputFile, 'utf-8') - .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0); - -console.log(`✅ Loaded ${userIds.length} user IDs`); - -let results = {}; -let processed = 0; -let errors = 0; - -if (fs.existsSync(outputFile)) { - try { - const existing = JSON.parse(fs.readFileSync(outputFile, 'utf-8')); - results = existing.users || {}; - processed = Object.keys(results).length; - errors = existing.errors || 0; - console.log(`📂 Found existing: ${processed} users`); - } catch (e) { - console.log(`⚠️ Starting fresh`); - } -} - -const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers // Required for roles! - ] -}); - -let guild = null; - -async function lookupUserWithRoles(userId) { - if (results[userId]) return results[userId]; - - try { - // Fetch basic user info - const user = await client.users.fetch(userId); - - // Try to fetch guild member (for roles) - let roles = []; - let serverNickname = null; - let joinedAt = null; - let isInServer = false; - - try { - const member = await guild.members.fetch(userId); - isInServer = true; - serverNickname = member.nickname; - joinedAt = member.joinedAt ? member.joinedAt.toISOString() : null; - - // Get all roles except @everyone - roles = member.roles.cache - .filter(role => role.name !== '@everyone') - .map(role => ({ - id: role.id, - name: role.name, - color: role.hexColor, - position: role.position - })) - .sort((a, b) => b.position - a.position); // Highest role first - - } catch (memberError) { - // User exists but not in this server - isInServer = false; - } - - return { - success: true, - id: user.id, - username: user.username, - globalName: user.globalName || user.username, - tag: user.tag, - bot: user.bot, - avatar: user.displayAvatarURL(), - // Server-specific data - server_nickname: serverNickname, - joined_at: joinedAt, - in_server: isInServer, - roles: roles, - role_names: roles.map(r => r.name), - highest_role: roles[0]?.name || null - }; - - } catch (error) { - return { - success: false, - id: userId, - error: error.message, - username: null, - globalName: null, - roles: [] - }; - } -} - -function saveResults() { - const output = { - timestamp: new Date().toISOString(), - total_users: userIds.length, - processed: processed, - successful: processed - errors, - errors: errors, - guild_id: GUILD_ID, - includes_roles: true, - users: results - }; - - fs.writeFileSync(outputFile, JSON.stringify(output, null, 2)); -} - -async function processUsers() { - console.log('\n🎭 Starting lookups WITH ROLES...'); - console.log(` Guild ID: ${GUILD_ID}`); - console.log(` Rate: 1 user/second\n`); - - // Fetch guild - guild = await client.guilds.fetch(GUILD_ID); - console.log(`✅ Connected to: ${guild.name}\n`); - - const startTime = Date.now(); - const toProcess = userIds.filter(id => !results[id]); - console.log(` ${toProcess.length} users remaining\n`); - - for (let i = 0; i < toProcess.length; i++) { - const userId = toProcess[i]; - - const result = await lookupUserWithRoles(userId); - results[result.id] = result; - - if (!result.success) errors++; - processed++; - - // Save every 10 users - if (processed % 10 === 0) { - saveResults(); - const elapsed = (Date.now() - startTime) / 1000; - const rate = (processed - (userIds.length - toProcess.length)) / elapsed; - const remaining = (toProcess.length - i - 1) / rate; - - // Show sample with roles - if (result.success && result.roles.length > 0) { - const rolePreview = result.role_names.slice(0, 2).join(', '); - console.log(`💾 ${processed}/${userIds.length} - ${result.globalName} [${rolePreview}] - ~${remaining.toFixed(0)}s left`); - } else { - console.log(`💾 ${processed}/${userIds.length} (${errors} errors) - ~${remaining.toFixed(0)}s left`); - } - } - - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - saveResults(); - - const totalTime = (Date.now() - startTime) / 1000; - - // Stats - const usersWithRoles = Object.values(results).filter(u => u.success && u.roles.length > 0).length; - const allRoleNames = new Set(); - Object.values(results).forEach(u => { - if (u.success) { - u.role_names?.forEach(r => allRoleNames.add(r)); - } - }); - - console.log(`\n${'='.repeat(70)}`); - console.log(`✅ Complete with Roles!`); - console.log(`${'='.repeat(70)}`); - console.log(` Time: ${totalTime.toFixed(1)}s`); - console.log(` Processed: ${processed}/${userIds.length}`); - console.log(` Successful: ${processed - errors}`); - console.log(` Users with roles: ${usersWithRoles}`); - console.log(` Unique roles found: ${allRoleNames.size}`); - console.log(`\n💾 Saved to: ${outputFile}\n`); - - // Show some roles - if (allRoleNames.size > 0) { - console.log('📋 Sample roles found:'); - Array.from(allRoleNames).slice(0, 10).forEach(r => console.log(` • ${r}`)); - } - - process.exit(0); -} - -client.once('ready', () => { - console.log(`✅ Logged in as ${client.user.tag}\n`); - processUsers(); -}); - -client.on('error', (error) => { - console.error('❌ Error:', error.message); -}); - -process.on('SIGINT', () => { - console.log('\n\n⚠️ Interrupted! Saving...'); - saveResults(); - console.log('✅ Saved. Resume by running same command.\n'); - process.exit(0); -}); - -console.log('🔌 Connecting to Discord...'); -client.login(TOKEN); diff --git a/scripts/map-batch-to-transcript.js b/scripts/map-batch-to-transcript.js deleted file mode 100644 index 892c4c7..0000000 --- a/scripts/map-batch-to-transcript.js +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env node -/** - * Map batch tickets (TICKET: guild_channelId_suffix) to transcript channel messages. - * - * Connection: - * - Batch line: TICKET: 798321161082896395_1423340928588054621_indiffe → channelId = 1423340928588054621. - * - Transcript channel (🖥️│transcripts): each message is an embed with "Ticket Name: indifferentketchup🍅-claimed-7235". - * - Embed does NOT include channel ID, so we match by (1) ticket name (when known) or (2) time: transcript posted when ticket closes. - * - * Usage: - * node scripts/map-batch-to-transcript.js list [limit] -- fetch transcript messages, output CSV (messageId, created, ticket_name) - * node scripts/map-batch-to-transcript.js find -- find transcript message(s) likely for this ticket (by time window) - * - * Known mapping (from embed): 1423340928588054621 ↔ message 1423400708769579120 (Ticket: indifferentketchup🍅-claimed-7235). - */ -const path = require('path'); -const fs = require('fs'); -const { Client, GatewayIntentBits } = require('discord.js'); - -require('dotenv').config({ path: path.join(__dirname, '../.env') }); -require('dotenv').config({ path: path.join(__dirname, '../../.env') }); - -const TOKEN = process.env.MEMBER_BOT_TOKEN || process.env.DISCORD_BOT_TOKEN; -const TRANSCRIPT_CHANNEL_ID = '1335424071227281520'; -const METRICS_CSV = path.join(__dirname, '../../Discord Ticket Transcripts/transcript_metrics_per_ticket.csv'); - -function getTicketNameFromEmbed(emb) { - const f = emb.fields?.find((x) => x.name && x.name.toLowerCase().includes('ticket name')); - return f ? f.value.trim() : null; -} - -async function fetchTranscriptMessages(client, limit = 100) { - const channel = await client.channels.fetch(TRANSCRIPT_CHANNEL_ID).catch(() => null); - if (!channel) return []; - const cap = Math.min(limit, 100); // Discord API max 100 per request - const messages = await channel.messages.fetch({ limit: cap }); - const out = []; - for (const [, m] of messages) { - const emb = m.embeds?.[0]; - const ticketName = emb ? getTicketNameFromEmbed(emb) : null; - out.push({ - messageId: m.id, - created: m.createdAt ? m.createdAt.toISOString() : m.createdTimestamp, - createdTs: m.createdTimestamp, - ticketName: ticketName || '', - }); - } - out.sort((a, b) => b.createdTs - a.createdTs); - return out; -} - -function loadMetricsCsv() { - if (!fs.existsSync(METRICS_CSV)) return []; - const text = fs.readFileSync(METRICS_CSV, 'utf8'); - const lines = text.split(/\r?\n/).filter((l) => l.trim()); - const header = lines[0].split(','); - const ticketIdIdx = header.indexOf('ticket_id'); - const lastTsIdx = header.indexOf('last_message_ts'); - if (ticketIdIdx === -1 || lastTsIdx === -1) return []; - const rows = []; - for (let i = 1; i < lines.length; i++) { - const parts = lines[i].split(','); - const ticketId = parts[ticketIdIdx]; - const lastTs = parseInt(parts[lastTsIdx], 10); - if (!ticketId || !ticketId.includes('_')) continue; - const channelId = ticketId.split('_')[1]; - if (channelId && !isNaN(lastTs)) rows.push({ ticketId, channelId, last_message_ts: lastTs }); - } - return rows; -} - -async function main() { - const cmd = process.argv[2]; - const arg = process.argv[3]; - - if (!TOKEN) { - console.error('No bot token'); - process.exit(1); - } - - const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], - }); - - await new Promise((resolve, reject) => { - client.once('ready', resolve); - client.login(TOKEN).catch(reject); - }); - - try { - if (cmd === 'list') { - const limit = Math.min(parseInt(arg, 10) || 100, 100); - const list = await fetchTranscriptMessages(client, limit); - console.log('transcript_message_id,created_iso,ticket_name'); - list.forEach((r) => console.log([r.messageId, r.created, r.ticketName].map((c) => `"${String(c).replace(/"/g, '""')}"`).join(','))); - return; - } - - if (cmd === 'find' && arg) { - const channelId = arg.trim(); - const metrics = loadMetricsCsv(); - const row = metrics.find((r) => r.channelId === channelId); - const closeTs = row ? row.last_message_ts : null; - const list = await fetchTranscriptMessages(client, 100); - const windowMs = 2 * 60 * 60 * 1000; // ±2 hours - const candidates = closeTs - ? list.filter((r) => Math.abs(r.createdTs - closeTs) <= windowMs) - : list.slice(0, 20); - console.log('Batch ticket channelId:', channelId); - if (row) console.log('Ticket close time (last_message_ts):', closeTs, new Date(closeTs).toISOString()); - console.log('Transcript channel messages (candidates by time or recent):'); - candidates.forEach((r) => { - const delta = closeTs != null ? (r.createdTs - closeTs) / 60000 : null; - console.log(' ', r.messageId, r.created, r.ticketName || '(no name)', delta != null ? `delta ${delta.toFixed(0)} min` : ''); - }); - return; - } - - console.log('Usage: node scripts/map-batch-to-transcript.js list [limit]'); - console.log(' node scripts/map-batch-to-transcript.js find '); - } finally { - client.destroy(); - } -} - -main().catch((e) => { - console.error(e); - process.exit(1); -});