Added the Fun lines and error images! Also added partitial error support!

This commit is contained in:
Deployor 2025-02-20 01:12:28 +01:00
parent 16c690583d
commit 0655a0d8a7
5 changed files with 244 additions and 28 deletions

3
.gitignore vendored
View file

@ -3,4 +3,5 @@
/.idea/
/.env
/bun.lockb
/package-lock.json
/package-lock.json
/.history

137
src/config/messages.js Normal file
View file

@ -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
};

View file

@ -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}`);

View file

@ -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}`;

View file

@ -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');