Integrate worker to offload main thread from heavy tasks
This commit is contained in:
17
bot.js
17
bot.js
@@ -1,7 +1,7 @@
|
|||||||
import {data} from "./wwwroot/core/appData.js";
|
import {data} from "./wwwroot/core/appData.js";
|
||||||
import JsonManager from "./wwwroot/core/utils/jsonManager.js";
|
import JsonManager from "./wwwroot/core/utils/jsonManager.js";
|
||||||
import {Logger} from "./wwwroot/core/logging/logger.js";
|
import {Logger} from "./wwwroot/core/logging/logger.js";
|
||||||
import {NameCardCreator} from "./wwwroot/core/welcome/nameCardCreator.js";
|
import {launchWorker} from "./wwwroot/core/namecards/workerLauncher.js";
|
||||||
|
|
||||||
const launch = async () => {
|
const launch = async () => {
|
||||||
try{
|
try{
|
||||||
@@ -30,8 +30,19 @@ const launch = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
data.client.on('guildMemberAdd', member => {
|
data.client.on('guildMemberAdd', async member => {
|
||||||
NameCardCreator.getWelcomeCard("", member.user.avatarURL(), member.user.displayName)
|
try {
|
||||||
|
launchWorker({
|
||||||
|
templatePath : data.nameCardTemplate,
|
||||||
|
avatarURL: member.user.avatarURL,
|
||||||
|
displayName: member.user.displayName,
|
||||||
|
}).then(buffer => {
|
||||||
|
//Send name card here...
|
||||||
|
console.log('Name card sent!');
|
||||||
|
}).catch(console.error);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to generate/send name card:', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
import {NameCardCreator} from "../wwwroot/core/welcome/nameCardCreator-2.js";
|
import {NameCardCreator} from "../wwwroot/core/namecards/nameCardCreator.js";
|
||||||
|
import {launchWorker} from "../wwwroot/core/namecards/workerLauncher.js";
|
||||||
|
import {data} from "../wwwroot/core/appData.js";
|
||||||
|
|
||||||
const templatePath = "./wwwroot/assets/name-card-template.png";
|
const templatePath = "./wwwroot/assets/name-card-template.png";
|
||||||
const avatarPath = "./tests/assets/avatar-test.png";
|
const avatarPath = "./tests/assets/avatar-test.png";
|
||||||
const name = "Aude Vaiselle";
|
const name = "Aude Vaiselle";
|
||||||
const creator = new NameCardCreator(templatePath);
|
|
||||||
await creator.getWelcomeCard(avatarPath, name)
|
|
||||||
|
try {
|
||||||
|
launchWorker({
|
||||||
|
templatePath : data.nameCardTemplate,
|
||||||
|
avatarURL: avatarPath,
|
||||||
|
username: name,
|
||||||
|
}).then(buffer => {
|
||||||
|
//Send name card here...
|
||||||
|
console.log('Name card sent!');
|
||||||
|
}).catch(console.error);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to generate/send name card:', err);
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@ import 'dotenv/config';
|
|||||||
import {Client, GatewayIntentBits} from "discord.js";
|
import {Client, GatewayIntentBits} from "discord.js";
|
||||||
import {MessageSender} from "./base/MessageSender.js";
|
import {MessageSender} from "./base/MessageSender.js";
|
||||||
import {InstagramTokenManager} from "./instagram/instagramTokenManager.js";
|
import {InstagramTokenManager} from "./instagram/instagramTokenManager.js";
|
||||||
import {TikTokTokenManager} from "./tiktok/tiktokTokenManager.js";
|
|
||||||
import JsonManager from "./utils/jsonManager.js";
|
|
||||||
import {InstagramPoller} from "./instagram/instagramPoller.js";
|
import {InstagramPoller} from "./instagram/instagramPoller.js";
|
||||||
import {UsersToken} from "./usersToken.js";
|
import {UsersToken} from "./usersToken.js";
|
||||||
|
|
||||||
@@ -16,6 +14,7 @@ const client = new Client({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nameCardTemplate = "./wwwroot/assets/name-card-template.png";
|
||||||
const sender = new MessageSender(client);
|
const sender = new MessageSender(client);
|
||||||
|
|
||||||
const instagramID = process.env.INSTAGRAM_CLIENT_ID;
|
const instagramID = process.env.INSTAGRAM_CLIENT_ID;
|
||||||
@@ -41,6 +40,7 @@ export const data = {
|
|||||||
updateChannelID,
|
updateChannelID,
|
||||||
socialChannelID,
|
socialChannelID,
|
||||||
instagramTokenManager,
|
instagramTokenManager,
|
||||||
|
nameCardTemplate
|
||||||
//tiktokTokenManager
|
//tiktokTokenManager
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class Logger{
|
|||||||
await mkdir(logsDir, { recursive: true });
|
await mkdir(logsDir, { recursive: true });
|
||||||
|
|
||||||
await appendFile(fullPath, `${time} - ${message} -> ${error.message} \n`);
|
await appendFile(fullPath, `${time} - ${message} -> ${error.message} \n`);
|
||||||
|
console.error(error.stackTrace);
|
||||||
console.error(`An error occured. The incident has been logged in ${fullPath}`)
|
console.error(`An error occured. The incident has been logged in ${fullPath}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error writing log:", err);
|
console.error("Error writing log:", err);
|
||||||
|
|||||||
@@ -78,13 +78,31 @@ export class NameCardCreator {
|
|||||||
ctx.fillText(line, messageX, startY + i * lineHeight);
|
ctx.fillText(line, messageX, startY + i * lineHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
const buffer = canvas.toBuffer("image/png");
|
const result = this.scallDown(canvas, 1500, 500)
|
||||||
fs.writeFileSync("./tests/result/namecard.png", buffer);
|
|
||||||
|
fs.writeFileSync("./tests/result/namecard.png", result);
|
||||||
console.log("✅ Name card created: namecard.png");
|
console.log("✅ Name card created: namecard.png");
|
||||||
return buffer;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error creating name card:", err);
|
console.error("Error creating name card:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param target
|
||||||
|
* @param width
|
||||||
|
* @param height
|
||||||
|
* @returns {Buffer}
|
||||||
|
*/
|
||||||
|
scallDown(target, width, height) {
|
||||||
|
const smallCanvas = createCanvas(width, height);
|
||||||
|
const smallCtx = smallCanvas.getContext('2d');
|
||||||
|
|
||||||
|
smallCtx.drawImage(target, 0, 0, width, height);
|
||||||
|
return smallCanvas.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
27
wwwroot/core/namecards/nameCardWorker.js
Normal file
27
wwwroot/core/namecards/nameCardWorker.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { parentPort } from 'worker_threads';
|
||||||
|
import {NameCardCreator} from "./nameCardCreator.js";
|
||||||
|
|
||||||
|
parentPort.on("message", async (data) => {
|
||||||
|
try {
|
||||||
|
const { templatePath, avatarURL, username } = data;
|
||||||
|
|
||||||
|
const creator = new NameCardCreator(templatePath);
|
||||||
|
const buffer = await creator.getWelcomeCard(avatarURL, username);
|
||||||
|
|
||||||
|
parentPort.postMessage({ result: buffer });
|
||||||
|
} catch (err) {
|
||||||
|
parentPort.postMessage({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*(async () => {
|
||||||
|
try {
|
||||||
|
const { creator, avatarURL, userName } = workerData;
|
||||||
|
|
||||||
|
const buffer = await creator.getWelcomeCard(avatarURL, userName);
|
||||||
|
|
||||||
|
parentPort.postMessage(buffer); // return the buffer to main thread
|
||||||
|
} catch (err) {
|
||||||
|
parentPort.postMessage({ error: err.message });
|
||||||
|
}
|
||||||
|
})();*/
|
||||||
31
wwwroot/core/namecards/workerLauncher.js
Normal file
31
wwwroot/core/namecards/workerLauncher.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { Worker } from 'worker_threads';
|
||||||
|
import {Logger} from "../logging/logger.js";
|
||||||
|
|
||||||
|
function launchWorker({templatePath, avatarURL, username}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const workerFile = path.resolve('./wwwroot/core/namecards/nameCardWorker.js');
|
||||||
|
const worker = new Worker(workerFile);
|
||||||
|
|
||||||
|
worker.postMessage({templatePath, avatarURL, username});
|
||||||
|
|
||||||
|
worker.on('message', (result) => {
|
||||||
|
resolve(result);
|
||||||
|
worker.terminate()
|
||||||
|
.catch(err => Logger.error("Unable to terminate worker", err.message));
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
worker.terminate()
|
||||||
|
.catch(err => Logger.error("Unable to terminate worker", err.message));
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.on('exit', (code) => {
|
||||||
|
if (code !== 0) reject(new Error(`Worker stopped with code ${code}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {launchWorker}
|
||||||
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
import sharp from "sharp";
|
|
||||||
import {Logger} from "../logging/logger.js";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { createCanvas, registerFont } from 'canvas';
|
|
||||||
|
|
||||||
export class NameCardCreator {
|
|
||||||
|
|
||||||
constructor(templatePath) {
|
|
||||||
this.templatePath = templatePath;
|
|
||||||
this.template = this.loadTemplate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the template file into a sharp instance
|
|
||||||
* @returns {sharp.Sharp} sharp image object
|
|
||||||
*/
|
|
||||||
loadTemplate() {
|
|
||||||
return sharp(this.templatePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFont(fontPath) {
|
|
||||||
const fullPath = path.resolve(fontPath);
|
|
||||||
console.debug(fullPath);
|
|
||||||
registerFont(fullPath, { family: 'Fredoka' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combines a template image with a user avatar and saves it
|
|
||||||
* @param avatarPath {string}
|
|
||||||
* @param name {string}
|
|
||||||
* @returns {Promise<sharp.OutputInfo>} resulting image buffer
|
|
||||||
*/
|
|
||||||
async getWelcomeCard(avatarPath, name) {
|
|
||||||
try{
|
|
||||||
|
|
||||||
const avatarSize = 550;
|
|
||||||
const avatar = await this.handleAvatar(avatarPath, avatarSize);
|
|
||||||
|
|
||||||
const messageWidth = 2000;
|
|
||||||
const messageHeight = 700;
|
|
||||||
const messageBuffer = await this.getMessageBuffer(name, messageWidth, messageHeight);
|
|
||||||
|
|
||||||
const avatarPos = { x : 150, y : (1000-avatarSize+16)/2}
|
|
||||||
const messagePos = { x : avatarPos.x + 700, y : (1000-messageHeight)/2}
|
|
||||||
|
|
||||||
const result = await this.template
|
|
||||||
.composite([
|
|
||||||
{ input: avatar, top: avatarPos.y, left: avatarPos.x },
|
|
||||||
{ input: messageBuffer, top: messagePos.y, left: messagePos.x }
|
|
||||||
])
|
|
||||||
.toFile("namecard-.png")
|
|
||||||
|
|
||||||
console.log("✅ Welcome card created: welcome-card.png");
|
|
||||||
return result;
|
|
||||||
} catch(err) {
|
|
||||||
console.log(err);
|
|
||||||
await Logger.error("Unable to create name card", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param avatarPath
|
|
||||||
* @param outputSize
|
|
||||||
* @returns {Promise<Buffer<ArrayBufferLike>>}
|
|
||||||
*/
|
|
||||||
async handleAvatar(avatarPath, outputSize) {
|
|
||||||
const avatarSize = outputSize;
|
|
||||||
const borderSize = 8;
|
|
||||||
const totalSize = avatarSize + borderSize * 2;
|
|
||||||
const avatarRadius = avatarSize / 2;
|
|
||||||
|
|
||||||
const maskBuffer = Buffer.from(`
|
|
||||||
<svg width="${avatarSize}" height="${avatarSize}" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="${avatarRadius}" cy="${avatarRadius}" r="${avatarRadius}" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
|
|
||||||
const avatarBuffer = await sharp(avatarPath)
|
|
||||||
.resize(avatarSize, avatarSize, { fit: "cover" })
|
|
||||||
.png()
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const roundedAvatarBuffer = await sharp(avatarBuffer)
|
|
||||||
.composite([{ input: maskBuffer, blend: "dest-in" }])
|
|
||||||
.png()
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const backgroundBuffer = Buffer.from(`
|
|
||||||
<svg width="${totalSize}" height="${totalSize}" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle
|
|
||||||
cx="${totalSize / 2}"
|
|
||||||
cy="${totalSize / 2}"
|
|
||||||
r="${avatarRadius}"
|
|
||||||
stroke="#ffffff"
|
|
||||||
stroke-width="${borderSize}"
|
|
||||||
fill="white"/>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
|
|
||||||
return await sharp({
|
|
||||||
create: {
|
|
||||||
width: totalSize,
|
|
||||||
height: totalSize,
|
|
||||||
channels: 4,
|
|
||||||
background: "#0000",
|
|
||||||
}})
|
|
||||||
.composite([
|
|
||||||
{ input: backgroundBuffer, top: 0, left: 0 },
|
|
||||||
{ input: roundedAvatarBuffer, top: borderSize, left: borderSize }
|
|
||||||
])
|
|
||||||
.png()
|
|
||||||
.toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param message
|
|
||||||
* @param outputWidth
|
|
||||||
* @param outputHeight
|
|
||||||
* @returns {Promise<void | Buffer<ArrayBufferLike>>}
|
|
||||||
*/
|
|
||||||
async getMessageBuffer(message, outputWidth, outputHeight){
|
|
||||||
return this.getMessageCanvas(message, outputWidth, outputHeight).toBuffer();
|
|
||||||
//return Buffer.from(messageSvg, "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param message {string}
|
|
||||||
* @param outputWidth
|
|
||||||
* @param outputHeight
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
/*getMessageSvg(message, outputWidth, outputHeight) {
|
|
||||||
const safeMessage =
|
|
||||||
message
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">");
|
|
||||||
|
|
||||||
return `
|
|
||||||
<svg width="${outputWidth}" height="${outputHeight}" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<style>
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Fredoka';
|
|
||||||
src: url("data:font/ttf;base64,${this.fontData}") format('truetype');
|
|
||||||
}
|
|
||||||
text {
|
|
||||||
font-family: 'Fredoka', sans-serif;
|
|
||||||
fill: #ede6e6;
|
|
||||||
font-size: 95px;
|
|
||||||
dominant-baseline: middle;
|
|
||||||
text-anchor: middle;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<text x="50%" y="50%" dy="0">Welcome ${safeMessage}, to the</text>
|
|
||||||
<text x="50%" y="50%" dy="1.3em">Spicy Jail ~</text>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @returns {Canvas}
|
|
||||||
*/
|
|
||||||
getMessageCanvas(name, outputWidth, outputHeight){
|
|
||||||
|
|
||||||
const canvas = createCanvas(outputWidth, outputHeight);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
ctx.fillStyle = '#0000';
|
|
||||||
ctx.fillRect(0, 0, outputWidth, outputHeight);
|
|
||||||
|
|
||||||
ctx.fillStyle = '#ede6e6';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.font = '110px "Fredoka"';
|
|
||||||
|
|
||||||
const message = `Welcome ${name}, to the Spicy Jail ~`;
|
|
||||||
ctx.fillText(message, outputWidth / 2, outputHeight / 2);
|
|
||||||
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*loadFontData(){
|
|
||||||
const fullPath = path.resolve(this.fontPath);
|
|
||||||
return fs.readFileSync(fullPath)
|
|
||||||
.toString("base64")
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user