drop-the-cap
Version:
Generates fake discord screenshots
175 lines (156 loc) • 6.09 kB
JavaScript
const fs = require("fs");
const { createCanvas } = require("canvas");
const sharp = require("sharp");
const path = require("path");
const { fileURLToPath } = require("url");
const fetch = require("node-fetch");
/**
* Resolves a file input path, converting it to a local file path if it is a file URL.
* If the input path is an HTTP URL, it returns the URL unchanged.
* For any other path formats, it returns the input path as is.
*
* @param {string} inputPath - The path to resolve, can be a file URL, HTTP URL, or a local path.
* @returns {string} - The resolved file path.
* @throws {Error} - If there is an error resolving the file input path.
*/
function resolveFileInput(inputPath) {
try {
if (inputPath.startsWith("file://")) {
return fileURLToPath(inputPath);
} else if (inputPath.startsWith("http")) {
return inputPath;
}
return inputPath;
} catch (error) {
throw new Error(`Error resolving file input path: ${error.message}`);
}
}
/**
* Loads an image from a URL or a local file path and returns its buffer.
*
* @param {string} inputPath - The URL or local file path of the image to load.
* @returns {Promise<Buffer>} - The image buffer.
* @throws {Error} - If there is an error loading the image.
*/
async function loadImageBuffer(inputPath) {
try {
const resolved = resolveFileInput(inputPath);
if (resolved.startsWith("http")) {
const res = await fetch(resolved);
if (!res.ok) throw new Error(`Failed to fetch image: ${res.status}`);
return Buffer.from(await res.arrayBuffer());
}
return fs.promises.readFile(resolved);
} catch (error) {
throw new Error(`Error loading image buffer: ${error.message}`);
}
}
/**
* Generates a random timestamp in the format "X:YY am/pm" where X is an hour between 1-12 and YY is a minute between 0-59.
* @returns {string} - The random timestamp.
* @throws {Error} - If there is an error generating the random timestamp.
*/
function getRandomTimestamp() {
try {
const hour = Math.floor(Math.random() * 12) + 1;
const minute = Math.floor(Math.random() * 60);
const isPM = Math.random() > 0.5;
const formattedMinute = minute.toString().padStart(2, "0");
const period = isPM ? "pm" : "am";
return `${hour}:${formattedMinute} ${period}`;
} catch (error) {
throw new Error(`Error generating random timestamp: ${error.message}`);
}
}
/**
* Generates a fake Discord message screenshot.
*
* @param {Object} options - The options to use when generating the fake Discord message.
* @param {string} [options.pfpPath="https://i.postimg.cc/Prhch3nx/image.png"] - The URL or local file path of the user's profile picture.
* @param {string} [options.outputPath="fake_discord_message.png"] - The path where the generated screenshot will be saved.
* @param {string} [options.username="Ethanol"] - The username to display.
* @param {string} [options.timestamp] - The timestamp to display. If not provided, a random one is generated.
* @param {string} [options.message="C'est la vie"] - The message content.
* @param {string} [options.backgroundColor] - The background color of the message box.
* @param {string} [options.usernameColor] - The color of the username text.
* @param {string} [options.timestampColor] - The color of the timestamp text.
* @param {string} [options.messageColor] - The color of the message text.
* @param {number} [options.timestampXOffset] - The horizontal offset for the timestamp (helpful for aligning with longer usernames).
* @returns {Promise<string>} - The path to the generated screenshot.
* @throws {Error} - If there is an error generating the fake Discord message.
*/
async function generateDiscordMessage({
pfpPath = "https://i.postimg.cc/Prhch3nx/image.png",
outputPath = "fake_discord_message.png",
username = "Ethanol",
timestamp,
message = "C'est la vie",
backgroundColor,
usernameColor,
timestampColor,
messageColor,
timestampXOffset,
} = {}) {
try {
const WIDTH = 500;
const HEIGHT = 80;
const AVATAR_SIZE = 52;
const SCALE = 4;
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const canvas = createCanvas(WIDTH * SCALE, HEIGHT * SCALE);
const ctx = canvas.getContext("2d");
ctx.scale(SCALE, SCALE);
ctx.fillStyle = backgroundColor || "#2b2d31";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
ctx.font = "bold 16px sans-serif";
ctx.fillStyle = usernameColor || "white";
ctx.fillText(username, 73, 32);
ctx.font = "12px sans-serif";
ctx.fillStyle = timestampColor || "#83838b";
ctx.fillText(
timestamp || getRandomTimestamp(),
70 + ctx.measureText(username).width + (timestampXOffset || 35),
32
);
ctx.font = "16px sans-serif";
ctx.fillStyle = messageColor || "#efeff0";
ctx.fillText(message, 73, 55);
const backgroundBuffer = canvas.toBuffer();
const circleMask = Buffer.from(
`<svg width="${AVATAR_SIZE}" height="${AVATAR_SIZE}">
<circle cx="${AVATAR_SIZE / 2}" cy="${AVATAR_SIZE / 2}" r="${
AVATAR_SIZE / 2
}" fill="white"/>
</svg>`
);
const avatarBuffer = await loadImageBuffer(pfpPath);
const circularAvatar = await sharp(avatarBuffer)
.resize(AVATAR_SIZE, AVATAR_SIZE)
.composite([{ input: circleMask, blend: "dest-in" }])
.png()
.toBuffer();
await sharp(backgroundBuffer, { density: 144 })
.resize(WIDTH, HEIGHT)
.composite([
{
input: circularAvatar,
top: 14,
left: 10,
},
])
.png()
.toFile(outputPath);
return outputPath;
} catch (error) {
throw new Error(`Error generating Discord message: ${error.message}`);
}
}
module.exports = {
generateDiscordMessage,
getRandomTimestamp,
resolveFileInput,
loadImageBuffer,
};