MediaAdapter = {} local function trim(value, maxLength) local text = tostring(value or ''):match('^%s*(.-)%s*$') if maxLength and #text > maxLength then text = text:sub(1, maxLength) end return text end local function extensionFromUrl(url) return tostring(url or ''):match('%.([%w]+)%??[^/]*$') end local function extensionFromName(name) return tostring(name or ''):match('%.([%w]+)$') end local function isAllowedExtension(ext) ext = tostring(ext or ''):lower() for _, allowed in ipairs(Config.Media.allowedExtensions or {}) do if ext == allowed then return true end end return false end local function mimeExtension(mime) return ({ ['image/jpeg'] = 'jpg', ['image/jpg'] = 'jpg', ['image/png'] = 'png' })[tostring(mime or ''):lower()] end local function urlEncode(value) return tostring(value or ''):gsub('\n', '\r\n'):gsub('([^%w%-_%.~])', function(char) return string.format('%%%02X', string.byte(char)) end) end local function decodeJson(value) if not value or value == '' then return nil end local ok, result = pcall(json.decode, value) if ok then return result end return nil end function MediaAdapter.IsAllowedExternalUrl(url) url = tostring(url or '') if Config.Media.requireHttps and not url:match('^https://') then return false, 'url_must_be_https' end if url:match('^https://i%.ibb%.co/') or url:match('^https://ibb%.co/') then return true end local ext = extensionFromUrl(url) if not ext then return false, 'missing_extension' end ext = ext:lower() if isAllowedExtension(ext) then return true end return false, 'unsupported_extension' end function MediaAdapter.ValidateUpload(payload) if type(payload) ~= 'table' then return false, 'invalid_payload' end local name = trim(payload.name, 180) local mime = trim(payload.mime, 80):lower() local data = tostring(payload.data or '') local size = tonumber(payload.size) or 0 if data == '' then return false, 'missing_data' end if size <= 0 then return false, 'missing_size' end if size > (Config.Media.maxBytes or 2097152) then return false, 'file_too_large' end local ext = extensionFromName(name) or mimeExtension(mime) if not isAllowedExtension(ext) then return false, 'unsupported_extension' end if mimeExtension(mime) and not isAllowedExtension(mimeExtension(mime)) then return false, 'unsupported_mime' end return true, { name = name ~= '' and name or ('bleeter_%s.%s'):format(os.time(), ext), mime = mime, data = data, size = size, extension = ext } end function MediaAdapter.Upload(payload, cb) local ok, upload = MediaAdapter.ValidateUpload(payload) if not ok then cb(false, upload) return end if Config.Media.provider ~= 'imgbb' then cb(false, 'provider_not_configured') return end -- Erst die Konsolenvariable, dann die Config. So bleibt der Schluessel -- aus der Versionsverwaltung heraus. local apiKey = trim(GetConvar('bleeter_imgbb_key', Config.Media.imgbbApiKey or '')) if apiKey == '' then cb(false, 'missing_imgbb_key') return end local body = ('image=%s&name=%s'):format(urlEncode(upload.data), urlEncode(upload.name)) local url = ('https://api.imgbb.com/1/upload?key=%s'):format(urlEncode(apiKey)) PerformHttpRequest(url, function(statusCode, responseText) if statusCode < 200 or statusCode >= 300 then cb(false, 'upload_failed') return end local decoded = decodeJson(responseText) local data = decoded and decoded.data local imageUrl = data and (data.url or data.display_url) if not imageUrl or imageUrl == '' then cb(false, 'missing_upload_url') return end cb(true, { url = imageUrl, deleteUrl = data.delete_url, size = upload.size, mime = upload.mime, name = upload.name }) end, 'POST', body, { ['Content-Type'] = 'application/x-www-form-urlencoded' }) end