Liens Discord expirés sur FiveM : passez vos images sur un CDN
Mugshots MDT et preuves en liens Discord affichent « This content is no longer available. » Pourquoi ils expirent, comment les repérer et les corriger.
Par Termi
- Discord
- Uploads
- MySQL
- MDT
Sur cette page
- Pourquoi les liens Discord cessent de fonctionner
- Combien de temps dure un lien
- Ce que veut dire « This content is no longer available. »
- Retrouver les liens morts dans votre base
- Envoyer vers un CDN à la place
- Récupérer les anciennes images
- Questions fréquentes
- Pourquoi les liens d’images Discord cessent-ils de fonctionner ?
- Que signifient ex, is et hm dans un lien Discord ?
- Combien de temps durent les liens de pièces jointes Discord ?
- Puis-je rendre permanent un lien de pièce jointe Discord ?
- Un webhook Discord est-il un bon endroit pour stocker les captures FiveM ?
Beaucoup de serveurs FiveM envoient des captures d’écran, des mugshots MDT et des photos de preuves vers un webhook Discord, et enregistrent en base le lien de pièce jointe que Discord renvoie. Ça marche pendant une journée. Puis le MDT affiche des images cassées, et ouvrir l’un des liens n’affiche qu’une ligne : This content is no longer available. Les fichiers sont généralement toujours dans le salon. Ce sont les liens de votre base qui ont expiré. Ce guide explique pourquoi, retrouve les liens morts et déplace les uploads vers un CDN dont les liens ne bougent pas.
Pourquoi les liens Discord cessent de fonctionner
La référence de l’API de Discord indique : « attachments uploaded to Discord’s CDN (like user and bot-uploaded images) have signed URLs with a preset expiry time. » Discord a annoncé le changement aux développeurs fin 2023 et l’a documenté en février 2024. Chaque lien de pièce jointe porte depuis trois paramètres d’URL :
https://cdn.discordapp.com/attachments/<channel id>/<attachment id>/screenshot.png
?ex=66424c96
&is=6640fb16
&hm=0b3d3210b4ea…287dace4&Dans Discord, rien ne semble anormal, car « Discord automatically refreshes attachment CDN URLs that appear within the client. » L’image du salon continue de se charger. Le lien que votre script a enregistré dans MySQL est une copie, et personne ne la renouvelle. Une fois son heure ex passée, le CDN le refuse.
Combien de temps dure un lien
Discord ne documente pas de durée fixe : lisez-la dans le lien. Les deux valeurs sont des timestamps Unix en hexadécimal : 6640fb16 correspond au 12 mai 2024 à 17:23:34 UTC et 66424c96 tombe exactement 24 heures plus tard. C’est le lien réel que nous avons testé plus bas, mais Discord peut émettre des liens pour d’autres durées : vérifiez ex sur vos propres lignes plutôt que de supposer une journée.
Ce que veut dire « This content is no longer available. »
Ouvrez un lien expiré en dehors de Discord et le CDN répond par un 404 en texte brut :
$ curl -i "https://cdn.discordapp.com/attachments/…/screenshot.png?ex=66424c96&is=6640fb16&hm=…"
HTTP/2 404
content-type: text/plain;charset=UTF-8
This content is no longer available.Les liens enregistrés avant que Discord ne commence à les signer, ceux sans aucun ex, is ni hm, reçoivent la même réponse, sur cdn.discordapp.com comme sur media.discordapp.net. Dans un MDT ou toute autre page NUI, cela ressemble à une image cassée, sans rien dans la console du serveur pour l’expliquer.
Retrouver les liens morts dans votre base
Tout lien de pièce jointe Discord contient discordapp. et /attachments/. Cette requête écrit une requête de comptage pour chaque colonne texte de la base que votre serveur utilise (celle de mysql_connection_string) :
SET SESSION group_concat_max_len = 1000000;
SELECT GROUP_CONCAT(
CONCAT(
'SELECT ''', table_name, '.', column_name, ''' AS place, COUNT(*) AS links FROM `',
table_name, '` WHERE `', column_name, '` LIKE ''%discordapp.%/attachments/%'''
)
SEPARATOR ' UNION ALL '
) AS query
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND data_type IN ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'json');Elle renvoie une seule valeur : une autre requête. Copiez-la et exécutez-la. Chaque ligne du résultat nomme une table.column et le nombre de liens Discord qu’elle contient. Tout ce qui dépasse zéro est une colonne que vos scripts ont remplie via un webhook.
Pour voir quelles lignes d’une colonne ont déjà expiré, décodez ex. Ici pour la table mdt_mugshots du guide des mugshots :
SELECT identifier, image,
IF(image LIKE '%?ex=%',
FROM_UNIXTIME(CONV(SUBSTRING_INDEX(SUBSTRING_INDEX(image, '?ex=', -1), '&', 1), 16, 10)),
NULL) AS expires
FROM mdt_mugshots
WHERE image LIKE '%discordapp.%/attachments/%';Une ligne dont expires est dans le passé ne se charge plus en dehors de Discord, pas plus qu’une ligne à NULL, qui signifie que le lien n’a jamais été signé.
Envoyer vers un CDN à la place
La solution : envoyer le fichier vers un stockage conçu pour le servir, garder ce lien en base, et continuer de poster sur Discord si le staff veut voir l’image dans un salon, avec le lien du CDN dans l’embed. Voici une capture de preuve prise avec screenshot-basic, envoyée à l’endpoint base64 de FiveDock, enregistrée, puis postée sur un webhook :
local UPLOAD_URL = 'https://api.fivedock.dev/api/v3/file/base64'
local API_TOKEN = GetConvar('fivedock_api_token', '')
local WEBHOOK = GetConvar('evidence_webhook', '')
local function postEvidence(playerId, caseId)
exports['screenshot-basic']:requestClientScreenshot(playerId, { encoding = 'webp', quality = 0.85 }, function(err, data)
if err then
return print(('Screenshot failed: %s'):format(err))
end
local key = ('evidence/%s/%d.webp'):format((tostring(caseId):gsub('[^%w_%-]', '_')), os.time())
PerformHttpRequest(UPLOAD_URL, function(status, body)
if status ~= 200 and status ~= 201 then
return print(('Evidence upload failed (%s): %s'):format(status, tostring(body)))
end
local response = json.decode(body)
local url = response and response.data and response.data.url
if not url then return end
MySQL.insert('INSERT INTO `mdt_evidence` (`case_id`, `image`) VALUES (?, ?)', { caseId, url })
PerformHttpRequest(WEBHOOK, function() end, 'POST', json.encode({
embeds = { { title = ('Case %s'):format(caseId), image = { url = url } } },
}), { ['Content-Type'] = 'application/json' })
end, 'POST', json.encode({ base64 = data, filename = 'evidence.webp', key = key }), {
['Content-Type'] = 'application/json',
['Authorization'] = API_TOKEN,
})
end)
endLa requête a la même forme que l’upload base64 de Fivemanage : un script écrit pour Fivemanage n’a besoin que d’une nouvelle URL et d’un nouveau token. Envoyez des fichiers vous-même couvre le panel, les liens à clé et l’endpoint S3.
Si vous utilisez l’agent FiveDock, exports.fivedock:screenshot prend la capture et l’envoie en un seul appel, sans token dans votre script. Voir la doc de l’agent.
Récupérer les anciennes images
Les liens de votre base sont morts, mais les fichiers sont généralement toujours dans le salon Discord, et Discord fournit un lien neuf chaque fois qu’une application récupère à nouveau le message. Un bot Discord qui peut lire le salon peut parcourir l’historique par pages de 100 messages et faire correspondre chaque pièce jointe à vos lignes : dans /attachments/<channel id>/<attachment id>/, le second nombre est l’ID de la pièce jointe. Téléchargez chaque fichier via son lien neuf, envoyez-le à FiveDock avec la clé que votre script utilise désormais, et mettez la ligne à jour.
Un webhook ne peut pas lister un salon : il faut donc un bot. Les fichiers dont le message a été supprimé, ou dont le salon n’existe plus, sont irrécupérables.
Questions fréquentes
Pourquoi les liens d’images Discord cessent-ils de fonctionner ?
Discord signe chaque lien de pièce jointe avec une date d’expiration et l’a documenté dans sa référence d’API en février 2024. L’application Discord renouvelle les liens qu’elle affiche, donc l’image se charge toujours dans le salon, mais un lien enregistré en base n’est jamais renouvelé. Une fois expiré, le CDN répond 404 avec le texte This content is no longer available.
Que signifient ex, is et hm dans un lien Discord ?
ex est l’heure d’expiration du lien et is l’heure de son émission, tous deux en timestamps Unix écrits en hexadécimal. hm est une signature valable jusqu’à ex. Dans MySQL, FROM_UNIXTIME(CONV('66424c96', 16, 10)) transforme une valeur ex en date.
Combien de temps durent les liens de pièces jointes Discord ?
Discord ne documente pas de durée fixe : chaque lien porte sa propre expiration dans le paramètre ex. Le lien réel que nous avons testé pour ce guide avait été émis pour exactement 24 heures. Considérez tout lien de pièce jointe Discord enregistré en base comme temporaire.
Puis-je rendre permanent un lien de pièce jointe Discord ?
Non. Le seul moyen de retrouver un lien qui fonctionne est de récupérer le message par l’API de Discord, ce qui demande un bot capable de lire le salon. Pour tout ce que vous devez recharger plus tard, envoyez le fichier vers un CDN, enregistrez ce lien, et postez-le sur Discord si vous voulez quand même l’image dans un salon.
Un webhook Discord est-il un bon endroit pour stocker les captures FiveM ?
C’est un bon endroit pour les afficher et un mauvais pour les conserver. Le lien renvoyé expire, et la limite d’upload par défaut de Discord, 20 MiB par fichier depuis septembre 2026, s’applique aussi aux webhooks. Stockez plutôt le fichier sur un CDN et envoyez le lien du CDN dans l’embed du webhook.