Advertisement
Ayan143

transcript code

Jun 27th, 2025
66
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 22.58 KB | Source Code | 0 0
  1. require('dotenv').config();
  2. const { Client, GatewayIntentBits } = require('discord.js');
  3. const { DateTime } = require('luxon');
  4. const escapeHtml = require('escape-html');
  5. const crypto = require('crypto');
  6. const mongoose = require('mongoose');
  7.  
  8. // Database Models
  9. const GuildSchema = new mongoose.Schema({
  10.   id: { type: String, required: true, unique: true },
  11.   name: String,
  12.   icon: String,
  13.   updatedAt: { type: Date, default: Date.now }
  14. });
  15.  
  16. const ChannelSchema = new mongoose.Schema({
  17.   id: { type: String, required: true, unique: true },
  18.   guildId: { type: String, required: true },
  19.   name: String,
  20.   topic: String,
  21.   updatedAt: { type: Date, default: Date.now }
  22. });
  23.  
  24. const MessageSchema = new mongoose.Schema({
  25.   id: { type: String, required: true, unique: true },
  26.   channelId: { type: String, required: true },
  27.   author: {
  28.     id: String,
  29.     username: String,
  30.     avatar: String,
  31.     bot: Boolean,
  32.     color: String
  33.   },
  34.   content: String,
  35.   attachments: [{
  36.     url: String,
  37.     proxyURL: String,
  38.     name: String,
  39.     size: Number,
  40.     contentType: String,
  41.     width: Number,
  42.     height: Number
  43.   }],
  44.   embeds: [Object],
  45.   stickers: [Object],
  46.   timestamp: Number,
  47.   editedTimestamp: Number,
  48.   updatedAt: { type: Date, default: Date.now }
  49. });
  50.  
  51. const Guild = mongoose.model('Guild', GuildSchema);
  52. const Channel = mongoose.model('Channel', ChannelSchema);
  53. const Message = mongoose.model('Message', MessageSchema);
  54.  
  55. // Security Configuration
  56. const ALLOWED_ORIGINS = [
  57.   'https://zia-bot-trans-api.netlify.app',
  58.   'https://discord.com'
  59. ];
  60. const BDFD_AGENT_PREFIX = 'BDFD-';
  61. const MAX_REQUESTS_PER_MINUTE = 10;
  62. const requestCounts = new Map();
  63.  
  64. // Initialize Discord client outside handler
  65. const discordClient = new Client({
  66.   intents: [
  67.     GatewayIntentBits.Guilds,
  68.     GatewayIntentBits.GuildMessages,
  69.     GatewayIntentBits.MessageContent
  70.   ]
  71. });
  72.  
  73. // Connect to Discord when Lambda starts
  74. let discordReady = false;
  75. discordClient.login(process.env.DISCORD_BOT_TOKEN)
  76.   .then(() => discordReady = true)
  77.   .catch(err => console.error('Discord login failed:', err));
  78.  
  79. // Database Connection Handler
  80. let dbConnection = null;
  81.  
  82. async function getDatabaseConnection() {
  83.   if (dbConnection) return dbConnection;
  84.  
  85.   try {
  86.     dbConnection = await mongoose.connect(process.env.MONGODB_URI, {
  87.       serverSelectionTimeoutMS: 3000,
  88.       maxPoolSize: 1,
  89.       socketTimeoutMS: 30000
  90.     });
  91.    
  92.     mongoose.connection.on('error', err => {
  93.       console.error('MongoDB connection error:', err);
  94.       dbConnection = null;
  95.     });
  96.    
  97.     return dbConnection;
  98.   } catch (err) {
  99.     console.error('MongoDB connection failed:', err);
  100.     dbConnection = null;
  101.     throw err;
  102.   }
  103. }
  104.  
  105. // Rate limiting middleware
  106. function checkRateLimit(ip) {
  107.   const now = Date.now();
  108.   const windowStart = now - 60000;
  109.  
  110.   if (!requestCounts.has(ip)) {
  111.     requestCounts.set(ip, { count: 1, lastRequest: now });
  112.     return true;
  113.   }
  114.  
  115.   const record = requestCounts.get(ip);
  116.   if (record.lastRequest < windowStart) {
  117.     record.count = 1;
  118.     record.lastRequest = now;
  119.     return true;
  120.   }
  121.  
  122.   if (record.count >= MAX_REQUESTS_PER_MINUTE) {
  123.     return false;
  124.   }
  125.  
  126.   record.count++;
  127.   record.lastRequest = now;
  128.   return true;
  129. }
  130.  
  131. // Token generation
  132. function generateSignedToken(guildId, channelId, limit) {
  133.   const secret = process.env.URL_SIGNING_SECRET;
  134.   const hmac = crypto.createHmac('sha256', secret);
  135.   hmac.update(`${guildId}:${channelId}:${limit}`);
  136.   return hmac.digest('hex');
  137. }
  138.  
  139. function verifySignedToken(token, guildId, channelId, limit) {
  140.   const expectedToken = generateSignedToken(guildId, channelId, limit);
  141.   return crypto.timingSafeEqual(
  142.     Buffer.from(token),
  143.     Buffer.from(expectedToken)
  144.   );
  145. }
  146.  
  147. // Cache updater functions
  148. async function updateGuildCache(guild) {
  149.   await getDatabaseConnection();
  150.   await Guild.findOneAndUpdate(
  151.     { id: guild.id },
  152.     {
  153.       name: guild.name,
  154.       icon: guild.iconURL({ size: 256 }),
  155.       updatedAt: new Date()
  156.     },
  157.     { upsert: true, new: true }
  158.   );
  159. }
  160.  
  161. async function updateChannelCache(channel) {
  162.   await getDatabaseConnection();
  163.   await Channel.findOneAndUpdate(
  164.     { id: channel.id },
  165.     {
  166.       guildId: channel.guild.id,
  167.       name: channel.name,
  168.       topic: channel.topic,
  169.       updatedAt: new Date()
  170.     },
  171.     { upsert: true, new: true }
  172.   );
  173. }
  174.  
  175. async function updateMessageCache(message) {
  176.   await getDatabaseConnection();
  177.   await Message.findOneAndUpdate(
  178.     { id: message.id },
  179.     {
  180.       channelId: message.channel.id,
  181.       author: {
  182.         id: message.author.id,
  183.         username: message.author.username,
  184.         avatar: message.author.displayAvatarURL({ size: 256 }),
  185.         bot: message.author.bot,
  186.         color: message.member?.displayHexColor || '#7289da'
  187.       },
  188.       content: message.content,
  189.       attachments: Array.from(message.attachments.values()).map(attach => ({
  190.         url: attach.url,
  191.         proxyURL: attach.proxyURL,
  192.         name: attach.name,
  193.         size: attach.size,
  194.         contentType: attach.contentType,
  195.         width: attach.width,
  196.         height: attach.height
  197.       })),
  198.       embeds: message.embeds,
  199.       stickers: Array.from(message.stickers.values()),
  200.       timestamp: message.createdTimestamp,
  201.       editedTimestamp: message.editedTimestamp,
  202.       updatedAt: new Date()
  203.     },
  204.     { upsert: true, new: true }
  205.   );
  206. }
  207.  
  208. // Secure origin check
  209. function isRequestAllowed(event) {
  210.   if (event.path.includes('/download')) {
  211.     const { token, guildId, channelId, limit } = event.queryStringParameters;
  212.     if (token && guildId && channelId && limit) {
  213.       return verifySignedToken(token, guildId, channelId, limit);
  214.     }
  215.   }
  216.  
  217.   const isBDFD = event.headers['user-agent']?.startsWith(BDFD_AGENT_PREFIX);
  218.   if (isBDFD) return true;
  219.  
  220.   const origin = event.headers.origin || event.headers.referer;
  221.   return ALLOWED_ORIGINS.some(o => origin?.includes(o));
  222. }
  223.  
  224. // Main handler
  225. exports.handler = async (event) => {
  226.   try {
  227.     // Get the real IP
  228.     const ips = (event.headers['x-forwarded-for'] || '').split(',').map(ip => ip.trim());
  229.     const ip = ips.length > 0 ? ips[0] : 'unknown';
  230.  
  231.     // Rate limiting
  232.     if (!checkRateLimit(ip)) {
  233.       return {
  234.         statusCode: 429,
  235.         body: JSON.stringify({ error: 'Too many requests' })
  236.       };
  237.     }
  238.  
  239.     // Security check
  240.     if (!isRequestAllowed(event)) {
  241.       return {
  242.         statusCode: 403,
  243.         body: JSON.stringify({
  244.           error: 'Unauthorized',
  245.           solution: 'Use the official download link provided by the bot'
  246.         })
  247.       };
  248.     }
  249.  
  250.     const prettyBytes = (await import('pretty-bytes')).default;
  251.     const { guildId, channelId, json, preview } = event.queryStringParameters;
  252.     const limit = Math.min(parseInt(event.queryStringParameters.limit) || 100, 500);
  253.  
  254.     if (!guildId || !channelId) {
  255.       return {
  256.         statusCode: 400,
  257.         body: JSON.stringify({ error: 'Missing guildId or channelId' })
  258.       };
  259.     }
  260.  
  261.     // Wait for Discord client to be ready with timeout
  262.     const discordReadyPromise = new Promise((resolve) => {
  263.       const check = () => {
  264.         if (discordReady) return resolve(true);
  265.         setTimeout(check, 100);
  266.       };
  267.       check();
  268.     });
  269.  
  270.     await Promise.race([
  271.       discordReadyPromise,
  272.       new Promise((_, reject) => setTimeout(() => reject(new Error('Discord client connection timeout')), 3000))
  273.     ]);
  274.  
  275.     // Try to get fresh data first
  276.     let guildInfo, channelInfo, messages = [];
  277.     try {
  278.       const guild = await discordClient.guilds.fetch(guildId);
  279.       guildInfo = {
  280.         id: guild.id,
  281.         name: guild.name,
  282.         icon: guild.iconURL({ size: 256 })
  283.       };
  284.      
  285.       const channel = await guild.channels.fetch(channelId);
  286.       channelInfo = {
  287.         id: channel.id,
  288.         name: channel.name,
  289.         topic: channel.topic
  290.       };
  291.  
  292.       // Update cache with fresh data
  293.       await Promise.all([
  294.         updateGuildCache(guild),
  295.         updateChannelCache(channel)
  296.       ]);
  297.  
  298.       // Fetch and cache messages with timeout
  299.       const freshMessages = await Promise.race([
  300.         channel.messages.fetch({ limit }),
  301.         new Promise((_, reject) => setTimeout(() => reject(new Error('Message fetch timeout')), 5000))
  302.       ]);
  303.      
  304.       messages = Array.from(freshMessages.values());
  305.       await Promise.all(messages.map(msg => updateMessageCache(msg)));
  306.     } catch (error) {
  307.       console.log('Using cached data due to error:', error.message);
  308.      
  309.       // Fallback to cached data
  310.       await getDatabaseConnection();
  311.       guildInfo = await Guild.findOne({ id: guildId }) || {
  312.         id: guildId,
  313.         name: 'Deleted Server',
  314.         icon: null
  315.       };
  316.      
  317.       channelInfo = await Channel.findOne({ id: channelId }) || {
  318.         id: channelId,
  319.         name: 'deleted-channel',
  320.         topic: null
  321.       };
  322.      
  323.       messages = await Message.find({ channelId })
  324.         .sort({ timestamp: -1 })
  325.         .limit(limit)
  326.         .lean();
  327.     }
  328.  
  329.     // Build transcript data
  330.     const transcriptData = {
  331.       guild: guildInfo,
  332.       channel: channelInfo,
  333.       messages: messages.map(msg => ({
  334.         id: msg.id,
  335.         author: msg.author,
  336.         content: msg.content,
  337.         attachments: msg.attachments,
  338.         embeds: msg.embeds,
  339.         stickers: msg.stickers,
  340.         timestamp: msg.timestamp,
  341.         editedTimestamp: msg.editedTimestamp
  342.       })),
  343.       generatedAt: Date.now(),
  344.       limit: limit
  345.     };
  346.  
  347.     const transcriptHTML = generateTranscriptHTML(transcriptData, prettyBytes);
  348.     const filename = `transcript-${channelInfo.name}-${DateTime.now().toFormat('yyyy-LL-dd')}.html`;
  349.     const downloadToken = generateSignedToken(guildId, channelId, limit);
  350.  
  351.     // Special response handling for BDFD
  352.     const isBDFD = event.headers['user-agent']?.startsWith(BDFD_AGENT_PREFIX);
  353.     if (isBDFD) {
  354.       return {
  355.         statusCode: 200,
  356.         headers: {
  357.           'Content-Type': 'application/json',
  358.           'Cache-Control': 'no-cache'
  359.         },
  360.         body: JSON.stringify({
  361.           success: true,
  362.           downloadUrl: `https://${event.headers.host}/download?guildId=${guildId}&channelId=${channelId}&limit=${limit}&token=${downloadToken}`,
  363.           filename,
  364.           messageCount: transcriptData.messages.length,
  365.           channelName: channelInfo.name,
  366.           guildName: guildInfo.name,
  367.           generationDate: DateTime.now().toLocaleString(DateTime.DATETIME_FULL),
  368.           isBDFD: true,
  369.           cacheStatus: messages.length > 0 ? 'CACHED' : 'NO_CACHE'
  370.         })
  371.       };
  372.     }
  373.  
  374.     // JSON response for API requests
  375.     if (json || event.headers['user-agent']?.includes('DiscordBot')) {
  376.       return {
  377.         statusCode: 200,
  378.         headers: { 'Content-Type': 'application/json' },
  379.         body: JSON.stringify({
  380.           success: true,
  381.           downloadUrl: `https://${event.headers.host}/download?guildId=${guildId}&channelId=${channelId}&limit=${limit}&token=${downloadToken}`,
  382.           filename,
  383.           messageCount: transcriptData.messages.length,
  384.           channelName: channelInfo.name,
  385.           guildName: guildInfo.name,
  386.           generationDate: DateTime.now().toLocaleString(DateTime.DATETIME_FULL),
  387.           mobileInstructions: 'Long-press link and "Open in new tab" for best results',
  388.           cacheStatus: messages.length > 0 ? 'CACHED' : 'NO_CACHE'
  389.         })
  390.       };
  391.     }
  392.  
  393.     // Default HTML response
  394.     return {
  395.       statusCode: 200,
  396.       headers: {
  397.         'Content-Type': 'text/html',
  398.         'Content-Disposition': preview ? '' : `attachment; filename="${filename}"`
  399.       },
  400.       body: transcriptHTML
  401.     };
  402.  
  403.   } catch (error) {
  404.     console.error('Error:', error);
  405.     return {
  406.       statusCode: 500,
  407.       body: JSON.stringify({
  408.         error: error.message,
  409.         stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
  410.       })
  411.     };
  412.   }
  413. };
  414.  
  415. function generateTranscriptHTML(data, prettyBytes) {
  416.   const messagesHTML = data.messages.map(msg => {
  417.     let attachmentsHTML = '';
  418.     if (msg.attachments && msg.attachments.length > 0) {
  419.       attachmentsHTML = `<div class="attachments">${
  420.         msg.attachments.map(attach => {
  421.           const imageUrl = attach.proxyURL || attach.url;
  422.          
  423.           if (attach.contentType?.startsWith('image/')) {
  424.             return `
  425.               <div class="attachment image">
  426.                 <img src="${imageUrl}"
  427.                      alt="${attach.name}"
  428.                      loading="lazy"
  429.                      width="${attach.width || ''}"
  430.                      height="${attach.height || ''}">
  431.               </div>`;
  432.           }
  433.           return `
  434.             <div class="attachment file">
  435.               <a href="${attach.url}" target="_blank">📄 ${attach.name} (${prettyBytes(attach.size)})</a>
  436.             </div>`;
  437.         }).join('')
  438.       }</div>`;
  439.     }
  440.  
  441.     let embedsHTML = '';
  442.     if (msg.embeds && msg.embeds.length > 0) {
  443.       embedsHTML = `<div class="embeds">${
  444.         msg.embeds.map(embed => `
  445.           <div class="embed">
  446.             ${embed.title ? `<div class="embed-title"><a href="${embed.url || '#'}" target="_blank">${escapeHtml(embed.title)}</a></div>` : ''}
  447.             ${embed.description ? `<div class="embed-description">${escapeHtml(embed.description)}</div>` : ''}
  448.             ${embed.image ? `<img src="${embed.image.proxyURL || embed.image.url}" class="embed-image" loading="lazy">` : ''}
  449.           </div>
  450.         `).join('')
  451.       }</div>`;
  452.     }
  453.  
  454.     let stickersHTML = '';
  455.     if (msg.stickers && msg.stickers.length > 0) {
  456.       stickersHTML = `<div class="stickers">${
  457.         msg.stickers.map(sticker => `
  458.           <div class="sticker">
  459.             <img src="${sticker.url}" alt="${sticker.name}" loading="lazy">
  460.           </div>
  461.         `).join('')
  462.       }</div>`;
  463.     }
  464.  
  465.     return `
  466.       <div class="message" data-message-id="${msg.id}">
  467.         <div class="message-header">
  468.           <img src="${msg.author.avatar}"
  469.                alt="${msg.author.username}"
  470.                class="avatar">
  471.           <span class="username" style="color: ${msg.author.color || '#7289da'}">
  472.             ${escapeHtml(msg.author.username)}
  473.             ${msg.author.bot ? '<span class="bot-tag">BOT</span>' : ''}
  474.           </span>
  475.           <span class="timestamp" data-timestamp="${msg.timestamp}">
  476.             ${DateTime.fromMillis(msg.timestamp).toLocaleString(DateTime.DATETIME_MED)}
  477.           </span>
  478.         </div>
  479.         <div class="message-content">
  480.           ${escapeHtml(msg.content || '').replace(/\n/g, '<br>')}
  481.           ${attachmentsHTML}
  482.           ${embedsHTML}
  483.           ${stickersHTML}
  484.         </div>
  485.       </div>
  486.     `;
  487.   }).join('');
  488.  
  489.   return `<!DOCTYPE html>
  490. <html lang="en">
  491. <head>
  492.   <meta charset="UTF-8">
  493.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  494.   <title>Transcript: #${escapeHtml(data.channel.name)} | ${escapeHtml(data.guild.name)}</title>
  495.   <style>
  496.     :root {
  497.       --background-primary: #36393f;
  498.       --background-secondary: #2f3136;
  499.       --background-tertiary: #202225;
  500.       --text-normal: #dcddde;
  501.       --text-muted: #72767d;
  502.       --text-link: #00b0f4;
  503.       --brand-color: #5865f2;
  504.       --online-color: #3ba55c;
  505.     }
  506.  
  507.     * {
  508.       margin: 0;
  509.       padding: 0;
  510.       box-sizing: border-box;
  511.     }
  512.  
  513.     body {
  514.       font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  515.       background-color: var(--background-primary);
  516.       color: var(--text-normal);
  517.       line-height: 1.5;
  518.       padding: 20px;
  519.       max-width: 1000px;
  520.       margin: 0 auto;
  521.     }
  522.  
  523.     .header {
  524.       background-color: var(--background-secondary);
  525.       padding: 20px;
  526.       border-radius: 8px;
  527.       margin-bottom: 20px;
  528.       position: relative;
  529.     }
  530.  
  531.     .channel-name {
  532.       font-size: 1.5em;
  533.       font-weight: bold;
  534.       color: white;
  535.       margin-bottom: 5px;
  536.     }
  537.  
  538.     .guild-name {
  539.       color: var(--text-muted);
  540.       font-size: 1.1em;
  541.       margin-bottom: 10px;
  542.     }
  543.  
  544.     .transcript-info {
  545.       color: var(--text-muted);
  546.       font-size: 0.9em;
  547.       line-height: 1.4;
  548.     }
  549.  
  550.     .message {
  551.       background-color: var(--background-secondary);
  552.       padding: 15px;
  553.       border-radius: 8px;
  554.       margin-bottom: 15px;
  555.       position: relative;
  556.     }
  557.  
  558.     .message:hover {
  559.       background-color: var(--background-tertiary);
  560.     }
  561.  
  562.     .message-header {
  563.       display: flex;
  564.       align-items: center;
  565.       margin-bottom: 8px;
  566.       flex-wrap: wrap;
  567.       gap: 6px;
  568.     }
  569.  
  570.     .avatar {
  571.       width: 40px;
  572.       height: 40px;
  573.       border-radius: 50%;
  574.       object-fit: cover;
  575.       flex-shrink: 0;
  576.     }
  577.  
  578.     .username {
  579.       font-weight: 600;
  580.       margin-right: 6px;
  581.     }
  582.  
  583.     .bot-tag {
  584.       background-color: var(--brand-color);
  585.       color: white;
  586.       font-size: 0.7em;
  587.       padding: 2px 4px;
  588.       border-radius: 3px;
  589.       margin-left: 4px;
  590.       vertical-align: middle;
  591.     }
  592.  
  593.     .timestamp {
  594.       color: var(--text-muted);
  595.       font-size: 0.8em;
  596.       cursor: pointer;
  597.     }
  598.  
  599.     .timestamp:hover {
  600.       text-decoration: underline;
  601.     }
  602.  
  603.     .message-content {
  604.       margin-left: 52px;
  605.       word-break: break-word;
  606.     }
  607.  
  608.     .message-content > br {
  609.       content: "";
  610.       display: block;
  611.       margin-top: 0.5em;
  612.     }
  613.  
  614.     .attachments {
  615.       margin-top: 8px;
  616.       display: flex;
  617.       flex-direction: column;
  618.       gap: 5px;
  619.     }
  620.  
  621.     .attachment.image img {
  622.       max-width: 500px;
  623.       max-height: 400px;
  624.       border-radius: 4px;
  625.       cursor: pointer;
  626.       transition: transform 0.2s;
  627.     }
  628.  
  629.     .attachment.image img:hover {
  630.       transform: scale(1.02);
  631.     }
  632.  
  633.     .attachment.file a {
  634.       color: var(--text-link);
  635.       text-decoration: none;
  636.       display: inline-flex;
  637.       align-items: center;
  638.       gap: 5px;
  639.       padding: 4px 8px;
  640.       background-color: var(--background-tertiary);
  641.       border-radius: 4px;
  642.     }
  643.  
  644.     .attachment.file a:hover {
  645.       text-decoration: underline;
  646.     }
  647.  
  648.     .embeds {
  649.       margin-top: 8px;
  650.       max-width: 520px;
  651.     }
  652.  
  653.     .embed {
  654.       border-left: 4px solid var(--brand-color);
  655.       padding: 8px 12px 12px;
  656.       background-color: var(--background-tertiary);
  657.       border-radius: 0 4px 4px 0;
  658.       margin-top: 8px;
  659.     }
  660.  
  661.     .embed-title {
  662.       font-weight: 600;
  663.       margin-bottom: 8px;
  664.       color: var(--text-link);
  665.     }
  666.  
  667.     .embed-title a {
  668.       color: inherit;
  669.       text-decoration: none;
  670.     }
  671.  
  672.     .embed-title a:hover {
  673.       text-decoration: underline;
  674.     }
  675.  
  676.     .embed-description {
  677.       margin-bottom: 8px;
  678.       line-height: 1.4;
  679.     }
  680.  
  681.     .embed-image {
  682.       max-width: 100%;
  683.       border-radius: 4px;
  684.       margin-top: 8px;
  685.     }
  686.  
  687.     .stickers {
  688.       display: flex;
  689.       flex-wrap: wrap;
  690.       gap: 5px;
  691.       margin-top: 8px;
  692.     }
  693.  
  694.     .sticker img {
  695.       max-width: 160px;
  696.       max-height: 160px;
  697.       border-radius: 4px;
  698.     }
  699.  
  700.     @media (max-width: 768px) {
  701.       body {
  702.         padding: 10px;
  703.       }
  704.  
  705.       .message-content {
  706.         margin-left: 0;
  707.         padding-top: 10px;
  708.       }
  709.  
  710.       .attachment.image img,
  711.       .embed-image,
  712.       .sticker img {
  713.         max-width: 100%;
  714.       }
  715.  
  716.       .embeds {
  717.         max-width: 100%;
  718.       }
  719.     }
  720.  
  721.     @media print {
  722.       body {
  723.         background-color: white !important;
  724.         color: black !important;
  725.         padding: 0;
  726.         font-size: 12pt;
  727.       }
  728.  
  729.       .header {
  730.         background-color: white !important;
  731.         border-bottom: 1px solid #eee;
  732.         padding: 10px;
  733.       }
  734.  
  735.       .channel-name {
  736.         color: black !important;
  737.       }
  738.  
  739.       .message {
  740.         page-break-inside: avoid;
  741.         background-color: white !important;
  742.         border: 1px solid #eee;
  743.         padding: 10px;
  744.         margin-bottom: 10px;
  745.       }
  746.  
  747.       .timestamp {
  748.         color: #666 !important;
  749.       }
  750.  
  751.       .embed {
  752.         background-color: #f9f9f9 !important;
  753.       }
  754.  
  755.       .attachment.file a {
  756.         color: #0066cc;
  757.       }
  758.     }
  759.  
  760.     @keyframes highlight {
  761.       0% { background-color: rgba(114, 137, 218, 0.3); }
  762.       100% { background-color: transparent; }
  763.     }
  764.  
  765.     .message.highlight {
  766.       animation: highlight 2s ease-out;
  767.     }
  768.   </style>
  769. </head>
  770. <body>
  771.   <div class="header">
  772.     ${data.guild.icon ? `<img src="${data.guild.icon}" alt="Guild icon" style="width:50px;height:50px;border-radius:50%;position:absolute;right:20px;top:20px;">` : ''}
  773.     <div class="channel-name">#${escapeHtml(data.channel.name)}</div>
  774.     <div class="guild-name">${escapeHtml(data.guild.name)}</div>
  775.     <div class="transcript-info">
  776.       Transcript generated on ${DateTime.fromMillis(data.generatedAt).toLocaleString(DateTime.DATETIME_FULL)}<br>
  777.       ${data.messages.length} messages | Channel ID: ${data.channel.id}<br>
  778.       ${data.channel.topic ? `Channel topic: ${escapeHtml(data.channel.topic)}` : ''}
  779.       ${data.messages.length > 0 && data.messages[0].timestamp < Date.now() - 86400000 ?
  780.        '<br><strong>Note:</strong> This transcript contains cached historical data' : ''}
  781.     </div>
  782.   </div>
  783.  
  784.   ${messagesHTML}
  785.  
  786.   <script>
  787.     document.addEventListener('DOMContentLoaded', function() {
  788.       if (!window.location.search.includes('preview=true')) {
  789.         const blob = new Blob([document.documentElement.outerHTML], {type: 'text/html'});
  790.         const url = URL.createObjectURL(blob);
  791.         const a = document.createElement('a');
  792.         a.href = url;
  793.         a.download = document.title + '.html';
  794.         document.body.appendChild(a);
  795.         a.click();
  796.         setTimeout(() => {
  797.           document.body.removeChild(a);
  798.           window.URL.revokeObjectURL(url);
  799.         }, 100);
  800.       }
  801.  
  802.       document.querySelectorAll('.timestamp').forEach(el => {
  803.         el.addEventListener('click', () => {
  804.           const timestamp = el.getAttribute('data-timestamp');
  805.           navigator.clipboard.writeText(new Date(parseInt(timestamp)).toISOString());
  806.           const originalText = el.textContent;
  807.           el.textContent = 'Copied!';
  808.           setTimeout(() => el.textContent = originalText, 2000);
  809.         });
  810.       });
  811.  
  812.       if (window.location.hash) {
  813.         const message = document.querySelector(\`[data-message-id="\${window.location.hash.substring(1)}"]\`);
  814.         if (message) {
  815.           message.classList.add('highlight');
  816.           message.scrollIntoView({ behavior: 'smooth' });
  817.         }
  818.       }
  819.     });
  820.   </script>
  821. </body>
  822. </html>`;
  823.       }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement