Version 2.1.0 - Update UI
This commit is contained in:
18 files changed
+3508
-330
No files matched your search
+121
-15
@@ -19,6 +19,7 @@ const players = require('../player/Player')
|
||||
const discordBot = require('../discord/Bot')
|
||||
const serverSettings = require('../discord/ServerSettings')
|
||||
const playlists = require('../playlists/PlaylistManager')
|
||||
const guildPlaylists = require('../playlists/GuildPlaylists')
|
||||
const warp = require('../utils/WarpProxy')
|
||||
const SafeMetric = require('../utils/SafeMetric')
|
||||
const LogReader = require('../utils/LogReader')
|
||||
@@ -53,7 +54,9 @@ const SPOTIFY_TEST_TIMEOUT = 15 * 1000
|
||||
// Changement des processus yt-dlp actifs (Youtube.js ne prévient pas le player) : sondage léger
|
||||
const YTDLP_PROCESS_POLL = 2000
|
||||
// Jamais "config" : contient le token du bot et les secrets
|
||||
const DATABASES = ["Users", "Playlists", "history", "media", "previous", "server_settings"]
|
||||
const DATABASES = ["Users", "Playlists", "GuildPlaylists", "history", "media", "previous", "server_settings"]
|
||||
// Playlists partagées : qui peut les modifier (tous les membres / modérateurs)
|
||||
const SHARED_PLAYLISTS_EDITORS = ["members", "mods"]
|
||||
const PLAYER_ACTIONS = ["pause", "resume", "skip", "previous", "stop", "leave", "loop", "shuffle", "clear"]
|
||||
const LOG_LEVELS = ["INFO", "WARN", "ERROR", "FATAL"]
|
||||
const USER_FILTERS = ["all", "online", "admin", "banned", "deleted"]
|
||||
@@ -447,7 +450,42 @@ function guildSummary(guildData, ctx = buildContext()) {
|
||||
missingPermissions,
|
||||
isMediaGuild: !!id && id === configuration.getMediaGuildId(),
|
||||
player: isConnectedPlayer(player) ? playerSummary(player) : null,
|
||||
stats: guildStats(id)
|
||||
stats: guildStats(id),
|
||||
sharedPlaylists: sharedPlaylistsSummary(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Option des playlists partagées d'un serveur + nombre de playlists enregistrées (même masquées)
|
||||
*/
|
||||
function sharedPlaylistsSummary(guildId) {
|
||||
const settings = typeof serverSettings.getSharedPlaylists === "function"
|
||||
? serverSettings.getSharedPlaylists(guildId)
|
||||
: { enabled: false, editors: "members" }
|
||||
return {
|
||||
enabled: !!settings?.enabled,
|
||||
editors: SHARED_PLAYLISTS_EDITORS.includes(settings?.editors) ? settings.editors : "members",
|
||||
count: guildPlaylists.countPlaylists(guildId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Résumé d'une playlist pour les listes d'administration (jamais la liste des titres)
|
||||
*/
|
||||
function playlistLite(p) {
|
||||
return {
|
||||
playlistId: p?.playlistId != null ? String(p.playlistId) : null,
|
||||
title: p?.title ?? null,
|
||||
type: p?.type ?? null,
|
||||
// "google" : synchronisée depuis le compte Google du propriétaire
|
||||
source: p?.source ?? null,
|
||||
url: typeof p?.url === "string" ? p.url : null,
|
||||
thumbnail: typeof p?.thumbnail === "string" ? p.thumbnail : null,
|
||||
duration: safeNum(p?.duration),
|
||||
syncedAt: toIso(p?.syncedAt),
|
||||
createdAt: toIso(p?.createdAt),
|
||||
updatedAt: toIso(p?.updatedAt),
|
||||
songCount: Array.isArray(p?.songs) ? p.songs.length : 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1601,6 +1639,27 @@ function attach({ io: ioInstance, socket, socketUser }) {
|
||||
return { secureRole: role ? { id: role.id, name: role.name } : null }
|
||||
})
|
||||
|
||||
// Playlists partagées d'un serveur : {guildId, enabled?, editors?} (désactiver masque sans rien supprimer)
|
||||
AdminRequest("/ADMIN/GUILDS/SET_SHARED_PLAYLISTS", async (payload, me) => {
|
||||
const data = asObject(payload)
|
||||
const guildId = requireId(data.guildId, "guildId")
|
||||
if (!discordBot.getGuilds().has(guildId)) throw new AdminError("NOT_FOUND", "Serveur introuvable")
|
||||
const patch = {}
|
||||
if (data.enabled !== undefined) patch.enabled = requireBool(data.enabled, "enabled")
|
||||
if (data.editors !== undefined) {
|
||||
if (!SHARED_PLAYLISTS_EDITORS.includes(data.editors)) throw new AdminError("INVALID_PARAMS", "Paramètre \"editors\" invalide (members ou mods)")
|
||||
patch.editors = data.editors
|
||||
}
|
||||
if (Object.keys(patch).length === 0) throw new AdminError("INVALID_PARAMS", "Aucune modification demandée (enabled ou editors)")
|
||||
const settings = serverSettings.setSharedPlaylists(guildId, patch)
|
||||
if (!settings) throw new AdminError("INVALID_PARAMS", "Réglage des playlists partagées invalide")
|
||||
audit(me, "playlists partagées", `${guildId} -> ${settings.enabled ? "activées" : "désactivées"}, modification : ${settings.editors}`)
|
||||
process.emit("GUILDS_UPDATE")
|
||||
// Les membres présents sur ce serveur rechargent leurs playlists partagées
|
||||
process.emit("GUILD_PLAYLISTS_UPDATE", guildId)
|
||||
return { sharedPlaylists: sharedPlaylistsSummary(guildId) }
|
||||
})
|
||||
|
||||
AdminRequest("/ADMIN/GUILDS/REFRESH", async (payload, me) => {
|
||||
if (typeof discordBot.refreshGuilds !== "function") throw new AdminError("UNAVAILABLE", "Rafraîchissement indisponible")
|
||||
await discordBot.refreshGuilds()
|
||||
@@ -1763,27 +1822,63 @@ function attach({ io: ioInstance, socket, socketUser }) {
|
||||
username: owner?.identity?.username ?? null,
|
||||
// Identité légère pour l'affichage (jamais auth ni tokens)
|
||||
owner: owner ? { ...userLite(owner.identity), isAdmin: owner.labels?.includes("ADMIN") ?? false, isDeleted: owner.labels?.includes("DELETED") ?? false } : null,
|
||||
playlists: list.map(p => ({
|
||||
playlistId: p?.playlistId != null ? String(p.playlistId) : null,
|
||||
title: p?.title ?? null,
|
||||
type: p?.type ?? null,
|
||||
// "google" : synchronisée depuis le compte Google du propriétaire
|
||||
source: p?.source ?? null,
|
||||
url: typeof p?.url === "string" ? p.url : null,
|
||||
thumbnail: typeof p?.thumbnail === "string" ? p.thumbnail : null,
|
||||
duration: safeNum(p?.duration),
|
||||
syncedAt: toIso(p?.syncedAt),
|
||||
songCount: Array.isArray(p?.songs) ? p.songs.length : 0
|
||||
}))
|
||||
playlists: list.map(playlistLite)
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
// Playlists partagées des serveurs : {guildId?}. Serveurs ayant des playlists (même désactivées ou quittés
|
||||
// par le bot) ou l'option activée
|
||||
AdminRequest("/ADMIN/PLAYLISTS/GUILDS", async (payload) => {
|
||||
const data = asObject(payload)
|
||||
const filter = data.guildId === undefined || data.guildId === null ? null : requireId(data.guildId, "guildId")
|
||||
const botGuilds = discordBot.getGuilds()
|
||||
const ids = new Set(guildPlaylists.listGuildIds())
|
||||
if (typeof serverSettings.getSharedPlaylists === "function") {
|
||||
for (const guildId of botGuilds.keys()) {
|
||||
if (serverSettings.getSharedPlaylists(guildId)?.enabled) ids.add(guildId)
|
||||
}
|
||||
}
|
||||
if (filter) {
|
||||
ids.clear()
|
||||
ids.add(filter)
|
||||
}
|
||||
const result = []
|
||||
for (const guildId of ids) {
|
||||
const list = guildPlaylists.getPlaylists(guildId)
|
||||
const shared = sharedPlaylistsSummary(guildId)
|
||||
if (!filter && list.length === 0 && !shared.enabled) continue
|
||||
const guild = botGuilds.get(guildId)
|
||||
result.push({
|
||||
guildId,
|
||||
guildName: guild?.name ?? null,
|
||||
guildIcon: guild?.icon ?? null,
|
||||
settings: { enabled: shared.enabled, editors: shared.editors },
|
||||
playlists: list.map(p => ({
|
||||
...playlistLite(p),
|
||||
createdBy: p?.createdBy ? userLite(p.createdBy) : null
|
||||
}))
|
||||
})
|
||||
}
|
||||
return result.sort((a, b) => (a.guildName || a.guildId).localeCompare(b.guildName || b.guildId))
|
||||
})
|
||||
|
||||
// {userId, playlistId} : playlist personnelle ; {guildId, playlistId} : playlist partagée d'un serveur
|
||||
AdminRequest("/ADMIN/PLAYLISTS/DELETE", async (payload, me) => {
|
||||
const data = asObject(payload)
|
||||
const userId = requireId(data.userId)
|
||||
const playlistId = requireString(typeof data.playlistId === "number" ? String(data.playlistId) : data.playlistId, "playlistId", 1, 64)
|
||||
if (data.guildId !== undefined && data.guildId !== null) {
|
||||
const guildId = requireId(data.guildId, "guildId")
|
||||
const playlist = guildPlaylists.getPlaylist(guildId, playlistId)
|
||||
if (!playlist) throw new AdminError("NOT_FOUND", "Playlist introuvable")
|
||||
if (!guildPlaylists.deletePlaylist(guildId, playlist.playlistId)) throw new AdminError("NOT_FOUND", "Playlist introuvable")
|
||||
audit(me, "suppression de la playlist partagée", `"${playlist.title}" (${playlistId}) du serveur ${guildId}`)
|
||||
process.emit("GUILDS_UPDATE")
|
||||
process.emit("GUILD_PLAYLISTS_UPDATE", guildId)
|
||||
return { deleted: true }
|
||||
}
|
||||
const userId = requireId(data.userId)
|
||||
const list = dbData("Playlists", {})[userId]
|
||||
const playlist = Array.isArray(list) ? list.find(p => p?.playlistId != null && String(p.playlistId) === playlistId) : null
|
||||
if (!playlist) throw new AdminError("NOT_FOUND", "Playlist introuvable")
|
||||
@@ -2016,6 +2111,17 @@ function init(ioInstance, deps = {}) {
|
||||
}, 500)
|
||||
process.on("GUILDS_UPDATE", pushGuilds)
|
||||
|
||||
// Playlists modifiées (personnelles ou partagées) : la section Playlists ouverte se relit d'elle-même
|
||||
const changedScopes = new Set()
|
||||
const pushPlaylists = debounce(() => {
|
||||
const scopes = [...changedScopes]
|
||||
changedScopes.clear()
|
||||
if (roomSize(ADMIN_ROOM) === 0) return
|
||||
io.to(ADMIN_ROOM).emit("/ADMIN/PLAYLISTS/UPDATE", { scopes })
|
||||
}, 800)
|
||||
process.on("PLAYLISTS_UPDATE", () => { changedScopes.add("personal"); pushPlaylists() })
|
||||
process.on("GUILD_PLAYLISTS_UPDATE", () => { changedScopes.add("guild"); pushPlaylists() })
|
||||
|
||||
// WARP : changement d'état du proxy
|
||||
process.on("WARP_UPDATE", () => {
|
||||
if (roomSize(ADMIN_ROOM) === 0) return
|
||||
|
||||
+498
-86
@@ -17,6 +17,8 @@ const Finder = require("../player/Finder")
|
||||
|
||||
const {__glob} = require("../utils/GlobalVars")
|
||||
const playlists = require("../playlists/PlaylistManager")
|
||||
const guildPlaylists = require("../playlists/GuildPlaylists")
|
||||
const playlistUtils = require("../playlists/PlaylistUtils")
|
||||
const history = require("../playlists/History")
|
||||
const lyrics = require("../lyrics/Lyrics")
|
||||
const serverSettings = require("../discord/ServerSettings")
|
||||
@@ -92,6 +94,104 @@ function notifyGuildUpdate(guildId) {
|
||||
io.to(rooms).emit("/GUILD/UPDATE")
|
||||
}
|
||||
|
||||
// Playlists partagées : messages par défaut des refus (enveloppe {ok:false, error, message})
|
||||
const GUILD_PLAYLIST_ERRORS = {
|
||||
NO_GUILD: "Aucun serveur actif ou accès refusé à ce serveur",
|
||||
DISABLED: "Les playlists partagées sont désactivées sur ce serveur",
|
||||
FORBIDDEN: "Vous n'avez pas les droits nécessaires sur les playlists de ce serveur",
|
||||
NOT_FOUND: "Playlist introuvable",
|
||||
INVALID_PARAMS: "Paramètres invalides",
|
||||
LIMIT: "Limite atteinte",
|
||||
FAILED: "L'opération a échoué",
|
||||
}
|
||||
|
||||
function refusal(error, message) {
|
||||
return { ok: false, error, message: message || GUILD_PLAYLIST_ERRORS[error] || GUILD_PLAYLIST_ERRORS.FAILED }
|
||||
}
|
||||
|
||||
function payloadObject(data) {
|
||||
return data !== null && typeof data === "object" && !Array.isArray(data) ? data : {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifiant de playlist : valeur brute (ancien client) ou {id} / {playlistId}
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function playlistIdFrom(data) {
|
||||
if(typeof data === "string" || typeof data === "number") return playlistUtils.parsePlaylistId(data)
|
||||
const payload = payloadObject(data)
|
||||
return playlistUtils.parsePlaylistId(payload.id ?? payload.playlistId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Création de playlist {name, url?} : nom de 1 à 100 caractères sans lien ; avec un lien (2000 caractères max),
|
||||
* un nom non vide remplace le titre importé (un nom identique au lien, envoyé par l'ancien client, est ignoré)
|
||||
* @returns {{name: string|null, url: string|null}|null}
|
||||
*/
|
||||
function parseCreateInput(data) {
|
||||
const payload = payloadObject(data)
|
||||
const hasUrl = payload.url !== undefined && payload.url !== null && payload.url !== ""
|
||||
if(hasUrl) {
|
||||
const url = playlistUtils.parseUrl(payload.url)
|
||||
if(!url) return null
|
||||
let name = null
|
||||
if(typeof payload.name === "string" && payload.name.trim() !== "" && payload.name.trim() !== url) {
|
||||
name = playlistUtils.parseName(payload.name)
|
||||
if(!name) return null
|
||||
} else if(payload.name !== undefined && payload.name !== null && typeof payload.name !== "string") {
|
||||
return null
|
||||
}
|
||||
return { name, url }
|
||||
}
|
||||
const name = playlistUtils.parseName(payload.name)
|
||||
return name ? { name, url: null } : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifiant de musique (chaîne ou nombre non vide)
|
||||
*/
|
||||
function songIdFrom(value) {
|
||||
if(typeof value === "number" && Number.isFinite(value)) return String(value)
|
||||
if(typeof value === "string" && value !== "" && value.length <= 200) return value
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiques publiques d'un serveur (accueil) : nombres finis, positifs, arrondis
|
||||
*/
|
||||
function guildStatsOf(guildId) {
|
||||
const values = new Map()
|
||||
for(const metric of SafeMetric.getGuildMetrics(guildId) || []) {
|
||||
if(metric && typeof metric.name === "string") values.set(metric.name, metric.value)
|
||||
}
|
||||
const read = (name) => {
|
||||
const n = Number(SafeMetric.toSafeNumber(values.get(name + "_" + guildId), 0))
|
||||
return Number.isFinite(n) && n > 0 ? Math.round(n) : 0
|
||||
}
|
||||
return {
|
||||
musicPlayed: read("numberOfMusicPlayed"),
|
||||
secondsPlayed: read("numberOfSecondsPlayed"),
|
||||
commands: read("numberOfCommands"),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appel protégé du module de paroles : toute absence, exception ou réponse mal formée devient UNAVAILABLE
|
||||
*/
|
||||
async function callLyrics(functionName, ...args) {
|
||||
const fn = lyrics && typeof lyrics[functionName] === "function" ? lyrics[functionName] : null
|
||||
if(!fn) return { ok: false, error: "UNAVAILABLE" }
|
||||
try {
|
||||
const result = await fn(...args)
|
||||
if(result && typeof result === "object" && typeof result.ok === "boolean") return result
|
||||
wlog.warn("Réponse inattendue du module de paroles (" + functionName + ")")
|
||||
return { ok: false, error: "UNAVAILABLE" }
|
||||
} catch(e) {
|
||||
wlog.error("Erreur du module de paroles (" + functionName + ") : " + (e?.stack || e))
|
||||
return { ok: false, error: "UNAVAILABLE" }
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
wlog.step.init("server_init", "Initialisation du serveur Socket.IO")
|
||||
@@ -177,6 +277,16 @@ function init() {
|
||||
io.to("user:" + userId).emit("/PLAYLISTS/UPDATE")
|
||||
})
|
||||
|
||||
// Playlists partagées d'un serveur (contenu ou réglage modifié) : sockets présents sur ce serveur
|
||||
process.on("GUILD_PLAYLISTS_UPDATE", (guildId) => {
|
||||
if(!io || typeof guildId !== "string" || guildId === "") return
|
||||
try {
|
||||
io.to(guildId).emit("/GUILD/PLAYLISTS/UPDATE", { guildId })
|
||||
} catch(e) {
|
||||
wlog.error("Erreur lors de l'envoi de la mise à jour des playlists partagées : " + (e?.stack || e))
|
||||
}
|
||||
})
|
||||
|
||||
io.on("connection", async (socket) => {
|
||||
var socketUser;
|
||||
var disconnectHandled = false
|
||||
@@ -384,23 +494,34 @@ function init() {
|
||||
|
||||
// PLAYERS
|
||||
|
||||
IORequest("/PLAYER/LYRICS", async () => {
|
||||
if(!checkUserGuild(socketUser, actualGuildId)) return IOAnswer("/PLAYER/LYRICS", false)
|
||||
const player = await verifyPlayerAction(actualGuildId)
|
||||
if(!player) return IOAnswer("/PLAYER/LYRICS", false)
|
||||
if(!player.queue?.current) {
|
||||
wlog.warn("Le player de la guilde : " + actualGuildId + " n'a pas de musique en cours")
|
||||
IOAnswer("/PLAYER/LYRICS", false)
|
||||
return
|
||||
}
|
||||
const song = player.queue.current
|
||||
const lyricsData = await lyrics.getLyrics(song.title + " " + song.author)
|
||||
if(!lyricsData) {
|
||||
wlog.warn("Aucune lyrics trouvée pour la musique : " + song.title + " de l'artiste : " + song.author)
|
||||
IOAnswer("/PLAYER/LYRICS", false)
|
||||
return
|
||||
}
|
||||
IOAnswer("/PLAYER/LYRICS", lyricsData)
|
||||
// PAROLES (LRCLIB, repli lyrics.ovh) : réponses {ok:true, ...} ou {ok:false, error}
|
||||
// NO_MUSIC | NOT_FOUND | UNAVAILABLE | INVALID_PARAMS, par acquittement ou par l'événement du même nom
|
||||
|
||||
// Paroles de la musique en cours sur le serveur actif (jamais de création de player)
|
||||
IORequest("/PLAYER/LYRICS", async (_, ack) => {
|
||||
const reply = (value) => answer("/PLAYER/LYRICS", value, ack)
|
||||
if(!checkUserGuild(socketUser, actualGuildId)) return reply({ ok: false, error: "NO_MUSIC" })
|
||||
const song = players.AllPlayers.get(actualGuildId)?.queue?.current
|
||||
if(!song) return reply({ ok: false, error: "NO_MUSIC" })
|
||||
reply(await callLyrics("getLyricsForSong", song))
|
||||
})
|
||||
|
||||
// Recherche manuelle : {q} ou texte (200 caractères max)
|
||||
IORequest("/LYRICS/SEARCH", async (data, ack) => {
|
||||
const reply = (value) => answer("/LYRICS/SEARCH", value, ack)
|
||||
const raw = typeof data === "string" ? data : payloadObject(data).q
|
||||
const query = typeof raw === "string" ? raw.trim() : ""
|
||||
if(query === "" || query.length > 200) return reply({ ok: false, error: "INVALID_PARAMS" })
|
||||
reply(await callLyrics("searchCandidates", query))
|
||||
})
|
||||
|
||||
// Paroles d'un résultat de recherche : {id} (identifiant LRCLIB, entier positif)
|
||||
IORequest("/LYRICS/GET", async (data, ack) => {
|
||||
const reply = (value) => answer("/LYRICS/GET", value, ack)
|
||||
const raw = typeof data === "number" || typeof data === "string" ? data : payloadObject(data).id
|
||||
const id = typeof raw === "string" && /^\d{1,15}$/.test(raw.trim()) ? Number(raw.trim()) : raw
|
||||
if(!Number.isSafeInteger(id) || id <= 0) return reply({ ok: false, error: "INVALID_PARAMS" })
|
||||
reply(await callLyrics("getById", id))
|
||||
})
|
||||
|
||||
|
||||
@@ -460,6 +581,9 @@ function init() {
|
||||
isAdmin: socketUser.isAdmin(),
|
||||
}
|
||||
guildData.members = connectedMembers(actualGuildId, guild.owner)
|
||||
// Option et droits des playlists partagées (le client adapte l'interface sans requête de plus)
|
||||
const access = guildPlaylistsAccess()
|
||||
guildData.sharedPlaylists = access ? sharedPlaylistsInfo(access) : { enabled: false, editors: "members", canEdit: false, canManage: false }
|
||||
IOAnswer("/GUILD/INFO", guildData, true)
|
||||
}, true)
|
||||
|
||||
@@ -469,6 +593,12 @@ function init() {
|
||||
IOAnswer("/GUILD/LIST", guildData, true)
|
||||
}, true)
|
||||
|
||||
// Statistiques publiques du serveur actif (accueil) : tout membre ayant accès
|
||||
IORequest("/GUILD/STATS", (_, ack) => {
|
||||
if(!checkUserGuild(socketUser, actualGuildId)) return answer("/GUILD/STATS", { ok: false, error: "NO_GUILD" }, ack, true)
|
||||
answer("/GUILD/STATS", { ok: true, ...guildStatsOf(actualGuildId) }, ack, true)
|
||||
}, true)
|
||||
|
||||
// CHECKED : 03/05/2025
|
||||
IORequest("/PLAYER/STATE", async () => {
|
||||
const player = await verifyPlayerAction(actualGuildId)
|
||||
@@ -632,11 +762,33 @@ function init() {
|
||||
IOAnswer("/SEARCH/PLAYLIST", true)
|
||||
})
|
||||
|
||||
IORequest("/SEARCH/LYRICS", async (name) => {
|
||||
if(!name) return IOAnswer("/SEARCH/LYRICS", false)
|
||||
const lyricsData = await lyrics.getLyrics(name)
|
||||
if(!lyricsData) return IOAnswer("/SEARCH/LYRICS", false)
|
||||
IOAnswer("/SEARCH/LYRICS", lyricsData)
|
||||
// Ancienne route : texte libre ou {track, artist?, duration?} → paroles du meilleur résultat
|
||||
IORequest("/SEARCH/LYRICS", async (data, ack) => {
|
||||
const reply = (value) => answer("/SEARCH/LYRICS", value, ack)
|
||||
const invalid = { ok: false, error: "INVALID_PARAMS" }
|
||||
let query
|
||||
if(typeof data === "string") {
|
||||
const text = data.trim()
|
||||
if(text === "" || text.length > 200) return reply(invalid)
|
||||
query = text
|
||||
} else {
|
||||
const payload = payloadObject(data)
|
||||
const title = typeof payload.track === "string" ? payload.track.trim() : ""
|
||||
const author = payload.artist === undefined || payload.artist === null ? "" : payload.artist
|
||||
if(title === "" || title.length > 200 || typeof author !== "string" || author.length > 200) return reply(invalid)
|
||||
let duration = 0
|
||||
if(payload.duration !== undefined && payload.duration !== null) {
|
||||
duration = Number(payload.duration)
|
||||
if(!Number.isFinite(duration) || duration < 0 || duration > 86400) return reply(invalid)
|
||||
}
|
||||
query = { track: title, artist: author.trim(), duration }
|
||||
}
|
||||
// findLyrics : texte libre (« artiste titre ») ou {track, artist, duration}
|
||||
if(typeof lyrics?.findLyrics === "function") return reply(await callLyrics("findLyrics", query))
|
||||
const song = typeof query === "string"
|
||||
? { title: query, author: "", duration: 0 }
|
||||
: { title: query.track, author: query.artist, duration: query.duration }
|
||||
reply(await callLyrics("getLyricsForSong", song))
|
||||
})
|
||||
|
||||
// UPLOAD
|
||||
@@ -734,104 +886,291 @@ function init() {
|
||||
IOAnswer("/CHANNEL", channel, true)
|
||||
}, true)
|
||||
|
||||
// PLAYLISTS PERSONNELLES
|
||||
// Réponses inchangées (playlist | false | true) mais par acquittement si le client en fournit un
|
||||
// (IORequestAck), sinon par l'événement du même nom. Chaque modification est poussée à toutes les
|
||||
// sessions de l'utilisateur (/PLAYLISTS/UPDATE) pour que ses autres appareils se mettent à jour.
|
||||
|
||||
// CHECKED : 30/04/2025
|
||||
IORequest("/PLAYLISTS/CREATE", async (data) => {
|
||||
if(!data) return IOAnswer("/PLAYLISTS/CREATE", false)
|
||||
const {name, url} = data
|
||||
if(!name || typeof name !== "string") return IOAnswer("/PLAYLISTS/CREATE", false)
|
||||
const playlist = await playlists.addPlaylist(socketUser.identity.id, name, url, socketUser.identity.username, socketUser.identity.id, socketUser.identity.avatar)
|
||||
if(!playlist) return IOAnswer("/PLAYLISTS/CREATE", false)
|
||||
IOAnswer("/PLAYLISTS/CREATE", playlist)
|
||||
IORequest("/PLAYLISTS/CREATE", async (data, ack) => {
|
||||
const reply = (value) => answer("/PLAYLISTS/CREATE", value, ack)
|
||||
const input = parseCreateInput(data)
|
||||
if(!input) return reply(false)
|
||||
// Identité figée avant l'import (socketUser est relu par chaque nouvelle requête)
|
||||
const identity = socketUser.identity
|
||||
const playlist = await playlists.addPlaylist(identity.id, input.name, input.url, identity.username, identity.id, identity.avatar)
|
||||
if(!playlist) return reply(false)
|
||||
personalPlaylistsChanged(identity.id)
|
||||
reply(playlist)
|
||||
})
|
||||
|
||||
// CHECKED : 30/04/2025
|
||||
IORequest("/PLAYLISTS/DELETE", (data) => {
|
||||
if(!data) return IOAnswer("/PLAYLISTS/DELETE", false)
|
||||
IOAnswer("/PLAYLISTS/DELETE", playlists.removePlaylist(socketUser.identity.id, data))
|
||||
IORequest("/PLAYLISTS/DELETE", (data, ack) => {
|
||||
const id = playlistIdFrom(data)
|
||||
if(!id) return answer("/PLAYLISTS/DELETE", false, ack)
|
||||
const userId = socketUser.identity.id
|
||||
const removed = playlists.removePlaylist(userId, id)
|
||||
if(removed) personalPlaylistsChanged(userId)
|
||||
answer("/PLAYLISTS/DELETE", removed, ack)
|
||||
})
|
||||
|
||||
// CHECKED : 24/04/2025
|
||||
IORequest("/PLAYLISTS/LIST", () => {
|
||||
const playlist = playlists.getPlaylistsOfUser(socketUser.identity.id)
|
||||
IOAnswer("/PLAYLISTS/LIST", playlist)
|
||||
IORequest("/PLAYLISTS/LIST", (_, ack) => {
|
||||
answer("/PLAYLISTS/LIST", playlists.getPlaylistsOfUser(socketUser.identity.id), ack)
|
||||
})
|
||||
|
||||
// Envoie une copie d'une playlist à un utilisateur partageant au moins un serveur
|
||||
// {playlistId (ou name, ancien nom du champ), toId}
|
||||
IORequest("/PLAYLISTS/SEND", (data) => {
|
||||
if(!data) return IOAnswer("/PLAYLISTS/SEND", false)
|
||||
const toId = data.toId
|
||||
const playlistId = data.playlistId ?? data.id ?? data.name
|
||||
if(!playlistId || !toId || typeof toId !== "string") return IOAnswer("/PLAYLISTS/SEND", false)
|
||||
IORequest("/PLAYLISTS/SEND", (data, ack) => {
|
||||
const reply = (value) => answer("/PLAYLISTS/SEND", value, ack)
|
||||
const payload = payloadObject(data)
|
||||
const toId = payload.toId
|
||||
const playlistId = playlistUtils.parsePlaylistId(payload.playlistId ?? payload.id ?? payload.name)
|
||||
if(!playlistId || !toId || typeof toId !== "string") return reply(false)
|
||||
const fromId = socketUser.identity.id
|
||||
if(toId === fromId) return IOAnswer("/PLAYLISTS/SEND", false)
|
||||
if(toId === fromId) return reply(false)
|
||||
const toUser = users.getUserById(toId)
|
||||
if(!toUser || toUser.isDeleted()) return IOAnswer("/PLAYLISTS/SEND", false)
|
||||
if(!toUser || toUser.isDeleted()) return reply(false)
|
||||
const shareGuild = [...discordBot.getGuilds().values()].some(g => g.allMembers?.includes(fromId) && g.allMembers?.includes(toId))
|
||||
if(!shareGuild) return IOAnswer("/PLAYLISTS/SEND", false)
|
||||
if(!shareGuild) return reply(false)
|
||||
// copyPlaylist refuse un doublon de titre chez le destinataire
|
||||
const copy = playlists.copyPlaylist(fromId, toId, playlistId)
|
||||
if(copy) process.emit("PLAYLISTS_UPDATE", toId)
|
||||
IOAnswer("/PLAYLISTS/SEND", !!copy)
|
||||
reply(!!copy)
|
||||
})
|
||||
|
||||
// CHECKED : 30/04/2025
|
||||
IORequest("/PLAYLISTS/RENAME", (data) => {
|
||||
if(!data) return IOAnswer("/PLAYLISTS/RENAME", false)
|
||||
const {id, newName} = data
|
||||
if(!id || !newName || typeof newName !== "string" || newName.trim() === "" || newName.length > 200) return IOAnswer("/PLAYLISTS/RENAME", false)
|
||||
var playlist = playlists.getPlaylist(socketUser.identity.id, id)
|
||||
if(!playlist) return IOAnswer("/PLAYLISTS/RENAME", false)
|
||||
playlists.renamePlaylist(socketUser.identity.id, id, newName.trim())
|
||||
playlist = playlists.getPlaylist(socketUser.identity.id, id)
|
||||
IOAnswer("/PLAYLISTS/RENAME", playlist)
|
||||
IORequest("/PLAYLISTS/RENAME", (data, ack) => {
|
||||
const reply = (value) => answer("/PLAYLISTS/RENAME", value, ack)
|
||||
const payload = payloadObject(data)
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
const newName = playlistUtils.parseName(payload.newName, 200)
|
||||
if(!id || !newName) return reply(false)
|
||||
const userId = socketUser.identity.id
|
||||
const playlist = playlists.renamePlaylist(userId, id, newName)
|
||||
if(!playlist) return reply(false)
|
||||
personalPlaylistsChanged(userId)
|
||||
reply(playlist)
|
||||
})
|
||||
|
||||
// CHECKED : 30/04/2025
|
||||
IORequest("/PLAYLISTS/ADD_SONG", (data) => {
|
||||
if(!data) return IOAnswer("/PLAYLISTS/ADD_SONG", false)
|
||||
const {id, song} = data
|
||||
if(!id || !song) return IOAnswer("/PLAYLISTS/ADD_SONG", false)
|
||||
var playlist = playlists.getPlaylistOfUser(socketUser.identity.id, id)
|
||||
if(!playlist) return IOAnswer("/PLAYLISTS/ADD_SONG", false)
|
||||
playlists.addSong(socketUser.identity.id, id, song)
|
||||
playlist = playlists.getPlaylistOfUser(socketUser.identity.id, id)
|
||||
IOAnswer("/PLAYLISTS/ADD_SONG", playlist)
|
||||
IORequest("/PLAYLISTS/ADD_SONG", (data, ack) => {
|
||||
const reply = (value) => answer("/PLAYLISTS/ADD_SONG", value, ack)
|
||||
const payload = payloadObject(data)
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
if(!id || !payload.song) return reply(false)
|
||||
const userId = socketUser.identity.id
|
||||
// null : playlist inconnue, musique invalide ou limite de titres atteinte
|
||||
const playlist = playlists.addSong(userId, id, payload.song)
|
||||
if(!playlist) return reply(false)
|
||||
personalPlaylistsChanged(userId)
|
||||
reply(playlist)
|
||||
})
|
||||
|
||||
// CHECKED : 30/04/2025
|
||||
IORequest("/PLAYLISTS/REMOVE_SONG", (data) => {
|
||||
if(!data) return IOAnswer("/PLAYLISTS/REMOVE_SONG", false)
|
||||
const {id, songId} = data
|
||||
if(!id || !songId) return IOAnswer("/PLAYLISTS/REMOVE_SONG", false)
|
||||
var playlist = playlists.getPlaylistOfUser(socketUser.identity.id, id)
|
||||
if(!playlist) return IOAnswer("/PLAYLISTS/REMOVE_SONG", false)
|
||||
// {id, songId, index?} : index désigne l'occurrence exacte quand la musique est en double
|
||||
IORequest("/PLAYLISTS/REMOVE_SONG", (data, ack) => {
|
||||
const reply = (value) => answer("/PLAYLISTS/REMOVE_SONG", value, ack)
|
||||
const payload = payloadObject(data)
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
const songId = songIdFrom(payload.songId)
|
||||
if(!id || !songId) return reply(false)
|
||||
const index = playlistUtils.parseIndex(payload.index)
|
||||
const userId = socketUser.identity.id
|
||||
const playlist = playlists.removeSong(userId, id, songId, index === null ? undefined : index)
|
||||
if(!playlist) return reply(false)
|
||||
personalPlaylistsChanged(userId)
|
||||
reply(playlist)
|
||||
})
|
||||
|
||||
playlists.removeSong(socketUser.identity.id, id, songId)
|
||||
playlist = playlists.getPlaylistOfUser(socketUser.identity.id, id)
|
||||
// Déplace une musique dans une playlist : {id, from, to}
|
||||
IORequest("/PLAYLISTS/MOVE_SONG", (data, ack) => {
|
||||
const reply = (value) => answer("/PLAYLISTS/MOVE_SONG", value, ack)
|
||||
const payload = payloadObject(data)
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
const from = playlistUtils.parseIndex(payload.from)
|
||||
const to = playlistUtils.parseIndex(payload.to)
|
||||
if(!id || from === null || to === null) return reply(false)
|
||||
const userId = socketUser.identity.id
|
||||
const playlist = playlists.moveSong(userId, id, from, to)
|
||||
if(!playlist) return reply(false)
|
||||
if(from !== to) personalPlaylistsChanged(userId)
|
||||
reply(playlist)
|
||||
})
|
||||
|
||||
IOAnswer("/PLAYLISTS/REMOVE_SONG", playlist)
|
||||
// Ordre d'affichage des playlists : {order: [playlistId...]} (identifiants absents conservés à la suite)
|
||||
IORequest("/PLAYLISTS/REORDER", (data, ack) => {
|
||||
const order = payloadObject(data).order
|
||||
if(!Array.isArray(order)) return answer("/PLAYLISTS/REORDER", false, ack)
|
||||
const userId = socketUser.identity.id
|
||||
const done = playlists.reorderPlaylists(userId, order)
|
||||
if(done) personalPlaylistsChanged(userId)
|
||||
answer("/PLAYLISTS/REORDER", done, ack)
|
||||
})
|
||||
|
||||
// CHECKED : 05/05/2025
|
||||
IORequest("/PLAYLISTS/PLAY", async (data) => {
|
||||
if(!data) return IOAnswer("/PLAYLISTS/PLAY", false)
|
||||
const {name, now} = data
|
||||
if(!name) return IOAnswer("/PLAYLISTS/PLAY", false)
|
||||
if(!checkUserGuild(socketUser, actualGuildId)) return IOAnswer("/PLAYLISTS/PLAY", false)
|
||||
const playlist = playlists.getPlaylistOfUser(socketUser.identity.id, name)
|
||||
if(!playlist) return IOAnswer("/PLAYLISTS/PLAY", false)
|
||||
// {id, now} (ou {name, now} : ancien nom du champ, qui contenait déjà l'identifiant)
|
||||
IORequest("/PLAYLISTS/PLAY", async (data, ack) => {
|
||||
const reply = (value) => answer("/PLAYLISTS/PLAY", value, ack)
|
||||
const payload = payloadObject(data)
|
||||
const id = playlistUtils.parsePlaylistId(payload.id ?? payload.name)
|
||||
if(!id) return reply(false)
|
||||
if(!checkUserGuild(socketUser, actualGuildId)) return reply(false)
|
||||
const playlist = playlists.getPlaylistOfUser(socketUser.identity.id, id)
|
||||
if(!playlist) return reply(false)
|
||||
const player = new Player(actualGuildId)
|
||||
if(!connectToPlayer(actualGuildId, player)) return IOAnswer("/PLAYLISTS/PLAY", false)
|
||||
player.readPlaylist(playlist, now)
|
||||
IOAnswer("/PLAYLISTS/PLAY", true)
|
||||
if(!connectToPlayer(actualGuildId, player)) return reply(false)
|
||||
player.readPlaylist(playlist, !!payload.now)
|
||||
reply(true)
|
||||
})
|
||||
|
||||
IORequest("/PLAYLISTS/REFRESH", async (data) => {
|
||||
if(!data) return IOAnswer("/PLAYLISTS/REFRESH", false)
|
||||
const playlist = await playlists.refreshPlaylist(socketUser.identity.id, data)
|
||||
if(!playlist) return IOAnswer("/PLAYLISTS/REFRESH", false)
|
||||
IOAnswer("/PLAYLISTS/REFRESH", playlist)
|
||||
// Identifiant brut ou {id}
|
||||
IORequest("/PLAYLISTS/REFRESH", async (data, ack) => {
|
||||
const reply = (value) => answer("/PLAYLISTS/REFRESH", value, ack)
|
||||
const id = playlistIdFrom(data)
|
||||
if(!id) return reply(false)
|
||||
const userId = socketUser.identity.id
|
||||
const playlist = await playlists.refreshPlaylist(userId, id)
|
||||
if(!playlist) return reply(false)
|
||||
personalPlaylistsChanged(userId)
|
||||
reply(playlist)
|
||||
})
|
||||
|
||||
// PLAYLISTS PARTAGÉES DU SERVEUR ACTIF
|
||||
// Toujours le serveur actif (actualGuildId, jamais un identifiant du client) et checkUserGuild d'abord.
|
||||
// Réponses {ok:true, ...} ou {ok:false, error, message} ; error : NO_GUILD, DISABLED, FORBIDDEN,
|
||||
// NOT_FOUND, INVALID_PARAMS, LIMIT, FAILED. Chaque modification pousse /GUILD/PLAYLISTS/UPDATE {guildId}.
|
||||
|
||||
IORequest("/GUILD/PLAYLISTS/LIST", (_, ack) => {
|
||||
const access = guildPlaylistsAccess()
|
||||
if(!access) return answer("/GUILD/PLAYLISTS/LIST", refusal("NO_GUILD"), ack, true)
|
||||
answer("/GUILD/PLAYLISTS/LIST", {
|
||||
ok: true,
|
||||
guildId: access.guildId,
|
||||
settings: { ...access.settings },
|
||||
canEdit: access.canEdit,
|
||||
canManage: access.canManage,
|
||||
// Désactivées : masquées (jamais supprimées), réapparaissent à la réactivation
|
||||
playlists: access.settings.enabled ? guildPlaylists.getPlaylists(access.guildId) : [],
|
||||
}, ack, true)
|
||||
}, true)
|
||||
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/CREATE", async (payload, access) => {
|
||||
const input = parseCreateInput(payload)
|
||||
if(!input) return refusal("INVALID_PARAMS", "Nom (1 à 100 caractères) ou lien de playlist invalide")
|
||||
return guildResult(await guildPlaylists.createPlaylist(access.guildId, input, access.user), access.guildId)
|
||||
}, { edit: true })
|
||||
|
||||
// Suppression : créateur de la playlist, modérateur, propriétaire du serveur ou administrateur
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/DELETE", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
if(!id) return refusal("INVALID_PARAMS")
|
||||
const playlist = guildPlaylists.getPlaylist(access.guildId, id)
|
||||
if(!playlist) return refusal("NOT_FOUND")
|
||||
const isCreator = !!playlist.createdBy?.id && playlist.createdBy.id === access.userId
|
||||
if(!isCreator && !access.isModerator) return refusal("FORBIDDEN", "Seuls le créateur de la playlist et les modérateurs peuvent la supprimer")
|
||||
if(!guildPlaylists.deletePlaylist(access.guildId, id)) return refusal("NOT_FOUND")
|
||||
guildPlaylistsChanged(access.guildId)
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/RENAME", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
const newName = playlistUtils.parseName(payload.newName)
|
||||
if(!id || !newName) return refusal("INVALID_PARAMS", "Nom invalide (1 à 100 caractères)")
|
||||
return guildResult(guildPlaylists.renamePlaylist(access.guildId, id, newName, access.user), access.guildId)
|
||||
}, { edit: true })
|
||||
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/ADD_SONG", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
if(!id || !payload.song) return refusal("INVALID_PARAMS")
|
||||
return guildResult(guildPlaylists.addSong(access.guildId, id, payload.song, access.user), access.guildId)
|
||||
}, { edit: true })
|
||||
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/REMOVE_SONG", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
const songId = songIdFrom(payload.songId)
|
||||
if(!id || !songId) return refusal("INVALID_PARAMS")
|
||||
const index = playlistUtils.parseIndex(payload.index)
|
||||
return guildResult(guildPlaylists.removeSong(access.guildId, id, songId, index === null ? undefined : index, access.user), access.guildId)
|
||||
}, { edit: true })
|
||||
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/MOVE_SONG", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
const from = playlistUtils.parseIndex(payload.from)
|
||||
const to = playlistUtils.parseIndex(payload.to)
|
||||
if(!id || from === null || to === null) return refusal("INVALID_PARAMS")
|
||||
const result = guildPlaylists.moveSong(access.guildId, id, from, to, access.user)
|
||||
// Déplacement sur place : aucune poussée inutile
|
||||
if(result?.ok && from === to) return { ok: true, playlist: result.playlist }
|
||||
return guildResult(result, access.guildId)
|
||||
}, { edit: true })
|
||||
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/REORDER", async (payload, access) => {
|
||||
if(!Array.isArray(payload.order)) return refusal("INVALID_PARAMS")
|
||||
if(!guildPlaylists.reorder(access.guildId, payload.order)) return refusal("INVALID_PARAMS")
|
||||
guildPlaylistsChanged(access.guildId)
|
||||
return { ok: true }
|
||||
}, { edit: true })
|
||||
|
||||
// Playlists importées depuis un lien uniquement
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/REFRESH", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
if(!id) return refusal("INVALID_PARAMS")
|
||||
return guildResult(await guildPlaylists.refreshPlaylist(access.guildId, id, access.user), access.guildId)
|
||||
}, { edit: true })
|
||||
|
||||
// Lecture : tout membre ayant accès (même logique de connexion que /PLAYLISTS/PLAY)
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/PLAY", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
if(!id) return refusal("INVALID_PARAMS")
|
||||
const playlist = guildPlaylists.getPlaylist(access.guildId, id)
|
||||
if(!playlist) return refusal("NOT_FOUND")
|
||||
const player = new Player(access.guildId)
|
||||
if(!connectToPlayer(access.guildId, player)) return refusal("FAILED", "Rejoignez d'abord un salon vocal de ce serveur")
|
||||
player.readPlaylist(playlist, !!payload.now)
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
// Partage sur le serveur d'une de MES playlists (copie indépendante) : {playlistId}
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/IMPORT", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.playlistId ?? payload.id)
|
||||
if(!id) return refusal("INVALID_PARAMS")
|
||||
const personal = playlists.getPlaylistOfUser(access.userId, id)
|
||||
if(!personal) return refusal("NOT_FOUND")
|
||||
return guildResult(guildPlaylists.importPlaylist(access.guildId, personal, access.user), access.guildId)
|
||||
}, { edit: true })
|
||||
|
||||
// Copie d'une playlist du serveur dans MES playlists : {id}
|
||||
GuildPlaylistRequest("/GUILD/PLAYLISTS/COPY", async (payload, access) => {
|
||||
const id = playlistUtils.parsePlaylistId(payload.id)
|
||||
if(!id) return refusal("INVALID_PARAMS")
|
||||
const playlist = guildPlaylists.getPlaylist(access.guildId, id)
|
||||
if(!playlist) return refusal("NOT_FOUND")
|
||||
if(!playlists.hasRoomForPlaylist(access.userId)) return refusal("LIMIT", "Vous avez atteint la limite de " + playlists.MAX_PLAYLISTS + " playlists : supprimez-en une avant d'en copier une autre")
|
||||
const copy = playlists.addPlaylistCopy(access.userId, playlist)
|
||||
if(!copy) return refusal("FAILED", "La copie de la playlist a échoué")
|
||||
personalPlaylistsChanged(access.userId)
|
||||
return { ok: true, playlist: copy }
|
||||
})
|
||||
|
||||
// Réglage du serveur actif : {enabled?, editors?} (propriétaire ou administrateur du bot)
|
||||
IORequest("/OWNER/PLAYLISTS/SETTINGS", (data, ack) => {
|
||||
const reply = (value) => answer("/OWNER/PLAYLISTS/SETTINGS", value, ack)
|
||||
if(!checkUserGuild(socketUser, actualGuildId)) return reply(refusal("NO_GUILD"))
|
||||
const guild = discordBot.getGuilds().get(actualGuildId)
|
||||
if(!isGuildOwnerOrAdmin(guild)) return reply(refusal("FORBIDDEN", "Réservé au propriétaire du serveur"))
|
||||
const payload = payloadObject(data)
|
||||
const patch = {}
|
||||
if(payload.enabled !== undefined) patch.enabled = payload.enabled
|
||||
if(payload.editors !== undefined) patch.editors = payload.editors
|
||||
const settings = serverSettings.setSharedPlaylists(actualGuildId, patch)
|
||||
if(!settings) return reply(refusal("INVALID_PARAMS", "Réglage invalide (enabled : booléen, editors : \"members\" ou \"mods\")"))
|
||||
wlog.log("Playlists partagées de la guilde " + actualGuildId + " modifiées par '" + socketUser.identity.username + "'")
|
||||
guildPlaylistsChanged(actualGuildId)
|
||||
// Liste des serveurs de l'administration (option affichée)
|
||||
process.emit("GUILDS_UPDATE")
|
||||
reply({ ok: true, settings })
|
||||
})
|
||||
|
||||
|
||||
@@ -900,6 +1239,79 @@ function init() {
|
||||
return socketUser.isAdmin()
|
||||
}
|
||||
|
||||
/**
|
||||
* Droits de l'utilisateur sur les playlists partagées du serveur actif (null : pas d'accès au serveur)
|
||||
* - voir / lire : option activée ; modifier : activée et (tous les membres ou modérateur / propriétaire / admin)
|
||||
* - gérer l'option : propriétaire ou administrateur du bot
|
||||
*/
|
||||
function guildPlaylistsAccess() {
|
||||
if(!checkUserGuild(socketUser, actualGuildId)) return null
|
||||
const guild = discordBot.getGuilds().get(actualGuildId)
|
||||
if(!guild) return null
|
||||
const settings = serverSettings.getSharedPlaylists(actualGuildId)
|
||||
const userId = socketUser.identity.id
|
||||
const isOwner = userId === guild.owner
|
||||
const isAdmin = socketUser.isAdmin()
|
||||
const isModerator = isOwner || isAdmin || socketUser.isMod(actualGuildId)
|
||||
return {
|
||||
guildId: actualGuildId,
|
||||
guild,
|
||||
settings,
|
||||
userId,
|
||||
// Identité figée au moment de la requête (createdBy / updatedBy)
|
||||
user: { ...socketUser.identity },
|
||||
isModerator,
|
||||
canEdit: settings.enabled && (settings.editors === "members" || isModerator),
|
||||
canManage: isOwner || isAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
function sharedPlaylistsInfo(access) {
|
||||
return {
|
||||
enabled: access.settings.enabled,
|
||||
editors: access.settings.editors,
|
||||
canEdit: access.canEdit,
|
||||
canManage: access.canManage,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Route des playlists partagées : accès au serveur, option activée et, si edit, droit de modification
|
||||
* vérifiés avant le handler ; réponse unique (acquittement ou événement), exception → FAILED
|
||||
*/
|
||||
function GuildPlaylistRequest(name, handler, { edit = false } = {}) {
|
||||
IORequest(name, async (data, ack) => {
|
||||
const reply = (value) => answer(name, value, ack)
|
||||
try {
|
||||
const access = guildPlaylistsAccess()
|
||||
if(!access) return reply(refusal("NO_GUILD"))
|
||||
if(!access.settings.enabled) return reply(refusal("DISABLED"))
|
||||
if(edit && !access.canEdit) return reply(refusal("FORBIDDEN", "Seuls les modérateurs peuvent modifier les playlists de ce serveur"))
|
||||
reply(await handler(payloadObject(data), access))
|
||||
} catch(e) {
|
||||
wlog.error("Erreur lors du traitement de " + name + " pour '" + socketUser?.identity?.username + "' : " + (e?.stack || e))
|
||||
reply(refusal("FAILED"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function guildPlaylistsChanged(guildId) {
|
||||
process.emit("GUILD_PLAYLISTS_UPDATE", guildId)
|
||||
}
|
||||
|
||||
function personalPlaylistsChanged(userId) {
|
||||
process.emit("PLAYLISTS_UPDATE", userId)
|
||||
}
|
||||
|
||||
// Résultat de GuildPlaylists → enveloppe de réponse (poussée aux membres présents en cas de succès)
|
||||
function guildResult(result, guildId) {
|
||||
if(result?.ok) {
|
||||
guildPlaylistsChanged(guildId)
|
||||
return { ok: true, playlist: result.playlist }
|
||||
}
|
||||
return refusal(result?.error || "FAILED", result?.message)
|
||||
}
|
||||
|
||||
// CHECKED : 24/04/2025
|
||||
IORequest("/MOD/USERS/BAN", async (userId) => {
|
||||
if(!userId || typeof userId !== "string" || !actualGuildId) return IOAnswer("/MOD/USERS/BAN", false)
|
||||
|
||||
Reference in new issue
Block a user