Linki do załączników z Discorda wygasają: obrazki z FiveM na CDN
Mugshoty z MDT i zdjęcia dowodów zapisane jako linki z Discorda pokazują „This content is no longer available.” Skąd to się bierze, jak je znaleźć i naprawić.
Autor: Termi
- Discord
- Uploads
- MySQL
- MDT
Na tej stronie
- Dlaczego linki z Discorda przestają działać
- Jak długo działa link
- Co znaczy „This content is no longer available.”
- Znajdź martwe linki w bazie
- Wgrywaj na CDN
- Jak odzyskać stare zdjęcia
- Częste pytania
- Dlaczego linki do obrazków z Discorda przestają działać?
- Co znaczą ex, is i hm w linku z Discorda?
- Jak długo są ważne linki do załączników z Discorda?
- Czy da się zrobić stały link do załącznika z Discorda?
- Czy webhook Discorda nadaje się do trzymania screenshotów z FiveM?
Mnóstwo serwerów FiveM wysyła screenshoty, mugshoty z MDT i zdjęcia dowodów na webhook Discorda i zapisuje w bazie link do załącznika, który Discord odsyła. Przez dzień wszystko działa. Potem MDT pokazuje puste obrazki, a otwarty link wypisuje jedną linijkę: This content is no longer available. Pliki zwykle dalej leżą na kanale. To linki w twojej bazie wygasły. Ten poradnik wyjaśnia dlaczego, znajduje martwe linki i przenosi uploady na CDN, którego linki się nie zmieniają.
Dlaczego linki z Discorda przestają działać
Dokumentacja API Discorda mówi, że załączniki wgrane na CDN Discorda, łącznie z obrazkami od użytkowników i botów, mają podpisane linki z ustalonym czasem ważności („signed URLs with a preset expiry time”). Discord zapowiedział tę zmianę deweloperom pod koniec 2023 roku, a w lutym 2024 dopisał ją do dokumentacji. Każdy link do załącznika ma teraz trzy parametry:
https://cdn.discordapp.com/attachments/<channel id>/<attachment id>/screenshot.png
?ex=66424c96
&is=6640fb16
&hm=0b3d3210b4ea…287dace4&W samym Discordzie nic nie widać, bo klient Discorda sam odświeża linki, które pokazuje („Discord automatically refreshes attachment CDN URLs that appear within the client”). Obrazek na kanale dalej się ładuje. Link, który twój skrypt zapisał w MySQL, to kopia i nic go nie odświeża. Kiedy minie czas z ex, CDN go odrzuca.
Jak długo działa link
Discord nie podaje stałego czasu ważności, więc odczytaj go z linku. Obie wartości to uniksowe znaczniki czasu w hex: 6640fb16 to 12 maja 2024, 17:23:34 UTC, a 66424c96 to dokładnie 24 godziny później. To prawdziwy link, który sprawdziliśmy niżej, ale Discord może wydawać linki na inny czas, więc zamiast zakładać jeden dzień, sprawdź ex we własnych wierszach.
Co znaczy „This content is no longer available.”
Otwórz wygasły link gdziekolwiek poza Discordem, a CDN odpowie zwykłym tekstem i kodem 404:
$ 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.Linki zapisane, zanim Discord zaczął je podpisywać, czyli bez ex, is i hm, dostają tę samą odpowiedź, zarówno na cdn.discordapp.com, jak i na media.discordapp.net. W MDT czy innej stronie NUI wygląda to jak pusty obrazek, a w konsoli serwera nie ma nic, co by to wyjaśniało.
Znajdź martwe linki w bazie
Każdy link do załącznika z Discorda zawiera discordapp. i /attachments/. To zapytanie pisze osobne zapytanie liczące dla każdej kolumny tekstowej w bazie, której używa serwer (tej z 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');Wynik to jedna wartość: kolejne zapytanie. Skopiuj je i uruchom. Każdy wiersz tego wyniku podaje tabela.kolumna i liczbę linków z Discorda, które w niej siedzą. Wszystko powyżej zera to kolumna, którą skrypty wypełniały przez webhook.
Żeby sprawdzić, które wiersze w jednej kolumnie już wygasły, odkoduj ex. Tu dla tabeli mdt_mugshots z poradnika o mugshotach:
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/%';Wiersz, którego expires jest w przeszłości, nie załaduje się już poza Discordem. Tak samo wiersz z NULL, bo to znaczy, że link w ogóle nie był podpisany.
Wgrywaj na CDN
Rozwiązanie: wgraj plik tam, gdzie da się go normalnie serwować, trzymaj ten link w bazie, a jeśli administracja chce widzieć zdjęcie na kanale, dalej wysyłaj je na Discorda, tylko z linkiem z CDN w embedzie. Oto screenshot dowodowy zrobiony przez screenshot-basic, wgrany na endpoint base64 FiveDock, zapisany w bazie i wysłany na 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)
endRequest ma ten sam kształt co upload base64 w Fivemanage, więc skrypt napisany pod Fivemanage potrzebuje tylko nowego URL-a i tokenu. Wgrywanie plików opisuje panel, linki z kluczem i endpoint S3.
Jeśli masz agenta FiveDock, exports.fivedock:screenshot robi screenshot i wgrywa go jednym wywołaniem, bez tokenu w skrypcie. Szczegóły w dokumentacji agenta.
Jak odzyskać stare zdjęcia
Linki w bazie są martwe, ale pliki zwykle dalej leżą na kanale Discorda, a Discord daje świeży link za każdym razem, gdy aplikacja pobierze wiadomość od nowa. Bot Discorda, który może czytać kanał, przejdzie przez jego historię po 100 wiadomości naraz i dopasuje każdy załącznik do twoich wierszy: w /attachments/<channel id>/<attachment id>/ druga liczba to ID załącznika. Pobierz każdy plik przez świeży link, wgraj go do FiveDock z kluczem, którego teraz używa skrypt, i zaktualizuj wiersz.
Webhook nie wylistuje kanału, więc potrzebny jest bot. Plików, których wiadomość usunięto albo których kanał już nie istnieje, nie da się odzyskać.
Częste pytania
Dlaczego linki do obrazków z Discorda przestają działać?
Discord podpisuje każdy link do załącznika czasem ważności i opisał to w dokumentacji API w lutym 2024. Aplikacja Discorda sama odświeża linki, które pokazuje, więc obrazek na kanale dalej się ładuje, ale link zapisany w bazie nigdy nie jest odświeżany. Kiedy wygaśnie, CDN odpowiada 404 z tekstem This content is no longer available.
Co znaczą ex, is i hm w linku z Discorda?
ex to moment wygaśnięcia linku, a is moment jego wydania, oba jako uniksowe znaczniki czasu zapisane w hex. hm to podpis ważny do czasu z ex. W MySQL FROM_UNIXTIME(CONV('66424c96', 16, 10)) zamienia wartość ex na datę.
Jak długo są ważne linki do załączników z Discorda?
Discord nie podaje stałego czasu; każdy link niesie własną datę wygaśnięcia w parametrze ex. Prawdziwy link, który sprawdziliśmy przy tym poradniku, był wydany na dokładnie 24 godziny. Każdy link do załącznika z Discorda zapisany w bazie traktuj jako tymczasowy.
Czy da się zrobić stały link do załącznika z Discorda?
Nie. Działający link można dostać tylko, pobierając wiadomość od nowa przez API Discorda, a do tego potrzebny jest bot z dostępem do kanału. Wszystko, co ma się ładować później, wgrywaj na CDN, zapisuj ten link i wysyłaj link z CDN na Discorda, jeśli dalej chcesz go mieć na kanale.
Czy webhook Discorda nadaje się do trzymania screenshotów z FiveM?
Nadaje się, żeby je pokazać, ale nie żeby je przechowywać. Link, który wraca, wygasa, a domyślny limit uploadu w Discordzie, 20 MiB na plik od września 2026, obejmuje też webhooki. Trzymaj plik na CDN, a w embedzie webhooka wysyłaj link z CDN.