FiveM: Discord-Bildlinks laufen ab, Bilder auf ein CDN umziehen
MDT-Mugshots und Beweisfotos als Discord-Links zeigen „This content is no longer available.“ Warum sie ablaufen, wie du sie findest und was du tust.
Von Termi
- Discord
- Uploads
- MySQL
- MDT
Auf dieser Seite
- Warum Discord-Links nicht mehr funktionieren
- Wie lange ein Link gilt
- Was „This content is no longer available.“ bedeutet
- Die toten Links in deiner Datenbank finden
- Stattdessen auf ein CDN hochladen
- Die alten Bilder zurückholen
- Häufige Fragen
- Warum funktionieren Discord-Bildlinks nicht mehr?
- Was bedeuten ex, is und hm in einem Discord-Link?
- Wie lange halten Discord-Anhangslinks?
- Kann ich einen Discord-Anhangslink dauerhaft machen?
- Taugt ein Discord-Webhook zum Speichern von FiveM-Screenshots?
Viele FiveM-Server schicken Screenshots, MDT-Mugshots und Beweisfotos an einen Discord-Webhook und speichern den Anhangslink, den Discord zurückgibt, in der Datenbank. Das funktioniert einen Tag lang. Dann zeigt das MDT kaputte Bilder, und wer einen der Links öffnet, bekommt nur eine Zeile: This content is no longer available. Die Dateien liegen meist noch im Channel. Abgelaufen sind die Links in deiner Datenbank. Diese Anleitung erklärt, warum, findet die toten Links und verlegt Uploads auf ein CDN, dessen Links bleiben.
Warum Discord-Links nicht mehr funktionieren
In Discords API-Referenz steht: „attachments uploaded to Discord’s CDN (like user and bot-uploaded images) have signed URLs with a preset expiry time.“ Discord hat die Änderung Ende 2023 Entwicklern angekündigt und im Februar 2024 dokumentiert. Jeder Anhangslink trägt seitdem drei Query-Parameter:
https://cdn.discordapp.com/attachments/<channel id>/<attachment id>/screenshot.png
?ex=66424c96
&is=6640fb16
&hm=0b3d3210b4ea…287dace4&In Discord selbst fällt nichts auf, denn „Discord automatically refreshes attachment CDN URLs that appear within the client.“ Das Bild im Channel lädt weiter. Der Link, den dein Script in MySQL gespeichert hat, ist eine Kopie, und die erneuert niemand. Ist seine ex-Zeit vorbei, lehnt das CDN ihn ab.
Wie lange ein Link gilt
Discord dokumentiert keine feste Lebensdauer, lies sie also aus dem Link. Beide Werte sind Unix-Zeitstempel in Hex: 6640fb16 ist der 12. Mai 2024 um 17:23:34 UTC, und 66424c96 liegt genau 24 Stunden später. Das ist der echte Link, den wir unten getestet haben. Discord kann Links aber auch für andere Zeiträume ausstellen. Prüfe deshalb ex in deinen eigenen Zeilen, statt von einem Tag auszugehen.
Was „This content is no longer available.“ bedeutet
Öffnest du einen abgelaufenen Link außerhalb von Discord, antwortet das CDN mit einem 404 als Klartext:
$ 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.Links, die vor Discords Signierung gespeichert wurden, also ganz ohne ex, is und hm, bekommen dieselbe Antwort, auf cdn.discordapp.com genauso wie auf media.discordapp.net. In einem MDT oder einer anderen NUI-Seite sieht das wie ein kaputtes Bild aus, ohne dass die Server-Konsole etwas dazu sagt.
Die toten Links in deiner Datenbank finden
Jeder Discord-Anhangslink enthält discordapp. und /attachments/. Diese Abfrage baut für jede Textspalte der Datenbank, die dein Server nutzt (die aus mysql_connection_string), eine Zähl-Abfrage:
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');Sie liefert genau einen Wert: eine weitere Abfrage. Kopiere sie und führe sie aus. Jede Zeile des Ergebnisses nennt eine table.column und wie viele Discord-Links darin stecken. Alles über null ist eine Spalte, die deine Scripts über einen Webhook gefüllt haben.
Um zu sehen, welche Zeilen einer Spalte schon abgelaufen sind, dekodierst du ex. Hier für die Tabelle mdt_mugshots aus der Mugshot-Anleitung:
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/%';Eine Zeile, deren expires in der Vergangenheit liegt, lädt außerhalb von Discord nicht mehr. Dasselbe gilt für eine Zeile mit NULL: Dieser Link wurde nie signiert.
Stattdessen auf ein CDN hochladen
Die Lösung: Lade die Datei in einen Speicher hoch, der zum Ausliefern gebaut ist, behalte diesen Link in der Datenbank und poste weiter nach Discord, wenn das Team das Bild im Channel sehen soll, mit dem CDN-Link im Embed. Hier ein Beweis-Screenshot, aufgenommen mit screenshot-basic, an den Base64-Endpoint von FiveDock hochgeladen, gespeichert und danach an einen Webhook gepostet:
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)
endDie Anfrage hat dieselbe Form wie der Base64-Upload von Fivemanage. Ein Script für Fivemanage braucht also nur eine neue URL und einen neuen Token. Dateien selbst hochladen erklärt das Panel, Links mit Key und den S3-Endpoint.
Läuft bei dir der FiveDock-Agent, nimmt exports.fivedock:screenshot den Screenshot auf und lädt ihn in einem Aufruf hoch, ohne Token im Script. Siehe die Agent-Doku.
Die alten Bilder zurückholen
Die Links in deiner Datenbank sind tot, aber die Dateien liegen meist noch im Discord-Channel, und Discord gibt jedes Mal einen frischen Link aus, wenn eine App die Nachricht neu abruft. Ein Discord-Bot, der den Channel lesen darf, kann den Verlauf in Blöcken von 100 Nachrichten durchgehen und jeden Anhang deinen Zeilen zuordnen: In /attachments/<channel id>/<attachment id>/ ist die zweite Zahl die ID des Anhangs. Lade jede Datei über ihren frischen Link herunter, lade sie mit dem Key, den dein Script jetzt nutzt, zu FiveDock hoch und aktualisiere die Zeile.
Ein Webhook kann keinen Channel auflisten, dafür brauchst du also einen Bot. Dateien, deren Nachricht gelöscht wurde oder deren Channel nicht mehr existiert, lassen sich gar nicht wiederherstellen.
Häufige Fragen
Warum funktionieren Discord-Bildlinks nicht mehr?
Discord signiert jeden Anhangslink mit einer Ablaufzeit und hat das im Februar 2024 in seiner API-Referenz dokumentiert. Die Discord-App erneuert die Links, die sie anzeigt, deshalb lädt das Bild im Channel weiter, aber ein in der Datenbank gespeicherter Link wird nie erneuert. Nach dem Ablauf antwortet das CDN mit 404 und dem Text This content is no longer available.
Was bedeuten ex, is und hm in einem Discord-Link?
ex ist der Zeitpunkt, an dem der Link abläuft, und is der, an dem er ausgestellt wurde, beide als Unix-Zeitstempel in Hex. hm ist eine Signatur, die bis ex gültig bleibt. In MySQL macht FROM_UNIXTIME(CONV('66424c96', 16, 10)) aus einem ex-Wert ein Datum.
Wie lange halten Discord-Anhangslinks?
Discord dokumentiert keine feste Lebensdauer, jeder Link trägt seinen eigenen Ablauf im Parameter ex. Der echte Link, den wir für diese Anleitung getestet haben, war genau 24 Stunden gültig. Behandle jeden Discord-Anhangslink in einer Datenbank als temporär.
Kann ich einen Discord-Anhangslink dauerhaft machen?
Nein. Einen funktionierenden Link bekommst du nur, indem du die Nachricht über Discords API neu abrufst, und dafür brauchst du einen Bot, der den Channel lesen darf. Alles, was du später noch laden willst, lädst du auf ein CDN hoch. Speichere diesen Link und poste ihn nach Discord, wenn du das Bild trotzdem im Channel haben willst.
Taugt ein Discord-Webhook zum Speichern von FiveM-Screenshots?
Zum Anzeigen taugt er, zum Aufbewahren nicht. Der zurückgegebene Link läuft ab, und Discords Standard-Uploadlimit, seit September 2026 20 MiB pro Datei, gilt auch für Webhooks. Speichere die Datei stattdessen auf einem CDN und schick den CDN-Link im Webhook-Embed.