40 Commits

Author SHA1 Message Date
dependabot[bot]
6282eb8f81 Bump axios from 1.6.0 to 1.8.2
Bumps [axios](https://github.com/axios/axios) from 1.6.0 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.0...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-08 18:32:03 +00:00
cab112e92b Merge pull request #78 from JanGross/dependabot/npm_and_yarn/braces-3.0.3
Bump braces from 3.0.2 to 3.0.3
2024-12-16 17:18:20 +01:00
dependabot[bot]
c2f09881d3 Bump nanoid from 3.3.4 to 3.3.8
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.4 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.4...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 17:11:36 +01:00
dependabot[bot]
ae3733c0f2 Bump path-to-regexp and express
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.12 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `path-to-regexp` from 0.1.7 to 0.1.12
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.7...v0.1.12)

Updates `express` from 4.19.2 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.2)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 17:11:16 +01:00
dependabot[bot]
f07bc524d6 Bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 15:33:14 +00:00
dependabot[bot]
265f4a7864 Bump undici from 5.26.3 to 5.28.4
Bumps [undici](https://github.com/nodejs/undici) from 5.26.3 to 5.28.4.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.26.3...v5.28.4)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 15:32:45 +00:00
dependabot[bot]
284c015509 Bump express from 4.18.2 to 4.19.2
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 15:32:33 +00:00
dependabot[bot]
e010a25c72 Bump follow-redirects from 1.15.2 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 15:32:22 +00:00
dependabot[bot]
4595ea9f07 Bump es5-ext from 0.10.62 to 0.10.64
Bumps [es5-ext](https://github.com/medikoo/es5-ext) from 0.10.62 to 0.10.64.
- [Release notes](https://github.com/medikoo/es5-ext/releases)
- [Changelog](https://github.com/medikoo/es5-ext/blob/main/CHANGELOG.md)
- [Commits](https://github.com/medikoo/es5-ext/compare/v0.10.62...v0.10.64)

---
updated-dependencies:
- dependency-name: es5-ext
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 15:32:10 +00:00
dependabot[bot]
b08fd54288 Bump axios from 0.27.2 to 1.6.0
Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.27.2...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-03 13:21:49 +01:00
dependabot[bot]
324f008fbf Bump undici from 5.21.0 to 5.26.3
Bumps [undici](https://github.com/nodejs/undici) from 5.21.0 to 5.26.3.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.21.0...v5.26.3)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-03 13:20:48 +01:00
111cf4aead Delete duplicate Docs/API directory
This was causing collisions on case-insensitive filesystems  
The proper spelling is all lowercase
2023-09-18 12:42:04 +02:00
1eab3e7853 Commands: remove legacy debugrendering command 2023-09-18 12:38:35 +02:00
d4a9c4543a Seeds: Update seed files to new schema 2023-09-18 12:19:27 +02:00
f5837c4b25 Debug: Extend rendering debug command 2023-08-20 11:53:50 +02:00
36de2c1715 Rendering: Properly handle error response from Jose 2023-08-17 17:08:12 +02:00
4d72f8562e Profile: Implement custom profile backgrounds 2023-08-07 17:34:19 +02:00
2f63f172c2 DB: Add fields for custom profile backgrounds 2023-08-07 17:34:19 +02:00
7962266b26 GeneralUtil: Add download file function 2023-08-07 17:34:19 +02:00
6edc6771ef Profile: Fix showcase card being out of order.
The showcase cards were rendered based on the order in which
the render calls returned within the slots.map call.
Using the original slot keys fixes this issue.
2023-08-07 17:34:19 +02:00
596a6b5dc5 Bot: Change ENV to NODE_ENV in ready.js 2023-08-07 17:34:19 +02:00
de37b9d348 Bot: Clear global commands when running as dev 2023-08-07 17:34:19 +02:00
643c027ce8 Rendering: Better card debug output
- Add timestamp
- Larger font
2023-08-07 17:34:19 +02:00
0c232a55cb Debug: Catch and print errors when fetching from Jose 2023-08-07 17:34:19 +02:00
d7a99968bc GeneralUtils/Profile: Shorten numbers to K M B format
- Implemented for currencies on the profile
2023-08-07 17:34:19 +02:00
b099c29ecf Rendering: Add debug output in develop mode
- Burn node info into rendered card
- Log job def onto console
2023-08-07 17:34:19 +02:00
aaaac430e6 Editprofile: Properly defer updates on non-modal interactions
Also fixes interaction filtering by user id.
Also fixes collector filtering by modal customID
2023-08-07 17:34:19 +02:00
56a76b0fbb Bump node versiom to 20
node:20-alpine
2023-08-07 17:34:19 +02:00
e50449ec03 Cleanup legacy rendering code and files
also removes dockerfile and imagemagick.
2023-08-07 17:34:19 +02:00
2a4b58f7af Rendering: Reposition labels for top-aligned default 2023-08-07 17:34:19 +02:00
e1cc82eb32 Rendering: Add placeholder frame
and fix profile card alignment
2023-08-07 17:34:19 +02:00
6d35019e3e Profile: Add stats bar to new rendering 2023-08-07 17:34:19 +02:00
490db1c70f Profile: Add userimage and status to new profile 2023-08-07 17:34:19 +02:00
21ccd3a3d1 Profile: Add new rendering for username and showcase 2023-08-07 17:34:19 +02:00
a33a7d737e Editprofile: Remove top level defer which broke modal inputs 2023-08-07 17:34:19 +02:00
5a31ef95fb Rendering: Hide card if it's unclaimed 2023-08-07 17:34:19 +02:00
f2ac0ed10e Rendering: Replace card stack rendering with external API call 2023-08-07 17:34:19 +02:00
6d86799e02 API: Serve static assets via express 2023-08-07 17:34:19 +02:00
189e126983 Rendering: Add configurable Jose endpoint 2023-08-07 17:34:19 +02:00
d99b8ab4d6 Rendering: WIP Replacing renderer with external API 2023-08-07 17:34:19 +02:00
25 changed files with 1098 additions and 584 deletions

View File

@@ -10,3 +10,7 @@ DB_PORT=
API_PORT=3080
API_ACCESS_TOKEN=
HOMEPAGE_URL=
#Rendering
JOSE_ENDPOINT=
ASSET_URL=

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@
assets/image_cache
assets/cards
assets/import
assets/userdata
### Visual Studio Code ###
.vscode

View File

@@ -1,3 +0,0 @@
# syntax=docker/dockerfile:1
FROM node:16.9.0-alpine
RUN apk add --no-cache imagemagick

View File

@@ -1,25 +0,0 @@
# API Documentation
## Introduction
This API provides CRU (Create, Read, Update) operations for various models.
The API requires an API key for some operations.
The endpoints provided by this API are:
## Generic:
- / - List all routes (JSON Response)
- /ping - Returns pong
- /stats - Simple stats about the bot (Record counts and uptime)
- /most-recent-drop - Returns the most recent drop (Requires API Key)
## For characters:
- [`/characters`](characters.md#get-characters)
- [`/characters/:character_id`](characters.md#get-characterscharacter_id)
## For groups:
_TODO_
## For Badges:
_TODO_

View File

@@ -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;

View File

View File

@@ -1,8 +1,9 @@
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 { customAlphabet, random } = require("nanoid");
const { Card, User, Wishlist, Character, sequelize } = require("../models");
const { UserUtils, CardUtils, GeneralUtils, Rendering } = require("../util");
const { PATREON } = require("../config/constants");
const axios = require('axios').default
const stores = require("../stores");
require('dotenv').config();
@@ -28,6 +29,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 +183,77 @@ 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 fetching test renders");
let testCard = await Card.build({
characterId: 1,
userId: Math.floor(Math.random() * 10),
identifier: "0xffff",
quality: 1,
printNr: 0,
});
let testCharacter = Character.build({
id: 0,
groupId: 0,
name: "test",
imageIdentifier: "azur-lane/akashi.png",
enabled: true
})
let testCards = [ { ...testCard},{ ...testCard},{ ...testCard},{ ...testCard},{ ...testCard}, { ...testCard},{ ...testCard},{ ...testCard},{ ...testCard},{ ...testCard} ];
let startTime = Date.now();
let renderedStack = await Rendering.renderCardStack([testCard, testCard, testCard]);
let execTime = Date.now() - startTime;
await interaction.channel.send(renderedStack);
await interaction.channel.send(`Stack rendering took ${execTime} ms`);
let total = 0;
startTime = Date.now()
await Promise.all(testCards.map(async card => {
console.log(`Iterating card `);
card.characterId = (await Character.findAll({where: {enabled: true},order: sequelize.random(),limit: 1}))[0].id;
card.id = 0;
card.identifier = CardUtils.generateIdentifier();
card.userId = 1;
let startTime = Date.now();
card['render'] = await Rendering.renderCard(card);
let execTime = Date.now() - startTime;
total += execTime;
card['timing'] = `${card.identifier} Card rendering took ${execTime} ms`;
}));
let toatalExecTime = Date.now() - startTime;
await interaction.channel.send(testCards.map(card => {return `${card['identifier']} ${card['render']}` }).join('\n'));
await interaction.channel.send(testCards.map(card => {return card['timing'] }).join('\n'))
let joseStats = (await axios.get(`${process.env.JOSE_ENDPOINT}/status`)).data;
await interaction.channel.send(`Active Nodes: ${joseStats.nodes.count} Queued Jobs: ${joseStats.jobs.queued.count}`);
await interaction.channel.send(`Total time for ${testCards.length} Cards: ${toatalExecTime}\nAverage time per card: ${total / testCards.length}`);
break;
}
});
break;
default:
interaction.editReply({

View File

@@ -1,32 +0,0 @@
const sharp = require('sharp');
const { SlashCommandBuilder, AttachmentBuilder, EmbedBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName("debugrendering")
.setDescription("Debug rendering"),
permissionLevel: 2,
async execute(interaction) {
const image = await sharp({
create: {
width: 900,
height: 500,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 0.5 }
}
})
.composite([
{ input: './assets/cards/test/test.png', gravity: 'northwest' },
{ input: './assets/cards/test/test.png', gravity: 'centre' },
{ input: './assets/cards/test/test.png', gravity: 'northeast' },
{ input: './assets/overlays/rainbow_overlay.png', gravity: 'northwest' },
{ input: './assets/overlays/rainbow_overlay.png', gravity: 'centre' },
{ input: './assets/overlays/rainbow_overlay.png', gravity: 'northeast' },
])
.png()
.toBuffer();
const file = new AttachmentBuilder(image);
const message = await interaction.reply({ content: 'asd', files: [file], fetchReply: true });
}
}

View File

@@ -80,7 +80,11 @@ module.exports = {
cards.sort((a, b) => a.characterId - b.characterId);
const row = new ActionRowBuilder();
let deckImage = await Rendering.renderCardStack(cards);
let deckImage = await Rendering.renderCardStack(cards).catch(async err => {
await interaction.channel.send(`Uooh an error! ${err.response?.status} ${err.response?.statusText} \n ${err.response?.data.message} \n ${err.response?.data.jobId}`);
});
if (!deckImage) { return; }
let notableProps = [];
let pings = [];
for (let i = 0; i < cards.length; i++) {
@@ -219,7 +223,10 @@ module.exports = {
console.log(`Collected ${collected.size} interactions.`);
let deckImage = await Rendering.renderCardStack(cards);
let deckImage = await Rendering.renderCardStack(cards).catch(async err => {
await interaction.channel.send(`Uooh an error! ${err.response?.status} ${err.response?.statusText} \n ${err.response?.data.message} \n ${err.response?.data.jobId}`);
});
if (!deckImage){ return; }
message.edit({ components: [], files: [new AttachmentBuilder(deckImage)] });
});

View File

@@ -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)

View File

@@ -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,157 @@ 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, '<tspan x="443" dy="1.2em">$1</tspan>');
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(
'<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 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) {
let cardImage = await Rendering.renderCard(card);
renderedCards.push(cardImage);
} else {
renderedCards.push('/app/assets/cards/missing_image.png');
}
}
let profileImage = await Compositing.renderProfile(profile, background, renderedCards);
await interaction.editReply({ files: [profileImage] });
},
encodeStr: function(str) {
let charMapping = {
'&': '&amp;',
'"': '&quot;',
'<': '&lt;',
'>': '&gt;'
};
return str.replace(/([\&"<>])/g, function(str, item) {
return charMapping[item];
console.log(`Iterating card ${card.id}`);
let cardImage = await Rendering.renderCard(card).catch(async err => {
await interaction.channel.send(`Uooh an error! ${err.response?.status} ${err.response?.statusText} \n ${err.response?.data.message} \n ${err.response?.data.jobId}`);
});
if (!cardImage) { return; }
renderedCards[slot] = cardImage;
} else {
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"
}
]
}
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"]] });
}
}

View File

@@ -70,9 +70,10 @@ module.exports = {
interaction.editReply({ content: "Card not found" });
return;
}
let cardImage = await Rendering.renderCard(card);
//get base filename
let filename = cardImage.split("/").pop();
let cardImage = await Rendering.renderCard(card).catch(async err => {
await interaction.channel.send(`Uooh an error! ${err.response?.status} ${err.response?.statusText} \n ${err.response?.data.message} \n ${err.response?.data.jobId}`);
});
if (!cardImage) { return; }
let description = "";
//Add a new line after every 4th (long) word or after a full stop
@@ -92,7 +93,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 +109,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 });
},
/**

View File

@@ -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
}
}
}

View File

@@ -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:

View File

@@ -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");
}

View 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');
}
};

View File

@@ -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,

949
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,14 +12,14 @@
"license": "ISC",
"dependencies": {
"@discordjs/rest": "^0.3.0",
"axios": "^0.27.2",
"axios": "^1.8.2",
"body-parser": "^1.20.2",
"discord-api-types": "^0.37.2",
"discord.js": "^14.0.0",
"dotenv": "^16.0.0",
"express": "^4.18.2",
"express": "^4.21.2",
"mysql2": "^2.3.3",
"nanoid": "^3.0.0",
"nanoid": "^3.3.8",
"nodemon": "^2.0.15",
"sequelize": ">=6.28.1",
"sequelize-cli": "^6.4.1",

View File

@@ -3,25 +3,25 @@
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Bands', [{
await queryInterface.bulkInsert('Groups', [{
id: 1,
name: 'TEST-BAND',
description: 'Test-Band (stylized as TEST-BAND) is an all girl rock band from Tokyo that formed in July 2013. The band combines a rock sound with a maid image modeled on Japanese maid cafés.',
name: 'TEST-GROUP',
description: 'Test-Group (stylized as TEST-GROUP) is an all girl rock band from Tokyo that formed in July 2013. The band combines a rock sound with a maid image modeled on Japanese maid cafés.',
imageURL: 'https://cdn.discordapp.com/attachments/851543504831119380/1009467684490063892/unknown.png',
enabled: true
}]);
await queryInterface.bulkInsert('Characters', [{
id: 1,
bandId: 1,
name: 'Band Member 1',
groupId: 1,
name: 'Group Member 1',
description: 'Band Member 1 is a Japanese singer, songwriter and guitarist. She is the initial founding member and main lyricist for TEST-BAND.',
imageIdentifier: 'testband/miku.png',
enabled: true
},
{
id: 2,
bandId: 1,
name: 'Band Member 2',
groupId: 1,
name: 'Group Member 2',
description: 'Band Member 2 is a Japanese drummer and founding member of TEST-BAND',
imageIdentifier: 'testband/akane.png',
enabled: true

View File

@@ -6,8 +6,9 @@ module.exports = {
id: 1,
maintenance: 0,
adminIDs: '["222457277708369928"]',
pullTimeout: 300000,
dropTimeout: 900000
claimTimeout: 300000,
dropTimeout: 900000,
patreonTierRoles: '{"1083018874263453868":1,"1083018984921759744":2,"1083019067184664607":3,"1083019116111216702":4,"1084519566354423918":5}'
}]);
},

View File

@@ -7,6 +7,9 @@ module.exports = {
discordId: '123456789',
active: false,
privacy: 0,
nextDropReset: new Date(),
nextClaimReset: new Date(),
nextDaily: new Date(),
createdAt: new Date(),
updatedAt: new Date()
}]);

View File

@@ -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;
}
}

View File

@@ -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);
},
}

View File

@@ -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({
renderCard: async function(card, character=null) {
if(!character) {
character = await Character.findOne({
where: {
id: card.characterId
}
});
}
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) {
return './assets/cards/card_cover.png';
characterImage = `${process.env.ASSET_URL}/cards/card_cover.png`;
character.name = ' ';
}
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`;
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"
}
]
}
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`);
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"
}
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
});
job.elements.push(debugElement)
}
if (isAnimated) {
await cardImage.gif({effort: 1})
} else {
await cardImage.png();
}
let extension = isAnimated ? 'gif' : 'png';
await cardImage.toFile(`./assets/image_cache/${hash}.${extension}`);
return `./assets/image_cache/${hash}.${extension}`;
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"];
}
}