FIX - stabilize CEF media capture and uploads

This commit is contained in:
DerEchteAlec
2026-08-12 18:59:14 +02:00
parent c3b7a0871a
commit ff7e00596b
15 changed files with 912 additions and 179 deletions
+6 -2
View File
@@ -538,12 +538,16 @@ Bridge.Callbacks.Register("sky_phone:darkchat:send", function(source, data)
media_waveform = voice.waveform
elseif message_type == "image" or message_type == "video" then
local media_type = message_type == "image" and "photo" or "video"
local media_url, media_error = SkyPhoneMedia.ResolveOwnedMedia(source, data.mediaAssetId, media_type)
local media_url, media_error, resolved_mime = SkyPhoneMedia.ResolveOwnedMedia(
source,
data.mediaAssetId,
media_type
)
if not media_url then
return { success = false, error = media_error }
end
media_payload = media_url
media_mime = message_type == "image" and "image/jpeg" or "video/mp4"
media_mime = resolved_mime
elseif message_type == "share" then
local share
local share_error
+32
View File
@@ -377,6 +377,7 @@ local schema = {
{ name = "url", type = "TEXT NOT NULL" },
{ name = "remote_id", type = "VARCHAR(128) NOT NULL" },
{ name = "media_type", type = "ENUM('photo', 'video') NOT NULL" },
{ name = "mime_type", type = "VARCHAR(120) NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
@@ -2524,6 +2525,37 @@ Bridge.Database.Query([[
ALTER TABLE `sky_phone_darkchat_messages`
MODIFY COLUMN `message_type` ENUM('text', 'emoji', 'gif', 'voice', 'image', 'video', 'share', 'system') NOT NULL DEFAULT 'text'
]], {})
Bridge.Database.Query([[
UPDATE `sky_phone_media`
SET `mime_type` = CASE
WHEN `media_type` = 'video' AND LOWER(SUBSTRING_INDEX(`url`, '?', 1)) LIKE '%.webm' THEN 'video/webm'
WHEN `media_type` = 'video' AND LOWER(SUBSTRING_INDEX(`url`, '?', 1)) LIKE '%.mp4' THEN 'video/mp4'
WHEN `media_type` = 'photo' AND LOWER(SUBSTRING_INDEX(`url`, '?', 1)) LIKE '%.png' THEN 'image/png'
WHEN `media_type` = 'photo' AND LOWER(SUBSTRING_INDEX(`url`, '?', 1)) LIKE '%.webp' THEN 'image/webp'
WHEN `media_type` = 'photo' AND (
LOWER(SUBSTRING_INDEX(`url`, '?', 1)) LIKE '%.jpg'
OR LOWER(SUBSTRING_INDEX(`url`, '?', 1)) LIKE '%.jpeg'
) THEN 'image/jpeg'
ELSE NULL
END
WHERE `mime_type` IS NULL OR `mime_type` = ''
]], {})
Bridge.Database.Query([[
UPDATE `sky_phone_sms_messages` message
INNER JOIN `sky_phone_media` media ON media.`url` = message.`media_payload`
SET message.`media_mime` = media.`mime_type`
WHERE message.`message_type` = 'video'
AND media.`mime_type` IN ('video/webm', 'video/mp4')
AND (message.`media_mime` IS NULL OR message.`media_mime` <> media.`mime_type`)
]], {})
Bridge.Database.Query([[
UPDATE `sky_phone_darkchat_messages` message
INNER JOIN `sky_phone_media` media ON media.`url` = message.`media_payload`
SET message.`media_mime` = media.`mime_type`
WHERE message.`message_type` = 'video'
AND media.`mime_type` IN ('video/webm', 'video/mp4')
AND (message.`media_mime` IS NULL OR message.`media_mime` <> media.`mime_type`)
]], {})
Bridge.Database.Query([[
ALTER TABLE `sky_phone_marketplace_images`
MODIFY COLUMN `gradient` VARCHAR(2200) NOT NULL
+55 -22
View File
@@ -3,14 +3,32 @@ SkyPhoneMedia = {}
local pending_uploads = {}
local pending_deletes = {}
local allowed_remote_mimes = {
photo = {
["image/jpeg"] = true,
["image/png"] = true,
["image/webp"] = true,
},
video = {
["video/mp4"] = true,
["video/webm"] = true,
},
}
local function media_config()
return Config.Media.FiveManage
end
local function media_api_key()
local convar = media_config().ApiKeyConvar
if type(convar) ~= "string" or convar == "" then
return ""
end
return GetConvar(convar, "")
end
local function api_configured()
local api_key = media_config().ApiKey
return type(api_key) == "string" and api_key ~= "" and api_key ~= "YOUR_API_TOKEN"
return media_api_key() ~= ""
end
local function http_request(url, method, body, headers, timeout_ms)
@@ -63,7 +81,7 @@ local function request_presigned_url()
tostring(config.BaseUrl):gsub("/+$", "") .. "/presigned-url",
"GET",
"",
{ ["Authorization"] = config.ApiKey },
{ ["Authorization"] = media_api_key() },
tonumber(config.RequestTimeoutMs) or 10000
)
local data, response_error = decode_response(response)
@@ -86,7 +104,7 @@ local function get_remote_file(remote_id)
("%s/%s"):format(tostring(config.BaseUrl):gsub("/+$", ""), remote_id),
"GET",
"",
{ ["Authorization"] = config.ApiKey },
{ ["Authorization"] = media_api_key() },
tonumber(config.RequestTimeoutMs) or 10000
)
return decode_response(response)
@@ -101,7 +119,7 @@ local function delete_remote_file(remote_id)
("%s/%s"):format(tostring(config.BaseUrl):gsub("/+$", ""), remote_id),
"DELETE",
"",
{ ["Authorization"] = config.ApiKey },
{ ["Authorization"] = media_api_key() },
tonumber(config.RequestTimeoutMs) or 10000
)
if response.status < 200 or response.status >= 300 then
@@ -153,7 +171,7 @@ function SkyPhoneMedia.ResolveOwnedMedia(source, media_id, media_type)
params[#params + 1] = value
end
local rows = Bridge.Database.Query(([[
SELECT `url`, `media_type` FROM `sky_phone_media`
SELECT `url`, `media_type`, `mime_type` FROM `sky_phone_media`
WHERE `id` = ? AND %s
LIMIT 1
]]):format(condition), params)
@@ -172,7 +190,7 @@ function SkyPhoneMedia.ResolveOwnedMedia(source, media_id, media_type)
)
return nil, "invalid_attachment"
end
return media.url
return media.url, nil, media.mime_type
end
local function upload_result(source, correlation_id, success, error_code, media)
@@ -224,18 +242,29 @@ local function verify_remote_upload(state, remote_id, uploaded_url)
if remote.url ~= uploaded_url and remote.originalUrl ~= uploaded_url then
return nil, "invalid_upload"
end
local remote_type = tostring(remote.type or remote.mimeType or ""):lower()
local remote_mime = tostring(remote.mimeType or ""):lower():match("^%s*([^;%s]+)") or ""
local remote_type = tostring(remote.type or ""):lower():match("^%s*([^;%s]+)") or ""
if remote_mime == "" and allowed_remote_mimes[state.media_type][remote_type] then
remote_mime = remote_type
end
if remote_type == "" then
remote_type = remote_mime
end
if state.media_type == "photo" and remote_type ~= "" and not remote_type:find("image", 1, true) then
return nil, "invalid_media_type"
end
if state.media_type == "video" and remote_type ~= "" and not remote_type:find("video", 1, true) then
return nil, "invalid_media_type"
end
if remote_mime ~= "" and not allowed_remote_mimes[state.media_type][remote_mime] then
return nil, "invalid_media_type"
end
local metadata = parse_metadata(remote.metadata)
if not metadata or metadata.captureToken ~= state.capture_token then
return nil, "invalid_upload_token"
end
return {
mime_type = allowed_remote_mimes[state.media_type][remote_mime] and remote_mime or state.mime_type,
remote_id = remote_id,
url = remote.url or uploaded_url,
}
@@ -255,7 +284,7 @@ Bridge.Callbacks.Register("sky_phone:gallery:list", function(source, data)
if not owner then
return error_response
end
data = data or {}
data = type(data) == "table" and data or {}
local limit = math.max(1, math.min(math.floor(tonumber(data.limit) or Config.Media.PageSize), 100))
local offset = math.max(0, math.floor(tonumber(data.offset) or 0))
local media_type = data.mediaType
@@ -342,7 +371,7 @@ Bridge.Callbacks.Register("sky_phone:messages:gifs", function(source, data)
if #query > 60 or offset < 0 or offset > 500 then
return { success = false, error = "invalid_request" }
end
local api_key = Config.Media.GiphyApiKey
local api_key = GetConvar(Config.Media.GiphyApiKeyConvar, "")
if api_key == "" then
return { success = false, error = "gif_provider_unconfigured" }
end
@@ -420,7 +449,7 @@ end)
RegisterNetEvent("sky_phone:media:request-upload", function(data)
local src = source
data = data or {}
data = type(data) == "table" and data or {}
local correlation_id = data.correlationId
local media_type = data.mediaType
if type(correlation_id) ~= "string" or #correlation_id > 80
@@ -454,6 +483,9 @@ RegisterNetEvent("sky_phone:media:request-upload", function(data)
capture_token = capture_token,
correlation_id = correlation_id,
media_type = media_type,
mime_type = media_type == "video" and "video/webm"
or ({ png = "image/png", webp = "image/webp" })[tostring(Config.Media.Photo.Encoding):lower()]
or "image/jpeg",
owner = owner,
source = src,
}
@@ -474,7 +506,7 @@ end)
RegisterNetEvent("sky_phone:media:complete-upload", function(data)
local src = source
data = data or {}
data = type(data) == "table" and data or {}
local request_id = data.requestId
local state = type(request_id) == "string" and pending_uploads[request_id] or nil
if not state or state.source ~= src or state.completing then
@@ -496,14 +528,14 @@ RegisterNetEvent("sky_phone:media:complete-upload", function(data)
local result
if owner.account_id then
result = Bridge.Database.Query([[
INSERT INTO `sky_phone_media` (`account_id`, `device_imei`, `url`, `remote_id`, `media_type`)
VALUES (?, NULL, ?, ?, ?)
]], { owner.account_id, verified.url, verified.remote_id, state.media_type })
INSERT INTO `sky_phone_media` (`account_id`, `device_imei`, `url`, `remote_id`, `media_type`, `mime_type`)
VALUES (?, NULL, ?, ?, ?, ?)
]], { owner.account_id, verified.url, verified.remote_id, state.media_type, verified.mime_type })
else
result = Bridge.Database.Query([[
INSERT INTO `sky_phone_media` (`account_id`, `device_imei`, `url`, `remote_id`, `media_type`)
VALUES (NULL, ?, ?, ?, ?)
]], { owner.imei, verified.url, verified.remote_id, state.media_type })
INSERT INTO `sky_phone_media` (`account_id`, `device_imei`, `url`, `remote_id`, `media_type`, `mime_type`)
VALUES (NULL, ?, ?, ?, ?, ?)
]], { owner.imei, verified.url, verified.remote_id, state.media_type, verified.mime_type })
end
pending_uploads[request_id] = nil
local media_id = type(result) == "number" and result or (type(result) == "table" and tonumber(result.insertId))
@@ -522,7 +554,7 @@ end)
RegisterNetEvent("sky_phone:media:cancel-upload", function(data)
local src = source
local request_id = data and data.requestId
local request_id = type(data) == "table" and data.requestId or nil
local state = type(request_id) == "string" and pending_uploads[request_id] or nil
if state and state.source == src and not state.completing then
pending_uploads[request_id] = nil
@@ -532,7 +564,7 @@ end)
RegisterNetEvent("sky_phone:media:fail-upload", function(data)
local src = source
local request_id = data and data.requestId
local request_id = type(data) == "table" and data.requestId or nil
local state = type(request_id) == "string" and pending_uploads[request_id] or nil
if not state or state.source ~= src or state.completing then
return
@@ -550,7 +582,7 @@ end)
RegisterNetEvent("sky_phone:media:delete", function(data)
local src = source
data = data or {}
data = type(data) == "table" and data or {}
local correlation_id = data.correlationId
local media_id = tonumber(data.id)
if type(correlation_id) ~= "string" or #correlation_id > 80 or not media_id then
@@ -646,6 +678,7 @@ AddEventHandler("playerDropped", function()
end)
if not api_configured() then
print("^3[sky_phone] Camera and Gallery uploads are disabled until Config.Media.FiveManage.ApiKey is set in config/media.lua.^7")
print(("^3[sky_phone] Camera and Gallery uploads are disabled until the %s server convar is set.^7")
:format(tostring(media_config().ApiKeyConvar)))
end
end)
+7 -4
View File
@@ -32,7 +32,7 @@ local attachment_assets = {
local attachment_mimes = {
gif = "image/gif",
image = "image/jpeg",
video = "video/mp4",
video = "video/webm",
}
local function allowed_media_url(value)
@@ -208,7 +208,9 @@ local function validate_attachment(source, device, message_type, data)
return nil
end
local payload = data.mediaAssetId
if not valid_attachment_asset(message_type, payload) then
local built_in_asset = valid_attachment_asset(message_type, payload)
local mime = built_in_asset and attachment_mimes[message_type] or nil
if not built_in_asset then
if message_type == "gif" then
return nil
end
@@ -226,7 +228,7 @@ local function validate_attachment(source, device, message_type, data)
params = { media_id, device.imei }
end
local rows = Bridge.Database.Query(([[
SELECT `url`, `media_type` FROM `sky_phone_media`
SELECT `url`, `media_type`, `mime_type` FROM `sky_phone_media`
WHERE `id` = ? AND %s
LIMIT 1
]]):format(condition), params)
@@ -241,6 +243,7 @@ local function validate_attachment(source, device, message_type, data)
return nil
end
payload = media.url
mime = type(media.mime_type) == "string" and media.mime_type ~= "" and media.mime_type or nil
end
local duration = nil
if message_type == "video" and data.mediaDurationMs ~= nil then
@@ -252,7 +255,7 @@ local function validate_attachment(source, device, message_type, data)
end
return {
duration = duration,
mime = attachment_mimes[message_type],
mime = mime,
payload = payload,
}
end