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

@ -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,7 +228,8 @@ export class GroupConfigurationRenderer {
private createActionRowBuildersForMenu() : ActionRowBuilder<MessageActionRowComponentBuilder>[] {
const {currentCollection, currentElement} = this.findCurrentUI();
const icons = Container.get<IconCache>(IconCache.name);
if (currentElement?.isConfiguration ?? false) {
return [
new ActionRowBuilder<ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder>()
@ -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 },
);
@ -43,4 +50,16 @@ const rest = new REST().setToken(environment.discord.token);
logger.log("Ensuring Database...");
const updater = new DatabaseUpdater(container.get<DatabaseConnection>(DatabaseConnection.name));
updater.ensureAvaliablity(Definitions);
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 = new DiscordClient()
client.applyEvents()
client.connect(container.get<Environment>(Environment.name).discord.token)
const client = container.get<DiscordClient>(DiscordClient.name);
client.connectRESTClient(env.discord.token);
await container.get<IconCache>(IconCache.name).populate()
client.applyEvents()
client.connect(env.discord.token)
})()