Merge new rendering into master #60
@@ -10,3 +10,7 @@ DB_PORT=
|
|||||||
API_PORT=3080
|
API_PORT=3080
|
||||||
API_ACCESS_TOKEN=
|
API_ACCESS_TOKEN=
|
||||||
HOMEPAGE_URL=
|
HOMEPAGE_URL=
|
||||||
|
|
||||||
|
#Rendering
|
||||||
|
JOSE_ENDPOINT=
|
||||||
|
ASSET_URL=
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
assets/image_cache
|
assets/image_cache
|
||||||
assets/cards
|
assets/cards
|
||||||
assets/import
|
assets/import
|
||||||
|
assets/userdata
|
||||||
|
|
||||||
### Visual Studio Code ###
|
### Visual Studio Code ###
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
FROM node:16.9.0-alpine
|
|
||||||
RUN apk add --no-cache imagemagick
|
|
||||||
@@ -93,4 +93,6 @@ app.use(PREFIX, router);
|
|||||||
app.use(PREFIX, groupRoutes);
|
app.use(PREFIX, groupRoutes);
|
||||||
app.use(PREFIX, badgeRoutes);
|
app.use(PREFIX, badgeRoutes);
|
||||||
app.use(PREFIX, characterRoutes);
|
app.use(PREFIX, characterRoutes);
|
||||||
|
app.use('/assets', express.static('assets'));
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
0
assets/userdata/profiles/.gitkeep
Normal file
0
assets/userdata/profiles/.gitkeep
Normal file
@@ -1,7 +1,7 @@
|
|||||||
const { SlashCommandBuilder, ComponentType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
|
const { SlashCommandBuilder, ComponentType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
|
||||||
const { customAlphabet } = require("nanoid");
|
const { customAlphabet } = require("nanoid");
|
||||||
const { Card, User, Wishlist, Character } = require("../models");
|
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 { PATREON } = require("../config/constants");
|
||||||
const stores = require("../stores");
|
const stores = require("../stores");
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
@@ -28,6 +28,7 @@ module.exports = {
|
|||||||
{ name: 'toggle_maintenance', value: 'toggle_maintenance' },
|
{ name: 'toggle_maintenance', value: 'toggle_maintenance' },
|
||||||
{ name: 'store', value: 'store' },
|
{ name: 'store', value: 'store' },
|
||||||
{ name: 'wishlist', value: 'wishlist' },
|
{ name: 'wishlist', value: 'wishlist' },
|
||||||
|
{ name: 'rendering', value: 'rendering' },
|
||||||
{ name: 'patreon', value: 'patreon' }
|
{ name: 'patreon', value: 'patreon' }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -181,6 +182,55 @@ module.exports = {
|
|||||||
|
|
||||||
let patreon = await UserUtils.getPatreonPerks(interaction.client, extUser ? extUser : user);
|
let patreon = await UserUtils.getPatreonPerks(interaction.client, extUser ? extUser : user);
|
||||||
interaction.channel.send(JSON.stringify(patreon));
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
interaction.editReply({
|
interaction.editReply({
|
||||||
|
|||||||
@@ -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 { Card, User, Character } = require("../models");
|
||||||
const { UserUtils, ReplyUtils } = require("../util");
|
const { UserUtils, ReplyUtils, GeneralUtils } = require("../util");
|
||||||
|
|
||||||
const pageSize = 8;
|
const pageSize = 8;
|
||||||
|
|
||||||
@@ -8,12 +8,18 @@ const pageSize = 8;
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("editprofile")
|
.setName("editprofile")
|
||||||
.setDescription("Edit your profile"),
|
.setDescription("Edit your profile")
|
||||||
|
.addAttachmentOption((option) =>
|
||||||
|
option
|
||||||
|
.setName("attachement")
|
||||||
|
.setDescription("Attachement to be used")
|
||||||
|
.setRequired(false)
|
||||||
|
),
|
||||||
permissionLevel: 0,
|
permissionLevel: 0,
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
let user = await UserUtils.getUserByDiscordId(interaction.member.id);
|
let user = await UserUtils.getUserByDiscordId(interaction.member.id);
|
||||||
|
let patreon = await UserUtils.getPatreonPerks(interaction.client, user);
|
||||||
let profile = await user.getProfile();
|
let profile = await user.getProfile();
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +37,15 @@ module.exports = {
|
|||||||
.setStyle(ButtonStyle.Primary)
|
.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();
|
const pingRow = new ActionRowBuilder();
|
||||||
pingRow.addComponents(
|
pingRow.addComponents(
|
||||||
new ButtonBuilder()
|
new ButtonBuilder()
|
||||||
@@ -51,11 +66,10 @@ module.exports = {
|
|||||||
let message = await interaction.editReply({ content: "", components: [mainRow, pingRow], fetchReply: true });
|
let message = await interaction.editReply({ content: "", components: [mainRow, pingRow], fetchReply: true });
|
||||||
|
|
||||||
//filter only events from the user who triggered the command
|
//filter only events from the user who triggered the command
|
||||||
const filter = (m) => m.author.id === interaction.author.id;
|
const filter = (m) => m.user.id === interaction.user.id;
|
||||||
const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 25000 })
|
const collector = message.createMessageComponentCollector({ filter: filter, componentType: ComponentType.Button, time: 300000 })
|
||||||
|
|
||||||
collector.on('collect', async (i) => {
|
collector.on('collect', async (i) => {
|
||||||
await i.deferReply();
|
|
||||||
switch (i.customId) {
|
switch (i.customId) {
|
||||||
case 'editStatus':
|
case 'editStatus':
|
||||||
await this.openStatusModal(i, user, profile);
|
await this.openStatusModal(i, user, profile);
|
||||||
@@ -63,19 +77,36 @@ module.exports = {
|
|||||||
case 'editShowcase':
|
case 'editShowcase':
|
||||||
await this.openShowcaseModal(i, user, profile);
|
await this.openShowcaseModal(i, user, profile);
|
||||||
break;
|
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':
|
case 'toggle-wishlist-ping':
|
||||||
|
await i.deferUpdate();
|
||||||
user.wishlistPing = !user.wishlistPing;
|
user.wishlistPing = !user.wishlistPing;
|
||||||
user.save();
|
user.save();
|
||||||
break;
|
break;
|
||||||
case 'toggle-drop-ping':
|
case 'toggle-drop-ping':
|
||||||
|
await i.deferUpdate();
|
||||||
user.dropPing = !user.dropPing;
|
user.dropPing = !user.dropPing;
|
||||||
user.save();
|
user.save();
|
||||||
break;
|
break;
|
||||||
case 'toggle-daily-ping':
|
case 'toggle-daily-ping':
|
||||||
|
await i.deferUpdate();
|
||||||
user.dailyPing = !user.dailyPing;
|
user.dailyPing = !user.dailyPing;
|
||||||
user.save();
|
user.save();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
await i.deferReply();
|
||||||
i.editReply({ content: "Invalid selection" });
|
i.editReply({ content: "Invalid selection" });
|
||||||
return;
|
return;
|
||||||
break;
|
break;
|
||||||
@@ -89,8 +120,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
await message.edit({ components: newComponents });
|
await message.edit({ components: newComponents });
|
||||||
let msg = await i.editReply({content: '...'});
|
|
||||||
await msg.delete();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async openShowcaseModal(interaction, user, profile) {
|
async openShowcaseModal(interaction, user, profile) {
|
||||||
@@ -118,13 +147,12 @@ module.exports = {
|
|||||||
|
|
||||||
let submitted = await interaction.awaitModalSubmit({
|
let submitted = await interaction.awaitModalSubmit({
|
||||||
time: 60000,
|
time: 60000,
|
||||||
filter: i => i.user.id === interaction.user.id,
|
filter: i => i.user.id === interaction.user.id && i.customId === 'cardSlotModal',
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
//Error includes timeout
|
//Error includes timeout
|
||||||
console.error(error)
|
console.error(error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
let updatePayload = {};
|
let updatePayload = {};
|
||||||
for (slot of slots) {
|
for (slot of slots) {
|
||||||
@@ -144,7 +172,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
async openStatusModal(interaction, user, profile) {
|
async openStatusModal(interaction, user, profile) {
|
||||||
const modal = new ModalBuilder()
|
const modal = new ModalBuilder()
|
||||||
.setCustomId('descriptionModal')
|
.setCustomId('statusModal')
|
||||||
.setTitle('Edit profile status/description');
|
.setTitle('Edit profile status/description');
|
||||||
|
|
||||||
let row = new ActionRowBuilder();
|
let row = new ActionRowBuilder();
|
||||||
@@ -162,7 +190,7 @@ module.exports = {
|
|||||||
|
|
||||||
let submitted = await interaction.awaitModalSubmit({
|
let submitted = await interaction.awaitModalSubmit({
|
||||||
time: 300000,
|
time: 300000,
|
||||||
filter: i => i.user.id === interaction.user.id,
|
filter: i => i.user.id === interaction.user.id && i.customId === 'statusModal',
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
//Error includes timeout
|
//Error includes timeout
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
const { SlashCommandBuilder, MessageAttachment, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
|
require("dotenv").config();
|
||||||
const { Card, User, Character } = require("../models");
|
const { SlashCommandBuilder } = require("discord.js");
|
||||||
const { UserUtils, Compositing, Rendering } = require("../util");
|
const { Card } = require("../models");
|
||||||
|
const { UserUtils, Rendering, GeneralUtils } = require("../util");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const sharp = require("sharp");
|
|
||||||
const { CURRENCY_NAMES } = require("../config/constants");
|
const { CURRENCY_NAMES } = require("../config/constants");
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const pageSize = 8;
|
const pageSize = 8;
|
||||||
|
|
||||||
@@ -25,56 +24,154 @@ module.exports = {
|
|||||||
|
|
||||||
let discordUser = interaction.options.getUser("user") ? interaction.options.getUser("user") : interaction.member.user;
|
let discordUser = interaction.options.getUser("user") ? interaction.options.getUser("user") : interaction.member.user;
|
||||||
let user = await UserUtils.getUserByDiscordId(discordUser.id);
|
let user = await UserUtils.getUserByDiscordId(discordUser.id);
|
||||||
|
let patreon = await UserUtils.getPatreonPerks(interaction.client, user);
|
||||||
let profile = await user.getProfile();
|
let profile = await user.getProfile();
|
||||||
|
|
||||||
let customStatus = this.encodeStr(profile.customStatus);
|
let customStatus = profile.customStatus;
|
||||||
customStatus = customStatus.replace(/(.{0,40}[\s])/g, '<tspan x="443" dy="1.2em">$1</tspan>');
|
|
||||||
|
|
||||||
let profileTemplate = fs.readFileSync('/app/assets/profile/profile.svg').toString();
|
let userImage = discordUser.displayAvatarURL({format: 'png', size: 128}).split('webp')[0] + 'png';
|
||||||
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(
|
|
||||||
'<svg><rect x="0" y="0" width="128" height="128" rx="100%" ry="100%"/></svg>'
|
|
||||||
);
|
|
||||||
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 slots = ['slotOne', 'slotTwo', 'slotThree', 'slotFour'];
|
let slots = ['slotOne', 'slotTwo', 'slotThree', 'slotFour'];
|
||||||
let renderedCards = [];
|
let renderedCards = [];
|
||||||
for (slot of slots) {
|
await Promise.all(slots.map(async slot => {
|
||||||
let card = await Card.findOne({ where: { id: profile[slot], burned: false } });
|
let card = await Card.findOne({ where: { id: profile[slot], burned: false } });
|
||||||
if (card) {
|
if (card) {
|
||||||
|
console.log(`Iterating card ${card.id}`);
|
||||||
let cardImage = await Rendering.renderCard(card);
|
let cardImage = await Rendering.renderCard(card);
|
||||||
renderedCards.push(cardImage);
|
renderedCards[slot] = cardImage;
|
||||||
} else {
|
} 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);
|
if (patreon.perks?.['custom_bg'] && profile.customBackground) {
|
||||||
await interaction.editReply({ files: [profileImage] });
|
job.elements.unshift(
|
||||||
},
|
{
|
||||||
encodeStr: function(str) {
|
"type": "image",
|
||||||
let charMapping = {
|
"asset": profile.customBackground,
|
||||||
'&': '&',
|
"x": 0,
|
||||||
'"': '"',
|
"y": 0,
|
||||||
'<': '<',
|
"width": 1200,
|
||||||
'>': '>'
|
"height": 600
|
||||||
};
|
}
|
||||||
return str.replace(/([\&"<>])/g, function(str, item) {
|
);
|
||||||
return charMapping[item];
|
}
|
||||||
});
|
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"]] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,8 +71,6 @@ module.exports = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cardImage = await Rendering.renderCard(card);
|
let cardImage = await Rendering.renderCard(card);
|
||||||
//get base filename
|
|
||||||
let filename = cardImage.split("/").pop();
|
|
||||||
|
|
||||||
let description = "";
|
let description = "";
|
||||||
//Add a new line after every 4th (long) word or after a full stop
|
//Add a new line after every 4th (long) word or after a full stop
|
||||||
@@ -92,7 +90,7 @@ module.exports = {
|
|||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(`${card.Character.name}`)
|
.setTitle(`${card.Character.name}`)
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
.setImage(`attachment://${filename}`)
|
.setImage(cardImage)
|
||||||
.setThumbnail(card.Character.Group.imageURL)
|
.setThumbnail(card.Character.Group.imageURL)
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: "Owned by", value: `<@${card.User.discordId}>` },
|
{ name: "Owned by", value: `<@${card.User.discordId}>` },
|
||||||
@@ -108,7 +106,7 @@ module.exports = {
|
|||||||
embed.setColor(0xff0000);
|
embed.setColor(0xff0000);
|
||||||
embed.addFields({ name: "Burned", value: "This card has been burned" });
|
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 });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ const PATREON = {
|
|||||||
wishlist: 15,
|
wishlist: 15,
|
||||||
currency: 1.25,
|
currency: 1.25,
|
||||||
daily: 1.5
|
daily: 1.5
|
||||||
}
|
},
|
||||||
|
custom_bg: true
|
||||||
},
|
},
|
||||||
4 : {
|
4 : {
|
||||||
modifiers: {
|
modifiers: {
|
||||||
@@ -110,7 +111,8 @@ const PATREON = {
|
|||||||
wishlist: 25,
|
wishlist: 25,
|
||||||
currency: 1.75,
|
currency: 1.75,
|
||||||
daily: 2
|
daily: 2
|
||||||
}
|
},
|
||||||
|
custom_bg: true
|
||||||
},
|
},
|
||||||
5 : {
|
5 : {
|
||||||
modifiers: {
|
modifiers: {
|
||||||
@@ -119,7 +121,8 @@ const PATREON = {
|
|||||||
wishlist: 45,
|
wishlist: 45,
|
||||||
currency: 2,
|
currency: 2,
|
||||||
daily: 4
|
daily: 4
|
||||||
}
|
},
|
||||||
|
custom_bg: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.7"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
bot:
|
bot:
|
||||||
build: .
|
image: node:20-alpine
|
||||||
command: sh -c "npm config set cache /app/.npm_cache --global && npm install && npx sequelize db:migrate && node ."
|
command: sh -c "npm config set cache /app/.npm_cache --global && npm install && npx sequelize db:migrate && node ."
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ module.exports = {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Registering commands...");
|
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.applicationCommands(CLIENT_ID), {body: commands });
|
||||||
await rest.put(Routes.applicationGuildCommands(CLIENT_ID, process.env.GUILD_ID), {body: [] }); //Clear Guild commands on prod
|
await rest.put(Routes.applicationGuildCommands(CLIENT_ID, process.env.GUILD_ID), {body: [] }); //Clear Guild commands on prod
|
||||||
console.log("Global commands registered");
|
console.log("Global commands registered");
|
||||||
} else {
|
} 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 });
|
await rest.put(Routes.applicationGuildCommands(CLIENT_ID, process.env.GUILD_ID), {body: commands });
|
||||||
console.log("Local commands registered");
|
console.log("Local commands registered");
|
||||||
}
|
}
|
||||||
|
|||||||
15
migrations/20230807103447-add_custom_profile_bg.js
Normal file
15
migrations/20230807103447-add_custom_profile_bg.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
Profile.init({
|
Profile.init({
|
||||||
userId: DataTypes.INTEGER,
|
userId: DataTypes.INTEGER,
|
||||||
customStatus: DataTypes.STRING,
|
customStatus: DataTypes.STRING,
|
||||||
|
customBackground: DataTypes.STRING,
|
||||||
slotOne: DataTypes.INTEGER,
|
slotOne: DataTypes.INTEGER,
|
||||||
slotTwo: DataTypes.INTEGER,
|
slotTwo: DataTypes.INTEGER,
|
||||||
slotThree: DataTypes.INTEGER,
|
slotThree: DataTypes.INTEGER,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
const { Bot } = require("../models");
|
const { Bot } = require("../models");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const { ReactionUserManager } = require("discord.js");
|
const { ReactionUserManager } = require("discord.js");
|
||||||
|
const axios = require("axios");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "GeneralUtils",
|
name: "GeneralUtils",
|
||||||
@@ -17,5 +19,28 @@ module.exports = {
|
|||||||
|
|
||||||
generateLogID: async function() {
|
generateLogID: async function() {
|
||||||
return crypto.randomBytes(4).toString("hex");
|
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);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const sharp = require('sharp');
|
require("dotenv").config();
|
||||||
const crypto = require('crypto');
|
|
||||||
const fs = require('fs');
|
|
||||||
const { Character } = require('../models');
|
const { Character } = require('../models');
|
||||||
|
const axios = require('axios').default
|
||||||
|
|
||||||
const QualityColors = {
|
const QualityColors = {
|
||||||
1: {r: 0, g: 0, b: 0}, //bad
|
1: {r: 0, g: 0, b: 0}, //bad
|
||||||
@@ -12,102 +11,127 @@ const QualityColors = {
|
|||||||
6: {r: 255, g: 255, b: 0} //shiny
|
6: {r: 255, g: 255, b: 0} //shiny
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Handle missing images
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "Rendering",
|
name: "Rendering",
|
||||||
renderCardStack: async function(cards) {
|
renderCardStack: async function(cards) {
|
||||||
|
|
||||||
for (let card of cards) {
|
await Promise.all(cards.map(async card => {
|
||||||
console.log(`Iterating card ${card.id}`);
|
console.log(`Iterating card ${card.id}`);
|
||||||
card['render'] = await this.renderCard(card);
|
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({
|
console.log("Fetching ", );
|
||||||
create: {
|
let { data } = await axios.post(`${process.env.JOSE_ENDPOINT}/jobs`, job);
|
||||||
width: 900,
|
console.log("Fetched ", data);
|
||||||
height: 500,
|
return data["path"];
|
||||||
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`;
|
|
||||||
|
|
||||||
},
|
},
|
||||||
renderCard: async function(card) {
|
renderCard: async function(card, character=null) {
|
||||||
const character = await Character.findOne({
|
if(!character) {
|
||||||
where: {
|
character = await Character.findOne({
|
||||||
id: card.characterId
|
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(`
|
|
||||||
<svg width="300" height="500">
|
|
||||||
<text x="50%" y="95%" text-anchor="middle" style="font-size:28px;">${character.name}</text>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAnimated) {
|
console.log(`Rendering card ${card.id} ${character.name} ${character.imageIdentifier}`);
|
||||||
await cardImage.gif({effort: 1})
|
|
||||||
} else {
|
let characterImage = `${process.env.ASSET_URL}/cards/${character.imageIdentifier}`;
|
||||||
await cardImage.png();
|
|
||||||
|
//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';
|
let job = {
|
||||||
await cardImage.toFile(`./assets/image_cache/${hash}.${extension}`);
|
"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"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user