From 0655a0d8a7bca9928feb0800210d28fc4b4c7a22 Mon Sep 17 00:00:00 2001 From: Deployor <129990841+deployor@users.noreply.github.com> Date: Thu, 20 Feb 2025 01:12:28 +0100 Subject: [PATCH] Added the Fun lines and error images! Also added partitial error support! --- .gitignore | 3 +- src/config/messages.js | 137 +++++++++++++++++++++++++++++++++++++++++ src/fileUpload.js | 126 +++++++++++++++++++++++++++++-------- src/storage.js | 2 +- src/upload.js | 4 +- 5 files changed, 244 insertions(+), 28 deletions(-) create mode 100644 src/config/messages.js diff --git a/.gitignore b/.gitignore index 5cc472e..a3b3a54 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /.idea/ /.env /bun.lockb -/package-lock.json \ No newline at end of file +/package-lock.json +/.history \ No newline at end of file diff --git a/src/config/messages.js b/src/config/messages.js new file mode 100644 index 0000000..cb579e3 --- /dev/null +++ b/src/config/messages.js @@ -0,0 +1,137 @@ +const messages = { + success: { + singleFile: "Hey <@{userId}>, here's your link:", + multipleFiles: "Hey <@{userId}>, here are your links:", + alternateSuccess: [ + "thanks!", + "thanks, i'm gonna sell these to adfly!", + "tysm!", + "file away!" + ] + }, + fileTypes: { + gif: [ + "_gif_ that file to me and i'll upload it", + "_gif_ me all all your files!" + ], + heic: [ + "What the heic???" + ], + mov: [ + "I'll _mov_ that to a permanent link for you" + ], + html: [ + "Oh, launching a new website?", + "uwu, what's this site?", + "WooOOAAah hey! Are you serving a site?", + "h-t-m-ello :wave:" + ], + rar: [ + ".rawr xD", + "i also go \"rar\" sometimes!" + ] + }, + errors: { + tooBig: { + messages: [ + "File too big!", + "That's a chonky file!", + "_orpheus struggles to lift the massive file_", + "Sorry, that file's too thicc for me to handle!" + ], + images: [ + "https://cloud-3tq9t10za-hack-club-bot.vercel.app/2too_big_4.png", + "https://cloud-3tq9t10za-hack-club-bot.vercel.app/3too_big_2.png", + "https://cloud-3tq9t10za-hack-club-bot.vercel.app/4too_big_1.png", + "https://cloud-3tq9t10za-hack-club-bot.vercel.app/6too_big_5.png", + "https://cloud-3tq9t10za-hack-club-bot.vercel.app/7too_big_3.png" + ] + }, + generic: { + messages: [ + "_orpheus sneezes and drops the files on the ground before blowing her nose on a blank jpeg._", + "_orpheus trips and your files slip out of her hands and into an inconveniently placed sewer grate._", + "_orpheus accidentally slips the files into a folder in her briefcase labeled \"homework\". she starts sweating profusely._" + ], + images: [ + "https://cloud-3tq9t10za-hack-club-bot.vercel.app/0generic_3.png", + "https://cloud-3tq9t10za-hack-club-bot.vercel.app/1generic_2.png", + "https://cloud-3tq9t10za-hack-club-bot.vercel.app/5generic_1.png" + ] + } + } +}; + +function getRandomItem(array) { + return array[Math.floor(Math.random() * array.length)]; +} + +function getFileTypeMessage(fileExtension) { + const ext = fileExtension.toLowerCase(); + return messages.fileTypes[ext] ? getRandomItem(messages.fileTypes[ext]) : null; +} + +function formatErrorMessage(failedFiles, isSizeError = false) { + const errorType = isSizeError ? messages.errors.tooBig : messages.errors.generic; + const errorMessage = getRandomItem(errorType.messages); + const errorImage = getRandomItem(errorType.images); + + return [ + errorMessage, + `Failed files: ${failedFiles.join(', ')}`, + '', + `<${errorImage}|image>` + ].join('\n'); +} + +function formatSuccessMessage(userId, files, failedFiles = [], sizeFailedFiles = []) { + const messageLines = []; + + const baseMessage = files.length === 1 ? + messages.success.singleFile : + messages.success.multipleFiles; + messageLines.push(baseMessage.replace('{userId}', userId), ''); + + const fileGroups = new Map(); + files.forEach(file => { + const ext = file.originalName.split('.').pop(); + const typeMessage = getFileTypeMessage(ext); + const key = typeMessage || 'noType'; + + if (!fileGroups.has(key)) { + fileGroups.set(key, []); + } + fileGroups.get(key).push(file); + }); + + fileGroups.forEach((groupFiles, typeMessage) => { + if (typeMessage !== 'noType') { + messageLines.push('', typeMessage); + } + + groupFiles.forEach(file => { + messageLines.push(`• ${file.originalName}: ${file.url}`); + }); + }); + + if (sizeFailedFiles.length > 0) { + messageLines.push(formatErrorMessage(sizeFailedFiles, true)); + } + if (failedFiles.length > 0) { + messageLines.push(formatErrorMessage(failedFiles, false)); + } + + if (files.length > 0) { + messageLines.push('', `_${getRandomItem(messages.success.alternateSuccess)}_`); + } + + return messageLines.join('\n'); +} + +module.exports = { + messages, + getFileTypeMessage, + formatSuccessMessage, + formatErrorMessage, + getRandomItem +}; diff --git a/src/fileUpload.js b/src/fileUpload.js index 458f894..3a3e616 100644 --- a/src/fileUpload.js +++ b/src/fileUpload.js @@ -3,8 +3,15 @@ const crypto = require('crypto'); const logger = require('./config/logger'); const storage = require('./storage'); const {generateFileUrl} = require('./utils'); +const path = require('path'); +const { + messages, + formatSuccessMessage, + formatErrorMessage, + getFileTypeMessage +} = require('./config/messages'); -const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024; // 2GB in bytes +const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB in bytes const CONCURRENT_UPLOADS = 3; // Max concurrent uploads (messages) const processedMessages = new Map(); @@ -43,16 +50,23 @@ function generateUniqueFileName(fileName) { async function processFiles(fileMessage, client) { const uploadedFiles = []; const failedFiles = []; + const sizeFailedFiles = []; + const fileTypeResponses = new Set(); logger.info(`Processing ${fileMessage.files?.length || 0} files`); for (const file of fileMessage.files || []) { try { if (file.size > MAX_FILE_SIZE) { - failedFiles.push(file.name); + sizeFailedFiles.push(file.name); continue; } + // Get file extension message if applicable + const ext = path.extname(file.name).slice(1); + const typeMessage = getFileTypeMessage(ext); + if (typeMessage) fileTypeResponses.add(typeMessage); + const response = await fetch(file.url_private, { headers: {Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`} }); @@ -71,6 +85,7 @@ async function processFiles(fileMessage, client) { uploadedFiles.push({ name: uniqueFileName, + originalName: file.name, url: generateFileUrl(userDir, uniqueFileName), contentType: file.mimetype }); @@ -81,8 +96,12 @@ async function processFiles(fileMessage, client) { } } - logger.info(`Completed: ${uploadedFiles.length} ok, ${failedFiles.length} failed`); - return {uploadedFiles, failedFiles}; + return { + uploadedFiles, + failedFiles, + sizeFailedFiles, + isSizeError: sizeFailedFiles.length > 0 + }; } // Slack interaction @@ -98,15 +117,26 @@ async function addProcessingReaction(client, event, fileMessage) { } } -async function updateReactions(client, event, fileMessage, success) { +async function updateReactions(client, event, fileMessage, totalFiles, failedCount) { try { await client.reactions.remove({ name: 'beachball', timestamp: fileMessage.ts, channel: event.channel_id }); + + // Choose reaction based on how many files failed or well succeded + let reactionName; + if (failedCount === totalFiles) { + reactionName = 'x'; // All files failed + } else if (failedCount > 0) { + reactionName = 'warning'; // Some files failed + } else { + reactionName = 'white_check_mark'; // All files succeeded + } + await client.reactions.add({ - name: success ? 'white_check_mark' : 'x', + name: reactionName, timestamp: fileMessage.ts, channel: event.channel_id }); @@ -133,7 +163,7 @@ async function findFileMessage(event, client) { throw new Error('No share info found for this channel'); } - // Get the exact message using the ts from share info + // Get the EXACT message using the ts from share info (channelShare) const messageTs = channelShare[0].ts; const messageInfo = await client.conversations.history({ @@ -154,21 +184,51 @@ async function findFileMessage(event, client) { } } -async function sendResultsMessage(client, channelId, fileMessage, uploadedFiles, failedFiles) { - let message = `Hey <@${fileMessage.user}>, `; - if (uploadedFiles.length > 0) { - message += `here ${uploadedFiles.length === 1 ? 'is your link' : 'are your links'}:\n`; - message += uploadedFiles.map(f => `• ${f.name}: ${f.url}`).join('\n'); - } - if (failedFiles.length > 0) { - message += `\n\nFailed to process: ${failedFiles.join(', ')}`; - } +async function sendResultsMessage(client, channelId, fileMessage, uploadedFiles, failedFiles, sizeFailedFiles) { + try { + let message; + if (uploadedFiles.length === 0 && (failedFiles.length > 0 || sizeFailedFiles.length > 0)) { + // All files failed - use appropriate error type + message = formatErrorMessage( + [...failedFiles, ...sizeFailedFiles], + sizeFailedFiles.length > 0 && failedFiles.length === 0 // Only use size error if all failures are size-related (i hope this is how it makes most sense) + ); + } else { + // Mixed success/failure or all success + message = formatSuccessMessage( + fileMessage.user, + uploadedFiles, + failedFiles, + sizeFailedFiles + ); + } - await client.chat.postMessage({ - channel: channelId, - thread_ts: fileMessage.ts, - text: message - }); + const lines = message.split('\n'); + const attachments = []; + let textBuffer = ''; + + for (const line of lines) { + if (line.match(/^<.*\|image>$/)) { + const imageUrl = line.replace(/^<|>$/g, '').replace('|image', ''); + attachments.push({ + image_url: imageUrl, + fallback: 'Error image' + }); + } else { + textBuffer += line + '\n'; + } + } + + await client.chat.postMessage({ + channel: channelId, + thread_ts: fileMessage.ts, + text: textBuffer.trim(), + attachments: attachments.length > 0 ? attachments : undefined + }); + } catch (error) { + logger.error('Failed to send results message:', error); + throw error; + } } async function handleError(client, channelId, fileMessage, reactionAdded) { @@ -210,9 +270,27 @@ async function handleFileUpload(event, client) { await addProcessingReaction(client, event, fileMessage); reactionAdded = true; - const {uploadedFiles, failedFiles} = await processFiles(fileMessage, client); - await sendResultsMessage(client, event.channel_id, fileMessage, uploadedFiles, failedFiles); - await updateReactions(client, event, fileMessage, failedFiles.length === 0); + const {uploadedFiles, failedFiles, sizeFailedFiles} = await processFiles(fileMessage, client); + + const totalFiles = uploadedFiles.length + failedFiles.length + sizeFailedFiles.length; + const failedCount = failedFiles.length + sizeFailedFiles.length; + + await sendResultsMessage( + client, + event.channel_id, + fileMessage, + uploadedFiles, + failedFiles, + sizeFailedFiles + ); + + await updateReactions( + client, + event, + fileMessage, + totalFiles, + failedCount + ); } catch (error) { logger.error(`Upload failed: ${error.message}`); diff --git a/src/storage.js b/src/storage.js index dbb755f..0968212 100644 --- a/src/storage.js +++ b/src/storage.js @@ -57,7 +57,7 @@ function sanitizeFileName(fileName) { return sanitizedFileName; } -// Generate a unique, non-guessable file name +// Generate a unique file name function generateUniqueFileName(fileName) { const sanitizedFileName = sanitizeFileName(fileName); const uniqueFileName = `${Date.now()}-${crypto.randomBytes(16).toString('hex')}-${sanitizedFileName}`; diff --git a/src/upload.js b/src/upload.js index 167d897..fa73ab2 100644 --- a/src/upload.js +++ b/src/upload.js @@ -9,11 +9,11 @@ const handleUpload = async (file) => { try { const buffer = fs.readFileSync(file.path); const fileName = path.basename(file.originalname); - // Add content type detection for S3 + // content type detection for S3 const contentType = file.mimetype || 'application/octet-stream'; const uniqueFileName = `${Date.now()}-${fileName}`; - // Upload to S3 storage with content type + // Upload to S3 logger.debug(`Uploading: ${uniqueFileName}`); const uploaded = await uploadToStorage('s/v3', uniqueFileName, buffer, contentType); if (!uploaded) throw new Error('Storage upload failed');