Files
GITLAB-Issue-Board-Discord/src/commands/handlers/PdfHandler.js

275 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const os = require('os');
const path = require('path');
const axios = require('axios');
const sharp = require('sharp');
const { createCanvas } = require('canvas');
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js');
const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js');
const logger = require('../../utils/logger');
/**
* Handler pour la commande /pdf
*/
class PdfHandler {
constructor() {
this.storageDir = process.env.PDF_STORAGE_DIR || path.join(process.cwd(), 'data', 'pdfs');
this.sessionFile = path.join(this.storageDir, 'sessions.json');
this.sessions = new Map(); // messageId -> { pdfPath, totalPages, ownerId, createdAt, downloadUrl, isPublic }
pdfjsLib.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js');
this.ensureStorageDir();
this.loadSessions();
}
/**
* Commande principale
*/
async handle(interaction) {
try {
const attachment = interaction.options.getAttachment('file');
const urlOption = interaction.options.getString('url');
const isPublic = interaction.options.getBoolean('public') === true;
const sourceUrl = attachment ? attachment.url : urlOption;
if (!attachment && !urlOption) {
await interaction.editReply({ content: '❌ Fournis un PDF (fichier) ou une URL https:// vers un PDF.' });
return;
}
if (urlOption && !urlOption.toLowerCase().startsWith('http')) {
await interaction.editReply({ content: '❌ URL invalide. Utilise http(s)://...' });
return;
}
if (attachment && attachment.contentType && !attachment.contentType.includes('pdf')) {
await interaction.editReply({ content: '❌ Le fichier doit être un PDF.' });
return;
}
if (attachment && attachment.size > 15 * 1024 * 1024) {
await interaction.editReply({ content: '❌ PDF trop lourd (max 15 Mo).' });
return;
}
const tmpPath = await this.downloadPdf(sourceUrl);
const { pdf, totalPages } = await this.loadPdf(tmpPath);
if (totalPages > 100) {
await interaction.editReply({ content: '❌ PDF trop long (max 100 pages).' });
fs.unlink(tmpPath, () => {});
return;
}
const pageBuffer = await this.renderPage(pdf, 1);
const title = this.getPdfTitle(sourceUrl);
const messageOptions = this.buildMessageOptions(pageBuffer, 1, totalPages, interaction.user.id, sourceUrl, title);
// Visibilité du message (ephemeral si non public)
if (!isPublic) {
messageOptions.flags = require('discord.js').MessageFlags.Ephemeral;
}
const reply = await interaction.editReply(messageOptions);
// Sauvegarde de la session pour la navigation
this.sessions.set(reply.id, {
pdfPath: tmpPath,
totalPages,
ownerId: interaction.user.id,
createdAt: Date.now(),
downloadUrl: sourceUrl,
isPublic
});
this.saveSessions();
logger.info(`PDF affiché (${totalPages} pages) par ${interaction.user.tag}`);
} catch (error) {
logger.error('Erreur lors du traitement du PDF:', error);
await interaction.editReply({ content: '❌ Erreur lors du traitement du PDF.' });
}
}
/**
* Gestion des boutons
*/
async handleButton(interaction) {
const parts = (interaction.customId || '').split(':'); // pdf-nav:{userId}:{page}:{total}
if (parts.length !== 4) return;
const [, ownerId, pageStr, totalStr] = parts;
const requestedPage = parseInt(pageStr, 10);
const totalPages = parseInt(totalStr, 10);
const session = this.sessions.get(interaction.message.id);
if (!session) {
await interaction.reply({
content: '❌ Session expirée ou introuvable.',
flags: require('discord.js').MessageFlags.Ephemeral
});
return;
}
if (!session.isPublic && interaction.user.id !== ownerId) {
await interaction.reply({
content: '❌ Seul lauteur de la commande peut naviguer.',
flags: require('discord.js').MessageFlags.Ephemeral
});
return;
}
const clampedPage = Math.min(Math.max(1, requestedPage), totalPages);
if (clampedPage === requestedPage && interaction.isRepliable()) {
await interaction.deferUpdate();
}
try {
const { pdf } = await this.loadPdf(session.pdfPath);
const pageBuffer = await this.renderPage(pdf, clampedPage);
const title = this.getPdfTitle(session.downloadUrl);
const messageOptions = this.buildMessageOptions(pageBuffer, clampedPage, totalPages, ownerId, session.downloadUrl, title);
await interaction.editReply(messageOptions);
} catch (error) {
logger.error('Erreur lors de la navigation PDF:', error);
await interaction.editReply({ content: '❌ Erreur lors du rendu de la page.' });
}
}
/**
* Télécharge le PDF dans un fichier temporaire
*/
async downloadPdf(url) {
const response = await axios.get(url, { responseType: 'arraybuffer' });
const fileName = `pdf-${Date.now()}-${Math.random().toString(36).slice(2)}.pdf`;
const targetPath = path.join(this.storageDir, fileName);
fs.writeFileSync(targetPath, response.data);
return targetPath;
}
/**
* Charge le PDF avec pdfjs
*/
async loadPdf(pdfPath) {
const data = new Uint8Array(fs.readFileSync(pdfPath));
const loadingTask = pdfjsLib.getDocument({ data });
const pdf = await loadingTask.promise;
return { pdf, totalPages: pdf.numPages };
}
/**
* Rendu d'une page en buffer PNG
*/
async renderPage(pdf, pageNumber) {
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale: 1.5 }); // Ajuster si besoin
const canvas = createCanvas(viewport.width, viewport.height);
const context = canvas.getContext('2d');
await page.render({
canvasContext: context,
viewport,
}).promise;
const buffer = canvas.toBuffer('image/png');
// Recompression légère pour réduire la taille
return sharp(buffer).png({ quality: 90 }).toBuffer();
}
/**
* Construit le message Discord avec image + boutons
*/
buildMessageOptions(pageBuffer, pageIndex, totalPages, ownerId, downloadUrl, title) {
const file = new AttachmentBuilder(pageBuffer, { name: `page-${pageIndex}.png` });
const embed = new EmbedBuilder()
.setTitle(title || 'Visualisation PDF')
.setDescription(`Page ${pageIndex} / ${totalPages}`)
.setImage(`attachment://page-${pageIndex}.png`)
.setColor(0x5865F2);
const prevDisabled = pageIndex <= 1;
const nextDisabled = pageIndex >= totalPages;
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId(`pdf-nav:${ownerId}:${pageIndex - 1}:${totalPages}`)
.setLabel('◀️ Précédent')
.setStyle(ButtonStyle.Primary)
.setDisabled(prevDisabled),
new ButtonBuilder()
.setCustomId(`pdf-nav:${ownerId}:${pageIndex + 1}:${totalPages}`)
.setLabel('Suivant ▶️')
.setStyle(ButtonStyle.Primary)
.setDisabled(nextDisabled),
new ButtonBuilder()
.setLabel('Télécharger PDF')
.setStyle(ButtonStyle.Link)
.setURL(downloadUrl || 'https://')
);
return {
embeds: [embed],
files: [file],
components: [row]
};
}
/**
* Déduit un titre à partir de l'URL du PDF
*/
getPdfTitle(downloadUrl) {
try {
if (!downloadUrl) return 'Visualisation PDF';
const url = new URL(downloadUrl);
const pathname = url.pathname || '';
const name = pathname.split('/').filter(Boolean).pop();
if (name) return name;
return 'Visualisation PDF';
} catch (e) {
return 'Visualisation PDF';
}
}
/**
* Persistance des sessions pour survivre aux redémarrages
*/
ensureStorageDir() {
fs.mkdirSync(this.storageDir, { recursive: true });
}
loadSessions() {
try {
if (!fs.existsSync(this.sessionFile)) return;
const raw = fs.readFileSync(this.sessionFile, 'utf8');
const data = JSON.parse(raw);
data.forEach(entry => {
if (entry.pdfPath && fs.existsSync(entry.pdfPath)) {
this.sessions.set(entry.messageId, {
pdfPath: entry.pdfPath,
totalPages: entry.totalPages,
ownerId: entry.ownerId,
createdAt: entry.createdAt,
downloadUrl: entry.downloadUrl,
isPublic: entry.isPublic
});
}
});
} catch (e) {
logger.error('Erreur lors du chargement des sessions PDF persistées:', e);
}
}
saveSessions() {
try {
const data = [];
for (const [messageId, session] of this.sessions.entries()) {
data.push({ messageId, ...session });
}
fs.writeFileSync(this.sessionFile, JSON.stringify(data, null, 2), 'utf8');
} catch (e) {
logger.error('Erreur lors de la sauvegarde des sessions PDF:', e);
}
}
}
module.exports = PdfHandler;