Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- require('dotenv').config();
- const { Client, GatewayIntentBits } = require('discord.js');
- const { DateTime } = require('luxon');
- const escapeHtml = require('escape-html');
- const crypto = require('crypto');
- const mongoose = require('mongoose');
- // Database Models
- const GuildSchema = new mongoose.Schema({
- id: { type: String, required: true, unique: true },
- name: String,
- icon: String,
- updatedAt: { type: Date, default: Date.now }
- });
- const ChannelSchema = new mongoose.Schema({
- id: { type: String, required: true, unique: true },
- guildId: { type: String, required: true },
- name: String,
- topic: String,
- updatedAt: { type: Date, default: Date.now }
- });
- const MessageSchema = new mongoose.Schema({
- id: { type: String, required: true, unique: true },
- channelId: { type: String, required: true },
- author: {
- id: String,
- username: String,
- avatar: String,
- bot: Boolean,
- color: String
- },
- content: String,
- attachments: [{
- url: String,
- proxyURL: String,
- name: String,
- size: Number,
- contentType: String,
- width: Number,
- height: Number
- }],
- embeds: [Object],
- stickers: [Object],
- timestamp: Number,
- editedTimestamp: Number,
- updatedAt: { type: Date, default: Date.now }
- });
- const Guild = mongoose.model('Guild', GuildSchema);
- const Channel = mongoose.model('Channel', ChannelSchema);
- const Message = mongoose.model('Message', MessageSchema);
- // Security Configuration
- const ALLOWED_ORIGINS = [
- 'https://zia-bot-trans-api.netlify.app',
- 'https://discord.com'
- ];
- const BDFD_AGENT_PREFIX = 'BDFD-';
- const MAX_REQUESTS_PER_MINUTE = 10;
- const requestCounts = new Map();
- // Initialize Discord client outside handler
- const discordClient = new Client({
- intents: [
- GatewayIntentBits.Guilds,
- GatewayIntentBits.GuildMessages,
- GatewayIntentBits.MessageContent
- ]
- });
- // Connect to Discord when Lambda starts
- let discordReady = false;
- discordClient.login(process.env.DISCORD_BOT_TOKEN)
- .then(() => discordReady = true)
- .catch(err => console.error('Discord login failed:', err));
- // Database Connection Handler
- let dbConnection = null;
- async function getDatabaseConnection() {
- if (dbConnection) return dbConnection;
- try {
- dbConnection = await mongoose.connect(process.env.MONGODB_URI, {
- serverSelectionTimeoutMS: 3000,
- maxPoolSize: 1,
- socketTimeoutMS: 30000
- });
- mongoose.connection.on('error', err => {
- console.error('MongoDB connection error:', err);
- dbConnection = null;
- });
- return dbConnection;
- } catch (err) {
- console.error('MongoDB connection failed:', err);
- dbConnection = null;
- throw err;
- }
- }
- // Rate limiting middleware
- function checkRateLimit(ip) {
- const now = Date.now();
- const windowStart = now - 60000;
- if (!requestCounts.has(ip)) {
- requestCounts.set(ip, { count: 1, lastRequest: now });
- return true;
- }
- const record = requestCounts.get(ip);
- if (record.lastRequest < windowStart) {
- record.count = 1;
- record.lastRequest = now;
- return true;
- }
- if (record.count >= MAX_REQUESTS_PER_MINUTE) {
- return false;
- }
- record.count++;
- record.lastRequest = now;
- return true;
- }
- // Token generation
- function generateSignedToken(guildId, channelId, limit) {
- const secret = process.env.URL_SIGNING_SECRET;
- const hmac = crypto.createHmac('sha256', secret);
- hmac.update(`${guildId}:${channelId}:${limit}`);
- return hmac.digest('hex');
- }
- function verifySignedToken(token, guildId, channelId, limit) {
- const expectedToken = generateSignedToken(guildId, channelId, limit);
- return crypto.timingSafeEqual(
- Buffer.from(token),
- Buffer.from(expectedToken)
- );
- }
- // Cache updater functions
- async function updateGuildCache(guild) {
- await getDatabaseConnection();
- await Guild.findOneAndUpdate(
- { id: guild.id },
- {
- name: guild.name,
- icon: guild.iconURL({ size: 256 }),
- updatedAt: new Date()
- },
- { upsert: true, new: true }
- );
- }
- async function updateChannelCache(channel) {
- await getDatabaseConnection();
- await Channel.findOneAndUpdate(
- { id: channel.id },
- {
- guildId: channel.guild.id,
- name: channel.name,
- topic: channel.topic,
- updatedAt: new Date()
- },
- { upsert: true, new: true }
- );
- }
- async function updateMessageCache(message) {
- await getDatabaseConnection();
- await Message.findOneAndUpdate(
- { id: message.id },
- {
- channelId: message.channel.id,
- author: {
- id: message.author.id,
- username: message.author.username,
- avatar: message.author.displayAvatarURL({ size: 256 }),
- bot: message.author.bot,
- color: message.member?.displayHexColor || '#7289da'
- },
- content: message.content,
- attachments: Array.from(message.attachments.values()).map(attach => ({
- url: attach.url,
- proxyURL: attach.proxyURL,
- name: attach.name,
- size: attach.size,
- contentType: attach.contentType,
- width: attach.width,
- height: attach.height
- })),
- embeds: message.embeds,
- stickers: Array.from(message.stickers.values()),
- timestamp: message.createdTimestamp,
- editedTimestamp: message.editedTimestamp,
- updatedAt: new Date()
- },
- { upsert: true, new: true }
- );
- }
- // Secure origin check
- function isRequestAllowed(event) {
- if (event.path.includes('/download')) {
- const { token, guildId, channelId, limit } = event.queryStringParameters;
- if (token && guildId && channelId && limit) {
- return verifySignedToken(token, guildId, channelId, limit);
- }
- }
- const isBDFD = event.headers['user-agent']?.startsWith(BDFD_AGENT_PREFIX);
- if (isBDFD) return true;
- const origin = event.headers.origin || event.headers.referer;
- return ALLOWED_ORIGINS.some(o => origin?.includes(o));
- }
- // Main handler
- exports.handler = async (event) => {
- try {
- // Get the real IP
- const ips = (event.headers['x-forwarded-for'] || '').split(',').map(ip => ip.trim());
- const ip = ips.length > 0 ? ips[0] : 'unknown';
- // Rate limiting
- if (!checkRateLimit(ip)) {
- return {
- statusCode: 429,
- body: JSON.stringify({ error: 'Too many requests' })
- };
- }
- // Security check
- if (!isRequestAllowed(event)) {
- return {
- statusCode: 403,
- body: JSON.stringify({
- error: 'Unauthorized',
- solution: 'Use the official download link provided by the bot'
- })
- };
- }
- const prettyBytes = (await import('pretty-bytes')).default;
- const { guildId, channelId, json, preview } = event.queryStringParameters;
- const limit = Math.min(parseInt(event.queryStringParameters.limit) || 100, 500);
- if (!guildId || !channelId) {
- return {
- statusCode: 400,
- body: JSON.stringify({ error: 'Missing guildId or channelId' })
- };
- }
- // Wait for Discord client to be ready with timeout
- const discordReadyPromise = new Promise((resolve) => {
- const check = () => {
- if (discordReady) return resolve(true);
- setTimeout(check, 100);
- };
- check();
- });
- await Promise.race([
- discordReadyPromise,
- new Promise((_, reject) => setTimeout(() => reject(new Error('Discord client connection timeout')), 3000))
- ]);
- // Try to get fresh data first
- let guildInfo, channelInfo, messages = [];
- try {
- const guild = await discordClient.guilds.fetch(guildId);
- guildInfo = {
- id: guild.id,
- name: guild.name,
- icon: guild.iconURL({ size: 256 })
- };
- const channel = await guild.channels.fetch(channelId);
- channelInfo = {
- id: channel.id,
- name: channel.name,
- topic: channel.topic
- };
- // Update cache with fresh data
- await Promise.all([
- updateGuildCache(guild),
- updateChannelCache(channel)
- ]);
- // Fetch and cache messages with timeout
- const freshMessages = await Promise.race([
- channel.messages.fetch({ limit }),
- new Promise((_, reject) => setTimeout(() => reject(new Error('Message fetch timeout')), 5000))
- ]);
- messages = Array.from(freshMessages.values());
- await Promise.all(messages.map(msg => updateMessageCache(msg)));
- } catch (error) {
- console.log('Using cached data due to error:', error.message);
- // Fallback to cached data
- await getDatabaseConnection();
- guildInfo = await Guild.findOne({ id: guildId }) || {
- id: guildId,
- name: 'Deleted Server',
- icon: null
- };
- channelInfo = await Channel.findOne({ id: channelId }) || {
- id: channelId,
- name: 'deleted-channel',
- topic: null
- };
- messages = await Message.find({ channelId })
- .sort({ timestamp: -1 })
- .limit(limit)
- .lean();
- }
- // Build transcript data
- const transcriptData = {
- guild: guildInfo,
- channel: channelInfo,
- messages: messages.map(msg => ({
- id: msg.id,
- author: msg.author,
- content: msg.content,
- attachments: msg.attachments,
- embeds: msg.embeds,
- stickers: msg.stickers,
- timestamp: msg.timestamp,
- editedTimestamp: msg.editedTimestamp
- })),
- generatedAt: Date.now(),
- limit: limit
- };
- const transcriptHTML = generateTranscriptHTML(transcriptData, prettyBytes);
- const filename = `transcript-${channelInfo.name}-${DateTime.now().toFormat('yyyy-LL-dd')}.html`;
- const downloadToken = generateSignedToken(guildId, channelId, limit);
- // Special response handling for BDFD
- const isBDFD = event.headers['user-agent']?.startsWith(BDFD_AGENT_PREFIX);
- if (isBDFD) {
- return {
- statusCode: 200,
- headers: {
- 'Content-Type': 'application/json',
- 'Cache-Control': 'no-cache'
- },
- body: JSON.stringify({
- success: true,
- downloadUrl: `https://${event.headers.host}/download?guildId=${guildId}&channelId=${channelId}&limit=${limit}&token=${downloadToken}`,
- filename,
- messageCount: transcriptData.messages.length,
- channelName: channelInfo.name,
- guildName: guildInfo.name,
- generationDate: DateTime.now().toLocaleString(DateTime.DATETIME_FULL),
- isBDFD: true,
- cacheStatus: messages.length > 0 ? 'CACHED' : 'NO_CACHE'
- })
- };
- }
- // JSON response for API requests
- if (json || event.headers['user-agent']?.includes('DiscordBot')) {
- return {
- statusCode: 200,
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- success: true,
- downloadUrl: `https://${event.headers.host}/download?guildId=${guildId}&channelId=${channelId}&limit=${limit}&token=${downloadToken}`,
- filename,
- messageCount: transcriptData.messages.length,
- channelName: channelInfo.name,
- guildName: guildInfo.name,
- generationDate: DateTime.now().toLocaleString(DateTime.DATETIME_FULL),
- mobileInstructions: 'Long-press link and "Open in new tab" for best results',
- cacheStatus: messages.length > 0 ? 'CACHED' : 'NO_CACHE'
- })
- };
- }
- // Default HTML response
- return {
- statusCode: 200,
- headers: {
- 'Content-Type': 'text/html',
- 'Content-Disposition': preview ? '' : `attachment; filename="${filename}"`
- },
- body: transcriptHTML
- };
- } catch (error) {
- console.error('Error:', error);
- return {
- statusCode: 500,
- body: JSON.stringify({
- error: error.message,
- stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
- })
- };
- }
- };
- function generateTranscriptHTML(data, prettyBytes) {
- const messagesHTML = data.messages.map(msg => {
- let attachmentsHTML = '';
- if (msg.attachments && msg.attachments.length > 0) {
- attachmentsHTML = `<div class="attachments">${
- msg.attachments.map(attach => {
- const imageUrl = attach.proxyURL || attach.url;
- if (attach.contentType?.startsWith('image/')) {
- return `
- <div class="attachment image">
- <img src="${imageUrl}"
- alt="${attach.name}"
- loading="lazy"
- width="${attach.width || ''}"
- height="${attach.height || ''}">
- </div>`;
- }
- return `
- <div class="attachment file">
- <a href="${attach.url}" target="_blank">📄 ${attach.name} (${prettyBytes(attach.size)})</a>
- </div>`;
- }).join('')
- }</div>`;
- }
- let embedsHTML = '';
- if (msg.embeds && msg.embeds.length > 0) {
- embedsHTML = `<div class="embeds">${
- msg.embeds.map(embed => `
- <div class="embed">
- ${embed.title ? `<div class="embed-title"><a href="${embed.url || '#'}" target="_blank">${escapeHtml(embed.title)}</a></div>` : ''}
- ${embed.description ? `<div class="embed-description">${escapeHtml(embed.description)}</div>` : ''}
- ${embed.image ? `<img src="${embed.image.proxyURL || embed.image.url}" class="embed-image" loading="lazy">` : ''}
- </div>
- `).join('')
- }</div>`;
- }
- let stickersHTML = '';
- if (msg.stickers && msg.stickers.length > 0) {
- stickersHTML = `<div class="stickers">${
- msg.stickers.map(sticker => `
- <div class="sticker">
- <img src="${sticker.url}" alt="${sticker.name}" loading="lazy">
- </div>
- `).join('')
- }</div>`;
- }
- return `
- <div class="message" data-message-id="${msg.id}">
- <div class="message-header">
- <img src="${msg.author.avatar}"
- alt="${msg.author.username}"
- class="avatar">
- <span class="username" style="color: ${msg.author.color || '#7289da'}">
- ${escapeHtml(msg.author.username)}
- ${msg.author.bot ? '<span class="bot-tag">BOT</span>' : ''}
- </span>
- <span class="timestamp" data-timestamp="${msg.timestamp}">
- ${DateTime.fromMillis(msg.timestamp).toLocaleString(DateTime.DATETIME_MED)}
- </span>
- </div>
- <div class="message-content">
- ${escapeHtml(msg.content || '').replace(/\n/g, '<br>')}
- ${attachmentsHTML}
- ${embedsHTML}
- ${stickersHTML}
- </div>
- </div>
- `;
- }).join('');
- return `<!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Transcript: #${escapeHtml(data.channel.name)} | ${escapeHtml(data.guild.name)}</title>
- <style>
- :root {
- --background-primary: #36393f;
- --background-secondary: #2f3136;
- --background-tertiary: #202225;
- --text-normal: #dcddde;
- --text-muted: #72767d;
- --text-link: #00b0f4;
- --brand-color: #5865f2;
- --online-color: #3ba55c;
- }
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
- background-color: var(--background-primary);
- color: var(--text-normal);
- line-height: 1.5;
- padding: 20px;
- max-width: 1000px;
- margin: 0 auto;
- }
- .header {
- background-color: var(--background-secondary);
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 20px;
- position: relative;
- }
- .channel-name {
- font-size: 1.5em;
- font-weight: bold;
- color: white;
- margin-bottom: 5px;
- }
- .guild-name {
- color: var(--text-muted);
- font-size: 1.1em;
- margin-bottom: 10px;
- }
- .transcript-info {
- color: var(--text-muted);
- font-size: 0.9em;
- line-height: 1.4;
- }
- .message {
- background-color: var(--background-secondary);
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 15px;
- position: relative;
- }
- .message:hover {
- background-color: var(--background-tertiary);
- }
- .message-header {
- display: flex;
- align-items: center;
- margin-bottom: 8px;
- flex-wrap: wrap;
- gap: 6px;
- }
- .avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- object-fit: cover;
- flex-shrink: 0;
- }
- .username {
- font-weight: 600;
- margin-right: 6px;
- }
- .bot-tag {
- background-color: var(--brand-color);
- color: white;
- font-size: 0.7em;
- padding: 2px 4px;
- border-radius: 3px;
- margin-left: 4px;
- vertical-align: middle;
- }
- .timestamp {
- color: var(--text-muted);
- font-size: 0.8em;
- cursor: pointer;
- }
- .timestamp:hover {
- text-decoration: underline;
- }
- .message-content {
- margin-left: 52px;
- word-break: break-word;
- }
- .message-content > br {
- content: "";
- display: block;
- margin-top: 0.5em;
- }
- .attachments {
- margin-top: 8px;
- display: flex;
- flex-direction: column;
- gap: 5px;
- }
- .attachment.image img {
- max-width: 500px;
- max-height: 400px;
- border-radius: 4px;
- cursor: pointer;
- transition: transform 0.2s;
- }
- .attachment.image img:hover {
- transform: scale(1.02);
- }
- .attachment.file a {
- color: var(--text-link);
- text-decoration: none;
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 4px 8px;
- background-color: var(--background-tertiary);
- border-radius: 4px;
- }
- .attachment.file a:hover {
- text-decoration: underline;
- }
- .embeds {
- margin-top: 8px;
- max-width: 520px;
- }
- .embed {
- border-left: 4px solid var(--brand-color);
- padding: 8px 12px 12px;
- background-color: var(--background-tertiary);
- border-radius: 0 4px 4px 0;
- margin-top: 8px;
- }
- .embed-title {
- font-weight: 600;
- margin-bottom: 8px;
- color: var(--text-link);
- }
- .embed-title a {
- color: inherit;
- text-decoration: none;
- }
- .embed-title a:hover {
- text-decoration: underline;
- }
- .embed-description {
- margin-bottom: 8px;
- line-height: 1.4;
- }
- .embed-image {
- max-width: 100%;
- border-radius: 4px;
- margin-top: 8px;
- }
- .stickers {
- display: flex;
- flex-wrap: wrap;
- gap: 5px;
- margin-top: 8px;
- }
- .sticker img {
- max-width: 160px;
- max-height: 160px;
- border-radius: 4px;
- }
- @media (max-width: 768px) {
- body {
- padding: 10px;
- }
- .message-content {
- margin-left: 0;
- padding-top: 10px;
- }
- .attachment.image img,
- .embed-image,
- .sticker img {
- max-width: 100%;
- }
- .embeds {
- max-width: 100%;
- }
- }
- @media print {
- body {
- background-color: white !important;
- color: black !important;
- padding: 0;
- font-size: 12pt;
- }
- .header {
- background-color: white !important;
- border-bottom: 1px solid #eee;
- padding: 10px;
- }
- .channel-name {
- color: black !important;
- }
- .message {
- page-break-inside: avoid;
- background-color: white !important;
- border: 1px solid #eee;
- padding: 10px;
- margin-bottom: 10px;
- }
- .timestamp {
- color: #666 !important;
- }
- .embed {
- background-color: #f9f9f9 !important;
- }
- .attachment.file a {
- color: #0066cc;
- }
- }
- @keyframes highlight {
- 0% { background-color: rgba(114, 137, 218, 0.3); }
- 100% { background-color: transparent; }
- }
- .message.highlight {
- animation: highlight 2s ease-out;
- }
- </style>
- </head>
- <body>
- <div class="header">
- ${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;">` : ''}
- <div class="channel-name">#${escapeHtml(data.channel.name)}</div>
- <div class="guild-name">${escapeHtml(data.guild.name)}</div>
- <div class="transcript-info">
- Transcript generated on ${DateTime.fromMillis(data.generatedAt).toLocaleString(DateTime.DATETIME_FULL)}<br>
- ${data.messages.length} messages | Channel ID: ${data.channel.id}<br>
- ${data.channel.topic ? `Channel topic: ${escapeHtml(data.channel.topic)}` : ''}
- ${data.messages.length > 0 && data.messages[0].timestamp < Date.now() - 86400000 ?
- '<br><strong>Note:</strong> This transcript contains cached historical data' : ''}
- </div>
- </div>
- ${messagesHTML}
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- if (!window.location.search.includes('preview=true')) {
- const blob = new Blob([document.documentElement.outerHTML], {type: 'text/html'});
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = document.title + '.html';
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- window.URL.revokeObjectURL(url);
- }, 100);
- }
- document.querySelectorAll('.timestamp').forEach(el => {
- el.addEventListener('click', () => {
- const timestamp = el.getAttribute('data-timestamp');
- navigator.clipboard.writeText(new Date(parseInt(timestamp)).toISOString());
- const originalText = el.textContent;
- el.textContent = 'Copied!';
- setTimeout(() => el.textContent = originalText, 2000);
- });
- });
- if (window.location.hash) {
- const message = document.querySelector(\`[data-message-id="\${window.location.hash.substring(1)}"]\`);
- if (message) {
- message.classList.add('highlight');
- message.scrollIntoView({ behavior: 'smooth' });
- }
- }
- });
- </script>
- </body>
- </html>`;
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement