Adds deployment for icons

This commit is contained in:
Michel Fedde 2025-05-23 21:41:04 +02:00
parent 154002f6f3
commit 0e10ea3cab
14 changed files with 1449 additions and 53 deletions

View file

@ -11,6 +11,9 @@ const context = await esbuild.context({
platform: 'node',
target: 'node10.4',
sourcemap: 'linked',
loader: {
'.node': 'copy',
}
})
export default context

1295
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,7 @@
"dotenv": "^16.4.7",
"esbuild": "^0.25.0",
"log4js": "^6.9.1",
"object-path-set": "^1.0.2"
"object-path-set": "^1.0.2",
"svg2img": "^1.0.0-beta.2"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#3d3846" d="M128 0c17.7 0 32 14.3 32 32l0 32 128 0 0-32c0-17.7 14.3-32 32-32s32 14.3 32 32l0 32 48 0c26.5 0 48 21.5 48 48l0 48L0 160l0-48C0 85.5 21.5 64 48 64l48 0 0-32c0-17.7 14.3-32 32-32zM0 192l448 0 0 272c0 26.5-21.5 48-48 48L48 512c-26.5 0-48-21.5-48-48L0 192zm64 80l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm128 0l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM64 400l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zm112 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z"/></svg>

After

Width:  |  Height:  |  Size: 406 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#9a9996" d="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32l0 96L0 384c0 35.3 28.7 64 64 64l192 0 0-64L64 384l0-224 192 0 0-64L64 96l0-64zM288 192c0 17.7 14.3 32 32 32l224 0c17.7 0 32-14.3 32-32l0-128c0-17.7-14.3-32-32-32l-98.7 0c-8.5 0-16.6-3.4-22.6-9.4L409.4 9.4c-6-6-14.1-9.4-22.6-9.4L320 0c-17.7 0-32 14.3-32 32l0 160zm0 288c0 17.7 14.3 32 32 32l224 0c17.7 0 32-14.3 32-32l0-128c0-17.7-14.3-32-32-32l-98.7 0c-8.5 0-16.6-3.4-22.6-9.4l-13.3-13.3c-6-6-14.1-9.4-22.6-9.4L320 288c-17.7 0-32 14.3-32 32l0 160z"/></svg>

After

Width:  |  Height:  |  Size: 732 B

View file

@ -7,6 +7,8 @@ import {GroupRepository} from "../Repositories/GroupRepository";
import {PlaydateRepository} from "../Repositories/PlaydateRepository";
import {GuildEmojiRoleManager} from "discord.js";
import {GroupConfigurationRepository} from "../Repositories/GroupConfigurationRepository";
import {DiscordClient} from "../Discord/DiscordClient";
import {IconCache} from "../Icons/IconCache";
export enum ServiceHint {
App,
@ -22,6 +24,12 @@ export class Services {
const database = new DatabaseConnection(env.database);
container.set<DatabaseConnection>(database);
const discordClient = new DiscordClient(env.discord.clientId);
container.set<DiscordClient>(discordClient);
const iconCache = new IconCache(discordClient);
container.set<IconCache>(iconCache);
// @ts-ignore
configure({
appenders: {

View file

@ -6,7 +6,7 @@ import {
ChatInputCommandInteraction,
MessageFlags,
Activity,
ActivityType
ActivityType, REST
} from "discord.js";
import Commands from "./Commands/Commands";
import {Container} from "../Container/Container";
@ -16,17 +16,33 @@ import {UserError} from "./UserError";
export class DiscordClient {
private readonly client: Client;
private commands: Commands;
private readonly restClient: REST;
public get Client (): Client {
return this.client;
}
constructor() {
public get Commands(): Commands {
return this.commands
}
public get RESTClient(): REST {
return this.restClient;
}
public get ApplicationId(): string {
return this.applicationId;
}
constructor(
private readonly applicationId: string
) {
this.client = new Client({
intents: [GatewayIntentBits.Guilds]
})
this.commands = new Commands();
this.restClient = new REST();
}
applyEvents() {
@ -52,6 +68,10 @@ export class DiscordClient {
this.client.login(token);
}
connectRESTClient(token: string) {
this.restClient.setToken(token);
}
private findCommandMethod(interaction: Interaction) {
if (interaction.isChatInputCommand()) {
const command = this.commands.getCommand(interaction.commandName);

View file

@ -33,6 +33,7 @@ import {unwatchFile} from "node:fs";
import {UserError} from "../Discord/UserError";
import {RuntimeGroupConfiguration} from "./RuntimeGroupConfiguration";
import {ChannelId} from "../types/DiscordTypes";
import {IconCache} from "../Icons/IconCache";
type UIElementCollection = Record<string, UIElement>;
type UIElement = {
@ -227,6 +228,7 @@ export class GroupConfigurationRenderer {
private createActionRowBuildersForMenu() : ActionRowBuilder<MessageActionRowComponentBuilder>[] {
const {currentCollection, currentElement} = this.findCurrentUI();
const icons = Container.get<IconCache>(IconCache.name);
if (currentElement?.isConfiguration ?? false) {
return [
@ -239,9 +241,10 @@ export class GroupConfigurationRenderer {
new ActionRowBuilder<ButtonBuilder>()
.setComponents(
...Object.values(currentCollection).map(elem => new ButtonBuilder()
.setLabel(elem.label)
.setLabel(` ${elem.label}`)
.setStyle(ButtonStyle.Primary)
.setCustomId(GroupConfigurationRenderer.MOVETO_COMMAND + elem.key)
.setEmoji(icons.get("folder_tree_solid") ?? '')
)
)
]

8
source/Icons/DiscordIcons.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
type DiscordIcon = {
id: string,
name: string,
}
type DiscordIconRequest = {
items: DiscordIcon[]
}

52
source/Icons/IconCache.ts Normal file
View file

@ -0,0 +1,52 @@
import {Routes} from "discord.js";
import {DiscordClient} from "../Discord/DiscordClient";
export class IconCache {
private existingIcons: Map<string, string>|null;
constructor(
private readonly client: DiscordClient
) {
}
public get(iconName: string): string | null {
if (!this.existingIcons?.has(iconName) ?? false) {
return null;
}
return this.existingIcons?.get(iconName) ?? null;
}
public async set(iconName: string, pngBuffer: Buffer) {
const pngBase64 = pngBuffer.toString("base64");
const iconDataUrl = `data:image/png;base64,${pngBase64}`;
await this.client.RESTClient.post(
Routes.applicationEmojis(this.client.ApplicationId),
{
body: {
name: iconName,
image: iconDataUrl
}
}
)
}
public async populate() {
if (this.existingIcons != null) {
return;
}
const existingEmojis: DiscordIconRequest = await this.client.RESTClient.get(
Routes.applicationEmojis(this.client.ApplicationId)
)
this.existingIcons = new Map<string, string>(
existingEmojis.items.map((item) => {
return [ item.name, item.id ]
})
)
}
}

View file

@ -0,0 +1,56 @@
import {REST, Routes} from "discord.js";
import path from "node:path";
import * as fs from "node:fs";
import svg2img from "svg2img";
import {IconCache} from "./IconCache";
export class IconDeployer {
static ICON_PATH = path.resolve('public/icons')
constructor(
private readonly iconCache: IconCache
) {}
public async ensureExistance() {
const directory = await fs.promises.opendir(IconDeployer.ICON_PATH);
const addIconPromises: Promise<void>[] = [];
for await (let dirname of directory) {
const iconName = path.basename(dirname.name, '.svg').replaceAll('-','_');
if (this.iconCache.get(iconName) !== null) {
continue;
}
addIconPromises.push(
this.addIcon(path.resolve(dirname.parentPath, dirname.name), iconName)
);
}
await Promise.all(addIconPromises);
}
private async addIcon(iconPath: string, iconName: string) {
const svgBuffer = await fs.promises.readFile(iconPath, 'utf-8');
const pngBuffer = await new Promise<Buffer>(resolve => {
svg2img(
svgBuffer,
{
format: "png",
resvg: {
fitTo: {
mode: "width",
value: 128
}
}
},
function (err, buffer) {
resolve(buffer);
}
)
}
)
await this.iconCache.set(iconName, pngBuffer);
}
}

View file

@ -7,16 +7,23 @@ import {Container} from "./Container/Container";
import {ServiceHint, Services} from "./Container/Services";
import {Logger} from "log4js";
const { REST, Routes } = require('discord.js');
import {REST, Routes} from 'discord.js';
import {IconDeployer} from "./Icons/IconDeployer";
import {DiscordClient} from "./Discord/DiscordClient";
import {IconCache} from "./Icons/IconCache";
const container = Container.getInstance();
Services.setup(container, ServiceHint.Deploy)
const commands = new Commands().allCommands;
const environment = container.get<Environment>(Environment.name);
const logger = container.get<Logger>("logger");
// Construct and prepare an instance of the REST module
const rest = new REST().setToken(environment.discord.token);
const client = container.get<DiscordClient>(DiscordClient.name);
client.connectRESTClient(environment.discord.token)
const commands = client.Commands.allCommands;
// and deploy your commands!
(async () => {
@ -29,7 +36,7 @@ const rest = new REST().setToken(environment.discord.token);
logger.log(`Started refreshing ${commandInfos.length} application (/) commands.`);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await rest.put(
const data = await client.RESTClient.put(
Routes.applicationGuildCommands(environment.discord.clientId, environment.discord.guildId),
{ body: commandInfos },
);
@ -44,3 +51,15 @@ const rest = new REST().setToken(environment.discord.token);
logger.log("Ensuring Database...");
const updater = new DatabaseUpdater(container.get<DatabaseConnection>(DatabaseConnection.name));
updater.ensureAvaliablity(Definitions);
logger.log("Ensuring icons...");
(async () => {
const iconCache = container.get<IconCache>(IconCache.name);
await iconCache.populate();
const deployer = new IconDeployer(
iconCache
);
deployer.ensureExistance()
})()

View file

@ -3,10 +3,18 @@ import {Environment} from "./Environment";
import {Container} from "./Container/Container";
import {DatabaseConnection} from "./Database/DatabaseConnection";
import {ServiceHint, Services} from "./Container/Services";
import {IconCache} from "./Icons/IconCache";
const container = Container.getInstance();
Services.setup(container, ServiceHint.App);
(async () => {
const env = container.get<Environment>(Environment.name);
const client = container.get<DiscordClient>(DiscordClient.name);
client.connectRESTClient(env.discord.token);
await container.get<IconCache>(IconCache.name).populate()
const client = new DiscordClient()
client.applyEvents()
client.connect(container.get<Environment>(Environment.name).discord.token)
client.connect(env.discord.token)
})()