mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
FIX - stabilize CEF media capture and uploads
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user