diff --git a/.env.example b/.env.example
index c0a73b7..8c6c4f7 100644
--- a/.env.example
+++ b/.env.example
@@ -9,4 +9,8 @@ DB_ROOTPW=
DB_PORT=
API_PORT=3080
API_ACCESS_TOKEN=
-HOMEPAGE_URL=
\ No newline at end of file
+HOMEPAGE_URL=
+
+#Rendering
+JOSE_ENDPOINT=
+ASSET_URL=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c3bec1c..1524d30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
assets/image_cache
assets/cards
assets/import
+assets/userdata
### Visual Studio Code ###
.vscode
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index d0f4688..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,3 +0,0 @@
-# syntax=docker/dockerfile:1
-FROM node:16.9.0-alpine
-RUN apk add --no-cache imagemagick
\ No newline at end of file
diff --git a/api/jsonApi.js b/api/jsonApi.js
index b30235d..2921cce 100644
--- a/api/jsonApi.js
+++ b/api/jsonApi.js
@@ -93,4 +93,6 @@ app.use(PREFIX, router);
app.use(PREFIX, groupRoutes);
app.use(PREFIX, badgeRoutes);
app.use(PREFIX, characterRoutes);
+app.use('/assets', express.static('assets'));
+
module.exports = app;
diff --git a/assets/userdata/profiles/.gitkeep b/assets/userdata/profiles/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/commands/debug.js b/commands/debug.js
index 4ca5321..1b8d973 100644
--- a/commands/debug.js
+++ b/commands/debug.js
@@ -1,7 +1,7 @@
const { SlashCommandBuilder, ComponentType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
const { customAlphabet } = require("nanoid");
const { Card, User, Wishlist, Character } = require("../models");
-const { UserUtils, CardUtils, GeneralUtils } = require("../util");
+const { UserUtils, CardUtils, GeneralUtils, Rendering } = require("../util");
const { PATREON } = require("../config/constants");
const stores = require("../stores");
require('dotenv').config();
@@ -28,6 +28,7 @@ module.exports = {
{ name: 'toggle_maintenance', value: 'toggle_maintenance' },
{ name: 'store', value: 'store' },
{ name: 'wishlist', value: 'wishlist' },
+ { name: 'rendering', value: 'rendering' },
{ name: 'patreon', value: 'patreon' }
)
)
@@ -181,6 +182,55 @@ module.exports = {
let patreon = await UserUtils.getPatreonPerks(interaction.client, extUser ? extUser : user);
interaction.channel.send(JSON.stringify(patreon));
+ break;
+ case "rendering":
+ const row = new ActionRowBuilder();
+ row.addComponents(
+ new ButtonBuilder()
+ .setCustomId(`testbatch`)
+ .setLabel(`Render test batch`)
+ .setStyle(ButtonStyle.Primary),
+ );
+ interaction.editReply({
+ content: `Jose endpoint: ${process.env.JOSE_ENDPOINT}\n Asset URL: ${process.env.ASSET_URL}`,
+ components: [row],
+ ephemeral: false
+ });
+ const filter = (m) => m.user.id === interaction.user.id;
+ const collector = interaction.channel.createMessageComponentCollector({ filter: filter, componentType: ComponentType.Button, time: 60000 });
+ collector.on('collect', async (i) => {
+ switch (i.customId) {
+ case 'testbatch':
+ i.deferUpdate();
+ interaction.channel.send("Beep boop test batch of 5");
+ let testCard = await Card.build({
+ characterId: 0,
+ userId: 1,
+ identifier: "0xffff",
+ quality: 1,
+ printNr: 0,
+
+ });
+ let testCharacter = Character.build({
+ id: 0,
+ groupId: 0,
+ name: "test",
+ imageIdentifier: "azur-lane/akashi.png",
+ enabled: true
+ })
+ for (let index = 0; index < 5; index++) {
+ testCard.printNr = index;
+ let render = await Rendering.renderCard(testCard, testCharacter).catch(async function(error){
+ await interaction.channel.send(JSON.stringify(error));
+ await interaction.channel.send(JSON.stringify(error.response?.data));
+ return;
+ });
+ await interaction.channel.send(render);
+ }
+ break;
+ }
+ });
+
break;
default:
interaction.editReply({
diff --git a/commands/editprofile.js b/commands/editprofile.js
index 349c54f..771a9d6 100644
--- a/commands/editprofile.js
+++ b/commands/editprofile.js
@@ -1,6 +1,6 @@
-const { SlashCommandBuilder, ComponentType, TextInputBuilder, TextInputStyle, ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder } = require("discord.js");
+const { SlashCommandBuilder, ComponentType, TextInputBuilder, TextInputStyle, ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, Attachment } = require("discord.js");
const { Card, User, Character } = require("../models");
-const { UserUtils, ReplyUtils } = require("../util");
+const { UserUtils, ReplyUtils, GeneralUtils } = require("../util");
const pageSize = 8;
@@ -8,12 +8,18 @@ const pageSize = 8;
module.exports = {
data: new SlashCommandBuilder()
.setName("editprofile")
- .setDescription("Edit your profile"),
+ .setDescription("Edit your profile")
+ .addAttachmentOption((option) =>
+ option
+ .setName("attachement")
+ .setDescription("Attachement to be used")
+ .setRequired(false)
+ ),
permissionLevel: 0,
async execute(interaction) {
await interaction.deferReply();
let user = await UserUtils.getUserByDiscordId(interaction.member.id);
-
+ let patreon = await UserUtils.getPatreonPerks(interaction.client, user);
let profile = await user.getProfile();
@@ -31,6 +37,15 @@ module.exports = {
.setStyle(ButtonStyle.Primary)
);
+ if (patreon.perks?.["custom_bg"] === true && interaction.options.getAttachment("attachement")) {
+ mainRow.addComponents(
+ new ButtonBuilder()
+ .setLabel('Set attachment as custom background')
+ .setCustomId('setCustomBg')
+ .setStyle(ButtonStyle.Primary)
+ );
+ }
+
const pingRow = new ActionRowBuilder();
pingRow.addComponents(
new ButtonBuilder()
@@ -51,11 +66,10 @@ module.exports = {
let message = await interaction.editReply({ content: "", components: [mainRow, pingRow], fetchReply: true });
//filter only events from the user who triggered the command
- const filter = (m) => m.author.id === interaction.author.id;
- const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 25000 })
+ const filter = (m) => m.user.id === interaction.user.id;
+ const collector = message.createMessageComponentCollector({ filter: filter, componentType: ComponentType.Button, time: 300000 })
collector.on('collect', async (i) => {
- await i.deferReply();
switch (i.customId) {
case 'editStatus':
await this.openStatusModal(i, user, profile);
@@ -63,19 +77,36 @@ module.exports = {
case 'editShowcase':
await this.openShowcaseModal(i, user, profile);
break;
+ case 'setCustomBg':
+ await i.deferReply();
+ let allowedContentTypes = [ "image/png", "image/jpeg" ];
+ let image = interaction.options.getAttachment("attachement");
+ if (!allowedContentTypes.includes(image.contentType)) {
+ await i.editReply({ content: "An invalid image has been attached. Allowed are .png and .jpeg", ephemeral: true });
+ return;
+ }
+ await GeneralUtils.downloadFile(image.url, `/app/assets/userdata/profiles/${image.id}_${image.name}`);
+ profile.customBackground = `${process.env.ASSET_URL}/userdata/profiles/${image.id}_${image.name}`;
+ await profile.save();
+ await i.editReply('Custom profile background has been set!');
+ break;
case 'toggle-wishlist-ping':
+ await i.deferUpdate();
user.wishlistPing = !user.wishlistPing;
user.save();
break;
case 'toggle-drop-ping':
+ await i.deferUpdate();
user.dropPing = !user.dropPing;
user.save();
break;
case 'toggle-daily-ping':
+ await i.deferUpdate();
user.dailyPing = !user.dailyPing;
user.save();
break;
default:
+ await i.deferReply();
i.editReply({ content: "Invalid selection" });
return;
break;
@@ -89,8 +120,6 @@ module.exports = {
}
});
await message.edit({ components: newComponents });
- let msg = await i.editReply({content: '...'});
- await msg.delete();
});
},
async openShowcaseModal(interaction, user, profile) {
@@ -118,13 +147,12 @@ module.exports = {
let submitted = await interaction.awaitModalSubmit({
time: 60000,
- filter: i => i.user.id === interaction.user.id,
+ filter: i => i.user.id === interaction.user.id && i.customId === 'cardSlotModal',
}).catch(error => {
//Error includes timeout
console.error(error)
return null
})
-
if (submitted) {
let updatePayload = {};
for (slot of slots) {
@@ -144,7 +172,7 @@ module.exports = {
},
async openStatusModal(interaction, user, profile) {
const modal = new ModalBuilder()
- .setCustomId('descriptionModal')
+ .setCustomId('statusModal')
.setTitle('Edit profile status/description');
let row = new ActionRowBuilder();
@@ -162,7 +190,7 @@ module.exports = {
let submitted = await interaction.awaitModalSubmit({
time: 300000,
- filter: i => i.user.id === interaction.user.id,
+ filter: i => i.user.id === interaction.user.id && i.customId === 'statusModal',
}).catch(error => {
//Error includes timeout
console.error(error)
diff --git a/commands/profile.js b/commands/profile.js
index caee635..feee576 100644
--- a/commands/profile.js
+++ b/commands/profile.js
@@ -1,10 +1,9 @@
-const { SlashCommandBuilder, MessageAttachment, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
-const { Card, User, Character } = require("../models");
-const { UserUtils, Compositing, Rendering } = require("../util");
+require("dotenv").config();
+const { SlashCommandBuilder } = require("discord.js");
+const { Card } = require("../models");
+const { UserUtils, Rendering, GeneralUtils } = require("../util");
const axios = require("axios");
-const sharp = require("sharp");
const { CURRENCY_NAMES } = require("../config/constants");
-const fs = require('fs');
const pageSize = 8;
@@ -25,56 +24,154 @@ module.exports = {
let discordUser = interaction.options.getUser("user") ? interaction.options.getUser("user") : interaction.member.user;
let user = await UserUtils.getUserByDiscordId(discordUser.id);
-
+ let patreon = await UserUtils.getPatreonPerks(interaction.client, user);
let profile = await user.getProfile();
- let customStatus = this.encodeStr(profile.customStatus);
- customStatus = customStatus.replace(/(.{0,40}[\s])/g, '$1');
+ let customStatus = profile.customStatus;
- let profileTemplate = fs.readFileSync('/app/assets/profile/profile.svg').toString();
- profileTemplate = profileTemplate.replace(/{{USERNAME}}/g, this.encodeStr(discordUser.username.substr(0,15)+(discordUser.username.length>15?'...':'')));
- profileTemplate = profileTemplate.replace(/{{PROFILE_TEXT}}/g, customStatus );
- profileTemplate = profileTemplate.replace(/{{HEADER_COLOR}}/g, '190,31,97');
- profileTemplate = profileTemplate.replace(/{{CC}}/g, await Card.count({where: {userId: user.id}}));
- profileTemplate = profileTemplate.replace(/{{LVL}}/g, await user.level().currentLevel);
- profileTemplate = profileTemplate.replace(/{{CUR_1}}/g, `${await user.primaryCurrency} ${CURRENCY_NAMES[1]}`);
- profileTemplate = profileTemplate.replace(/{{CUR_2}}/g, `${await user.secondaryCurrency} ${CURRENCY_NAMES[2]}`);
-
- let userImageBuffer = await axios.get(discordUser.displayAvatarURL({format: 'png', size: 128}), { responseType: 'arraybuffer' });
- userImage = await sharp(userImageBuffer.data);
- const rect = new Buffer.from(
- ''
- );
- userImage = await userImage.composite([{input: rect, blend: 'dest-in' }]).png().toBuffer();
-
- let background = await sharp(Buffer.from(profileTemplate, 'utf8'))
- .composite([{ input: userImage, left: 360, top: 20 }]).png().toBuffer();
+ let userImage = discordUser.displayAvatarURL({format: 'png', size: 128}).split('webp')[0] + 'png';
let slots = ['slotOne', 'slotTwo', 'slotThree', 'slotFour'];
let renderedCards = [];
- for (slot of slots) {
+ await Promise.all(slots.map(async slot => {
let card = await Card.findOne({ where: { id: profile[slot], burned: false } });
if (card) {
+ console.log(`Iterating card ${card.id}`);
let cardImage = await Rendering.renderCard(card);
- renderedCards.push(cardImage);
+ renderedCards[slot] = cardImage;
} else {
- renderedCards.push('/app/assets/cards/missing_image.png');
+ renderedCards[slot] = `${process.env.ASSET_URL}/cards/card_cover.png`;
}
+ }));
+ let job = {
+ "type": "profile",
+ "size": {
+ "width": 1200,
+ "height": 600
+ },
+ "elements": [
+ {
+ "type": "image",
+ "asset": `${renderedCards['slotOne']}`,
+ "x": 25,
+ "y": 85,
+ "width": 300,
+ "height": 500
+ },
+ {
+ "type": "image",
+ "asset": `${renderedCards['slotTwo']}`,
+ "x": 375,
+ "y": 310,
+ "width": 175,
+ "height": 275
+ },
+ {
+ "type": "image",
+ "asset": `${renderedCards['slotThree']}`,
+ "x": 560,
+ "y": 310,
+ "width": 175,
+ "height": 275
+ },
+ {
+ "type": "image",
+ "asset": `${renderedCards['slotFour']}`,
+ "x": 745,
+ "y": 310,
+ "width": 175,
+ "height": 275
+ },
+ {
+ "type": "image",
+ "asset": userImage,
+ "x": 350,
+ "y": 50,
+ "width": 150,
+ "height": 150
+ },
+ {
+ "type": "text",
+ "text": discordUser.username.substr(0,15)+(discordUser.username.length>15?'...':''),
+ "fontSize": 32,
+ "x": 25,
+ "y": 20,
+ "width": 300,
+ "height": 30,
+ "horizontalAlignment": "center"
+ },
+ {
+ "type": "text",
+ "text": `CC: ${GeneralUtils.formatNumber(await Card.count({where: {userId: user.id}}))}`,
+ "fontSize": 30,
+ "x": 550,
+ "y": 20,
+ "width": 150,
+ "height": 30,
+ "horizontalAlignment": "left"
+ },
+ {
+ "type": "text",
+ "text": `LVL: ${await user.level().currentLevel}`,
+ "fontSize": 30,
+ "x": 700,
+ "y": 20,
+ "width": 150,
+ "height": 30,
+ "horizontalAlignment": "left"
+ },
+ {
+ "type": "text",
+ "text": `${GeneralUtils.formatNumber(await user.primaryCurrency)} ${CURRENCY_NAMES[1]}`,
+ "fontSize": 30,
+ "x": 850,
+ "y": 20,
+ "width": 170,
+ "height": 30,
+ "horizontalAlignment": "left"
+ },
+ {
+ "type": "text",
+ "text": `${await GeneralUtils.formatNumber(user.secondaryCurrency)} ${CURRENCY_NAMES[2]}`,
+ "fontSize": 30,
+ "x": 1020,
+ "y": 20,
+ "width": 170,
+ "height": 30,
+ "horizontalAlignment": "left"
+ },
+ {
+ "type": "text",
+ "text": customStatus,
+ "fontSize": 25,
+ "x": 550,
+ "y": 65,
+ "width": 625,
+ "height": 300,
+ "horizontalAlignment": "left"
+ }
+ ]
}
-
- let profileImage = await Compositing.renderProfile(profile, background, renderedCards);
- await interaction.editReply({ files: [profileImage] });
- },
- encodeStr: function(str) {
- let charMapping = {
- '&': '&',
- '"': '"',
- '<': '<',
- '>': '>'
- };
- return str.replace(/([\&"<>])/g, function(str, item) {
- return charMapping[item];
- });
+
+ if (patreon.perks?.['custom_bg'] && profile.customBackground) {
+ job.elements.unshift(
+ {
+ "type": "image",
+ "asset": profile.customBackground,
+ "x": 0,
+ "y": 0,
+ "width": 1200,
+ "height": 600
+ }
+ );
+ }
+ console.log("Fetching ", );
+ if(process.env.NODE_ENV === "development") {
+ await interaction.channel.send(`\`\`\`${JSON.stringify(job)}\`\`\``);
+ }
+ let { data } = await axios.post(`${process.env.JOSE_ENDPOINT}/jobs`, job);
+ console.log("Fetched ", data);
+ await interaction.editReply({ files: [data["path"]] });
}
}
\ No newline at end of file
diff --git a/commands/view.js b/commands/view.js
index 7716580..d044838 100644
--- a/commands/view.js
+++ b/commands/view.js
@@ -71,8 +71,6 @@ module.exports = {
return;
}
let cardImage = await Rendering.renderCard(card);
- //get base filename
- let filename = cardImage.split("/").pop();
let description = "";
//Add a new line after every 4th (long) word or after a full stop
@@ -92,7 +90,7 @@ module.exports = {
const embed = new EmbedBuilder()
.setTitle(`${card.Character.name}`)
.setDescription(description)
- .setImage(`attachment://${filename}`)
+ .setImage(cardImage)
.setThumbnail(card.Character.Group.imageURL)
.addFields(
{ name: "Owned by", value: `<@${card.User.discordId}>` },
@@ -108,7 +106,7 @@ module.exports = {
embed.setColor(0xff0000);
embed.addFields({ name: "Burned", value: "This card has been burned" });
}
- const message = await interaction.editReply({ embeds: [embed], files: [cardImage], fetchReply: true });
+ const message = await interaction.editReply({ embeds: [embed], fetchReply: true });
},
/**
diff --git a/config/constants.js b/config/constants.js
index b05c89b..645e279 100644
--- a/config/constants.js
+++ b/config/constants.js
@@ -101,7 +101,8 @@ const PATREON = {
wishlist: 15,
currency: 1.25,
daily: 1.5
- }
+ },
+ custom_bg: true
},
4 : {
modifiers: {
@@ -110,7 +111,8 @@ const PATREON = {
wishlist: 25,
currency: 1.75,
daily: 2
- }
+ },
+ custom_bg: true
},
5 : {
modifiers: {
@@ -119,7 +121,8 @@ const PATREON = {
wishlist: 45,
currency: 2,
daily: 4
- }
+ },
+ custom_bg: true
}
}
}
diff --git a/docker-compose.yml b/docker-compose.yml
index e093317..197e6b5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,7 @@ version: "3.7"
services:
bot:
- build: .
+ image: node:20-alpine
command: sh -c "npm config set cache /app/.npm_cache --global && npm install && npx sequelize db:migrate && node ."
restart: unless-stopped
environment:
diff --git a/events/ready.js b/events/ready.js
index 2984d9a..1ab2fb7 100644
--- a/events/ready.js
+++ b/events/ready.js
@@ -16,11 +16,12 @@ module.exports = {
(async () => {
try {
console.log("Registering commands...");
- if(process.env.ENV === "production") {
+ if(process.env.NODE_ENV === "production") {
await rest.put(Routes.applicationCommands(CLIENT_ID), {body: commands });
await rest.put(Routes.applicationGuildCommands(CLIENT_ID, process.env.GUILD_ID), {body: [] }); //Clear Guild commands on prod
console.log("Global commands registered");
} else {
+ await rest.put(Routes.applicationCommands(CLIENT_ID), {body: [] }); //Clear global commands on dev
await rest.put(Routes.applicationGuildCommands(CLIENT_ID, process.env.GUILD_ID), {body: commands });
console.log("Local commands registered");
}
diff --git a/migrations/20230807103447-add_custom_profile_bg.js b/migrations/20230807103447-add_custom_profile_bg.js
new file mode 100644
index 0000000..4509860
--- /dev/null
+++ b/migrations/20230807103447-add_custom_profile_bg.js
@@ -0,0 +1,15 @@
+'use strict';
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+ async up (queryInterface, Sequelize) {
+ await queryInterface.addColumn('Profiles', 'customBackground', {
+ type: Sequelize.STRING,
+ allowNull: true
+ });
+ },
+
+ async down (queryInterface, Sequelize) {
+ await queryInterface.removeColumn('Profiles', 'customBackground');
+ }
+};
diff --git a/models/profile.js b/models/profile.js
index bbed633..489fd54 100644
--- a/models/profile.js
+++ b/models/profile.js
@@ -20,6 +20,7 @@ module.exports = (sequelize, DataTypes) => {
Profile.init({
userId: DataTypes.INTEGER,
customStatus: DataTypes.STRING,
+ customBackground: DataTypes.STRING,
slotOne: DataTypes.INTEGER,
slotTwo: DataTypes.INTEGER,
slotThree: DataTypes.INTEGER,
diff --git a/util/compositing.js b/util/compositing.js
deleted file mode 100644
index eb660db..0000000
--- a/util/compositing.js
+++ /dev/null
@@ -1,42 +0,0 @@
-const { spawn } = require('child_process');
-const crypto = require('crypto');
-const fs = require('fs');
-const { Card } = require('../models');
-
-//TODO: Handle missing images
-module.exports = {
- name: "Compositing",
- renderProfile: async function(profile, background, renderedCards) {
- let hash = crypto.createHash('md5').update(JSON.stringify(profile) + background).digest('hex');
-
- let outFile = `/app/assets/image_cache/profiles/${hash}.gif`;
- console.log('Rendering profile to ' + outFile);
-
- //composite {overlay} {background} [{mask}] [-compose {method}] {result}
- let args = ['png:-', 'null:',
- '\(', `${renderedCards[0]}`, '-coalesce', '\)',
- '-geometry', '+25+85', '-compose', 'over', '-layers', 'composite',
- 'null:', '\(', `${renderedCards[1]}`, '-coalesce', '-resize', '170x283', '\)',
- '-geometry', '+350+300', '-compose', 'over', '-layers', 'composite',
- 'null:', '\(', `${renderedCards[2]}`, '-coalesce', '-resize', '170x283', '\)',
- '-geometry', '+535+300', '-compose', 'over', '-layers', 'composite',
- 'null:', '\(', `${renderedCards[3]}`, '-coalesce', '-resize', '170x283', '\)',
- '-geometry', '+720+300', '-compose', 'over', '-layers', 'composite',
- '-layers', 'optimize', outFile];
-
- console.log('GM Args: ' + args);
-
- const composite = spawn('convert', args);
- composite.stdin.write(background);
- composite.stdin.end();
-
- composite.stderr.on('data', (data) => {
- console.log(`stdout: ${data}`);
- });
- const exitCode = await new Promise( (resolve, reject) => {
- composite.on('close', resolve);
- })
-
- return outFile;
- }
-}
diff --git a/util/general.js b/util/general.js
index 739ec57..1b7b53a 100644
--- a/util/general.js
+++ b/util/general.js
@@ -1,6 +1,8 @@
const { Bot } = require("../models");
const crypto = require("crypto");
const { ReactionUserManager } = require("discord.js");
+const axios = require("axios");
+const fs = require("fs");
module.exports = {
name: "GeneralUtils",
@@ -17,5 +19,28 @@ module.exports = {
generateLogID: async function() {
return crypto.randomBytes(4).toString("hex");
- }
+ },
+
+ formatNumber: function(num, precision = 1) {
+ const map = [
+ { suffix: 'T', threshold: 1e12 },
+ { suffix: 'B', threshold: 1e9 },
+ { suffix: 'M', threshold: 1e6 },
+ { suffix: 'K', threshold: 1e3 },
+ { suffix: '', threshold: 1 },
+ ];
+
+ const found = map.find((x) => Math.abs(num) >= x.threshold);
+ if (found) {
+ const formatted = (Math.floor((num / found.threshold)*10) / 10) + found.suffix;
+ return formatted;
+ }
+
+ return num;
+ },
+
+ downloadFile: async function(url, path) {
+ let imageBuffer = await axios.get(url, { responseType: 'arraybuffer' });
+ fs.writeFileSync(path, imageBuffer.data);
+ },
}
diff --git a/util/rendering.js b/util/rendering.js
index b028943..6c3fdbd 100644
--- a/util/rendering.js
+++ b/util/rendering.js
@@ -1,7 +1,6 @@
-const sharp = require('sharp');
-const crypto = require('crypto');
-const fs = require('fs');
+require("dotenv").config();
const { Character } = require('../models');
+const axios = require('axios').default
const QualityColors = {
1: {r: 0, g: 0, b: 0}, //bad
@@ -12,102 +11,127 @@ const QualityColors = {
6: {r: 255, g: 255, b: 0} //shiny
}
-//TODO: Handle missing images
module.exports = {
name: "Rendering",
renderCardStack: async function(cards) {
- for (let card of cards) {
+ await Promise.all(cards.map(async card => {
console.log(`Iterating card ${card.id}`);
card['render'] = await this.renderCard(card);
+ }));
+
+ let job = {
+ "type": "stack",
+ "size": {
+ "width": 900,
+ "height": 500
+ },
+ "elements": [
+ {
+ "type": "image",
+ "asset": `${cards[0].render}`,
+ "x": 0,
+ "y": 0,
+ "width": 300,
+ "height": 500
+ },
+ {
+ "type": "image",
+ "asset": `${cards[1].render}`,
+ "x": 300,
+ "y": 0,
+ "width": 300,
+ "height": 500
+ },
+ {
+ "type": "image",
+ "asset": `${cards[2].render}`,
+ "x": 600,
+ "y": 0,
+ "width": 300,
+ "height": 500
+ }
+ ]
}
- let image = await sharp({
- create: {
- width: 900,
- height: 500,
- channels: 4,
- background: { r: 0, g: 0, b: 0, alpha: 0.0 },
- animated: true
- }
- }).composite([
- { input: cards[0].render, gravity: 'northwest' },
- { input: cards[1].render, gravity: 'centre' },
- { input: cards[2].render, gravity: 'northeast' },
- ]);
-
- let hash = crypto.createHash('md5').update("CHANGEME").digest('hex');
- try {
- await image.gif({effort: 1}).toFile(`./assets/image_cache/${hash}.gif`);
- } catch (error) {
- console.log(error);
- }
-
- return `./assets/image_cache/${hash}.gif`;
+ console.log("Fetching ", );
+ let { data } = await axios.post(`${process.env.JOSE_ENDPOINT}/jobs`, job);
+ console.log("Fetched ", data);
+ return data["path"];
},
- renderCard: async function(card) {
- const character = await Character.findOne({
- where: {
- id: card.characterId
- }
- });
-
- if (!card.userId) {
- return './assets/cards/card_cover.png';
- }
-
- let hash = crypto.createHash('md5').update(character.imageIdentifier + card.quality + (card.userId == 1 ? 'unclaimed' : 'claimed')).digest('hex');
- //TODO: Add switch to turn off or bypass caching
- if (fs.existsSync(`./assets/image_cache/${hash}.gif`)) {
- return `./assets/image_cache/${hash}.gif`;
- }
-
- console.log(`Rendering card ${hash} for character ${character.name} ${character.imageIdentifier}`);
-
- let filetype = character.imageIdentifier.split('.').pop();
- let isAnimated = ['gif', 'webp'].includes(filetype);
-
- let border = await sharp(`./assets/overlays/border.svg`).tint(QualityColors[card.quality]).toBuffer();
- //BUGBUG: Custom fonts not loading
- let label = Buffer.from(`
-
- `);
-
- let cardImage;
- try {
- console.log("Loading character image");
- cardImage = await sharp(`./assets/cards/${character.imageIdentifier}`,
- { animated: isAnimated, pages: (isAnimated ? -1 : 1) });
- await cardImage.toBuffer();
- } catch (error) {
- console.log(`Missing character image: ${character.imageIdentifier}`);
- cardImage = await sharp(`./assets/cards/missing_image.png`);
- }
- console.log("rendering");
- await cardImage.resize(300, 500);
- await cardImage.composite([
- {input: border, top:0, left: 0, tile: true},
- {input: label, top:0, left: 0, tile: true}]);
- //BUGBUG: Grayscale does not apply to card border
- if (card.userId === 1) {
- await cardImage.grayscale()
- .modulate({
- brightness: 0.5
+ renderCard: async function(card, character=null) {
+ if(!character) {
+ character = await Character.findOne({
+ where: {
+ id: card.characterId
+ }
});
}
- if (isAnimated) {
- await cardImage.gif({effort: 1})
- } else {
- await cardImage.png();
+ console.log(`Rendering card ${card.id} ${character.name} ${character.imageIdentifier}`);
+
+ let characterImage = `${process.env.ASSET_URL}/cards/${character.imageIdentifier}`;
+
+ //Hide character info if the card is unclaimed
+ if (!card.userId) {
+ characterImage = `${process.env.ASSET_URL}/cards/card_cover.png`;
+ character.name = ' ';
}
- let extension = isAnimated ? 'gif' : 'png';
- await cardImage.toFile(`./assets/image_cache/${hash}.${extension}`);
+ let job = {
+ "type": "card",
+ "size": {
+ "width": 600,
+ "height": 1000
+ },
+ "elements": [
+ {
+ "type": "image",
+ "asset": `${characterImage}`,
+ "x": 10,
+ "y": 10,
+ "width": 580,
+ "height": 980
+ },
+ {
+ "type": "image",
+ "asset": `${process.env.ASSET_URL}/overlays/default_frame.png`,
+ "x": 0,
+ "y": 0,
+ "width": 600,
+ "height": 1000
+ },
+ {
+ "type": "text",
+ "text": `${character.name}`,
+ "fontSize": 55,
+ "x": 0,
+ "y": 850,
+ "width": 600,
+ "height": 300,
+ "horizontalAlignment": "center"
+ }
+ ]
+ }
- return `./assets/image_cache/${hash}.${extension}`;
+ if(process.env.NODE_ENV === "development") {
+ debugElement = {
+ "type": "text",
+ "text": `Jose-Endpoint: \n${process.env.JOSE_ENDPOINT}\nNode: \n%nodeid% \nPrint: ${card.printNr} uid: ${card.identifier}\n Serve-Mode: %servemode%\n TS:%timestamp%`,
+ "fontSize": 35,
+ "x": 0,
+ "y": 50,
+ "width": 600,
+ "height": 800,
+ "horizontalAlignment": "center"
+ }
+ job.elements.push(debugElement)
+ }
+
+ console.log("Fetching ", JSON.stringify(job));
+ let { data } = await axios.post(`${process.env.JOSE_ENDPOINT}/jobs`, job);
+ console.log("Fetched ", JSON.stringify(data));
+ return data["path"];
}
}