ADD - implement CrewLink location groups

This commit is contained in:
smx.pusha
2026-08-10 21:02:09 +02:00
parent a2e227d624
commit db623e73e6
20 changed files with 3866 additions and 0 deletions
+23
View File
@@ -365,6 +365,29 @@ Config.MapMarkers = {
ActionsPerMinute = 60,
}
Config.CrewLink = {
UsernameMinLength = 3,
UsernameMaxLength = 20,
GroupNameMinLength = 3,
GroupNameMaxLength = 32,
MaximumGroupsPerProfile = 5,
MaximumMembersPerGroup = 16,
InviteCodeLength = 8,
InviteLifetimeSeconds = 30,
NearbyInviteDistance = 5.0,
NearbyScanLimit = 12,
PingLabelMaxLength = 48,
PingLifetimeSeconds = 300,
MaximumActivePings = 12,
ActionsPerMinute = 30,
LiveRequestsPerMinute = 120,
OverheadRefreshMilliseconds = 3000,
OverheadDistance = 50.0,
ExternalPingResources = {
-- ["example_resource"] = true,
},
}
Config.Calendar = {
TitleMaxLength = 120,
NoteMaxLength = 2000,
+37
View File
@@ -80,6 +80,43 @@ Locales["en"] = {
},
},
Apps = {
crewlink = {
name = "CrewLink", connecting = "Connecting your crew...", privateNetwork = "Private location network",
signInTitle = "Connect your iFruit Account", signInBody = "CrewLink uses your private iFruit identity to keep groups and roles available across your phones.", openSettings = "Open iFruit Settings",
welcomeEyebrow = "Your crew. One signal.", welcomeTitle = "Find your people", welcomeBody = "Choose a private CrewLink username. Only confirmed group members can see your presence and shared location.",
username = "CrewLink username", usernamePlaceholder = "e.g. Skyline", createProfile = "Create CrewLink Profile", profileCreated = "Your CrewLink profile is ready.", privacyNote = "Private by design. No public player search.",
noGroupEyebrow = "No active crew", noGroupTitle = "Build your private network", noGroupBody = "Create a crew or join friends with a private invitation code.",
createGroup = "Create Group", createGroupBody = "Give your crew a recognizable name and signal colour.", joinGroup = "Join Group", joinWithCode = "Join with Code", joinWithCodeBody = "Enter the private eight-character code shared by a coordinator.",
groupName = "Group name", groupNamePlaceholder = "e.g. Night Shift", groupColour = "Signal colour", createCrew = "Create Crew", joinCrew = "Join Crew", inviteCode = "Invitation code", inviteCodePlaceholder = "AB12CD34",
groupCreated = "Crew created.", joinedGroup = "You joined the crew.", activeGroupChanged = "Active crew changed.", pendingInvitations = "Invitations", invitedBy = "Invited by @{username}", inviteAccepted = "Invitation accepted.", inviteDeclined = "Invitation declined.",
navigation = "CrewLink navigation", map = "Map", crew = "Crew", pings = "Pings", profile = "Profile", onlineNow = "online now", zoomIn = "Zoom in", zoomOut = "Zoom out", myLocation = "Center my location",
member = "Member", members = "Members", openCrew = "View team", newPing = "New Ping", shareLocation = "Mark a place", locationUnavailable = "Your live location is currently hidden.", activeCrew = "Active private crew", groupSummarySingle = "{online} of {total} member online", groupSummary = "{online} of {total} members online", private = "Private",
nearby = "Nearby", copyCode = "Copy code", manage = "Manage", refresh = "Refresh", codeCopied = "Invitation code copied.", codeRotated = "A new invitation code is active.",
peopleNearby = "People Nearby", peopleNearbyBody = "Only CrewLink users within {distance} meters appear here.", metersAway = "{distance} m away", invite = "Invite", inviteSent = "Invitation sent to @{username}.", nobodyNearby = "Nobody in range", nobodyNearbyBody = "Move closer to another CrewLink user and scan again.", scanAgain = "Scan Again",
liveCoordination = "Live coordination", noPings = "No active pings", noPingsBody = "Share a meeting point, warning, or target with your crew.", expiresMinutes = "{count} min left", expiresSeconds = "{count} sec left", sharedBy = "Shared by @{username}", setRoute = "Set Route", routeSet = "GPS route set.", pingCreated = "Ping shared with your crew.", pingRemoved = "Ping removed.",
newPingBody = "Share your current position or place the ping at the center of the map.", pingLabel = "Ping label", pingLabelPlaceholder = "e.g. Meet at the garage", placeOnMap = "Use map center", placeOnMapBody = "Otherwise your current position is used.", sharePing = "Share Ping",
pingTypes = { meeting = "Meet", danger = "Danger", help = "Help", target = "Target", info = "Info" },
status = { live = "Live", hidden = "Location hidden", offline = "Offline" },
roles = { owner = "Owner", coordinator = "Coordinator", moderator = "Moderator", member = "Member", guest = "Guest" },
roleDescriptions = { coordinator = "Manage invitations, settings, roles, and pings.", moderator = "Invite nearby users and moderate group pings.", member = "Share presence and create pings when enabled.", guest = "View the active crew without coordination permissions." },
manageCrew = "Manage Crew", manageCrewBody = "Update identity, permissions, and invitations.", memberPings = "Member pings", memberPingsBody = "Allow members and guests to create pings.", allowOverhead = "Overhead labels", allowOverheadBody = "Permit nearby opt-in member labels in the world.", groupSaved = "Crew settings saved.",
assignRole = "Assign Role", roleUpdated = "Member role updated.", transferOwnership = "Transfer Ownership", removeMember = "Remove from Crew", yourCrewLinkId = "Your CrewLink ID", privacyVisibility = "Privacy & Visibility", shareOnMap = "Share on group map", shareOnMapBody = "Active members can see your live position.", overheadLabels = "Nearby overhead label", overheadLabelsBody = "Show your CrewLink name to nearby opted-in members.",
yourGroups = "Your Groups", createAnotherGroup = "Create another group", account = "CrewLink Account", externalApi = "Connected resources", externalApiBody = "Approved scripts may add temporary group pings.", editUsername = "Edit Username", editUsernameBody = "Your username is unique across CrewLink.", profileSaved = "CrewLink profile saved.", deleteGroup = "Delete Group", leaveGroup = "Leave Group", confirmAction = "Confirm",
confirm = {
["delete-group"] = { title = "Delete this crew?", body = "The group, memberships, invitations, and active pings will be permanently removed." },
["leave-group"] = { title = "Leave this crew?", body = "You will lose access to its members, map, and pings." },
["remove-member"] = { title = "Remove this member?", body = "They will immediately lose access to this crew." },
["transfer-owner"] = { title = "Transfer ownership?", body = "The selected member becomes owner and you become coordinator." },
},
["delete-groupDone"] = "Crew deleted.", ["leave-groupDone"] = "You left the crew.", ["remove-memberDone"] = "Member removed.", ["transfer-ownerDone"] = "Ownership transferred.",
notifications = { invite = "{actor} invited you to {group}.", member_joined = "{actor} joined {group}.", ping = "{actor} shared '{ping}' with {group}.", role = "Your CrewLink role changed in {group}.", removed = "You were removed from {group}.", default = "Your CrewLink group has an update." },
errors = {
not_authenticated = "Sign in to your iFruit Account first.", profile_required = "Create your CrewLink profile first.", invalid_username = "Use 3-20 letters, numbers, dots, or underscores.", invalid_profile = "Check your profile details.", username_taken = "That CrewLink username is already taken.",
invalid_group = "Choose a valid group name and signal colour.", group_limit = "You have reached your group limit.", member_limit = "This group has reached its member limit.", group_not_found = "This group is no longer available.", invalid_code = "That invitation code is invalid.", already_member = "You are already a member of this group.", forbidden = "Your current role cannot perform this action.",
player_not_nearby = "That player is no longer nearby.", player_unavailable = "That player cannot receive a CrewLink invitation.", invalid_invitation = "This invitation is invalid.", invitation_expired = "This invitation has expired.", invalid_role = "That role cannot be assigned.", owner_must_transfer = "Transfer ownership before leaving this group.",
invalid_ping = "Choose a valid ping type, label, and position.", ping_limit = "This crew already has too many active pings.", ping_not_found = "That ping is no longer active.", rate_limited = "Too many CrewLink actions. Try again shortly.", request_failed = "CrewLink is temporarily unavailable.",
},
},
flare = {
name = "Flare", signInTitle = "Sign in to Flare", signInBody = "Flare profiles and matches are linked to your private iFruit account.",
createProfile = "Create your profile", welcome = "Find your spark", welcomeBody = "Build a profile and meet people across Los Santos.",
+2
View File
@@ -31,6 +31,7 @@ client_scripts {
'source/client/garage.lua',
'source/client/skyride.lua',
'source/client/housing.lua',
'source/client/crewlink.lua',
'source/bridge/client/radio.lua',
'source/client/main.lua',
'source/client/radio.lua',
@@ -71,6 +72,7 @@ server_scripts {
'source/server/picstagram.lua',
'source/server/feather.lua',
'source/server/map.lua',
'source/server/crewlink.lua',
'source/server/skyride.lua',
'source/server/calendar.lua',
'source/server/music.lua',
+44
View File
@@ -0,0 +1,44 @@
local overhead_members = {}
local function draw_overhead_label(coords, username, role)
SetDrawOrigin(coords.x, coords.y, coords.z + 1.05, 0)
SetTextScale(0.0, 0.29)
SetTextFont(4)
SetTextProportional(true)
SetTextColour(235, 245, 255, 235)
SetTextCentre(true)
SetTextOutline()
BeginTextCommandDisplayText("STRING")
AddTextComponentSubstringPlayerName(("~b~%s~s~ %s"):format(username, role))
EndTextCommandDisplayText(0.0, 0.0)
ClearDrawOrigin()
end
CreateThread(function()
while true do
local result = Bridge.Callbacks.Trigger("sky_phone:crewlink:overhead", {})
overhead_members = result and result.success and result.data and result.data.members or {}
Wait(Config.CrewLink.OverheadRefreshMilliseconds)
end
end)
CreateThread(function()
while true do
local sleep = 1000
if #overhead_members > 0 then
sleep = 0
local player_coords = GetEntityCoords(PlayerPedId())
for _, member in ipairs(overhead_members) do
local player = GetPlayerFromServerId(member.source)
if player ~= -1 then
local ped = GetPlayerPed(player)
local coords = GetEntityCoords(ped)
if #(player_coords - coords) <= Config.CrewLink.OverheadDistance then
draw_overhead_label(coords, member.username, member.roleLabel)
end
end
end
end
Wait(sleep)
end
end)
+35
View File
@@ -147,6 +147,25 @@ local server_callbacks = {
"map:markers",
"map:create-marker",
"map:delete-marker",
"crewlink:bootstrap",
"crewlink:create-profile",
"crewlink:update-profile",
"crewlink:create-group",
"crewlink:update-group",
"crewlink:delete-group",
"crewlink:set-active",
"crewlink:join-code",
"crewlink:rotate-code",
"crewlink:nearby",
"crewlink:invite-nearby",
"crewlink:respond-invite",
"crewlink:update-member",
"crewlink:transfer-owner",
"crewlink:remove-member",
"crewlink:leave",
"crewlink:create-ping",
"crewlink:remove-ping",
"crewlink:live",
"sim:insert",
"sim:eject",
"contacts:list",
@@ -617,6 +636,22 @@ RegisterNetEvent("sky_phone:billing:new", function(data)
SendNUIMessage({ type = "billing:new", data = data })
end)
RegisterNetEvent("sky_phone:crewlink:changed", function(data)
SendNUIMessage({ type = "crewlink:changed", data = data })
end)
RegisterNetEvent("sky_phone:crewlink:notification", function(data)
local crewlink_locale = get_locale().Nui.Apps.crewlink
local notification_text = crewlink_locale.notifications[data.kind]
or crewlink_locale.notifications.default
data.title = crewlink_locale.name
data.text = notification_text
:gsub("{actor}", tostring(data.actor or ""))
:gsub("{group}", tostring(data.groupName or ""))
:gsub("{ping}", tostring(data.pingLabel or ""))
SendNUIMessage({ type = "crewlink:notification", data = data })
end)
RegisterNetEvent("sky_phone:messages:changed", function(data)
SendNUIMessage({ type = "messages:changed", data = data })
end)
File diff suppressed because it is too large Load Diff
+122
View File
@@ -1947,6 +1947,128 @@ local schema = {
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_crewlink_profiles",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "account_id", type = "BIGINT UNSIGNED NOT NULL" },
{ name = "username", type = "VARCHAR(20) NOT NULL", characterSet = "ascii", collation = "ascii_general_ci" },
{ name = "active_group_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "map_visible", type = "TINYINT(1) NOT NULL DEFAULT 1" },
{ name = "overhead_visible", type = "TINYINT(1) NOT NULL DEFAULT 0" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_crewlink_account", columns = "(`account_id`)" },
{ name = "uniq_sky_phone_crewlink_username", columns = "(`username`)" },
},
indexes = {
{ name = "idx_sky_phone_crewlink_active", columns = "(`active_group_id`)" },
},
foreignKeys = {
{ column = "account_id", references = "`sky_phone_accounts` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_crewlink_groups",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "name", type = "VARCHAR(32) NOT NULL" },
{ name = "colour", type = "ENUM('cyan','blue','violet','orange','green','rose') NOT NULL DEFAULT 'cyan'" },
{ name = "owner_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "invite_code", type = "VARCHAR(12) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "allow_member_pings", type = "TINYINT(1) NOT NULL DEFAULT 1" },
{ name = "overhead_allowed", type = "TINYINT(1) NOT NULL DEFAULT 1" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_crewlink_invite_code", columns = "(`invite_code`)" },
},
indexes = {
{ name = "idx_sky_phone_crewlink_owner", columns = "(`owner_profile_id`)" },
},
foreignKeys = {
{ column = "owner_profile_id", references = "`sky_phone_crewlink_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_crewlink_memberships",
columns = {
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
{ name = "group_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "role", type = "ENUM('owner','coordinator','moderator','member','guest') NOT NULL DEFAULT 'member'" },
{ name = "joined_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_crewlink_membership", columns = "(`group_id`, `profile_id`)" },
},
indexes = {
{ name = "idx_sky_phone_crewlink_profile_groups", columns = "(`profile_id`, `joined_at`)" },
},
foreignKeys = {
{ column = "group_id", references = "`sky_phone_crewlink_groups` (`id`) ON DELETE CASCADE" },
{ column = "profile_id", references = "`sky_phone_crewlink_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_crewlink_invitations",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "group_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "inviter_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "invitee_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "status", type = "ENUM('pending','accepted','declined','expired') NOT NULL DEFAULT 'pending'" },
{ name = "expires_at", type = "DATETIME NOT NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_crewlink_pending_invite", columns = "(`group_id`, `invitee_profile_id`)" },
},
indexes = {
{ name = "idx_sky_phone_crewlink_invitee", columns = "(`invitee_profile_id`, `status`, `expires_at`)" },
},
foreignKeys = {
{ column = "group_id", references = "`sky_phone_crewlink_groups` (`id`) ON DELETE CASCADE" },
{ column = "inviter_profile_id", references = "`sky_phone_crewlink_profiles` (`id`) ON DELETE CASCADE" },
{ column = "invitee_profile_id", references = "`sky_phone_crewlink_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_crewlink_pings",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "group_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "creator_profile_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "source_resource", type = "VARCHAR(64) NULL", characterSet = "ascii", collation = "ascii_general_ci" },
{ name = "type", type = "ENUM('meeting','danger','help','target','info') NOT NULL" },
{ name = "label", type = "VARCHAR(48) NOT NULL" },
{ name = "position_x", type = "DECIMAL(10,3) NOT NULL" },
{ name = "position_y", type = "DECIMAL(10,3) NOT NULL" },
{ name = "position_z", type = "DECIMAL(10,3) NOT NULL" },
{ name = "expires_at", type = "DATETIME NOT NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_crewlink_pings", columns = "(`group_id`, `expires_at`)" },
},
foreignKeys = {
{ column = "group_id", references = "`sky_phone_crewlink_groups` (`id`) ON DELETE CASCADE" },
{ column = "creator_profile_id", references = "`sky_phone_crewlink_profiles` (`id`) ON DELETE SET NULL" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
}
Bridge.Database.Migrate("sky_phone", schema)
+79
View File
@@ -850,3 +850,82 @@ CREATE TABLE IF NOT EXISTS `sky_phone_feather_reports` (
FOREIGN KEY (`reporter_id`) REFERENCES `sky_phone_feather_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`post_id`) REFERENCES `sky_phone_feather_posts` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_crewlink_profiles` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`account_id` BIGINT UNSIGNED NOT NULL,
`username` VARCHAR(20) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`active_group_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
`map_visible` TINYINT(1) NOT NULL DEFAULT 1,
`overhead_visible` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_crewlink_account` (`account_id`),
UNIQUE KEY `uniq_sky_phone_crewlink_username` (`username`),
KEY `idx_sky_phone_crewlink_active` (`active_group_id`),
FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_crewlink_groups` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`name` VARCHAR(32) NOT NULL,
`colour` ENUM('cyan','blue','violet','orange','green','rose') NOT NULL DEFAULT 'cyan',
`owner_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`invite_code` VARCHAR(12) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`allow_member_pings` TINYINT(1) NOT NULL DEFAULT 1,
`overhead_allowed` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_crewlink_invite_code` (`invite_code`),
KEY `idx_sky_phone_crewlink_owner` (`owner_profile_id`),
FOREIGN KEY (`owner_profile_id`) REFERENCES `sky_phone_crewlink_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_crewlink_memberships` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`group_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`role` ENUM('owner','coordinator','moderator','member','guest') NOT NULL DEFAULT 'member',
`joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_crewlink_membership` (`group_id`,`profile_id`),
KEY `idx_sky_phone_crewlink_profile_groups` (`profile_id`,`joined_at`),
FOREIGN KEY (`group_id`) REFERENCES `sky_phone_crewlink_groups` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_crewlink_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_crewlink_invitations` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`group_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`inviter_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`invitee_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`status` ENUM('pending','accepted','declined','expired') NOT NULL DEFAULT 'pending',
`expires_at` DATETIME NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_crewlink_pending_invite` (`group_id`,`invitee_profile_id`),
KEY `idx_sky_phone_crewlink_invitee` (`invitee_profile_id`,`status`,`expires_at`),
FOREIGN KEY (`group_id`) REFERENCES `sky_phone_crewlink_groups` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`inviter_profile_id`) REFERENCES `sky_phone_crewlink_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`invitee_profile_id`) REFERENCES `sky_phone_crewlink_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_crewlink_pings` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`group_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`creator_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
`source_resource` VARCHAR(64) CHARACTER SET ascii COLLATE ascii_general_ci NULL,
`type` ENUM('meeting','danger','help','target','info') NOT NULL,
`label` VARCHAR(48) NOT NULL,
`position_x` DECIMAL(10,3) NOT NULL,
`position_y` DECIMAL(10,3) NOT NULL,
`position_z` DECIMAL(10,3) NOT NULL,
`expires_at` DATETIME NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sky_phone_crewlink_pings` (`group_id`,`expires_at`),
FOREIGN KEY (`group_id`) REFERENCES `sky_phone_crewlink_groups` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`creator_profile_id`) REFERENCES `sky_phone_crewlink_profiles` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;