Links do Discord expiram: mova as imagens do FiveM para um CDN
Mugshots do MDT e provas salvas como links do Discord mostram “This content is no longer available.” Por que expiram, como achar os links e como resolver.
Por Termi
- Discord
- Uploads
- MySQL
- MDT
Nesta página
- Por que os links do Discord param de funcionar
- Quanto tempo dura um link
- O que “This content is no longer available.” significa
- Ache os links mortos no seu banco
- Faça upload para um CDN
- Como recuperar as imagens antigas
- Perguntas frequentes
- Por que os links de imagem do Discord param de funcionar?
- O que significam ex, is e hm num link do Discord?
- Quanto tempo duram os links de anexo do Discord?
- Dá para tornar permanente um link de anexo do Discord?
- Um webhook do Discord é um bom lugar para guardar screenshots do FiveM?
Muitos servidores de FiveM mandam screenshots, mugshots do MDT e fotos de prova para um webhook do Discord e salvam no banco o link do anexo que o Discord devolve. Funciona por um dia. Depois o MDT mostra imagens quebradas, e abrir um dos links mostra uma linha só: This content is no longer available. Os arquivos normalmente ainda estão no canal. O que expirou foram os links do seu banco de dados. Este guia explica por quê, encontra os links mortos e move os uploads para um CDN cujos links não mudam.
Por que os links do Discord param de funcionar
A referência da API do Discord diz: “attachments uploaded to Discord’s CDN (like user and bot-uploaded images) have signed URLs with a preset expiry time.” O Discord avisou os desenvolvedores da mudança no fim de 2023 e a documentou em fevereiro de 2024. Desde então, todo link de anexo carrega três parâmetros na URL:
https://cdn.discordapp.com/attachments/<channel id>/<attachment id>/screenshot.png
?ex=66424c96
&is=6640fb16
&hm=0b3d3210b4ea…287dace4&Dentro do Discord nada parece errado, porque “Discord automatically refreshes attachment CDN URLs that appear within the client.” A imagem no canal continua carregando. O link que o seu script salvou no MySQL é uma cópia, e ninguém a renova. Passado o horário do ex, o CDN recusa.
Quanto tempo dura um link
O Discord não documenta uma duração fixa, então leia no próprio link. Os dois valores são timestamps Unix em hexadecimal: 6640fb16 é 12 de maio de 2024 às 17:23:34 UTC e 66424c96 fica exatamente 24 horas depois. Esse é o link real que testamos abaixo, mas o Discord pode emitir links para outros períodos, então confira o ex nas suas próprias linhas em vez de presumir um dia.
O que “This content is no longer available.” significa
Abra um link expirado fora do Discord e o CDN responde com um 404 em texto puro:
$ 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 salvos antes de o Discord começar a assiná-los, os que não têm ex, is nem hm, recebem a mesma resposta, tanto em cdn.discordapp.com quanto em media.discordapp.net. Num MDT ou em qualquer outra página NUI isso aparece como uma imagem quebrada, sem nada no console do servidor para explicar.
Ache os links mortos no seu banco
Todo link de anexo do Discord contém discordapp. e /attachments/. Esta query escreve uma query de contagem para cada coluna de texto do banco que o seu servidor usa (o 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');Ela retorna um único valor: outra query. Copie e rode. Cada linha do resultado nomeia uma table.column e quantos links do Discord ela tem. Qualquer coisa acima de zero é uma coluna que os seus scripts preencheram por um webhook.
Para ver quais linhas de uma coluna já expiraram, decodifique o ex. Aqui para a tabela mdt_mugshots do guia de 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/%';Uma linha cujo expires está no passado não carrega mais fora do Discord, e uma linha com NULL também não: significa que o link nunca foi assinado.
Faça upload para um CDN
A solução é enviar o arquivo para um armazenamento feito para servi-lo, guardar esse link no banco e continuar postando no Discord se a staff quiser ver a imagem num canal, com o link do CDN no embed. Aqui vai um screenshot de prova tirado com o screenshot-basic, enviado ao endpoint base64 do FiveDock, salvo e depois postado num 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)
endA requisição tem o mesmo formato do upload base64 do Fivemanage, então um script feito para o Fivemanage só precisa de outra URL e outro token. Envie arquivos você mesmo cobre o painel, os links com chave e o endpoint S3.
Se você usa o agente do FiveDock, exports.fivedock:screenshot tira o screenshot e envia numa só chamada, sem token no script. Veja a documentação do agente.
Como recuperar as imagens antigas
Os links do seu banco estão mortos, mas os arquivos normalmente ainda estão no canal do Discord, e o Discord entrega um link novo toda vez que um app busca a mensagem de novo. Um bot do Discord que consiga ler o canal pode percorrer o histórico de 100 em 100 mensagens e casar cada anexo com as suas linhas: em /attachments/<channel id>/<attachment id>/, o segundo número é o ID do anexo. Baixe cada arquivo pelo link novo, envie ao FiveDock com a key que o seu script passou a usar e atualize a linha.
Um webhook não consegue listar um canal, então é preciso um bot. Arquivos cuja mensagem foi apagada, ou cujo canal não existe mais, não têm como ser recuperados.
Perguntas frequentes
Por que os links de imagem do Discord param de funcionar?
O Discord assina cada link de anexo com um horário de expiração e documentou isso na referência da API em fevereiro de 2024. O app do Discord renova os links que mostra, por isso a imagem continua carregando no canal, mas um link salvo no banco nunca é renovado. Depois de expirar, o CDN responde 404 com o texto This content is no longer available.
O que significam ex, is e hm num link do Discord?
ex é o horário em que o link expira e is o horário em que foi emitido, ambos como timestamps Unix escritos em hexadecimal. hm é uma assinatura que vale até ex. No MySQL, FROM_UNIXTIME(CONV('66424c96', 16, 10)) transforma um valor de ex em data.
Quanto tempo duram os links de anexo do Discord?
O Discord não documenta uma duração fixa; cada link traz a própria expiração no parâmetro ex. O link real que testamos para este guia foi emitido por exatamente 24 horas. Trate qualquer link de anexo do Discord salvo num banco como temporário.
Dá para tornar permanente um link de anexo do Discord?
Não. A única forma de ter um link funcionando de novo é buscar a mensagem pela API do Discord, o que exige um bot que consiga ler o canal. Para tudo o que você precisa carregar depois, envie o arquivo a um CDN, guarde esse link e poste o link do CDN no Discord se ainda quiser a imagem num canal.
Um webhook do Discord é um bom lugar para guardar screenshots do FiveM?
É um bom lugar para mostrá-los e um mau lugar para guardá-los. O link devolvido expira, e o limite de upload padrão do Discord, 20 MiB por arquivo desde setembro de 2026, vale também para webhooks. Guarde o arquivo num CDN e mande o link do CDN no embed do webhook.