This commit is contained in:
Louis Mazin 2026-04-16 17:17:04 +02:00
parent 616a2cc7b5
commit 6273c03431
2 changed files with 90 additions and 16 deletions

View File

@ -52,6 +52,7 @@ client.once('clientReady', async () => {
deploy(process.env.DISCORD_TOKEN); deploy(process.env.DISCORD_TOKEN);
clean(client); clean(client);
await update(client);
}); });
client.on(Events.InteractionCreate, async interaction => { client.on(Events.InteractionCreate, async interaction => {

View File

@ -1,9 +1,11 @@
const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require('discord.js'); const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder, AttachmentBuilder } = require('discord.js');
const fs = require('node:fs'); const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const PINBOARD_SELECT_CUSTOM_ID = 'pinboard-image-select'; const PINBOARD_SELECT_CUSTOM_ID = 'pinboard-image-select';
const selectedImageIndexByMessage = new Map(); const selectedImageIndexByMessage = new Map();
const preloadedImagesByUrl = new Map();
let lastKnownStatus = null;
const getCacheBustValue = () => { const getCacheBustValue = () => {
const bucketSeconds = Number.parseInt(process.env.PINBOARD_CACHE_BUST_SECONDS || '60', 10); const bucketSeconds = Number.parseInt(process.env.PINBOARD_CACHE_BUST_SECONDS || '60', 10);
@ -95,12 +97,71 @@ const clampIndex = (index, length) => {
return index; return index;
}; };
const buildSelectedImageEmbed = (item, index, total) => ( const sanitizeAttachmentName = (name) => (
new EmbedBuilder() (name || 'pinboard-image')
.toLowerCase()
.replace(/[^a-z0-9._-]/g, '-')
.replace(/-+/g, '-')
.slice(0, 64)
);
const guessFileExtensionFromContentType = (contentType) => {
if (!contentType) return 'jpg';
if (contentType.includes('png')) return 'png';
if (contentType.includes('webp')) return 'webp';
if (contentType.includes('gif')) return 'gif';
if (contentType.includes('jpeg') || contentType.includes('jpg')) return 'jpg';
return 'jpg';
};
const preloadPinboardImages = async (items) => {
const uniqueUrls = [...new Set(items.map(item => item.url))];
await Promise.all(uniqueUrls.map(async (url, index) => {
try {
const response = await fetch(toCacheBustedUrl(url));
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const contentType = response.headers.get('content-type') || '';
const ext = guessFileExtensionFromContentType(contentType.toLowerCase());
const attachmentName = `${sanitizeAttachmentName(`pinboard-${index + 1}`)}.${ext}`;
preloadedImagesByUrl.set(url, {
buffer,
attachmentName,
loadedAt: Date.now()
});
} catch (error) {
// Keep previous successful cache entry if refresh fails.
if (!preloadedImagesByUrl.has(url)) {
preloadedImagesByUrl.set(url, null);
}
console.log(`⚠️ Preload image impossible (${url}): ${error.message}`);
}
}));
const knownUrls = new Set(uniqueUrls);
for (const cachedUrl of preloadedImagesByUrl.keys()) {
if (!knownUrls.has(cachedUrl)) {
preloadedImagesByUrl.delete(cachedUrl);
}
}
};
const buildSelectedImageEmbed = (item, imageReference) => {
const imageUrl = imageReference?.attachmentName
? `attachment://${imageReference.attachmentName}`
: toCacheBustedUrl(item.url);
return new EmbedBuilder()
.setColor('#f59e0b') .setColor('#f59e0b')
.setTitle(item.name) .setTitle(item.name)
.setImage(toCacheBustedUrl(item.url)) .setImage(imageUrl);
); };
const buildImageSelectorRow = (items, selectedIndex) => { const buildImageSelectorRow = (items, selectedIndex) => {
const options = items.slice(0, 25).map((item, index) => ({ const options = items.slice(0, 25).map((item, index) => ({
@ -159,24 +220,31 @@ const buildPanelEmbed = (status) => {
return message; return message;
}; };
const buildPanelPayload = (status, messageId) => { const buildPanelPayload = (status, messageId, pinboardItems = null) => {
const panelEmbed = buildPanelEmbed(status); const panelEmbed = buildPanelEmbed(status);
const pinboardItems = parsePinboardImageItems().slice(0, 25); const items = (pinboardItems || parsePinboardImageItems()).slice(0, 25);
if (pinboardItems.length === 0) { if (items.length === 0) {
return { content: '', embeds: [panelEmbed], components: [] }; return { content: '', embeds: [panelEmbed], components: [] };
} }
const currentIndex = clampIndex(selectedImageIndexByMessage.get(messageId) ?? 0, pinboardItems.length); const currentIndex = clampIndex(selectedImageIndexByMessage.get(messageId) ?? 0, items.length);
selectedImageIndexByMessage.set(messageId, currentIndex); selectedImageIndexByMessage.set(messageId, currentIndex);
const selectedImageEmbed = buildSelectedImageEmbed(pinboardItems[currentIndex], currentIndex, pinboardItems.length); const selectedItem = items[currentIndex];
const selectorRow = buildImageSelectorRow(pinboardItems, currentIndex); const cachedImage = preloadedImagesByUrl.get(selectedItem.url);
const selectedImageEmbed = buildSelectedImageEmbed(selectedItem, cachedImage);
const selectorRow = buildImageSelectorRow(items, currentIndex);
const files = cachedImage?.buffer
? [new AttachmentBuilder(cachedImage.buffer, { name: cachedImage.attachmentName })]
: [];
return { return {
content: '', content: '',
embeds: [panelEmbed, selectedImageEmbed], embeds: [panelEmbed, selectedImageEmbed],
components: [selectorRow] components: [selectorRow],
files
}; };
}; };
@ -215,10 +283,15 @@ const update = async (client) => {
return; return;
} }
const status = await getMinecraftStatus(); const pinboardItems = parsePinboardImageItems().slice(0, 25);
const [status] = await Promise.all([
getMinecraftStatus(),
preloadPinboardImages(pinboardItems)
]);
lastKnownStatus = status;
const { message } = await resolvePanelMessage(client, channelId, messageId); const { message } = await resolvePanelMessage(client, channelId, messageId);
const payload = buildPanelPayload(status, message.id); const payload = buildPanelPayload(status, message.id, pinboardItems);
await message.edit(payload); await message.edit(payload);
@ -245,8 +318,8 @@ const handlePinboardSelection = async (interaction) => {
const selectedIndex = clampIndex(selectedRaw, pinboardItems.length); const selectedIndex = clampIndex(selectedRaw, pinboardItems.length);
selectedImageIndexByMessage.set(interaction.message.id, selectedIndex); selectedImageIndexByMessage.set(interaction.message.id, selectedIndex);
const status = await getMinecraftStatus(); const status = lastKnownStatus || await getMinecraftStatus();
const payload = buildPanelPayload(status, interaction.message.id); const payload = buildPanelPayload(status, interaction.message.id, pinboardItems);
await interaction.update(payload); await interaction.update(payload);
return true; return true;
} catch (error) { } catch (error) {