ajouter une fonctionnalité d'envoi de message avec options d'embed et historique des messages

This commit is contained in:
Arthur Puechberty
2026-01-18 13:53:11 +01:00
parent 77842b3685
commit a3ebe47c24
5 changed files with 707 additions and 0 deletions
+165
View File
@@ -799,3 +799,168 @@ body {
background: #d9a02a; background: #d9a02a;
} }
/* ===== Send Message Preview ===== */
.message-preview {
background: var(--bg-dark);
border-radius: var(--radius-md);
padding: var(--spacing-md);
min-height: 100px;
}
.preview-content {
color: var(--text-primary);
margin-bottom: var(--spacing-md);
white-space: pre-wrap;
word-break: break-word;
}
.preview-embed {
background: var(--bg-card);
border-left: 4px solid var(--primary-color);
border-radius: var(--radius-sm);
padding: var(--spacing-md);
position: relative;
max-width: 520px;
}
.preview-embed-author {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.preview-author-icon {
width: 24px;
height: 24px;
border-radius: 50%;
}
.preview-embed-author a {
color: var(--text-secondary);
text-decoration: none;
}
.preview-embed-author a:hover {
text-decoration: underline;
}
.preview-embed-title {
font-weight: 600;
color: var(--text-primary);
font-size: 1rem;
margin-bottom: var(--spacing-xs);
}
.preview-embed-description {
color: var(--text-secondary);
font-size: 0.9rem;
white-space: pre-wrap;
word-break: break-word;
}
.preview-embed-thumbnail {
position: absolute;
top: var(--spacing-md);
right: var(--spacing-md);
max-width: 80px;
max-height: 80px;
border-radius: var(--radius-sm);
}
.preview-embed-fields {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.preview-embed-field {
flex: 1 1 100%;
}
.preview-embed-field.inline {
flex: 1 1 calc(33% - var(--spacing-sm));
min-width: 150px;
}
.preview-field-name {
font-weight: 600;
font-size: 0.85rem;
color: var(--text-primary);
margin-bottom: 2px;
}
.preview-field-value {
font-size: 0.85rem;
color: var(--text-secondary);
}
.preview-embed-image {
max-width: 100%;
max-height: 300px;
border-radius: var(--radius-sm);
margin-top: var(--spacing-md);
}
.preview-embed-footer {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-top: var(--spacing-md);
font-size: 0.75rem;
color: var(--text-muted);
}
.preview-footer-icon {
width: 20px;
height: 20px;
border-radius: 50%;
}
/* ===== Field Items ===== */
.field-item {
background: var(--bg-card);
padding: var(--spacing-sm);
border-radius: var(--radius-sm);
margin-bottom: var(--spacing-sm);
}
.field-item .form-row {
align-items: center;
}
/* ===== Send Message History ===== */
.history-item {
background: var(--bg-card);
border-radius: var(--radius-sm);
padding: var(--spacing-md);
margin-bottom: var(--spacing-sm);
}
.history-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xs);
}
.history-channel {
color: var(--primary-color);
font-weight: 500;
}
.history-time {
color: var(--text-muted);
font-size: 0.8rem;
}
.history-content {
color: var(--text-secondary);
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
+131
View File
@@ -66,6 +66,10 @@
<span class="nav-item-icon"></span> <span class="nav-item-icon"></span>
Messages programmés Messages programmés
</a> </a>
<a class="nav-item" data-section="sendmessage">
<span class="nav-item-icon">✉️</span>
Envoyer un message
</a>
</div> </div>
</nav> </nav>
@@ -932,6 +936,132 @@
</div> </div>
</section> </section>
<!-- Section: Envoyer un message -->
<section class="config-section" id="section-sendmessage">
<div class="config-card">
<div class="config-card-header">
<div>
<h2 class="config-card-title">✉️ Envoyer un message</h2>
<p class="config-card-subtitle">Envoyez un message dans un salon de votre serveur</p>
</div>
</div>
<div class="config-card-body">
<div class="form-group">
<label class="form-label">📺 Salon</label>
<select class="form-select" id="sendmsg-channel-select">
<option value="">-- Sélectionner un salon --</option>
</select>
</div>
<div class="form-group">
<label class="form-label">💬 Contenu du message</label>
<textarea class="form-textarea" id="sendmsg-content" rows="4" placeholder="Le contenu de votre message..."></textarea>
</div>
<!-- Embed optionnel -->
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="sendmsg-embed-enabled">
📦 Ajouter un embed
</label>
</div>
<div id="sendmsg-embed-options" style="display: none;">
<div class="form-row">
<div class="form-group">
<label class="form-label">Titre</label>
<input type="text" class="form-input" id="sendmsg-embed-title" placeholder="Titre de l'embed...">
</div>
<div class="form-group">
<label class="form-label">Couleur</label>
<input type="color" class="form-input" id="sendmsg-embed-color" value="#5865F2" style="height: 40px;">
</div>
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea class="form-textarea" id="sendmsg-embed-description" rows="4" placeholder="Description de l'embed..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">URL de l'auteur</label>
<input type="text" class="form-input" id="sendmsg-embed-author-url" placeholder="https://...">
</div>
<div class="form-group">
<label class="form-label">Nom de l'auteur</label>
<input type="text" class="form-input" id="sendmsg-embed-author-name" placeholder="Nom...">
</div>
<div class="form-group">
<label class="form-label">Icône de l'auteur (URL)</label>
<input type="text" class="form-input" id="sendmsg-embed-author-icon" placeholder="https://...">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">🖼️ Image (URL)</label>
<input type="text" class="form-input" id="sendmsg-embed-image" placeholder="https://...">
</div>
<div class="form-group">
<label class="form-label">📎 Miniature (URL)</label>
<input type="text" class="form-input" id="sendmsg-embed-thumbnail" placeholder="https://...">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Texte du footer</label>
<input type="text" class="form-input" id="sendmsg-embed-footer-text" placeholder="Footer...">
</div>
<div class="form-group">
<label class="form-label">Icône du footer (URL)</label>
<input type="text" class="form-input" id="sendmsg-embed-footer-icon" placeholder="https://...">
</div>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="sendmsg-embed-timestamp">
Ajouter un timestamp (date/heure actuelle)
</label>
</div>
<!-- Champs (Fields) -->
<div class="form-group">
<label class="form-label">📝 Champs (Fields)</label>
<div id="sendmsg-fields-container"></div>
<button type="button" class="btn btn-sm btn-secondary" id="sendmsg-add-field">+ Ajouter un champ</button>
</div>
</div>
<!-- Aperçu -->
<div class="form-group">
<label class="form-label">👁️ Aperçu</label>
<div id="sendmsg-preview" class="message-preview">
<p class="text-muted">L'aperçu apparaîtra ici...</p>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" id="sendmsg-send-btn">
📤 Envoyer le message
</button>
</div>
<!-- Historique -->
<div class="sub-section" style="margin-top: 2rem;">
<h4 class="sub-section-title">📋 Derniers messages envoyés</h4>
<div id="sendmsg-history">
<p class="text-muted">Aucun message envoyé récemment.</p>
</div>
</div>
</div>
</div>
</section>
</div> </div>
</main> </main>
@@ -950,5 +1080,6 @@
<script src="/guild/countingForm.js"></script> <script src="/guild/countingForm.js"></script>
<script src="/guild/statsChannelsForm.js"></script> <script src="/guild/statsChannelsForm.js"></script>
<script src="/guild/scheduledMessagesForm.js"></script> <script src="/guild/scheduledMessagesForm.js"></script>
<script src="/guild/sendMessageForm.js"></script>
</body> </body>
</html> </html>
+2
View File
@@ -60,6 +60,7 @@ fetch(`/api/bot/get-text-channels/${guildId}`)
const goodbye = document.getElementById("goodbye-channel"); const goodbye = document.getElementById("goodbye-channel");
const levelAnnouncements = document.getElementById("level-announcements-channel"); const levelAnnouncements = document.getElementById("level-announcements-channel");
const levelChannelRestrict = document.getElementById("level-channel-with-or-without-xp"); const levelChannelRestrict = document.getElementById("level-channel-with-or-without-xp");
const sendmsgChannel = document.getElementById("sendmsg-channel-select");
channels.forEach(c => { channels.forEach(c => {
const opt = new Option(`#${c.name}`, c.id); const opt = new Option(`#${c.name}`, c.id);
@@ -67,6 +68,7 @@ fetch(`/api/bot/get-text-channels/${guildId}`)
goodbye?.appendChild(opt.cloneNode(true)); goodbye?.appendChild(opt.cloneNode(true));
levelAnnouncements?.appendChild(opt.cloneNode(true)); levelAnnouncements?.appendChild(opt.cloneNode(true));
levelChannelRestrict?.appendChild(opt.cloneNode(true)); levelChannelRestrict?.appendChild(opt.cloneNode(true));
sendmsgChannel?.appendChild(opt.cloneNode(true));
}); });
}); });
+316
View File
@@ -0,0 +1,316 @@
// ===== SEND MESSAGE FORM =====
(async function () {
const channelSelect = document.getElementById("sendmsg-channel-select");
const messageContent = document.getElementById("sendmsg-content");
const embedEnabled = document.getElementById("sendmsg-embed-enabled");
const embedOptions = document.getElementById("sendmsg-embed-options");
const embedTitle = document.getElementById("sendmsg-embed-title");
const embedDescription = document.getElementById("sendmsg-embed-description");
const embedColor = document.getElementById("sendmsg-embed-color");
const embedAuthorName = document.getElementById("sendmsg-embed-author-name");
const embedAuthorUrl = document.getElementById("sendmsg-embed-author-url");
const embedAuthorIcon = document.getElementById("sendmsg-embed-author-icon");
const embedImage = document.getElementById("sendmsg-embed-image");
const embedThumbnail = document.getElementById("sendmsg-embed-thumbnail");
const embedFooterText = document.getElementById("sendmsg-embed-footer-text");
const embedFooterIcon = document.getElementById("sendmsg-embed-footer-icon");
const embedTimestamp = document.getElementById("sendmsg-embed-timestamp");
const fieldsContainer = document.getElementById("sendmsg-fields-container");
const addFieldBtn = document.getElementById("sendmsg-add-field");
const previewContainer = document.getElementById("sendmsg-preview");
const sendBtn = document.getElementById("sendmsg-send-btn");
const historyContainer = document.getElementById("sendmsg-history");
let fields = [];
let sentMessages = [];
// Toggle embed options
embedEnabled.addEventListener("change", () => {
embedOptions.style.display = embedEnabled.checked ? "block" : "none";
updatePreview();
});
// Add field
addFieldBtn.addEventListener("click", () => {
const fieldIndex = fields.length;
fields.push({ name: "", value: "", inline: false });
renderFields();
});
function renderFields() {
fieldsContainer.innerHTML = "";
fields.forEach((field, index) => {
const fieldDiv = document.createElement("div");
fieldDiv.className = "field-item";
fieldDiv.innerHTML = `
<div class="form-row" style="margin-bottom: 0.5rem;">
<div class="form-group" style="flex: 1;">
<input type="text" class="form-input field-name" data-index="${index}" placeholder="Nom du champ" value="${field.name}">
</div>
<div class="form-group" style="flex: 2;">
<input type="text" class="form-input field-value" data-index="${index}" placeholder="Valeur" value="${field.value}">
</div>
<div class="form-group" style="flex: 0 0 auto; display: flex; align-items: center; gap: 0.5rem;">
<label style="display: flex; align-items: center; gap: 0.25rem; white-space: nowrap;">
<input type="checkbox" class="field-inline" data-index="${index}" ${field.inline ? "checked" : ""}> Inline
</label>
<button type="button" class="btn btn-sm btn-danger field-remove" data-index="${index}">✕</button>
</div>
</div>
`;
fieldsContainer.appendChild(fieldDiv);
});
// Event listeners for fields
document.querySelectorAll(".field-name").forEach(input => {
input.addEventListener("input", (e) => {
fields[e.target.dataset.index].name = e.target.value;
updatePreview();
});
});
document.querySelectorAll(".field-value").forEach(input => {
input.addEventListener("input", (e) => {
fields[e.target.dataset.index].value = e.target.value;
updatePreview();
});
});
document.querySelectorAll(".field-inline").forEach(input => {
input.addEventListener("change", (e) => {
fields[e.target.dataset.index].inline = e.target.checked;
updatePreview();
});
});
document.querySelectorAll(".field-remove").forEach(btn => {
btn.addEventListener("click", (e) => {
fields.splice(e.target.dataset.index, 1);
renderFields();
updatePreview();
});
});
}
// Update preview
function updatePreview() {
let html = "";
const content = messageContent.value.trim();
if (content) {
html += `<div class="preview-content">${escapeHtml(content)}</div>`;
}
if (embedEnabled.checked) {
const color = embedColor.value || "#5865F2";
html += `<div class="preview-embed" style="border-left-color: ${color};">`;
// Author
if (embedAuthorName.value.trim()) {
html += `<div class="preview-embed-author">`;
if (embedAuthorIcon.value.trim()) {
html += `<img src="${escapeHtml(embedAuthorIcon.value)}" class="preview-author-icon" onerror="this.style.display='none'">`;
}
if (embedAuthorUrl.value.trim()) {
html += `<a href="${escapeHtml(embedAuthorUrl.value)}" target="_blank">${escapeHtml(embedAuthorName.value)}</a>`;
} else {
html += `<span>${escapeHtml(embedAuthorName.value)}</span>`;
}
html += `</div>`;
}
// Title
if (embedTitle.value.trim()) {
html += `<div class="preview-embed-title">${escapeHtml(embedTitle.value)}</div>`;
}
// Description
if (embedDescription.value.trim()) {
html += `<div class="preview-embed-description">${escapeHtml(embedDescription.value)}</div>`;
}
// Thumbnail
if (embedThumbnail.value.trim()) {
html += `<img src="${escapeHtml(embedThumbnail.value)}" class="preview-embed-thumbnail" onerror="this.style.display='none'">`;
}
// Fields
const validFields = fields.filter(f => f.name.trim() && f.value.trim());
if (validFields.length > 0) {
html += `<div class="preview-embed-fields">`;
validFields.forEach(f => {
html += `<div class="preview-embed-field ${f.inline ? 'inline' : ''}">
<div class="preview-field-name">${escapeHtml(f.name)}</div>
<div class="preview-field-value">${escapeHtml(f.value)}</div>
</div>`;
});
html += `</div>`;
}
// Image
if (embedImage.value.trim()) {
html += `<img src="${escapeHtml(embedImage.value)}" class="preview-embed-image" onerror="this.style.display='none'">`;
}
// Footer
if (embedFooterText.value.trim() || embedTimestamp.checked) {
html += `<div class="preview-embed-footer">`;
if (embedFooterIcon.value.trim()) {
html += `<img src="${escapeHtml(embedFooterIcon.value)}" class="preview-footer-icon" onerror="this.style.display='none'">`;
}
let footerParts = [];
if (embedFooterText.value.trim()) {
footerParts.push(escapeHtml(embedFooterText.value));
}
if (embedTimestamp.checked) {
footerParts.push(new Date().toLocaleString("fr-FR"));
}
html += `<span>${footerParts.join(" • ")}</span>`;
html += `</div>`;
}
html += `</div>`;
}
if (!html) {
html = `<p class="text-muted">L'aperçu apparaîtra ici...</p>`;
}
previewContainer.innerHTML = html;
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Add event listeners for preview updates
[messageContent, embedTitle, embedDescription, embedAuthorName, embedAuthorUrl,
embedAuthorIcon, embedImage, embedThumbnail, embedFooterText, embedFooterIcon].forEach(el => {
el.addEventListener("input", updatePreview);
});
embedColor.addEventListener("change", updatePreview);
embedTimestamp.addEventListener("change", updatePreview);
// Send message
sendBtn.addEventListener("click", async () => {
const channelId = channelSelect.value;
if (!channelId) {
alert("Veuillez sélectionner un salon.");
return;
}
const content = messageContent.value.trim();
const hasEmbed = embedEnabled.checked;
if (!content && !hasEmbed) {
alert("Veuillez entrer un message ou activer l'embed.");
return;
}
// Build embed data
let embed = null;
if (hasEmbed) {
embed = {
title: embedTitle.value.trim() || null,
description: embedDescription.value.trim() || null,
color: embedColor.value,
author: null,
thumbnail: embedThumbnail.value.trim() ? { url: embedThumbnail.value.trim() } : null,
image: embedImage.value.trim() ? { url: embedImage.value.trim() } : null,
footer: null,
timestamp: embedTimestamp.checked,
fields: fields.filter(f => f.name.trim() && f.value.trim())
};
if (embedAuthorName.value.trim()) {
embed.author = {
name: embedAuthorName.value.trim(),
url: embedAuthorUrl.value.trim() || null,
icon_url: embedAuthorIcon.value.trim() || null
};
}
if (embedFooterText.value.trim()) {
embed.footer = {
text: embedFooterText.value.trim(),
icon_url: embedFooterIcon.value.trim() || null
};
}
}
sendBtn.disabled = true;
sendBtn.textContent = "⏳ Envoi en cours...";
try {
const res = await fetch("/api/bot/send-message", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
guildId: window.guildId,
channelId,
content: content || null,
embed
})
});
const data = await res.json();
if (data.success) {
// Add to history
sentMessages.unshift({
channelId,
channelName: channelSelect.options[channelSelect.selectedIndex].text,
content: content || "(Embed uniquement)",
timestamp: new Date().toLocaleString("fr-FR")
});
if (sentMessages.length > 10) sentMessages.pop();
renderHistory();
// Clear form
messageContent.value = "";
embedTitle.value = "";
embedDescription.value = "";
embedAuthorName.value = "";
embedAuthorUrl.value = "";
embedAuthorIcon.value = "";
embedImage.value = "";
embedThumbnail.value = "";
embedFooterText.value = "";
embedFooterIcon.value = "";
embedTimestamp.checked = false;
fields = [];
renderFields();
updatePreview();
alert("✅ Message envoyé avec succès !");
} else {
alert("❌ Erreur lors de l'envoi: " + (data.error || "Erreur inconnue"));
}
} catch (err) {
console.error(err);
alert("❌ Erreur lors de l'envoi du message.");
}
sendBtn.disabled = false;
sendBtn.textContent = "📤 Envoyer le message";
});
function renderHistory() {
if (sentMessages.length === 0) {
historyContainer.innerHTML = `<p class="text-muted">Aucun message envoyé récemment.</p>`;
return;
}
historyContainer.innerHTML = sentMessages.map(msg => `
<div class="history-item">
<div class="history-item-header">
<span class="history-channel">#${escapeHtml(msg.channelName)}</span>
<span class="history-time">${msg.timestamp}</span>
</div>
<div class="history-content">${escapeHtml(msg.content.substring(0, 100))}${msg.content.length > 100 ? '...' : ''}</div>
</div>
`).join("");
}
// Initialize
updatePreview();
})();
+93
View File
@@ -1122,5 +1122,98 @@ module.exports = (app, db, client) => {
); );
}); });
// Envoyer un message instantané
router.post("/bot/send-message", express.json(), async (req, res) => {
const { guildId, channelId, content, embed } = req.body;
if (!req.session.guilds) {
return res.status(401).json({ success: false, error: "Non connecté" });
}
const isAdmin = req.session.guilds.find(
g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n
);
if (!isAdmin) {
return res.status(403).json({ success: false, error: "Permission refusée" });
}
try {
const guild = client.guilds.cache.get(guildId);
if (!guild) {
return res.status(404).json({ success: false, error: "Serveur non trouvé" });
}
const channel = guild.channels.cache.get(channelId);
if (!channel) {
return res.status(404).json({ success: false, error: "Salon non trouvé" });
}
// Build message options
const messageOptions = {};
if (content) {
messageOptions.content = content;
}
if (embed) {
const { EmbedBuilder } = require("discord.js");
const embedBuilder = new EmbedBuilder();
if (embed.title) embedBuilder.setTitle(embed.title);
if (embed.description) embedBuilder.setDescription(embed.description);
if (embed.color) embedBuilder.setColor(embed.color);
if (embed.author && embed.author.name) {
embedBuilder.setAuthor({
name: embed.author.name,
url: embed.author.url || undefined,
iconURL: embed.author.icon_url || undefined
});
}
if (embed.thumbnail && embed.thumbnail.url) {
embedBuilder.setThumbnail(embed.thumbnail.url);
}
if (embed.image && embed.image.url) {
embedBuilder.setImage(embed.image.url);
}
if (embed.footer && embed.footer.text) {
embedBuilder.setFooter({
text: embed.footer.text,
iconURL: embed.footer.icon_url || undefined
});
}
if (embed.timestamp) {
embedBuilder.setTimestamp();
}
if (embed.fields && embed.fields.length > 0) {
embed.fields.forEach(field => {
if (field.name && field.value) {
embedBuilder.addFields({
name: field.name,
value: field.value,
inline: field.inline || false
});
}
});
}
messageOptions.embeds = [embedBuilder];
}
await channel.send(messageOptions);
res.json({ success: true });
} catch (err) {
console.error("Erreur envoi message:", err);
res.status(500).json({ success: false, error: err.message });
}
});
app.use("/api", router); app.use("/api", router);
}; };