ADD - share contacts and rich content in chats

This commit is contained in:
smx.pusha
2026-08-11 21:37:39 +02:00
parent 7d146e709a
commit cdf8d276ef
25 changed files with 1350 additions and 79 deletions
+20 -2
View File
@@ -181,6 +181,10 @@ local function message_payload(row, profile_id)
local deleted_everywhere = row.deleted_for_everyone == 1 or row.deleted_for_everyone == true
local reactions = decode_array(row.reactions, "reactions")
local waveform = row.media_waveform and decode_array(row.media_waveform, "media_waveform") or nil
local share_payload = row.message_type == "share" and json.decode(row.media_payload or "") or nil
if row.message_type == "share" and type(share_payload) ~= "table" then
error(("[sky_phone] DarkChat message %s has an invalid share payload."):format(tostring(row.id)))
end
return {
id = row.id,
conversationId = row.conversation_id,
@@ -189,12 +193,14 @@ local function message_payload(row, profile_id)
messageType = deleted_everywhere and "system" or row.message_type,
body = deleted_everywhere and "message_deleted" or row.body,
mediaMime = deleted_everywhere and nil or row.media_mime,
mediaPayload = not deleted_everywhere and row.message_type ~= "voice" and row.media_payload or nil,
mediaPayload = not deleted_everywhere and row.message_type ~= "voice"
and row.message_type ~= "share" and row.media_payload or nil,
mediaDurationMs = deleted_everywhere and nil or tonumber(row.media_duration_ms),
mediaWaveform = deleted_everywhere and nil or waveform,
replyToId = row.reply_to_id,
replyBody = row.reply_body,
reactions = reactions,
sharePayload = not deleted_everywhere and share_payload or nil,
expiresAt = row.expires_at,
createdAt = row.created_at,
readAt = row.peer_last_read_at and row.peer_last_read_at >= row.created_at and row.peer_last_read_at or nil,
@@ -538,6 +544,17 @@ Bridge.Callbacks.Register("sky_phone:darkchat:send", function(source, data)
end
media_payload = media_url
media_mime = message_type == "image" and "image/jpeg" or "video/mp4"
elseif message_type == "share" then
local share
local share_error
share, share_error, media_payload = SkyPhoneEasyShare.SanitizeChatPayload(source, data.sharePayload)
if not share then
return { success = false, error = share_error or "invalid_payload" }
end
if body ~= "" and #body > Config.DarkChat.BodyMaxLength then
return { success = false, error = "invalid_message" }
end
body = body ~= "" and body or share.title
else
return { success = false, error = "invalid_message" }
end
@@ -579,7 +596,8 @@ Bridge.Callbacks.Register("sky_phone:darkchat:send", function(source, data)
conversationId = conversation_id,
sender = profile.alias,
messageType = message_type,
preview = (message_type == "text" or message_type == "emoji") and body or message_type,
preview = (message_type == "text" or message_type == "emoji" or message_type == "share")
and body or message_type,
})
return { success = true, data = message }
end)
+7 -6
View File
@@ -521,7 +521,7 @@ local schema = {
{ name = "recipient_sim_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "sender_number", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "recipient_number", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "message_type", type = "ENUM('text', 'voice', 'image', 'gif', 'video') NOT NULL DEFAULT 'text'" },
{ name = "message_type", type = "ENUM('text', 'voice', 'image', 'gif', 'video', 'contact', 'share') NOT NULL DEFAULT 'text'" },
{ name = "body", type = "VARCHAR(2000) NOT NULL" },
{ name = "media_payload", type = "MEDIUMTEXT NULL" },
{ name = "media_mime", type = "VARCHAR(64) NULL", characterSet = "ascii", collation = "ascii_general_ci" },
@@ -1022,7 +1022,7 @@ local schema = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "conversation_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "sender_profile_id", type = "BIGINT UNSIGNED NULL" },
{ name = "message_type", type = "ENUM('text', 'emoji', 'gif', 'voice', 'image', 'video', 'system') NOT NULL DEFAULT 'text'" },
{ name = "message_type", type = "ENUM('text', 'emoji', 'gif', 'voice', 'image', 'video', 'share', 'system') NOT NULL DEFAULT 'text'" },
{ name = "body", type = "TEXT NOT NULL" },
{ name = "media_payload", type = "LONGTEXT NULL" },
{ name = "media_mime", type = "VARCHAR(80) NULL", characterSet = "ascii", collation = "ascii_bin" },
@@ -1680,8 +1680,9 @@ local schema = {
{ name = "match_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "sender_account_id", type = "BIGINT UNSIGNED NOT NULL" },
{ name = "body", type = "VARCHAR(1000) NOT NULL" },
{ name = "message_type", type = "ENUM('text', 'image', 'gif', 'video') NOT NULL DEFAULT 'text'" },
{ name = "message_type", type = "ENUM('text', 'image', 'gif', 'video', 'share') NOT NULL DEFAULT 'text'" },
{ name = "media_url", type = "VARCHAR(2048) NULL" },
{ name = "share_payload", type = "LONGTEXT NULL" },
{ name = "media_duration_ms", type = "INT UNSIGNED NULL" },
{ name = "read_at", type = "DATETIME NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
@@ -2173,15 +2174,15 @@ Bridge.Database.Query([[
]], {})
Bridge.Database.Query([[
ALTER TABLE `sky_phone_flare_messages`
MODIFY COLUMN `message_type` ENUM('text', 'image', 'gif', 'video') NOT NULL DEFAULT 'text'
MODIFY COLUMN `message_type` ENUM('text', 'image', 'gif', 'video', 'share') NOT NULL DEFAULT 'text'
]], {})
Bridge.Database.Query([[
ALTER TABLE `sky_phone_sms_messages`
MODIFY COLUMN `message_type` ENUM('text', 'voice', 'image', 'gif', 'video') NOT NULL DEFAULT 'text'
MODIFY COLUMN `message_type` ENUM('text', 'voice', 'image', 'gif', 'video', 'contact', 'share') NOT NULL DEFAULT 'text'
]], {})
Bridge.Database.Query([[
ALTER TABLE `sky_phone_darkchat_messages`
MODIFY COLUMN `message_type` ENUM('text', 'emoji', 'gif', 'voice', 'image', 'video', 'system') NOT NULL DEFAULT 'text'
MODIFY COLUMN `message_type` ENUM('text', 'emoji', 'gif', 'voice', 'image', 'video', 'share', 'system') NOT NULL DEFAULT 'text'
]], {})
Bridge.Database.Query([[
ALTER TABLE `sky_phone_marketplace_images`
+10
View File
@@ -1,3 +1,5 @@
SkyPhoneEasyShare = {}
Bridge.Database.AfterMigration("sky_phone", function()
local active_transfers = {}
local valid_kinds = {
@@ -259,6 +261,14 @@ local function sanitize_payload(source, device, data)
return payload, nil, encoded
end
function SkyPhoneEasyShare.SanitizeChatPayload(source, data)
local device, error_response = current_device(source)
if not device then
return nil, error_response.error
end
return sanitize_payload(source, device, data)
end
local function transfer_for_source(transfer, source)
local incoming = transfer.recipient_source == source
return {
+27 -5
View File
@@ -2,7 +2,7 @@ Bridge.Database.AfterMigration("sky_phone", function()
local genders = { woman = true, man = true, nonbinary = true }
local interests = { woman = true, man = true, nonbinary = true, everyone = true }
local looking_for = { longTerm = true, dates = true, friends = true }
local message_types = { text = true, image = true, gif = true, video = true }
local message_types = { text = true, image = true, gif = true, video = true, share = true }
local function allowed_gif_url(value)
if type(value) ~= "string" or #value == 0 or #value > Config.Media.UrlMaxLength then
@@ -386,6 +386,22 @@ local function validate_message(source, data)
return { body = body, message_type = message_type }
end
if message_type == "share" then
local share, share_error, encoded = SkyPhoneEasyShare.SanitizeChatPayload(source, data.sharePayload)
if not share then
return nil, share_error or "invalid_payload"
end
local body = trim(data.body) or ""
if #body > 1000 then
return nil, "invalid_message"
end
return {
body = body ~= "" and body or share.title,
message_type = message_type,
share_payload = encoded,
}
end
if type(data.mediaAssetId) ~= "string" then
return nil, "invalid_attachment"
end
@@ -427,6 +443,10 @@ local function validate_message(source, data)
end
local function message_payload(row, account_id)
local share_payload = row.message_type == "share" and json.decode(row.share_payload or "") or nil
if row.message_type == "share" and type(share_payload) ~= "table" then
error(("[sky_phone] Flare message %s has an invalid share payload."):format(tostring(row.id)))
end
return {
id = row.id,
direction = tonumber(row.sender_account_id) == tonumber(account_id) and "sent" or "received",
@@ -435,6 +455,7 @@ local function message_payload(row, account_id)
messageType = row.message_type or "text",
mediaUrl = row.media_url,
mediaDurationMs = tonumber(row.media_duration_ms),
sharePayload = share_payload,
}
end
@@ -670,7 +691,7 @@ Bridge.Callbacks.Register("sky_phone:flare:thread", function(source, data)
WHERE `match_id` = ? AND `sender_account_id` <> ? AND `read_at` IS NULL
]], { match.id, account.id })
local rows = Bridge.Database.Query([[
SELECT `id`, `sender_account_id`, `body`, `message_type`, `media_url`,
SELECT `id`, `sender_account_id`, `body`, `message_type`, `media_url`, `share_payload`,
`media_duration_ms`, UNIX_TIMESTAMP(`created_at`) * 1000 AS `created_at_ms`
FROM `sky_phone_flare_messages`
WHERE `match_id` = ?
@@ -705,11 +726,11 @@ Bridge.Callbacks.Register("sky_phone:flare:send", function(source, data)
Bridge.Database.Query([[
INSERT INTO `sky_phone_flare_messages`
(`id`, `match_id`, `sender_account_id`, `body`, `message_type`, `media_url`,
`media_duration_ms`)
VALUES (?, ?, ?, ?, ?, ?, ?)
`media_duration_ms`, `share_payload`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
]], {
id, match.id, account.id, message.body, message.message_type, message.media_url,
message.media_duration_ms,
message.media_duration_ms, message.share_payload,
})
local recipient_account_id = tonumber(match.account_a_id) == tonumber(account.id)
and tonumber(match.account_b_id) or tonumber(match.account_a_id)
@@ -728,6 +749,7 @@ Bridge.Callbacks.Register("sky_phone:flare:send", function(source, data)
message_type = message.message_type,
media_url = message.media_url,
media_duration_ms = message.media_duration_ms,
share_payload = message.share_payload,
}, account.id),
}
end)
+100 -1
View File
@@ -97,9 +97,59 @@ local function current_device(source)
return device
end
local function shared_contact(device, contact_id)
if type(contact_id) ~= "string" or contact_id == "" or #contact_id > 36 then
return nil, "invalid_contact"
end
local condition
local params
if device.account_id then
condition = "contact.`account_id` = ?"
params = { contact_id, tonumber(device.account_id) }
else
condition = "contact.`account_id` IS NULL AND contact.`device_imei` = ?"
params = { contact_id, device.imei }
end
local rows = Bridge.Database.Query(([[
SELECT contact.`name`, contact.`organization`, contact.`phone_number`, media.`url` AS `avatar_url`
FROM `sky_phone_contacts` contact
LEFT JOIN `sky_phone_media` media ON media.`id` = contact.`avatar_media_id`
WHERE contact.`contact_id` = ? AND %s
LIMIT 1
]]):format(condition), params)
local contact = rows[1]
if not contact then
return nil, "contact_not_found"
end
local name = trim(contact.name)
local organization = trim(contact.organization)
local number = SkyPhoneSimNumber.Normalize(
contact.phone_number,
Config.Sim.NumberLength,
Config.Sim.NumberPrefix
)
if not name or name == "" or #name > Config.Calls.ContactNameMaxLength
or (organization and #organization > Config.Calls.ContactNameMaxLength) or not number
then
error(("[sky_phone] Contact %s cannot be shared because its stored data is invalid."):format(contact_id))
end
local snapshot = {
avatar_url = contact.avatar_url,
name = name,
organization = organization ~= "" and organization or nil,
phone_number = number,
}
return {
data = snapshot,
payload = json.encode(snapshot),
}
end
local function format_message(row)
row.media_duration_ms = tonumber(row.media_duration_ms)
row.media_asset_id = nil
row.contact = nil
row.share = nil
if row.message_type == "voice" then
local waveform = row.media_waveform and json.decode(row.media_waveform) or nil
if type(waveform) ~= "table" then
@@ -107,6 +157,33 @@ local function format_message(row)
end
row.media_waveform = waveform
row.media_payload = nil
elseif row.message_type == "contact" then
local contact = row.media_payload and json.decode(row.media_payload) or nil
if type(contact) ~= "table" or type(contact.name) ~= "string"
or type(contact.phone_number) ~= "string"
or (contact.avatar_url ~= nil and type(contact.avatar_url) ~= "string")
or (contact.organization ~= nil and type(contact.organization) ~= "string")
then
error(("[sky_phone] Message %s has an invalid contact payload."):format(tostring(row.id)))
end
row.contact = contact
row.media_payload = nil
row.media_duration_ms = nil
row.media_mime = nil
row.media_waveform = nil
elseif row.message_type == "share" then
local share = row.media_payload and json.decode(row.media_payload) or nil
if type(share) ~= "table" or type(share.appId) ~= "string"
or type(share.kind) ~= "string" or type(share.title) ~= "string"
or type(share.copyText) ~= "string"
then
error(("[sky_phone] Message %s has an invalid share payload."):format(tostring(row.id)))
end
row.share = share
row.media_payload = nil
row.media_duration_ms = nil
row.media_mime = nil
row.media_waveform = nil
elseif attachment_assets[row.message_type] then
if not valid_stored_attachment(row.message_type, row.media_payload) then
error(("[sky_phone] Message %s has an invalid attachment asset."):format(tostring(row.id)))
@@ -414,6 +491,8 @@ Bridge.Callbacks.Register("sky_phone:messages:send", function(source, data)
local body = trim(data.body) or ""
local voice = nil
local attachment = nil
local contact = nil
local share = nil
if message_type == "text" then
if body == "" or #body > Config.Messages.BodyMaxLength then
return { success = false, error = "invalid_message" }
@@ -424,6 +503,25 @@ Bridge.Callbacks.Register("sky_phone:messages:send", function(source, data)
return { success = false, error = "invalid_voice" }
end
body = ""
elseif message_type == "contact" then
local contact_error
contact, contact_error = shared_contact(device, data.contactId)
if not contact then
return { success = false, error = contact_error }
end
body = contact.data.name
elseif message_type == "share" then
local share_error
local encoded
share, share_error, encoded = SkyPhoneEasyShare.SanitizeChatPayload(source, data.sharePayload)
if not share then
return { success = false, error = share_error or "invalid_payload" }
end
if body ~= "" and #body > Config.Messages.BodyMaxLength then
return { success = false, error = "invalid_message" }
end
share = { data = share, payload = encoded }
body = body ~= "" and body or share.data.title
elseif attachment_assets[message_type] then
attachment = validate_attachment(source, device, message_type, data)
if not attachment then
@@ -459,7 +557,8 @@ Bridge.Callbacks.Register("sky_phone:messages:send", function(source, data)
number,
message_type,
body,
voice and voice.payload or attachment and attachment.payload or nil,
voice and voice.payload or attachment and attachment.payload or contact and contact.payload
or share and share.payload or nil,
voice and voice.mime or attachment and attachment.mime or nil,
voice and voice.duration or attachment and attachment.duration or nil,
voice and voice.waveform or nil,