Added Thumbnail & Vibrant colour support

pull/19/head
tycrek 3 years ago
parent e6e6164cde
commit fbf484e8aa
No known key found for this signature in database
GPG Key ID: 25D74F3943625263

@ -81,6 +81,7 @@ In your Cloudflare DNS dashboard, make sure your domain/subdomain is set to **DN
- Value: (the value provided by `npm start` on first run)
5. **Response** tab:
- URL: `$json:.resource$`
- Thumbnail: `$json:.thumbnail$`
- Deletion URL: `$json:.delete$`
- MagicCap users: **do not** include the `.` in the above (i.e. `$json:resource$`)
6. The file `sample_config.sxcu` can also be modified & imported to suit your needs
@ -107,7 +108,7 @@ If you primarily share media on Discord, you can add these additional (optional)
| **`X-Ass-OG-Author-Url`** | URL to open when the Author is clicked |
| **`X-Ass-OG-Provider`** | Smaller text shown above the author |
| **`X-Ass-OG-Provider-Url`** | URL to open when the Provider is clicked |
| **`X-Ass-OG-Color`** | Colour shown on the left side of the embed. Must be either a hex colour value (for example: `#fe3c29`) or `&random` |
| **`X-Ass-OG-Color`** | Colour shown on the left side of the embed. Must be one of `&random`, `&vibrant`, or a hex colour value (for example: `#fe3c29`). Random is a randomly generated hex value and Vibrant is sourced from the image itself |
#### Embed placeholders

116
ass.js

@ -19,6 +19,8 @@ const multer = require('multer');
const DateTime = require('luxon').DateTime;
const { WebhookClient, MessageEmbed } = require('discord.js');
const OpenGraph = require('./ogp');
const Thumbnail = require('./thumbnails');
const Vibrant = require('./vibrant');
const { path, saveData, log, verify, generateToken, generateId, formatBytes, randomHexColour } = require('./utils');
//#endregion
@ -135,53 +137,63 @@ function startup() {
color: req.headers['x-ass-og-color']
};
// Save the file information
let resourceId = generateId(generator, resourceIdSize, req.headers['x-ass-gfycat'] || gfyIdSize, req.file.originalname);
data[resourceId.split('.')[0]] = req.file;
saveData(data);
// Log the upload
let logInfo = `${req.file.originalname} (${req.file.mimetype})`;
log(`Uploaded: ${logInfo} (user: ${users[uploadToken] ? users[uploadToken].username : '<token-only>'})`);
// Build the URLs
let resourceUrl = `${getTrueHttp()}${trueDomain}/${resourceId}`;
let deleteUrl = `${getTrueHttp()}${trueDomain}/delete/${req.file.filename}`;
// Send the response
res.type('json').send({ resource: resourceUrl, delete: deleteUrl })
.on('finish', () => {
// After we have sent the user the response, also send a Webhook to Discord (if headers are present)
if (req.headers['x-ass-webhook-client'] && req.headers['x-ass-webhook-token']) {
// Build the webhook client & embed
let whc = new WebhookClient(req.headers['x-ass-webhook-client'], req.headers['x-ass-webhook-token']);
let embed = new MessageEmbed()
.setTitle(logInfo)
.setDescription(`**Size:** \`${formatBytes(req.file.size)}\`\n**[Delete](${deleteUrl})**`)
.setThumbnail(resourceUrl)
.setColor(randomHexColour())
.setTimestamp(req.file.timestamp);
// Send the embed to the webhook, then delete the client after to free resources
whc.send(null, {
username: req.headers['x-ass-webhook-username'] || 'ass',
avatarURL: req.headers['x-ass-webhook-avatar'] || ASS_LOGO,
embeds: [embed]
}).then((_msg) => whc.destroy());
}
// Also update the users upload count
if (!users[uploadToken]) {
let generator = () => generateId('random', 20, null);
let username = generator();
while (Object.values(users).findIndex((user) => user.username == username) != -1)
username = generator();
users[uploadToken] = { username, count: 0 };
}
users[uploadToken].count += 1;
fs.writeJsonSync(path('auth.json'), { tokens, users }, { spaces: 4 })
// Generate a thumbnail & get the Vibrant colour
Promise.all([Thumbnail(req.file), Vibrant(req.file)])
.then(([thumbnail, vibrant]) => (req.file.thumbnail = thumbnail, req.file.vibrant = vibrant))
.catch(console.error)
// Finish processing the file
.then(() => {
// Save the file information
let resourceId = generateId(generator, resourceIdSize, req.headers['x-ass-gfycat'] || gfyIdSize, req.file.originalname);
data[resourceId.split('.')[0]] = req.file;
saveData(data);
// Log the upload
let logInfo = `${req.file.originalname} (${req.file.mimetype})`;
log(`Uploaded: ${logInfo} (user: ${users[uploadToken] ? users[uploadToken].username : '<token-only>'})`);
// Build the URLs
let resourceUrl = `${getTrueHttp()}${trueDomain}/${resourceId}`;
let thumbnailUrl = `${getTrueHttp()}${trueDomain}/${resourceId}/thumbnail`;
let deleteUrl = `${getTrueHttp()}${trueDomain}/delete/${req.file.filename}`;
// Send the response
res.type('json').send({ resource: resourceUrl, thumbnail: thumbnailUrl, delete: deleteUrl })
.on('finish', () => {
// After we have sent the user the response, also send a Webhook to Discord (if headers are present)
if (req.headers['x-ass-webhook-client'] && req.headers['x-ass-webhook-token']) {
// Build the webhook client & embed
let whc = new WebhookClient(req.headers['x-ass-webhook-client'], req.headers['x-ass-webhook-token']);
let embed = new MessageEmbed()
.setTitle(logInfo)
.setURL(resourceUrl)
.setDescription(`**Size:** \`${formatBytes(req.file.size)}\`\n**[Delete](${deleteUrl})**`)
.setThumbnail(thumbnailUrl)
.setColor(req.file.vibrant)
.setTimestamp(req.file.timestamp);
// Send the embed to the webhook, then delete the client after to free resources
whc.send(null, {
username: req.headers['x-ass-webhook-username'] || 'ass',
avatarURL: req.headers['x-ass-webhook-avatar'] || ASS_LOGO,
embeds: [embed]
}).then((_msg) => whc.destroy());
}
// Also update the users upload count
if (!users[uploadToken]) {
let generator = () => generateId('random', 20, null);
let username = generator();
while (Object.values(users).findIndex((user) => user.username == username) != -1)
username = generator();
users[uploadToken] = { username, count: 0 };
}
users[uploadToken].count += 1;
fs.writeJsonSync(path('auth.json'), { tokens, users }, { spaces: 4 })
});
});
});
@ -201,6 +213,16 @@ function startup() {
.catch(console.error);
});
// Thumbnail response
app.get('/:resourceId/thumbnail', (req, res) => {
let resourceId = req.ass.resourceId;
// Read the file and send it to the client
fs.readFile(path('uploads/thumbnails/', data[resourceId].thumbnail))
.then((fileData) => res.type('jpg').send(fileData))
.catch(console.error);
});
// oEmbed response for clickable authors/providers
// https://oembed.com/
// https://old.reddit.com/r/discordapp/comments/82p8i6/a_basic_tutorial_on_how_to_get_the_most_out_of/

@ -13,13 +13,14 @@ class OpenGraph {
type;
size;
timestamp;
vibrant;
title;
description;
author;
color;
constructor(http, domain, resourceId, { originalname, mimetype, size, timestamp, opengraph }) {
constructor(http, domain, resourceId, { originalname, mimetype, size, timestamp, opengraph, vibrant }) {
this.http = http;
this.domain = domain;
this.resourceId = resourceId;
@ -28,6 +29,7 @@ class OpenGraph {
this.filename = originalname;
this.size = size;
this.timestamp = timestamp;
this.vibrant = vibrant;
this.title = opengraph.title || '';
this.description = opengraph.description || '';
@ -50,13 +52,17 @@ class OpenGraph {
title: (this.title.length != 0) ? `<meta property="og:title" content="${this.title}">` : '',
description: (this.description.length != 0) ? `<meta property="og:description" content="${this.description}">` : '',
site: (this.author.length != 0) ? `<meta property="og:site_name" content="${this.author}">` : '',
color: (this.color.length != 0) ? `<meta name="theme-color" content="${this.color === '&random' ? randomHexColour() : this.color}">` : '',
color: (this.color.length != 0) ? `<meta name="theme-color" content="${this.getBuildColor()}">` : '',
card: !this.type.includes('video') ? `<meta name="twitter:card" content="summary_large_image">` : '',
})
.replace(new RegExp('&size', 'g'), formatBytes(this.size))
.replace(new RegExp('&filename', 'g'), this.filename)
.replace(new RegExp('&timestamp', 'g'), DateTime.fromMillis(this.timestamp).toLocaleString(DateTime.DATETIME_MED));
}
getBuildColor() {
return this.color === '&random' ? randomHexColour() : this.color === '&vibrant' ? this.vibrant : this.color;
}
}
const html = `

@ -0,0 +1,48 @@
const ffmpeg = require('ffmpeg-static');
const Jimp = require('jimp');
const shell = require('any-shell-escape');
const { exec } = require('child_process');
const { path } = require('./utils');
const THUMBNAIL_QUALITY = 50;
const THUMBNAIL_SIZE = 512;
function getCommand(src, dest) {
return shell([
ffmpeg, '-y', '-v', process.env.NODE_ENV == 'production' ? 'error' : 'debug',
'-i', src,
'-ss', '00:00:01.000',
'-frames:v', '1',
'-s', `${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE}`,
dest
]);
}
function getVideoThumbnail(file) {
return new Promise((resolve, reject) => exec(getCommand(path(file.path), getNewNamePath(file.originalname)), (err) => err ? reject(err) : resolve()));
}
function getResizedThumbnail(file) {
return new Promise((resolve, reject) =>
Jimp.read(path(file.path))
.then((image) => image
.quality(THUMBNAIL_QUALITY)
.resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE, Jimp.RESIZE_BICUBIC)
.write(getNewNamePath(file.originalname)))
.then(resolve)
.catch(reject));
}
function getNewNamePath(oldName) {
return path('uploads/thumbnails/', getNewName(oldName));
}
function getNewName(oldName) {
return oldName.concat('.thumbnail.jpg');
}
module.exports = (file) =>
new Promise((resolve, reject) =>
(file.mimetype.includes('video') ? getVideoThumbnail : getResizedThumbnail)(file)
.then(() => resolve(getNewName(file.originalname)))
.catch(reject));

@ -0,0 +1,7 @@
const Vibrant = require('node-vibrant');
const { path } = require('./utils');
module.exports = (file) =>
new Promise((resolve, reject) =>
Vibrant.from(path(file.path)).getPalette()
.then((palette) => resolve(palette.Vibrant.hex))
.catch(reject));
Loading…
Cancel
Save