Files
sky_phone-sky-systems/sky_phone/source/server/fliptok.lua
T
Leon.Schmidt 332060b8e3 ENH - modernize FlipTok and Messages
Replace the mixed legacy Konsta presentation and fixed light surfaces with the shared Sky UI system. Add FlipTok comment replies and reactions, editable profile media, follower lists, external sound metadata, and the required schema migration.
2026-08-14 01:28:48 +02:00

867 lines
49 KiB
Lua

Bridge.Database.AfterMigration("sky_phone", function()
local account_types = { person = true, business = true, organization = true, media = true, event = true }
local visibilities = { public = true, followers = true, private = true }
local report_reasons = { spam = true, harassment = true, dangerous = true, illegal = true, other = true }
local report_actions = { dismiss = true, remove = true }
local music_tracks = {}
local music_track_list = {}
local custom_music_extensions = {
aac = true,
m4a = true,
mp3 = true,
oga = true,
ogg = true,
opus = true,
wav = true,
webm = true,
}
local password_pepper = GetConvar(Config.FlipTok.PasswordPepperConvar, "")
if password_pepper == "" then
Bridge.Debug(
"warn",
"[sky_phone] FlipTok password pepper convar '%s' is empty; configure it before production use.",
Config.FlipTok.PasswordPepperConvar,
{ always = true }
)
end
for _, track in ipairs(Config.FlipTok.MusicTracks) do
local id = tostring(track.Id or track.id or "")
local title = tostring(track.Title or track.title or "")
local artist = tostring(track.Artist or track.artist or "")
local url = tostring(track.Url or track.url or "")
if id == "" or title == "" or artist == "" or url == "" then
error("[sky_phone] Every configured FlipTok music track requires Id, Title, Artist, and Url.")
end
local item = { id = id, title = title, artist = artist, url = url }
music_tracks[id] = item
music_track_list[#music_track_list + 1] = item
end
local function trim(value)
if type(value) ~= "string" then return nil end
return value:match("^%s*(.-)%s*$")
end
local function valid_text(value, minimum, maximum)
local length = type(value) == "string" and utf8.len(value) or nil
return length and length >= minimum and length <= maximum
end
local function valid_custom_music_url(value)
if type(value) ~= "string" or value == "" or #value > Config.Media.UrlMaxLength
or value:find("[^\33-\126]")
then
return false
end
local authority = value:match("^https://([^/%?#]+)")
if not authority or authority:find("@", 1, true) then return false end
local host = authority:match("^([^:]+)")
local port = host and authority:sub(#host + 1) or ""
if not host or (port ~= "" and port ~= ":443") then return false end
host = host:lower()
if host == "localhost" or host:match("%.localhost$") or host:match("%.local$") or host:match("%.internal$")
or host:match("^%d+%.%d+%.%d+%.%d+$") or not host:find("%.")
or host:find("[^a-z0-9%.%-]")
then
return false
end
for label in host:gmatch("[^.]+") do
if #label > 63 or label:match("^%-") or label:match("%-$") then return false end
end
local path = value:match("^https://[^/]+(/[^?#]*)") or ""
local extension = path:match("%.([%w]+)$")
return extension and custom_music_extensions[extension:lower()] == true or false
end
local function affected_rows(result)
if type(result) == "number" then return result end
if type(result) == "table" then return tonumber(result.affectedRows) or tonumber(result.affected_rows) or 0 end
return 0
end
local function are_profiles_blocked(first_id, second_id)
return Bridge.Database.Query([[SELECT `id` FROM `sky_phone_fliptok_blocks` WHERE
(`blocker_id` = ? AND `blocked_id` = ?) OR (`blocker_id` = ? AND `blocked_id` = ?) LIMIT 1]], {
first_id, second_id, second_id, first_id,
})[1] ~= nil
end
local function new_id()
local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
if not rows[1] or type(rows[1].id) ~= "string" then
error("[sky_phone] Database did not generate a FlipTok id.")
end
return rows[1].id
end
local function normalize_handle(value)
local handle = trim(value)
if not handle then return nil end
handle = handle:lower():gsub("^@", "")
if #handle < 3 or #handle > 24 or not handle:match("^[a-z0-9][a-z0-9._]*[a-z0-9]$") or handle:find("..", 1, true) then
return nil
end
return handle
end
local function valid_password(value)
local length = type(value) == "string" and utf8.len(value) or nil
return length and length >= Config.FlipTok.PasswordMinLength and length <= Config.FlipTok.PasswordMaxLength
end
local function profile_for_session(source)
local session, error_response = SkyPhone.RequireSession(source)
if not session then return nil, error_response end
local rows = Bridge.Database.Query([[SELECT p.* FROM `sky_phone_fliptok_sessions` s
JOIN `sky_phone_fliptok_profiles` p ON p.`id` = s.`profile_id`
WHERE s.`device_imei` = ? LIMIT 1]], { session.imei })
if not rows[1] then return nil, { success = false, error = "fliptok_not_authenticated" } end
return rows[1], nil
end
local function require_profile(source)
return profile_for_session(source)
end
local function hydrate_profile(profile, viewer_id)
profile.id = tonumber(profile.id)
profile.avatar_media_id = tonumber(profile.avatar_media_id)
profile.verified = tonumber(profile.verified) == 1
profile.is_following = viewer_id and tonumber(profile.is_following) == 1 or false
profile.is_owner = viewer_id and profile.id == viewer_id or false
profile.followers = tonumber(profile.followers) or 0
profile.following = tonumber(profile.following) or 0
profile.video_count = tonumber(profile.video_count) or 0
return profile
end
local function load_profile(profile_id, viewer_id)
local rows = Bridge.Database.Query([[
SELECT p.*, avatar.`url` AS `avatar_url`,
EXISTS(SELECT 1 FROM `sky_phone_fliptok_follows` f WHERE f.`follower_id` = ? AND f.`following_id` = p.`id`) AS `is_following`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_follows` f WHERE f.`following_id` = p.`id`) AS `followers`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_follows` f WHERE f.`follower_id` = p.`id`) AS `following`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_videos` v WHERE v.`profile_id` = p.`id` AND v.`status` = 'published') AS `video_count`
FROM `sky_phone_fliptok_profiles` p
LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = p.`avatar_media_id`
WHERE p.`id` = ? LIMIT 1
]], { viewer_id, profile_id })
return rows[1] and hydrate_profile(rows[1], viewer_id) or nil
end
local function notify_profile(recipient_id, actor_id, kind, video_id)
local rows = Bridge.Database.Query([[SELECT recipient.`account_id`, actor.`display_name` AS `actor_name`
FROM `sky_phone_fliptok_profiles` recipient
JOIN `sky_phone_fliptok_profiles` actor ON actor.`id` = ?
WHERE recipient.`id` = ? LIMIT 1]], { actor_id, recipient_id })
SkyPhone.NotifyAccountDevices(tonumber(rows[1].account_id), "sky_phone:fliptok:new", {
actor = rows[1].actor_name,
kind = kind,
videoId = video_id,
})
end
local function hydrate_videos(rows)
for _, video in ipairs(rows) do
video.profile_id = tonumber(video.profile_id)
video.verified = tonumber(video.verified) == 1
video.comments_enabled = tonumber(video.comments_enabled) == 1
video.is_liked = tonumber(video.is_liked) == 1
video.is_saved = tonumber(video.is_saved) == 1
video.is_following = tonumber(video.is_following) == 1
video.is_owner = tonumber(video.is_owner) == 1
video.like_count = tonumber(video.like_count) or 0
video.comment_count = tonumber(video.comment_count) or 0
video.view_count = tonumber(video.view_count) or 0
video.share_count = tonumber(video.share_count) or 0
video.trim_start_ms = tonumber(video.trim_start_ms) or 0
video.trim_end_ms = tonumber(video.trim_end_ms)
video.cover_time_ms = tonumber(video.cover_time_ms) or 0
video.original_volume = tonumber(video.original_volume) or 100
video.music_volume = tonumber(video.music_volume) or 0
local track = music_tracks[video.music_track]
local custom_music_url = type(video.custom_music_url) == "string" and video.custom_music_url or ""
local youtube_id = SkyPhoneYouTube.ParseId(custom_music_url)
video.music_title = track and track.title or video.custom_music_title or ""
video.music_artist = track and track.artist or video.custom_music_artist or ""
video.music_url = track and track.url or (youtube_id and "" or custom_music_url)
video.music_source = track and "audio" or (youtube_id and "youtube" or (custom_music_url ~= "" and "audio" or ""))
video.music_video_id = youtube_id or ""
video.custom_music_url = nil
video.custom_music_title = nil
video.custom_music_artist = nil
video.created_at = (tonumber(video.created_at_unix) or 0) * 1000
video.created_at_unix = nil
end
return rows
end
local function list_videos(viewer_id, where_clause, values, limit, offset, ranking)
local parameters = { viewer_id, viewer_id, viewer_id, viewer_id }
for _, value in ipairs(values) do parameters[#parameters + 1] = value end
parameters[#parameters + 1] = limit
parameters[#parameters + 1] = offset
return hydrate_videos(Bridge.Database.Query(([[
SELECT v.`id`, v.`profile_id`, v.`caption`, v.`location`, v.`comments_enabled`, v.`view_count`, v.`share_count`,
v.`trim_start_ms`, v.`trim_end_ms`, v.`cover_time_ms`, v.`original_volume`, v.`music_volume`, v.`music_track`,
v.`custom_music_url`, v.`custom_music_title`, v.`custom_music_artist`,
m.`url`, UNIX_TIMESTAMP(v.`created_at`) AS `created_at_unix`, p.`handle`, p.`display_name`, p.`verified`,
avatar.`url` AS `avatar_url`,
(v.`profile_id` = ?) AS `is_owner`,
EXISTS(SELECT 1 FROM `sky_phone_fliptok_reactions` r WHERE r.`video_id` = v.`id` AND r.`profile_id` = ? AND r.`kind` = 'like') AS `is_liked`,
EXISTS(SELECT 1 FROM `sky_phone_fliptok_reactions` r WHERE r.`video_id` = v.`id` AND r.`profile_id` = ? AND r.`kind` = 'save') AS `is_saved`,
EXISTS(SELECT 1 FROM `sky_phone_fliptok_follows` f WHERE f.`follower_id` = ? AND f.`following_id` = v.`profile_id`) AS `is_following`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_reactions` r WHERE r.`video_id` = v.`id` AND r.`kind` = 'like') AS `like_count`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_comments` c WHERE c.`video_id` = v.`id` AND c.`status` = 'visible') AS `comment_count`
FROM `sky_phone_fliptok_videos` v
JOIN `sky_phone_fliptok_profiles` p ON p.`id` = v.`profile_id`
JOIN `sky_phone_media` m ON m.`id` = v.`media_id`
LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = p.`avatar_media_id`
WHERE v.`status` = 'published' AND %s
AND NOT EXISTS(SELECT 1 FROM `sky_phone_fliptok_blocks` b WHERE
(b.`blocker_id` = ? AND b.`blocked_id` = v.`profile_id`) OR (b.`blocked_id` = ? AND b.`blocker_id` = v.`profile_id`))
ORDER BY %s LIMIT ? OFFSET ?
]]):format(where_clause, ranking), parameters))
end
local function feed(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
data = type(data) == "table" and data or {}
local offset = math.max(0, math.floor(tonumber(data.offset) or 0))
local limit = Config.FlipTok.PageSize
local where = "v.`visibility` = 'public'"
local ranking = "(v.`view_count` + v.`share_count` * 8 + (SELECT COUNT(*) FROM `sky_phone_fliptok_reactions` rr WHERE rr.`video_id` = v.`id`) * 4) DESC, v.`created_at` DESC"
if data.mode == "following" then
where = "v.`visibility` IN ('public', 'followers') AND EXISTS(SELECT 1 FROM `sky_phone_fliptok_follows` ff WHERE ff.`follower_id` = ? AND ff.`following_id` = v.`profile_id`)"
ranking = "v.`created_at` DESC"
end
local values = data.mode == "following" and { profile.id, profile.id, profile.id } or { profile.id, profile.id }
local rows = list_videos(profile.id, where, values, limit + 1, offset, ranking)
local has_more = #rows > limit
if has_more then rows[#rows] = nil end
return { success = true, data = { items = rows, offset = offset, hasMore = has_more } }
end
Bridge.Callbacks.Register("sky_phone:fliptok:register", function(source, data)
if not SkyPhone.AllowOperation(source, "fliptok:register", 5, 60) then
return { success = false, error = "rate_limited" }
end
local account, error_response = SkyPhone.RequireAccount(source)
if not account then return error_response end
if type(data) ~= "table" then return { success = false, error = "invalid_request" } end
local handle = normalize_handle(data.handle)
local display_name = trim(data.displayName)
if not handle then return { success = false, error = "invalid_handle" } end
if not valid_text(display_name, 1, 40) then return { success = false, error = "invalid_display_name" } end
if not valid_password(data.password) then return { success = false, error = "invalid_password" } end
local profiles = Bridge.Database.Query("SELECT `id` FROM `sky_phone_fliptok_profiles` WHERE `account_id` = ? LIMIT 1", { account.id })
local profile_id = profiles[1] and tonumber(profiles[1].id) or nil
local duplicates = profile_id and Bridge.Database.Query(
"SELECT `id` FROM `sky_phone_fliptok_profiles` WHERE `handle` = ? AND `id` <> ? LIMIT 1",
{ handle, profile_id }
) or Bridge.Database.Query(
"SELECT `id` FROM `sky_phone_fliptok_profiles` WHERE `handle` = ? LIMIT 1",
{ handle }
)
if duplicates[1] then return { success = false, error = "handle_taken" } end
if profile_id then
local credentials = Bridge.Database.Query("SELECT `profile_id` FROM `sky_phone_fliptok_credentials` WHERE `profile_id` = ? LIMIT 1", { profile_id })
if credentials[1] then return { success = false, error = "already_registered" } end
Bridge.Database.Query("UPDATE `sky_phone_fliptok_profiles` SET `handle` = ?, `display_name` = ? WHERE `id` = ?", {
handle, display_name, profile_id,
})
else
local result = Bridge.Database.Query([[INSERT IGNORE INTO `sky_phone_fliptok_profiles`
(`account_id`, `handle`, `display_name`) VALUES (?, ?, ?)]], { account.id, handle, display_name })
if affected_rows(result) ~= 1 then return { success = false, error = "handle_taken" } end
local created = Bridge.Database.Query("SELECT `id` FROM `sky_phone_fliptok_profiles` WHERE `account_id` = ? LIMIT 1", { account.id })
if not created[1] then error("[sky_phone] FlipTok profile insert did not return the created profile.") end
profile_id = tonumber(created[1].id)
end
local salts = Bridge.Database.Query("SELECT REPLACE(UUID(), '-', '') AS `salt`", {})
local salt = salts[1] and salts[1].salt
if type(salt) ~= "string" or #salt ~= 32 then error("[sky_phone] Database did not generate a FlipTok password salt.") end
local credential_result = Bridge.Database.Query([[INSERT IGNORE INTO `sky_phone_fliptok_credentials`
(`profile_id`, `password_hash`, `password_salt`) VALUES (?, UNHEX(SHA2(CONCAT(?, ?, ?), 256)), ?)]], {
profile_id, password_pepper, salt, data.password, salt,
})
if affected_rows(credential_result) ~= 1 then return { success = false, error = "already_registered" } end
Bridge.Database.Query([[INSERT INTO `sky_phone_fliptok_sessions` (`device_imei`, `profile_id`) VALUES (?, ?)
ON DUPLICATE KEY UPDATE `profile_id` = VALUES(`profile_id`), `updated_at` = CURRENT_TIMESTAMP]], {
account.imei, profile_id,
})
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:login", function(source, data)
if not SkyPhone.AllowOperation(source, "fliptok:login", 10, 60) then
return { success = false, error = "rate_limited" }
end
local session, error_response = SkyPhone.RequireSession(source)
if not session then return error_response end
if type(data) ~= "table" then return { success = false, error = "invalid_credentials" } end
local handle = normalize_handle(data.handle)
if not handle or not valid_password(data.password) then
return { success = false, error = "invalid_credentials" }
end
local profiles = Bridge.Database.Query([[SELECT p.`id` FROM `sky_phone_fliptok_profiles` p
JOIN `sky_phone_fliptok_credentials` c ON c.`profile_id` = p.`id`
WHERE p.`handle` = ?
AND c.`password_hash` = UNHEX(SHA2(CONCAT(?, c.`password_salt`, ?), 256))
LIMIT 1]], { handle, password_pepper, data.password })
if not profiles[1] then return { success = false, error = "invalid_credentials" } end
Bridge.Database.Query([[INSERT INTO `sky_phone_fliptok_sessions` (`device_imei`, `profile_id`) VALUES (?, ?)
ON DUPLICATE KEY UPDATE `profile_id` = VALUES(`profile_id`), `updated_at` = CURRENT_TIMESTAMP]], {
session.imei, profiles[1].id,
})
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:logout", function(source)
local session, error_response = SkyPhone.RequireSession(source)
if not session then return error_response end
Bridge.Database.Query("DELETE FROM `sky_phone_fliptok_sessions` WHERE `device_imei` = ?", { session.imei })
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:bootstrap", function(source)
local profile, error_response = require_profile(source)
if not profile then
if error_response.error == "fliptok_not_authenticated" then
return { success = true, data = { authenticated = false, musicTracks = music_track_list } }
end
return error_response
end
local result = feed(source, { mode = "for-you", offset = 0 })
if not result.success then return result end
return { success = true, data = {
authenticated = true,
profile = load_profile(profile.id, profile.id),
feed = result.data,
isAdmin = Bridge.Framework.HasAdminGroup(source, Config.FlipTok.ReportAdminGroups),
musicTracks = music_track_list,
} }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:feed", feed)
Bridge.Callbacks.Register("sky_phone:fliptok:video", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:video", 60, 60) then
return { success = false, error = "rate_limited" }
end
local video_id = type(data) == "table" and data.id or nil
if type(video_id) ~= "string" or #video_id > 64 then
return { success = false, error = "video_not_found" }
end
local rows = list_videos(
profile.id,
[[v.`id` = ? AND (v.`visibility` = 'public' OR v.`profile_id` = ? OR
(v.`visibility` = 'followers' AND EXISTS(
SELECT 1 FROM `sky_phone_fliptok_follows` follow
WHERE follow.`follower_id` = ? AND follow.`following_id` = v.`profile_id`)))]],
{ video_id, profile.id, profile.id, profile.id, profile.id },
1,
0,
"v.`created_at` DESC"
)
return rows[1]
and { success = true, data = rows[1] }
or { success = false, error = "video_not_found" }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:discover", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
local search = type(data) == "table" and trim(data.search) or ""
if search and utf8.len(search) > 50 then return { success = false, error = "invalid_request" } end
local pattern = "%" .. (search or "") .. "%"
local rows = list_videos(profile.id, "v.`visibility` = 'public' AND (p.`handle` LIKE ? OR p.`display_name` LIKE ? OR v.`caption` LIKE ?)", { pattern, pattern, pattern, profile.id, profile.id }, Config.FlipTok.PageSize, 0, "v.`created_at` DESC")
return { success = true, data = rows }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:music-metadata", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:music-metadata", 12, 60) then
return { success = false, error = "rate_limited" }
end
local value = trim(type(data) == "table" and data.url or nil)
local video_id = SkyPhoneYouTube.ParseId(value)
if not video_id then return { success = false, error = "invalid_music_url" } end
local metadata = SkyPhoneYouTube.FetchMetadata(video_id)
return { success = true, data = {
url = SkyPhoneYouTube.WatchUrl(video_id),
videoId = video_id,
title = metadata and metadata.title or "YouTube " .. video_id,
artist = metadata and metadata.artist or "YouTube",
} }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:publish", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:publish", 6, 60) then return { success = false, error = "rate_limited" } end
if type(data) ~= "table" then return { success = false, error = "invalid_video" } end
local media_id = tonumber(data.mediaId)
local caption = trim(data.caption) or ""
local location = trim(data.location) or ""
local visibility = data.visibility or "public"
local trim_start_ms = math.floor(tonumber(data.trimStartMs) or 0)
local trim_end_ms = data.trimEndMs ~= nil and math.floor(tonumber(data.trimEndMs) or -1) or nil
local cover_time_ms = math.floor(tonumber(data.coverTimeMs) or 0)
local original_volume = math.floor(tonumber(data.originalVolume) or 100)
local music_volume = math.floor(tonumber(data.musicVolume) or 0)
local music_track = type(data.musicTrack) == "string" and data.musicTrack or ""
local custom_music_url = trim(data.customMusicUrl) or ""
local custom_music_title = ""
local custom_music_artist = ""
if not media_id or media_id < 1 or media_id ~= math.floor(media_id)
or not valid_text(caption, 0, Config.FlipTok.CaptionMaxLength)
or not valid_text(location, 0, 80) or not visibilities[visibility]
or type(data.commentsEnabled) ~= "boolean"
or trim_start_ms < 0 or trim_start_ms > Config.FlipTok.MaxVideoDurationMs
or (trim_end_ms and (trim_end_ms <= trim_start_ms or trim_end_ms > Config.FlipTok.MaxVideoDurationMs))
or cover_time_ms < trim_start_ms or (trim_end_ms and cover_time_ms > trim_end_ms)
or original_volume < 0 or original_volume > 100 or music_volume < 0 or music_volume > 100
or (music_track ~= "" and not music_tracks[music_track])
then return { success = false, error = "invalid_video" } end
if custom_music_url ~= "" then
if music_track ~= "" then return { success = false, error = "invalid_music_url" } end
local youtube_id = SkyPhoneYouTube.ParseId(custom_music_url)
if youtube_id then
local metadata = SkyPhoneYouTube.FetchMetadata(youtube_id)
custom_music_url = SkyPhoneYouTube.WatchUrl(youtube_id)
custom_music_title = metadata and metadata.title or "YouTube " .. youtube_id
custom_music_artist = metadata and metadata.artist or "YouTube"
elseif not valid_custom_music_url(custom_music_url) then
return { success = false, error = "invalid_music_url" }
end
end
if music_track == "" and custom_music_url == "" then music_volume = 0 end
if not SkyPhoneMedia.ResolveOwnedMedia(source, tostring(media_id), "video") then
return { success = false, error = "invalid_media" }
end
local id = new_id()
Bridge.Database.Query([[INSERT INTO `sky_phone_fliptok_videos`
(`id`, `profile_id`, `media_id`, `caption`, `location`, `visibility`, `comments_enabled`, `trim_start_ms`, `trim_end_ms`,
`cover_time_ms`, `original_volume`, `music_volume`, `music_track`, `custom_music_url`, `custom_music_title`,
`custom_music_artist`, `status`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)]], {
id, profile.id, media_id, caption, location, visibility, data.commentsEnabled and 1 or 0, trim_start_ms, trim_end_ms,
cover_time_ms, original_volume, music_volume, music_track, custom_music_url, custom_music_title, custom_music_artist,
data.draft == true and "draft" or "published",
})
return { success = true, data = { id = id } }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:react", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:react", 60, 60) then return { success = false, error = "rate_limited" } end
if type(data) ~= "table" or type(data.id) ~= "string" or (data.kind ~= "like" and data.kind ~= "save") or type(data.active) ~= "boolean" then
return { success = false, error = "invalid_request" }
end
local videos = Bridge.Database.Query("SELECT `profile_id` FROM `sky_phone_fliptok_videos` WHERE `id` = ? AND `status` = 'published' LIMIT 1", { data.id })
if not videos[1] then return { success = false, error = "video_not_found" } end
local owner_id = tonumber(videos[1].profile_id)
if owner_id ~= profile.id and are_profiles_blocked(profile.id, owner_id) then
return { success = false, error = "blocked" }
end
if data.active then
local inserted = Bridge.Database.Query("INSERT IGNORE INTO `sky_phone_fliptok_reactions` (`video_id`, `profile_id`, `kind`) VALUES (?, ?, ?)", { data.id, profile.id, data.kind })
if affected_rows(inserted) > 0 and data.kind == "like" and owner_id ~= profile.id then
Bridge.Database.Query("INSERT INTO `sky_phone_fliptok_notifications` (`id`, `recipient_id`, `actor_id`, `video_id`, `kind`) VALUES (?, ?, ?, ?, 'like')", { new_id(), videos[1].profile_id, profile.id, data.id })
notify_profile(owner_id, profile.id, "like", data.id)
end
else
Bridge.Database.Query("DELETE FROM `sky_phone_fliptok_reactions` WHERE `video_id` = ? AND `profile_id` = ? AND `kind` = ?", { data.id, profile.id, data.kind })
end
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:follow", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
local target_id = type(data) == "table" and tonumber(data.profileId) or nil
if not target_id or target_id == profile.id or type(data.active) ~= "boolean" then return { success = false, error = "invalid_request" } end
if are_profiles_blocked(profile.id, target_id) then return { success = false, error = "blocked" } end
if data.active then
local inserted = Bridge.Database.Query("INSERT IGNORE INTO `sky_phone_fliptok_follows` (`follower_id`, `following_id`) SELECT ?, `id` FROM `sky_phone_fliptok_profiles` WHERE `id` = ?", { profile.id, target_id })
if affected_rows(inserted) > 0 then
Bridge.Database.Query("INSERT INTO `sky_phone_fliptok_notifications` (`id`, `recipient_id`, `actor_id`, `kind`) VALUES (?, ?, ?, 'follow')", { new_id(), target_id, profile.id })
notify_profile(target_id, profile.id, "follow")
end
else
Bridge.Database.Query("DELETE FROM `sky_phone_fliptok_follows` WHERE `follower_id` = ? AND `following_id` = ?", { profile.id, target_id })
end
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:comments", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if type(data) ~= "table" or type(data.id) ~= "string" then return { success = false, error = "invalid_request" } end
local rows = Bridge.Database.Query([[SELECT c.`id`, c.`parent_id`, c.`body`, UNIX_TIMESTAMP(c.`created_at`) * 1000 AS `created_at`,
p.`id` AS `profile_id`, p.`handle`, p.`display_name`, p.`verified`, avatar.`url` AS `avatar_url`,
parent_author.`handle` AS `reply_to_handle`,
EXISTS(SELECT 1 FROM `sky_phone_fliptok_comment_reactions` reaction
WHERE reaction.`comment_id` = c.`id` AND reaction.`profile_id` = ?) AS `is_liked`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_comment_reactions` reaction
WHERE reaction.`comment_id` = c.`id`) AS `like_count`
FROM `sky_phone_fliptok_comments` c
JOIN `sky_phone_fliptok_profiles` p ON p.`id` = c.`profile_id`
LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = p.`avatar_media_id`
LEFT JOIN `sky_phone_fliptok_comments` parent ON parent.`id` = c.`parent_id`
LEFT JOIN `sky_phone_fliptok_profiles` parent_author ON parent_author.`id` = parent.`profile_id`
WHERE c.`video_id` = ? AND c.`status` = 'visible'
AND NOT EXISTS(SELECT 1 FROM `sky_phone_fliptok_blocks` b WHERE
(b.`blocker_id` = ? AND b.`blocked_id` = c.`profile_id`) OR (b.`blocked_id` = ? AND b.`blocker_id` = c.`profile_id`))
ORDER BY COALESCE(parent.`created_at`, c.`created_at`) DESC,
c.`parent_id` IS NOT NULL, c.`created_at` ASC LIMIT 100]], {
profile.id, data.id, profile.id, profile.id,
})
for _, row in ipairs(rows) do
row.verified = tonumber(row.verified) == 1
row.is_liked = tonumber(row.is_liked) == 1
row.like_count = tonumber(row.like_count) or 0
end
return { success = true, data = rows }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:comment", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:comment", 20, 60) then return { success = false, error = "rate_limited" } end
local body = type(data) == "table" and trim(data.body) or nil
local parent_id = type(data) == "table" and trim(data.parentId) or nil
if type(data) ~= "table" or type(data.id) ~= "string" or not valid_text(body, 1, Config.FlipTok.CommentMaxLength) then return { success = false, error = "invalid_comment" } end
if parent_id and #parent_id ~= 36 then return { success = false, error = "invalid_comment" } end
local videos = Bridge.Database.Query("SELECT `profile_id` FROM `sky_phone_fliptok_videos` WHERE `id` = ? AND `status` = 'published' AND `comments_enabled` = 1 LIMIT 1", { data.id })
if not videos[1] then return { success = false, error = "comments_disabled" } end
local owner_id = tonumber(videos[1].profile_id)
if owner_id ~= profile.id and are_profiles_blocked(profile.id, owner_id) then
return { success = false, error = "blocked" }
end
if parent_id then
local parents = Bridge.Database.Query([[SELECT c.`id`, c.`profile_id` FROM `sky_phone_fliptok_comments` c
WHERE c.`id` = ? AND c.`video_id` = ? AND c.`status` = 'visible' LIMIT 1]], { parent_id, data.id })
if not parents[1] or are_profiles_blocked(profile.id, tonumber(parents[1].profile_id)) then
return { success = false, error = "invalid_comment" }
end
end
local comment_id = new_id()
Bridge.Database.Query("INSERT INTO `sky_phone_fliptok_comments` (`id`, `video_id`, `profile_id`, `parent_id`, `body`) VALUES (?, ?, ?, ?, ?)", {
comment_id, data.id, profile.id, parent_id, body,
})
if tonumber(videos[1].profile_id) ~= profile.id then
Bridge.Database.Query("INSERT INTO `sky_phone_fliptok_notifications` (`id`, `recipient_id`, `actor_id`, `video_id`, `kind`) VALUES (?, ?, ?, ?, 'comment')", { new_id(), videos[1].profile_id, profile.id, data.id })
notify_profile(owner_id, profile.id, "comment", data.id)
end
return { success = true, data = { id = comment_id } }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:comment-react", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:comment-react", 60, 60) then
return { success = false, error = "rate_limited" }
end
local comment_id = type(data) == "table" and data.id or nil
if type(comment_id) ~= "string" or #comment_id ~= 36 or type(data.active) ~= "boolean" then
return { success = false, error = "invalid_request" }
end
local comments = Bridge.Database.Query([[SELECT c.`profile_id` FROM `sky_phone_fliptok_comments` c
JOIN `sky_phone_fliptok_videos` v ON v.`id` = c.`video_id`
WHERE c.`id` = ? AND c.`status` = 'visible' AND v.`status` = 'published' LIMIT 1]], { comment_id })
if not comments[1] then return { success = false, error = "invalid_comment" } end
if are_profiles_blocked(profile.id, tonumber(comments[1].profile_id)) then
return { success = false, error = "blocked" }
end
if data.active then
Bridge.Database.Query([[INSERT IGNORE INTO `sky_phone_fliptok_comment_reactions`
(`comment_id`, `profile_id`) VALUES (?, ?)]], { comment_id, profile.id })
else
Bridge.Database.Query([[DELETE FROM `sky_phone_fliptok_comment_reactions`
WHERE `comment_id` = ? AND `profile_id` = ?]], { comment_id, profile.id })
end
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:view", function(source, data)
local _, error_response = require_profile(source)
if error_response then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:view", 120, 60) then return { success = false, error = "rate_limited" } end
if type(data) ~= "table" or type(data.id) ~= "string" then return { success = false, error = "invalid_request" } end
Bridge.Database.Query("UPDATE `sky_phone_fliptok_videos` SET `view_count` = `view_count` + 1 WHERE `id` = ? AND `status` = 'published'", { data.id })
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:share", function(source, data)
local _, error_response = require_profile(source)
if error_response then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:share", 30, 60) then return { success = false, error = "rate_limited" } end
if type(data) ~= "table" or type(data.id) ~= "string" then return { success = false, error = "invalid_request" } end
Bridge.Database.Query("UPDATE `sky_phone_fliptok_videos` SET `share_count` = `share_count` + 1 WHERE `id` = ? AND `status` = 'published'", { data.id })
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:profile", function(source, data)
local viewer, error_response = require_profile(source)
if not viewer then return error_response end
local handle = type(data) == "table" and trim(data.handle) or nil
local id = type(data) == "table" and tonumber(data.profileId) or viewer.id
local rows = handle and Bridge.Database.Query("SELECT `id` FROM `sky_phone_fliptok_profiles` WHERE `handle` = ? LIMIT 1", { handle }) or { { id = id } }
if not rows[1] then return { success = false, error = "profile_not_found" } end
if tonumber(rows[1].id) ~= viewer.id and are_profiles_blocked(viewer.id, tonumber(rows[1].id)) then
return { success = false, error = "profile_not_found" }
end
local target = load_profile(tonumber(rows[1].id), viewer.id)
if not target then return { success = false, error = "profile_not_found" } end
local videos = list_videos(viewer.id, "v.`profile_id` = ? AND (v.`visibility` = 'public' OR v.`profile_id` = ?)", { target.id, viewer.id, viewer.id, viewer.id }, 60, 0, "v.`created_at` DESC")
return { success = true, data = { profile = target, videos = videos } }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:connections", function(source, data)
local viewer, error_response = require_profile(source)
if not viewer then return error_response end
local target_id = type(data) == "table" and tonumber(data.profileId) or nil
local mode = type(data) == "table" and data.mode or nil
if not target_id or (mode ~= "followers" and mode ~= "following") then
return { success = false, error = "invalid_request" }
end
if target_id ~= viewer.id and are_profiles_blocked(viewer.id, target_id) then
return { success = false, error = "profile_not_found" }
end
local join_column = mode == "followers" and "f.`follower_id`" or "f.`following_id`"
local filter_column = mode == "followers" and "f.`following_id`" or "f.`follower_id`"
local rows = Bridge.Database.Query(([[SELECT p.*, avatar.`url` AS `avatar_url`,
EXISTS(SELECT 1 FROM `sky_phone_fliptok_follows` own_follow
WHERE own_follow.`follower_id` = ? AND own_follow.`following_id` = p.`id`) AS `is_following`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_follows` followers WHERE followers.`following_id` = p.`id`) AS `followers`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_follows` following WHERE following.`follower_id` = p.`id`) AS `following`,
(SELECT COUNT(*) FROM `sky_phone_fliptok_videos` video WHERE video.`profile_id` = p.`id` AND video.`status` = 'published') AS `video_count`
FROM `sky_phone_fliptok_follows` f
JOIN `sky_phone_fliptok_profiles` p ON p.`id` = %s
LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = p.`avatar_media_id`
WHERE %s = ? AND NOT EXISTS(SELECT 1 FROM `sky_phone_fliptok_blocks` block WHERE
(block.`blocker_id` = ? AND block.`blocked_id` = p.`id`) OR
(block.`blocked_id` = ? AND block.`blocker_id` = p.`id`))
ORDER BY p.`display_name`, p.`handle` LIMIT 200]]):format(join_column, filter_column), {
viewer.id, target_id, viewer.id, viewer.id,
})
for _, row in ipairs(rows) do hydrate_profile(row, viewer.id) end
return { success = true, data = rows }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:update-profile", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if type(data) ~= "table" then return { success = false, error = "invalid_request" } end
local handle = normalize_handle(data.handle)
local display_name = trim(data.displayName)
local bio = trim(data.bio)
local account_type = data.accountType
if not handle
or not valid_text(display_name, 1, 40) or not valid_text(bio, 0, Config.FlipTok.BioMaxLength) or not account_types[account_type]
then return { success = false, error = "invalid_profile" } end
local avatar_media_id = profile.avatar_media_id and tonumber(profile.avatar_media_id) or nil
if data.avatarMediaId ~= nil then
local requested_avatar_id = tonumber(data.avatarMediaId)
if not requested_avatar_id or requested_avatar_id < 0 or requested_avatar_id ~= math.floor(requested_avatar_id) then
return { success = false, error = "invalid_profile_image" }
end
if requested_avatar_id > 0 and not SkyPhoneMedia.ResolveOwnedMedia(source, requested_avatar_id, "photo") then
return { success = false, error = "invalid_profile_image" }
end
avatar_media_id = requested_avatar_id > 0 and requested_avatar_id or nil
end
local duplicate = Bridge.Database.Query("SELECT `id` FROM `sky_phone_fliptok_profiles` WHERE `handle` = ? AND `id` <> ? LIMIT 1", { handle, profile.id })
if duplicate[1] then return { success = false, error = "handle_taken" } end
Bridge.Database.Query("UPDATE `sky_phone_fliptok_profiles` SET `handle` = ?, `display_name` = ?, `bio` = ?, `account_type` = ?, `avatar_media_id` = ? WHERE `id` = ?", {
handle, display_name, bio, account_type, avatar_media_id, profile.id,
})
return { success = true, data = load_profile(profile.id, profile.id) }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:activities", function(source)
local profile, error_response = require_profile(source)
if not profile then return error_response end
local rows = Bridge.Database.Query([[SELECT n.`id`, n.`kind`, n.`video_id`, n.`read_at`, UNIX_TIMESTAMP(n.`created_at`) * 1000 AS `created_at`,
p.`id` AS `profile_id`, p.`handle`, p.`display_name`, p.`verified`, avatar.`url` AS `avatar_url`
FROM `sky_phone_fliptok_notifications` n JOIN `sky_phone_fliptok_profiles` p ON p.`id` = n.`actor_id`
LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = p.`avatar_media_id`
WHERE n.`recipient_id` = ?
AND (n.`kind` = 'verified' OR NOT EXISTS(SELECT 1 FROM `sky_phone_fliptok_blocks` b WHERE
(b.`blocker_id` = n.`recipient_id` AND b.`blocked_id` = n.`actor_id`) OR
(b.`blocked_id` = n.`recipient_id` AND b.`blocker_id` = n.`actor_id`)))
ORDER BY n.`created_at` DESC LIMIT 100]], { profile.id })
for _, row in ipairs(rows) do row.verified = tonumber(row.verified) == 1 end
return { success = true, data = rows }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:mark-activities", function(source)
local profile, error_response = require_profile(source)
if not profile then return error_response end
Bridge.Database.Query("UPDATE `sky_phone_fliptok_notifications` SET `read_at` = NOW() WHERE `recipient_id` = ? AND `read_at` IS NULL", { profile.id })
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:report", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:report", 10, 60) then return { success = false, error = "rate_limited" } end
local reason = type(data) == "table" and data.reason or nil
local details = type(data) == "table" and trim(data.details) or ""
if type(data) ~= "table" or type(data.id) ~= "string" or not report_reasons[reason] or not valid_text(details, 0, 500) then return { success = false, error = "invalid_report" } end
local videos = Bridge.Database.Query([[SELECT v.`profile_id` FROM `sky_phone_fliptok_videos` v
WHERE v.`id` = ? AND v.`status` = 'published'
AND (v.`profile_id` = ? OR v.`visibility` = 'public' OR
(v.`visibility` = 'followers' AND EXISTS(SELECT 1 FROM `sky_phone_fliptok_follows` f
WHERE f.`follower_id` = ? AND f.`following_id` = v.`profile_id`)))
AND NOT EXISTS(SELECT 1 FROM `sky_phone_fliptok_blocks` b WHERE
(b.`blocker_id` = ? AND b.`blocked_id` = v.`profile_id`) OR
(b.`blocked_id` = ? AND b.`blocker_id` = v.`profile_id`))
LIMIT 1]], { data.id, profile.id, profile.id, profile.id, profile.id })
if not videos[1] then return { success = false, error = "video_not_found" } end
Bridge.Database.Query("INSERT IGNORE INTO `sky_phone_fliptok_reports` (`id`, `reporter_id`, `video_id`, `reason`, `details`) VALUES (?, ?, ?, ?, ?)", { new_id(), profile.id, data.id, reason, details })
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:admin-reports", function(source)
local _, error_response = require_profile(source)
if error_response then return error_response end
if not Bridge.Framework.HasAdminGroup(source, Config.FlipTok.ReportAdminGroups) then
return { success = false, error = "not_authorized" }
end
local rows = Bridge.Database.Query([[SELECT r.`id`, r.`video_id`, r.`reason`, r.`details`,
UNIX_TIMESTAMP(r.`created_at`) * 1000 AS `created_at`, v.`caption`, m.`url`,
reporter.`handle` AS `reporter_handle`, reporter.`display_name` AS `reporter_display_name`,
creator.`handle` AS `creator_handle`, creator.`display_name` AS `creator_display_name`
FROM `sky_phone_fliptok_reports` r
JOIN `sky_phone_fliptok_videos` v ON v.`id` = r.`video_id`
JOIN `sky_phone_media` m ON m.`id` = v.`media_id`
JOIN `sky_phone_fliptok_profiles` reporter ON reporter.`id` = r.`reporter_id`
JOIN `sky_phone_fliptok_profiles` creator ON creator.`id` = v.`profile_id`
WHERE r.`status` = 'open' ORDER BY r.`created_at` ASC LIMIT 200]], {})
return { success = true, data = rows }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:admin-resolve-report", function(source, data)
local _, error_response = require_profile(source)
if error_response then return error_response end
if not Bridge.Framework.HasAdminGroup(source, Config.FlipTok.ReportAdminGroups) then
return { success = false, error = "not_authorized" }
end
local id = type(data) == "table" and data.id or nil
local action = type(data) == "table" and data.action or nil
if type(id) ~= "string" or not report_actions[action] then
return { success = false, error = "invalid_request" }
end
local reports = Bridge.Database.Query("SELECT `video_id` FROM `sky_phone_fliptok_reports` WHERE `id` = ? AND `status` = 'open' LIMIT 1", { id })
if not reports[1] then return { success = false, error = "report_not_found" } end
if action == "remove" then
Bridge.Database.Transaction({
{ query = "UPDATE `sky_phone_fliptok_videos` SET `status` = 'removed' WHERE `id` = ?", params = { reports[1].video_id } },
{ query = "UPDATE `sky_phone_fliptok_reports` SET `status` = 'reviewed' WHERE `video_id` = ? AND `status` = 'open'", params = { reports[1].video_id } },
})
else
Bridge.Database.Query("UPDATE `sky_phone_fliptok_reports` SET `status` = 'dismissed' WHERE `id` = ?", { id })
end
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:block", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
local target_id = type(data) == "table" and tonumber(data.profileId) or nil
if not target_id or target_id == profile.id then return { success = false, error = "invalid_request" } end
Bridge.Database.Transaction({
{ query = "INSERT IGNORE INTO `sky_phone_fliptok_blocks` (`blocker_id`, `blocked_id`) VALUES (?, ?)", params = { profile.id, target_id } },
{ query = "DELETE FROM `sky_phone_fliptok_follows` WHERE (`follower_id` = ? AND `following_id` = ?) OR (`follower_id` = ? AND `following_id` = ?)", params = { profile.id, target_id, target_id, profile.id } },
})
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:delete", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if type(data) ~= "table" or type(data.id) ~= "string" then return { success = false, error = "video_not_found" } end
local result = Bridge.Database.Query("UPDATE `sky_phone_fliptok_videos` SET `status` = 'removed' WHERE `id` = ? AND `profile_id` = ?", { data.id, profile.id })
local affected = type(result) == "number" and result or type(result) == "table" and tonumber(result.affectedRows) or 0
return affected > 0 and { success = true } or { success = false, error = "video_not_found" }
end)
RegisterCommand(Config.FlipTok.VerifyCommand, function(source, arguments)
local command_locale = (Locales[Config.Bridge.Locale] or Locales["en"]).FlipTokCommand
local function command_message(template, values)
return template:gsub("{(%w+)}", function(key) return values[key] or "" end)
end
local function send_command_feedback(message, notification_type)
if source == 0 then
print(message)
return
end
TriggerClientEvent("sky_phone:fliptok:command-feedback", source, {
message = message,
notificationType = notification_type,
})
end
if source ~= 0 and not Bridge.Framework.HasAdminGroup(source, Config.FlipTok.AdminGroups) then
send_command_feedback(command_locale.noPermission, "error")
print(("[sky_phone] Player %d attempted to use the FlipTok verification command without an admin group."):format(source))
return
end
local handle = type(arguments[1]) == "string" and arguments[1]:lower():gsub("^@", "") or ""
local requested = type(arguments[2]) == "string" and arguments[2]:lower() or nil
if handle == "" or (requested and requested ~= "on" and requested ~= "off") then
local message = command_message(command_locale.usage, { command = Config.FlipTok.VerifyCommand })
send_command_feedback(message, "error")
return
end
local rows = Bridge.Database.Query("SELECT `id`, `verified` FROM `sky_phone_fliptok_profiles` WHERE `handle` = ? LIMIT 1", { handle })
if not rows[1] then
local message = command_message(command_locale.notFound, { handle = handle })
send_command_feedback(message, "error")
return
end
local verified
if requested then
verified = requested == "on"
else
verified = tonumber(rows[1].verified) ~= 1
end
Bridge.Database.Query("UPDATE `sky_phone_fliptok_profiles` SET `verified` = ? WHERE `id` = ?", { verified and 1 or 0, rows[1].id })
if verified then
Bridge.Database.Query("INSERT INTO `sky_phone_fliptok_notifications` (`id`, `recipient_id`, `actor_id`, `kind`) VALUES (?, ?, ?, 'verified')", {
new_id(), rows[1].id, rows[1].id,
})
notify_profile(tonumber(rows[1].id), tonumber(rows[1].id), "verified")
end
TriggerClientEvent("sky_phone:fliptok:verification-changed", -1, {
profileId = tonumber(rows[1].id),
verified = verified,
})
local message = command_message(command_locale.updated, {
handle = handle,
state = verified and command_locale.verified or command_locale.unverified,
})
send_command_feedback(message, "success")
end, false)
end)