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(` - - ${character.name} - - `); - - 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"]; } }