mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
3173 lines
90 KiB
JavaScript
3173 lines
90 KiB
JavaScript
const cors = require('cors')
|
|
const express = require('express')
|
|
|
|
const app = express()
|
|
const port = Number(process.argv[2]) || 3001
|
|
|
|
app.use(cors())
|
|
app.use(express.json())
|
|
|
|
function calendarTime(dayOffset, hour, minute = 0) {
|
|
const value = new Date()
|
|
value.setDate(value.getDate() + dayOffset)
|
|
value.setHours(hour, minute, 0, 0)
|
|
return value.getTime()
|
|
}
|
|
|
|
function isoTime(offsetMilliseconds) {
|
|
return new Date(Date.now() + offsetMilliseconds).toISOString()
|
|
}
|
|
|
|
let authenticated = true
|
|
let draft = null
|
|
let mockBankBalance = 24787
|
|
let mockCashBalance = 2350
|
|
let nextBankTransactionId = 7
|
|
let mockMapMarkers = [
|
|
{
|
|
color: 'blue',
|
|
coords: { x: -75.2, y: -818.9, z: 0 },
|
|
id: 'mock-map-marker-1',
|
|
label: 'Meeting point',
|
|
},
|
|
]
|
|
const flipTokProfile = {
|
|
id: 1,
|
|
handle: 'skyline',
|
|
display_name: 'Skyline',
|
|
bio: 'Life around Los Santos.',
|
|
account_type: 'media',
|
|
verified: true,
|
|
is_following: false,
|
|
is_owner: true,
|
|
followers: 18400,
|
|
following: 128,
|
|
video_count: 2,
|
|
}
|
|
let flipTokAuthenticated = true
|
|
const flipTokMusicTracks = [
|
|
{
|
|
id: 'night-drive',
|
|
title: 'Night Drive',
|
|
artist: 'Los Santos Radio',
|
|
url: 'https://media.w3.org/2010/07/bunny/04-Death_Becomes_Fur.oga',
|
|
},
|
|
]
|
|
let flipTokVideos = [
|
|
{
|
|
id: 'fliptok-1',
|
|
profile_id: 2,
|
|
handle: 'novals',
|
|
display_name: 'Nova',
|
|
verified: true,
|
|
caption: 'A quiet minute above Vinewood. #LosSantos',
|
|
location: 'Vinewood Hills',
|
|
trim_start_ms: 0,
|
|
trim_end_ms: null,
|
|
cover_time_ms: 1200,
|
|
original_volume: 100,
|
|
music_volume: 0,
|
|
music_track: '',
|
|
music_title: '',
|
|
music_artist: '',
|
|
music_url: '',
|
|
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
|
|
comments_enabled: true,
|
|
is_liked: false,
|
|
is_saved: false,
|
|
is_following: false,
|
|
is_owner: false,
|
|
like_count: 12840,
|
|
comment_count: 384,
|
|
view_count: 245100,
|
|
share_count: 932,
|
|
created_at: Date.now() - 3600000,
|
|
},
|
|
{
|
|
id: 'fliptok-2',
|
|
profile_id: 1,
|
|
handle: 'skyline',
|
|
display_name: 'Skyline',
|
|
verified: true,
|
|
caption: 'Tonight in the city.',
|
|
location: 'Downtown Los Santos',
|
|
trim_start_ms: 800,
|
|
trim_end_ms: 12000,
|
|
cover_time_ms: 2200,
|
|
original_volume: 70,
|
|
music_volume: 25,
|
|
music_track: 'night-drive',
|
|
music_title: 'Night Drive',
|
|
music_artist: 'Los Santos Radio',
|
|
music_url: flipTokMusicTracks[0].url,
|
|
url: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
|
|
comments_enabled: true,
|
|
is_liked: true,
|
|
is_saved: true,
|
|
is_following: false,
|
|
is_owner: true,
|
|
like_count: 4921,
|
|
comment_count: 97,
|
|
view_count: 88300,
|
|
share_count: 220,
|
|
created_at: Date.now() - 7200000,
|
|
},
|
|
]
|
|
let flipTokComments = [
|
|
{
|
|
id: 'comment-1',
|
|
profile_id: 2,
|
|
handle: 'nova',
|
|
display_name: 'Nova',
|
|
verified: true,
|
|
body: 'This view is perfect.',
|
|
created_at: Date.now() - 300000,
|
|
},
|
|
]
|
|
let flipTokActivities = [
|
|
{
|
|
id: 'activity-1',
|
|
profile_id: 2,
|
|
handle: 'nova',
|
|
display_name: 'Nova',
|
|
verified: true,
|
|
kind: 'like',
|
|
video_id: 'fliptok-2',
|
|
read_at: null,
|
|
created_at: Date.now() - 240000,
|
|
},
|
|
]
|
|
let flipTokReports = [
|
|
{
|
|
id: 'report-1',
|
|
video_id: 'fliptok-1',
|
|
reason: 'dangerous',
|
|
details: 'Please review the driving shown in this clip.',
|
|
created_at: Date.now() - 600000,
|
|
caption: flipTokVideos[0].caption,
|
|
url: flipTokVideos[0].url,
|
|
reporter_handle: 'skyline',
|
|
reporter_display_name: 'Skyline',
|
|
creator_handle: 'novals',
|
|
creator_display_name: 'Nova',
|
|
},
|
|
]
|
|
const mockBankTransactions = [
|
|
{
|
|
id: 1,
|
|
kind: 'transfer_in',
|
|
amount: 3200,
|
|
label: 'Sofia Turner',
|
|
reference: 'mock-1',
|
|
createdAt: Date.now() - 45 * 60 * 1000,
|
|
},
|
|
{
|
|
id: 2,
|
|
kind: 'transfer_out',
|
|
amount: 680,
|
|
label: 'Vincent Cole',
|
|
reference: 'mock-2',
|
|
createdAt: Date.now() - 5 * 60 * 60 * 1000,
|
|
},
|
|
{
|
|
id: 3,
|
|
kind: 'deposit',
|
|
amount: 1250,
|
|
label: '',
|
|
reference: 'mock-3',
|
|
createdAt: Date.now() - 25 * 60 * 60 * 1000,
|
|
},
|
|
{
|
|
id: 4,
|
|
kind: 'transfer_out',
|
|
amount: 420,
|
|
label: 'Maya Brooks',
|
|
reference: 'mock-4',
|
|
createdAt: Date.now() - 50 * 60 * 60 * 1000,
|
|
},
|
|
{
|
|
id: 5,
|
|
kind: 'withdrawal',
|
|
amount: 300,
|
|
label: '',
|
|
reference: 'mock-5',
|
|
createdAt: Date.now() - 76 * 60 * 60 * 1000,
|
|
},
|
|
{
|
|
id: 6,
|
|
kind: 'transfer_in',
|
|
amount: 950,
|
|
label: 'Noah Bennett',
|
|
reference: 'mock-6',
|
|
createdAt: Date.now() - 120 * 60 * 60 * 1000,
|
|
},
|
|
]
|
|
const mockGarageVehicles = [
|
|
{
|
|
id: 'vehicle-1',
|
|
plate: 'SKY 204',
|
|
vin: '1S9SKY204LS000001',
|
|
nickname: 'Midnight',
|
|
model: 'sultanrs',
|
|
name: 'Karin Sultan RS',
|
|
kind: 'car',
|
|
status: 'garaged',
|
|
location: 'Legion Square',
|
|
fuel: 82,
|
|
engine: 96,
|
|
body: 91,
|
|
},
|
|
{
|
|
id: 'vehicle-2',
|
|
plate: 'NOVA 77',
|
|
vin: '1S9NOVA77LS000002',
|
|
nickname: '',
|
|
model: 'comet6',
|
|
name: 'Pfister Comet S2',
|
|
kind: 'car',
|
|
status: 'out',
|
|
location: '',
|
|
fuel: 46,
|
|
engine: 88,
|
|
body: 73,
|
|
},
|
|
{
|
|
id: 'vehicle-3',
|
|
plate: 'SEA 911',
|
|
vin: '1S9SEA911LS000003',
|
|
nickname: 'Blue Current',
|
|
model: 'speeder',
|
|
name: 'Pegassi Speeder',
|
|
kind: 'boat',
|
|
status: 'garaged',
|
|
location: 'La Puerta Pier',
|
|
fuel: 67,
|
|
engine: 84,
|
|
body: 79,
|
|
},
|
|
{
|
|
id: 'vehicle-4',
|
|
plate: 'AIR 404',
|
|
vin: '1S9AIR404LS000004',
|
|
nickname: '',
|
|
model: 'maverick',
|
|
name: 'Buckingham Maverick',
|
|
kind: 'helicopter',
|
|
status: 'impounded',
|
|
location: 'Impound Heli',
|
|
fuel: 23,
|
|
engine: 58,
|
|
body: 44,
|
|
},
|
|
]
|
|
|
|
let mockGarageValet = null
|
|
|
|
let contactSequence = 2
|
|
const contacts = [
|
|
{
|
|
created_at: '2026-08-04 12:00:00',
|
|
id: 'contact-1',
|
|
name: 'Jenica Chong',
|
|
phone_number: '5558675309',
|
|
updated_at: '2026-08-04 12:00:00',
|
|
},
|
|
]
|
|
const attachmentAssets = {
|
|
gif: new Set(['celebrate', 'hearts', 'party', 'thumbs_up', 'wow']),
|
|
image: new Set([
|
|
'camera-1',
|
|
'camera-2',
|
|
'camera-3',
|
|
'city-lights',
|
|
'desert-road',
|
|
'ocean-air',
|
|
'sunset-drive',
|
|
]),
|
|
video: new Set(['city-loop', 'ocean-loop', 'sunset-loop']),
|
|
}
|
|
const gifMocks = [
|
|
['ICOgUNjpvO0PC', 'Cat reaction'],
|
|
['MDJ9IbxxvDUQM', 'Happy dog'],
|
|
['l0HlPystfePnAI3G8', 'Celebrate'],
|
|
['26ufdipQqU2lhNA4g', 'Wow'],
|
|
['3o7abKhOpu0NwenH3O', 'Perfect'],
|
|
['xT0xeJpnrWC4XWblEk', 'Party'],
|
|
['111ebonMs90YLu', 'Thumbs up'],
|
|
['5GoVLqeAOo6PK', 'Excited'],
|
|
['TdfyKrN7HGTIY', 'Happy dance'],
|
|
['14udF3WUwwGMaA', 'Surprised'],
|
|
['3o6Zt6ML6BklcajjsA', 'Applause'],
|
|
['13CoXDiaCcCoyk', 'Let us go'],
|
|
['R6gvnAxj2ISzJdbA63', 'Yes'],
|
|
['xUPGcEliCc7bETyfO8', 'Laughing'],
|
|
['26BRuo6sLetdllPAQ', 'Dancing'],
|
|
['l46CyJmS9KUbokzsI', 'Amazing'],
|
|
['g9582DNuQppxC', 'Celebration'],
|
|
['artj92V8o75VPL7AeQ', 'High five'],
|
|
]
|
|
const accountDevices = [
|
|
{
|
|
created_at: '2026-08-04 12:00:00',
|
|
current: true,
|
|
device_name: 'iFruit Phone',
|
|
imei: '356938035643809',
|
|
updated_at: '2026-08-04 12:00:00',
|
|
},
|
|
]
|
|
const messages = [
|
|
{
|
|
body: '## Welcome to iFruit Mail\n\nYour shared mailbox is **ready to use**.\n\n- Send formatted messages\n- Keep drafts on every linked device\n- Reply without losing the conversation\n\n> Tip: use the small formatting bar while composing.',
|
|
created_at: '2026-08-04 11:30:00',
|
|
folder: 'inbox',
|
|
id: 1,
|
|
is_read: false,
|
|
message_id: 'b73f3872-54cc-4d74-a058-6caa24f0dfff',
|
|
preview:
|
|
'Your shared mailbox is **ready to use**. Send formatted messages and keep drafts on every linked device.',
|
|
recipients: ['demo@ifruit.com'],
|
|
sender: 'support@ifruit.com',
|
|
subject: 'Welcome to iFruit Mail',
|
|
trashed_at: null,
|
|
},
|
|
{
|
|
body: 'The meet is behind the casino at nine. Bring the blue car.',
|
|
created_at: '2026-08-04 10:15:00',
|
|
folder: 'inbox',
|
|
id: 2,
|
|
is_read: true,
|
|
message_id: '79042d13-f86d-4418-a490-8b558539c20b',
|
|
preview: 'The meet is behind the casino at nine. Bring the blue car.',
|
|
recipients: ['demo@ifruit.com', 'jamie@ifruit.com'],
|
|
sender: 'morgan@ifruit.com',
|
|
subject: 'Tonight',
|
|
trashed_at: null,
|
|
},
|
|
{
|
|
body: 'I will be there.',
|
|
created_at: '2026-08-03 18:05:00',
|
|
folder: 'sent',
|
|
id: 3,
|
|
is_read: true,
|
|
message_id: 'c4414c1e-5fe3-46ea-bc0b-b5b9785c91bd',
|
|
preview: 'I will be there.',
|
|
recipients: ['morgan@ifruit.com'],
|
|
sender: 'demo@ifruit.com',
|
|
subject: 'Re: Tonight',
|
|
trashed_at: null,
|
|
},
|
|
{
|
|
body: 'This is an old message.',
|
|
created_at: '2026-08-02 09:20:00',
|
|
folder: 'inbox',
|
|
id: 4,
|
|
is_read: true,
|
|
message_id: 'd99669ab-c1ac-42d4-8ed4-3c4fe7fa37fb',
|
|
preview: 'This is an old message.',
|
|
recipients: ['demo@ifruit.com'],
|
|
sender: 'news@ifruit.com',
|
|
subject: 'Old mail',
|
|
trashed_at: '2026-08-04 08:00:00',
|
|
},
|
|
]
|
|
const smsMessages = [
|
|
{
|
|
body: 'Bin gleich am Würfelpark. Kommst du auch?',
|
|
created_at: '2026-08-06 13:04:00',
|
|
direction: 'received',
|
|
id: 'sms-1',
|
|
media_duration_ms: null,
|
|
media_mime: null,
|
|
media_payload: null,
|
|
media_waveform: null,
|
|
message_type: 'text',
|
|
media_asset_id: null,
|
|
read_at: null,
|
|
recipient_number: '5551234567',
|
|
sender_number: '5558675309',
|
|
},
|
|
{
|
|
body: 'Ja, gib mir fünf Minuten.',
|
|
created_at: '2026-08-06 13:05:00',
|
|
direction: 'sent',
|
|
id: 'sms-2',
|
|
media_duration_ms: null,
|
|
media_mime: null,
|
|
media_payload: null,
|
|
media_waveform: null,
|
|
message_type: 'text',
|
|
media_asset_id: null,
|
|
read_at: null,
|
|
recipient_number: '5558675309',
|
|
sender_number: '5551234567',
|
|
},
|
|
]
|
|
const darkChatProfile = {
|
|
id: 1,
|
|
darkId: 'dark:7X4K-P92D',
|
|
inviteCode: 'DC-7X4K-NOVA',
|
|
alias: 'Nightshade',
|
|
avatarSeed: 267,
|
|
notificationMode: 'private',
|
|
activityVisible: false,
|
|
createdAt: '2026-08-01 21:20:00',
|
|
}
|
|
const darkChatPeers = [
|
|
{
|
|
id: 2,
|
|
darkId: 'dark:N0VA-41KQ',
|
|
alias: 'Nova',
|
|
originalAlias: 'Nova',
|
|
avatarSeed: 142,
|
|
activityVisible: true,
|
|
isContact: true,
|
|
blocked: false,
|
|
},
|
|
{
|
|
id: 3,
|
|
darkId: 'dark:ECH0-77LM',
|
|
alias: 'Echo',
|
|
originalAlias: 'Echo',
|
|
avatarSeed: 311,
|
|
activityVisible: false,
|
|
isContact: true,
|
|
blocked: false,
|
|
},
|
|
]
|
|
const darkChatConversations = [
|
|
{
|
|
id: 'dc-conversation-nova-0000-000000000001',
|
|
peer: darkChatPeers[0],
|
|
disappearingSeconds: 3600,
|
|
notificationsEnabled: true,
|
|
readReceipts: true,
|
|
blockedByPeer: false,
|
|
createdAt: '2026-08-03 22:12:00',
|
|
},
|
|
]
|
|
const darkChatMessages = [
|
|
{
|
|
id: 'dc-message-00000000-0000-000000000001',
|
|
conversationId: darkChatConversations[0].id,
|
|
direction: 'received',
|
|
senderProfileId: 2,
|
|
messageType: 'text',
|
|
body: 'The east gate is clear. Are you close?',
|
|
reactions: {},
|
|
createdAt: '2026-08-06 22:42:00',
|
|
readAt: '2026-08-06 22:43:00',
|
|
},
|
|
{
|
|
id: 'dc-message-00000000-0000-000000000002',
|
|
conversationId: darkChatConversations[0].id,
|
|
direction: 'sent',
|
|
senderProfileId: 1,
|
|
messageType: 'text',
|
|
body: 'Two minutes. Keep this channel quiet. 🟣',
|
|
reactions: { 2: '👍' },
|
|
createdAt: '2026-08-06 22:43:00',
|
|
readAt: '2026-08-06 22:43:30',
|
|
},
|
|
{
|
|
id: 'dc-message-00000000-0000-000000000003',
|
|
conversationId: darkChatConversations[0].id,
|
|
direction: 'received',
|
|
senderProfileId: 2,
|
|
messageType: 'gif',
|
|
body: '',
|
|
mediaPayload: 'https://media.giphy.com/media/ICOgUNjpvO0PC/giphy.gif',
|
|
reactions: {},
|
|
createdAt: '2026-08-06 22:44:00',
|
|
readAt: null,
|
|
},
|
|
]
|
|
|
|
function darkChatBootstrap() {
|
|
return {
|
|
profile: darkChatProfile,
|
|
contacts: darkChatPeers
|
|
.filter((peer) => peer.isContact)
|
|
.map((peer) => ({ ...peer, createdAt: '2026-08-03 22:12:00' })),
|
|
conversations: darkChatConversations.map((conversation) => {
|
|
const thread = darkChatMessages.filter(
|
|
(message) => message.conversationId === conversation.id,
|
|
)
|
|
const last = thread.at(-1)
|
|
return {
|
|
id: conversation.id,
|
|
peer: conversation.peer,
|
|
disappearingSeconds: conversation.disappearingSeconds,
|
|
blocked: conversation.peer.blocked,
|
|
lastMessage: last?.body ?? '',
|
|
lastMessageType: last?.messageType ?? 'system',
|
|
lastMessageAt: last?.createdAt ?? conversation.createdAt,
|
|
unread: thread.filter(
|
|
(message) => message.direction === 'received' && !message.readAt,
|
|
).length,
|
|
}
|
|
}),
|
|
}
|
|
}
|
|
const marketplaceListings = [
|
|
{
|
|
id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01',
|
|
seller_account_id: 2,
|
|
seller_name: 'morgan',
|
|
seller_since: '2026-07-02 10:00:00',
|
|
seller_active: 2,
|
|
title: 'Sultan RS in excellent condition',
|
|
description:
|
|
'Clean Sultan RS with fresh service, tidy interior and no known damage. Viewing and test drive available in Vinewood.',
|
|
category: 'vehicles',
|
|
item_condition: 'very_good',
|
|
price_type: 'negotiable',
|
|
price: 185000,
|
|
district: 'vinewood',
|
|
status: 'active',
|
|
revision: 1,
|
|
show_phone: 0,
|
|
phone_number: null,
|
|
created_at: '2026-08-06 09:20:00',
|
|
updated_at: '2026-08-06 09:20:00',
|
|
expires_at: '2026-08-13 09:20:00',
|
|
image: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
|
|
is_favorite: false,
|
|
images: [
|
|
{
|
|
media_id: 'capture-car',
|
|
gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
|
|
sort_order: 1,
|
|
},
|
|
{
|
|
media_id: 'city-lights',
|
|
gradient: 'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)',
|
|
sort_order: 2,
|
|
},
|
|
{
|
|
media_id: 'ocean-air',
|
|
gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
|
|
sort_order: 3,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf02',
|
|
seller_account_id: 3,
|
|
seller_name: 'jamie',
|
|
seller_since: '2026-06-14 10:00:00',
|
|
seller_active: 1,
|
|
title: 'Modern apartment near Vespucci',
|
|
description:
|
|
'Bright apartment with a city view, underground parking and modern furniture. Available immediately after viewing.',
|
|
category: 'property',
|
|
item_condition: 'very_good',
|
|
price_type: 'fixed',
|
|
price: 420000,
|
|
district: 'vespucci',
|
|
status: 'active',
|
|
revision: 1,
|
|
show_phone: 0,
|
|
phone_number: null,
|
|
created_at: '2026-08-05 17:10:00',
|
|
updated_at: '2026-08-05 17:10:00',
|
|
expires_at: '2026-08-12 17:10:00',
|
|
image: null,
|
|
is_favorite: true,
|
|
images: [],
|
|
},
|
|
{
|
|
id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf03',
|
|
seller_account_id: 4,
|
|
seller_name: 'citytech',
|
|
seller_since: '2026-05-22 10:00:00',
|
|
seller_active: 3,
|
|
title: 'Nearly new gaming laptop',
|
|
description:
|
|
'Fast gaming laptop including charger and carrying bag. Runs quietly and can be tested before purchase.',
|
|
category: 'electronics',
|
|
item_condition: 'very_good',
|
|
price_type: 'fixed',
|
|
price: 3500,
|
|
district: 'los_santos',
|
|
status: 'reserved',
|
|
revision: 2,
|
|
show_phone: 0,
|
|
phone_number: null,
|
|
created_at: '2026-08-05 12:30:00',
|
|
updated_at: '2026-08-06 08:00:00',
|
|
expires_at: '2026-08-12 12:30:00',
|
|
image: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
|
|
is_favorite: false,
|
|
images: [
|
|
{
|
|
media_id: 'ocean-air',
|
|
gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
|
|
sort_order: 1,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf04',
|
|
seller_account_id: 1,
|
|
seller_name: 'demo',
|
|
seller_since: '2026-08-04 12:00:00',
|
|
seller_active: 1,
|
|
title: 'Complete mechanic tool set',
|
|
description:
|
|
'Complete mechanic tool set with trolley, sockets and diagnostic equipment. Everything is clean and ready for work.',
|
|
category: 'tools',
|
|
item_condition: 'very_good',
|
|
price_type: 'fixed',
|
|
price: 7800,
|
|
district: 'south_los_santos',
|
|
status: 'active',
|
|
revision: 1,
|
|
show_phone: 1,
|
|
phone_number: '5551234567',
|
|
created_at: '2026-08-06 10:45:00',
|
|
updated_at: '2026-08-06 10:45:00',
|
|
expires_at: '2026-08-13 10:45:00',
|
|
image: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
|
|
is_favorite: false,
|
|
images: [
|
|
{
|
|
media_id: 'capture-tools',
|
|
gradient: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
|
|
sort_order: 1,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
let linkedAccount = {
|
|
devices: accountDevices,
|
|
email: 'demo@ifruit.com',
|
|
id: 1,
|
|
}
|
|
let mockNotes = [
|
|
{
|
|
body: 'Meet Morgan in Vinewood and inspect the Sultan RS.',
|
|
createdAt: Date.now() - 3_600_000,
|
|
id: 'demo-note-marketplace',
|
|
pinned: true,
|
|
revision: 1,
|
|
title: 'CityMarkt viewing',
|
|
updatedAt: Date.now() - 3_600_000,
|
|
},
|
|
{
|
|
body: 'Repair kit\nRadio\nFlashlight\nTwo bottles of water',
|
|
createdAt: Date.now() - 7_200_000,
|
|
id: 'demo-note-packing-list',
|
|
pinned: true,
|
|
revision: 2,
|
|
title: 'Patrol equipment',
|
|
updatedAt: Date.now() - 1_800_000,
|
|
},
|
|
{
|
|
body: 'Call Jamie after the garage inspection and confirm the final price.',
|
|
createdAt: Date.now() - 86_400_000,
|
|
id: 'demo-note-call-jamie',
|
|
pinned: false,
|
|
revision: 1,
|
|
title: 'Call Jamie',
|
|
updatedAt: Date.now() - 86_400_000,
|
|
},
|
|
{
|
|
body: 'Observatory, Vespucci Pier, Paleto lookout, Sandy airfield.',
|
|
createdAt: Date.now() - 172_800_000,
|
|
id: 'demo-note-photo-spots',
|
|
pinned: false,
|
|
revision: 1,
|
|
title: 'Photo locations',
|
|
updatedAt: Date.now() - 172_800_000,
|
|
},
|
|
]
|
|
let calendarEvents = [
|
|
{
|
|
endsAt: calendarTime(0, 10),
|
|
id: 'calendar-team-briefing',
|
|
note: 'Meet in the Mission Row briefing room. Bring the updated patrol plan.',
|
|
remindedAt: null,
|
|
reminderMinutes: 30,
|
|
revision: 1,
|
|
startsAt: calendarTime(0, 9),
|
|
title: 'Team briefing',
|
|
},
|
|
{
|
|
endsAt: calendarTime(0, 21, 30),
|
|
id: 'calendar-citymarkt-viewing',
|
|
note: 'Inspect the blue Sultan RS at the Vinewood garage and take a short test drive.',
|
|
remindedAt: null,
|
|
reminderMinutes: 60,
|
|
revision: 2,
|
|
startsAt: calendarTime(0, 20),
|
|
title: 'CityMarkt vehicle viewing',
|
|
},
|
|
{
|
|
endsAt: calendarTime(1, 15, 30),
|
|
id: 'calendar-garage-service',
|
|
note: 'Routine service, wheel alignment and diagnostics.',
|
|
remindedAt: null,
|
|
reminderMinutes: 10,
|
|
revision: 1,
|
|
startsAt: calendarTime(1, 14),
|
|
title: 'Garage appointment',
|
|
},
|
|
{
|
|
endsAt: calendarTime(3, 22),
|
|
id: 'calendar-birthday-dinner',
|
|
note: 'Table is booked on the terrace. Pick up the cake first.',
|
|
remindedAt: null,
|
|
reminderMinutes: 1440,
|
|
revision: 1,
|
|
startsAt: calendarTime(3, 19, 30),
|
|
title: "Jamie's birthday dinner",
|
|
},
|
|
{
|
|
endsAt: calendarTime(-2, 18),
|
|
id: 'calendar-past-beach-cruise',
|
|
note: 'Vespucci Pier to the Paleto lookout.',
|
|
remindedAt: calendarTime(-2, 16),
|
|
reminderMinutes: 60,
|
|
revision: 1,
|
|
startsAt: calendarTime(-2, 17),
|
|
title: 'Beach cruise',
|
|
},
|
|
]
|
|
const deviceData = {
|
|
alarms: {
|
|
payload: [
|
|
{
|
|
enabled: true,
|
|
id: 'demo-weekday-alarm',
|
|
lastTriggeredMinute: null,
|
|
note: 'Morning patrol',
|
|
sound: 'radar',
|
|
time: '07:15',
|
|
weekdays: [1, 2, 3, 4, 5],
|
|
},
|
|
{
|
|
enabled: true,
|
|
id: 'demo-garage-alarm',
|
|
lastTriggeredMinute: null,
|
|
note: 'Garage appointment',
|
|
sound: 'chimes',
|
|
time: '13:30',
|
|
weekdays: [],
|
|
},
|
|
{
|
|
enabled: false,
|
|
id: 'demo-weekend-alarm',
|
|
lastTriggeredMinute: null,
|
|
note: 'Weekend drive',
|
|
sound: 'apex',
|
|
time: '09:30',
|
|
weekdays: [0, 6],
|
|
},
|
|
],
|
|
revision: 3,
|
|
},
|
|
apps: {
|
|
payload: {
|
|
claimedApps: [],
|
|
},
|
|
revision: 2,
|
|
},
|
|
games: {
|
|
payload: {
|
|
memory: {
|
|
best: {
|
|
small: { moves: 12, timeMs: 38_400 },
|
|
medium: { moves: 29, timeMs: 92_100 },
|
|
},
|
|
soundEnabled: true,
|
|
},
|
|
minesweeper: {
|
|
best: {
|
|
quick: { timeMs: 44_200 },
|
|
classic: { timeMs: 128_700 },
|
|
},
|
|
elapsedMs: 0,
|
|
game: null,
|
|
soundEnabled: true,
|
|
},
|
|
'neon-drop': { bestLines: 18, bestScore: 12_450, soundEnabled: true },
|
|
'number-merge': {
|
|
bestScore: 8_920,
|
|
game: null,
|
|
highestTile: 1024,
|
|
soundEnabled: true,
|
|
},
|
|
'sky-flappy': { design: 'neon', highScore: 23, soundEnabled: true },
|
|
snake: { highScore: 17, speed: 'normal' },
|
|
'tower-stack': { highHeight: 21, highScore: 4_850, soundEnabled: true },
|
|
},
|
|
revision: 4,
|
|
},
|
|
media: {
|
|
payload: {
|
|
captures: [
|
|
{
|
|
capturedAt: Date.now() - 1_200_000,
|
|
gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
|
|
id: 'capture-car',
|
|
titleKey: 'Apps.photos.samples.capture',
|
|
},
|
|
{
|
|
capturedAt: Date.now() - 5_400_000,
|
|
gradient: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
|
|
id: 'capture-tools',
|
|
titleKey: 'Apps.photos.samples.capture',
|
|
},
|
|
],
|
|
claimedApps: [],
|
|
},
|
|
revision: 2,
|
|
},
|
|
settings: {
|
|
payload: {
|
|
settings: {
|
|
airplaneMode: false,
|
|
appearanceMode: 'automatic',
|
|
frame: 'black',
|
|
notificationDurationSeconds: 10,
|
|
notificationSound: 'chime',
|
|
notificationVolume: 70,
|
|
phoneScale: 100,
|
|
ringtone: 'skyline',
|
|
ringtoneVolume: 80,
|
|
streamerMode: false,
|
|
wallpaper: 'midnight',
|
|
},
|
|
version: 1,
|
|
},
|
|
revision: 1,
|
|
},
|
|
}
|
|
let mockPasscode = ''
|
|
let mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
|
|
let mockContacts = [
|
|
{
|
|
created_at: isoTime(-14 * 86_400_000),
|
|
id: 'contact-morgan',
|
|
name: 'Morgan Reed',
|
|
phone_number: '5550192847',
|
|
updated_at: isoTime(-2 * 86_400_000),
|
|
},
|
|
{
|
|
created_at: isoTime(-30 * 86_400_000),
|
|
id: 'contact-jamie',
|
|
name: 'Jamie Chen',
|
|
phone_number: '5559876543',
|
|
updated_at: isoTime(-30 * 86_400_000),
|
|
},
|
|
{
|
|
created_at: isoTime(-5 * 86_400_000),
|
|
id: 'contact-mechanic',
|
|
name: 'Downtown Customs',
|
|
phone_number: '5550100101',
|
|
updated_at: isoTime(-5 * 86_400_000),
|
|
},
|
|
{
|
|
created_at: isoTime(-3 * 86_400_000),
|
|
id: 'contact-taxi',
|
|
name: 'Los Santos Taxi',
|
|
phone_number: '5552222222',
|
|
updated_at: isoTime(-3 * 86_400_000),
|
|
},
|
|
]
|
|
let recentCalls = [
|
|
{
|
|
call_id: 'call-morgan-incoming',
|
|
created_at: isoTime(-18 * 60_000),
|
|
direction: 'incoming',
|
|
duration_seconds: 246,
|
|
id: 1,
|
|
other_number: '5550192847',
|
|
status: 'completed',
|
|
},
|
|
{
|
|
call_id: 'call-jamie-missed',
|
|
created_at: isoTime(-95 * 60_000),
|
|
direction: 'incoming',
|
|
duration_seconds: 0,
|
|
id: 2,
|
|
other_number: '5559876543',
|
|
status: 'missed',
|
|
},
|
|
{
|
|
call_id: 'call-customs-outgoing',
|
|
created_at: isoTime(-25 * 60 * 60_000),
|
|
direction: 'outgoing',
|
|
duration_seconds: 83,
|
|
id: 3,
|
|
other_number: '5550100101',
|
|
status: 'completed',
|
|
},
|
|
{
|
|
call_id: 'call-unknown-declined',
|
|
created_at: isoTime(-2 * 86_400_000),
|
|
direction: 'incoming',
|
|
duration_seconds: 0,
|
|
id: 4,
|
|
other_number: '5554040404',
|
|
status: 'declined',
|
|
},
|
|
]
|
|
let mockMedia = [
|
|
{
|
|
createdAt: Date.now() - 60_000,
|
|
id: 1,
|
|
mediaType: 'photo',
|
|
url: 'https://picsum.photos/seed/sky-phone-1/600/800',
|
|
},
|
|
{
|
|
createdAt: Date.now() - 120_000,
|
|
id: 2,
|
|
mediaType: 'video',
|
|
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
|
|
},
|
|
]
|
|
const marketplaceInquiries = [
|
|
{
|
|
id: '4903b923-409a-437e-971f-b7a2b10e9e31',
|
|
listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01',
|
|
seller_account_id: 2,
|
|
buyer_account_id: 1,
|
|
seller_name: 'morgan',
|
|
buyer_name: 'demo',
|
|
title: 'Sultan RS in excellent condition',
|
|
price: 185000,
|
|
price_type: 'negotiable',
|
|
status: 'active',
|
|
image: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
|
|
other_name: 'morgan',
|
|
last_message: 'Sure, come by the Vinewood garage around 8 PM.',
|
|
offer_id: 2,
|
|
offer_amount: 178000,
|
|
offer_proposer_account_id: 2,
|
|
offer_status: 'pending',
|
|
offer_revision: 2,
|
|
unread: 2,
|
|
updated_at: '2026-08-06 11:42:00',
|
|
},
|
|
{
|
|
id: '4903b923-409a-437e-971f-b7a2b10e9e32',
|
|
listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf04',
|
|
seller_account_id: 1,
|
|
buyer_account_id: 3,
|
|
seller_name: 'demo',
|
|
buyer_name: 'jamie',
|
|
title: 'Complete mechanic tool set',
|
|
price: 7800,
|
|
price_type: 'fixed',
|
|
status: 'active',
|
|
image: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
|
|
other_name: 'jamie',
|
|
last_message: 'I can collect it today and pay the full price.',
|
|
offer_id: 3,
|
|
offer_amount: 7000,
|
|
offer_proposer_account_id: 3,
|
|
offer_status: 'pending',
|
|
offer_revision: 1,
|
|
unread: 3,
|
|
updated_at: '2026-08-06 11:55:00',
|
|
},
|
|
]
|
|
const marketplaceMessages = [
|
|
{
|
|
id: 1,
|
|
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
|
|
sender_account_id: 1,
|
|
body: 'Hi, is the Sultan still available?',
|
|
created_at: '2026-08-06 11:35:00',
|
|
read_at: '2026-08-06 11:36:00',
|
|
},
|
|
{
|
|
id: 2,
|
|
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
|
|
sender_account_id: 2,
|
|
body: 'Yes, it is. You can also take it for a short test drive.',
|
|
created_at: '2026-08-06 11:38:00',
|
|
read_at: '2026-08-06 11:39:00',
|
|
},
|
|
{
|
|
id: 3,
|
|
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
|
|
sender_account_id: 2,
|
|
body: 'Sure, come by the Vinewood garage around 8 PM.',
|
|
created_at: '2026-08-06 11:42:00',
|
|
read_at: null,
|
|
},
|
|
{
|
|
id: 4,
|
|
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32',
|
|
sender_account_id: 3,
|
|
body: 'Hello, does the diagnostic equipment work with every vehicle?',
|
|
created_at: '2026-08-06 11:51:00',
|
|
read_at: null,
|
|
},
|
|
{
|
|
id: 5,
|
|
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32',
|
|
sender_account_id: 3,
|
|
body: 'I can collect it today and pay the full price.',
|
|
created_at: '2026-08-06 11:55:00',
|
|
read_at: null,
|
|
},
|
|
]
|
|
const marketplaceOffers = [
|
|
{
|
|
id: 1,
|
|
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
|
|
proposer_account_id: 1,
|
|
amount: 170000,
|
|
status: 'countered',
|
|
read_at: '2026-08-06 11:43:00',
|
|
response_read_at: null,
|
|
created_at: '2026-08-06 11:40:00',
|
|
updated_at: '2026-08-06 11:44:00',
|
|
},
|
|
{
|
|
id: 2,
|
|
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31',
|
|
proposer_account_id: 2,
|
|
amount: 178000,
|
|
status: 'pending',
|
|
read_at: null,
|
|
response_read_at: null,
|
|
created_at: '2026-08-06 11:44:00',
|
|
updated_at: '2026-08-06 11:44:00',
|
|
},
|
|
{
|
|
id: 3,
|
|
inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32',
|
|
proposer_account_id: 3,
|
|
amount: 7000,
|
|
status: 'pending',
|
|
read_at: null,
|
|
response_read_at: null,
|
|
created_at: '2026-08-06 11:56:00',
|
|
updated_at: '2026-08-06 11:56:00',
|
|
},
|
|
]
|
|
const pagesPosts = [
|
|
{
|
|
id: 'pages-citymarkt-1',
|
|
account_id: 2,
|
|
author_name: 'morgan',
|
|
source_type: 'citymarkt',
|
|
citymarkt_listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01',
|
|
title: 'Sultan RS in excellent condition',
|
|
body: 'Clean Sultan RS with fresh service, tidy interior and no known damage. Viewing and test drive available in Vinewood.',
|
|
category: 'citymarkt',
|
|
district: 'vinewood',
|
|
created_at: Date.parse('2026-08-06T12:50:00Z'),
|
|
like_count: 24,
|
|
images: [
|
|
{
|
|
media_id: 'capture-car',
|
|
gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
|
|
sort_order: 1,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'pages-1',
|
|
account_id: 2,
|
|
author_name: 'morgan',
|
|
source_type: 'personal',
|
|
citymarkt_listing_id: null,
|
|
title: 'Best sunset view above Vinewood',
|
|
body: 'Take the small trail behind the observatory shortly before sunset. The view across Los Santos is incredible and there is enough space to park two cars.',
|
|
category: 'recommendation',
|
|
district: 'vinewood',
|
|
created_at: Date.parse('2026-08-06T12:20:00Z'),
|
|
like_count: 18,
|
|
images: [
|
|
{
|
|
media_id: 'sunset-drive',
|
|
gradient: 'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)',
|
|
sort_order: 1,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'pages-2',
|
|
account_id: 1,
|
|
author_name: 'demo',
|
|
source_type: 'personal',
|
|
citymarkt_listing_id: null,
|
|
title: 'Looking for people for a beach cruise',
|
|
body: 'Meeting at Vespucci Pier tonight. Bring a clean car and a good playlist. Everyone is welcome.',
|
|
category: 'community',
|
|
district: 'vespucci',
|
|
created_at: Date.parse('2026-08-06T11:10:00Z'),
|
|
like_count: 7,
|
|
images: [
|
|
{
|
|
media_id: 'ocean-air',
|
|
gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
|
|
sort_order: 1,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'pages-3',
|
|
account_id: 3,
|
|
author_name: 'jamie',
|
|
source_type: 'personal',
|
|
citymarkt_listing_id: null,
|
|
title: 'Mobile repair help around Sandy Shores',
|
|
body: 'I can help with small repairs and jump starts around Sandy Shores this afternoon. Send me a message when you see me nearby.',
|
|
category: 'service',
|
|
district: 'sandy_shores',
|
|
created_at: Date.parse('2026-08-05T19:40:00Z'),
|
|
like_count: 12,
|
|
images: [],
|
|
},
|
|
]
|
|
const pagesReactions = [
|
|
{ post_id: 'pages-1', account_id: 1, kind: 'like' },
|
|
{ post_id: 'pages-3', account_id: 1, kind: 'save' },
|
|
]
|
|
|
|
function pageView(post) {
|
|
const listing = marketplaceListings.find(
|
|
(item) => item.id === post.citymarkt_listing_id,
|
|
)
|
|
return {
|
|
...post,
|
|
citymarkt_price: listing?.price ?? null,
|
|
image: post.images[0]?.gradient ?? null,
|
|
is_liked: pagesReactions.some(
|
|
(item) =>
|
|
item.post_id === post.id &&
|
|
item.account_id === 1 &&
|
|
item.kind === 'like',
|
|
),
|
|
is_owner: authenticated && post.account_id === 1,
|
|
is_saved: pagesReactions.some(
|
|
(item) =>
|
|
item.post_id === post.id &&
|
|
item.account_id === 1 &&
|
|
item.kind === 'save',
|
|
),
|
|
}
|
|
}
|
|
|
|
function counts() {
|
|
return {
|
|
drafts: draft ? 1 : 0,
|
|
inbox: messages.filter(
|
|
(item) => item.folder === 'inbox' && !item.trashed_at,
|
|
).length,
|
|
sent: messages.filter((item) => item.folder === 'sent' && !item.trashed_at)
|
|
.length,
|
|
trash: messages.filter((item) => item.trashed_at).length,
|
|
unread: messages.filter(
|
|
(item) => item.folder === 'inbox' && !item.trashed_at && !item.is_read,
|
|
).length,
|
|
}
|
|
}
|
|
|
|
let flareProfile = {
|
|
id: 1,
|
|
name: 'Alex',
|
|
age: 27,
|
|
bio: 'Late-night drives, good coffee, and plans that turn into stories.',
|
|
gender: 'nonbinary',
|
|
interestedIn: 'everyone',
|
|
minAge: 21,
|
|
maxAge: 39,
|
|
avatar: 5,
|
|
interests: ['Night drives', 'Music', 'Coffee'],
|
|
lookingFor: 'dates',
|
|
discoverable: true,
|
|
photoMediaIds: [],
|
|
photoUrls: [],
|
|
}
|
|
let flareSuggestions = [
|
|
{
|
|
id: 11,
|
|
name: 'Maya',
|
|
age: 26,
|
|
bio: 'Ocean air, rooftop sunsets and always up for finding the best tacos in the city.',
|
|
gender: 'woman',
|
|
avatar: 0,
|
|
interests: ['Beach days', 'Food spots', 'Art'],
|
|
lookingFor: 'longTerm',
|
|
photoUrls: [],
|
|
},
|
|
{
|
|
id: 12,
|
|
name: 'Noah',
|
|
age: 29,
|
|
bio: 'Architect by day, terrible karaoke singer by night.',
|
|
gender: 'man',
|
|
avatar: 1,
|
|
interests: ['Architecture', 'Karaoke', 'Travel'],
|
|
lookingFor: 'dates',
|
|
photoUrls: [],
|
|
},
|
|
{
|
|
id: 13,
|
|
name: 'Sofia',
|
|
age: 25,
|
|
bio: 'Tell me your favorite hidden corner of Los Santos.',
|
|
gender: 'woman',
|
|
avatar: 2,
|
|
interests: ['Coffee', 'Photography', 'Dogs'],
|
|
lookingFor: 'friends',
|
|
photoUrls: [],
|
|
},
|
|
{
|
|
id: 14,
|
|
name: 'Leo',
|
|
age: 28,
|
|
bio: 'Desert roads, classic cars and a playlist for every mood.',
|
|
gender: 'man',
|
|
avatar: 3,
|
|
interests: ['Cars', 'Road trips', 'Vinyl'],
|
|
lookingFor: 'longTerm',
|
|
photoUrls: [],
|
|
},
|
|
{
|
|
id: 15,
|
|
name: 'Jade',
|
|
age: 24,
|
|
bio: 'Usually somewhere near the water with an iced coffee.',
|
|
gender: 'woman',
|
|
avatar: 4,
|
|
interests: ['Sailing', 'Fitness', 'Brunch'],
|
|
lookingFor: 'dates',
|
|
photoUrls: [],
|
|
},
|
|
]
|
|
const flareMatches = [
|
|
{
|
|
id: 'flare-match-demo-0000-0000-0000000001',
|
|
profile: {
|
|
id: 16,
|
|
name: 'Marcus',
|
|
age: 30,
|
|
bio: 'Live music and late dinners.',
|
|
gender: 'man',
|
|
avatar: 5,
|
|
interests: ['Live music', 'Cooking'],
|
|
lookingFor: 'dates',
|
|
photoUrls: [],
|
|
},
|
|
lastMessage: 'That place sounds perfect. Friday?',
|
|
lastMessageAt: isoTime(-38 * 60 * 1000),
|
|
lastMessageType: 'text',
|
|
unread: 1,
|
|
},
|
|
]
|
|
let flareLikes = [{ ...flareSuggestions[0], superLiked: true }]
|
|
let flareLastSwipe = null
|
|
const flareMessages = {
|
|
'flare-match-demo-0000-0000-0000000001': [
|
|
{
|
|
id: 'flare-message-1',
|
|
direction: 'sent',
|
|
body: 'Have you tried the little jazz bar in Vinewood?',
|
|
createdAt: isoTime(-55 * 60 * 1000),
|
|
mediaDurationMs: null,
|
|
mediaUrl: null,
|
|
messageType: 'text',
|
|
},
|
|
{
|
|
id: 'flare-message-2',
|
|
direction: 'received',
|
|
body: 'That place sounds perfect. Friday?',
|
|
createdAt: isoTime(-38 * 60 * 1000),
|
|
mediaDurationMs: null,
|
|
mediaUrl: null,
|
|
messageType: 'text',
|
|
},
|
|
],
|
|
}
|
|
|
|
function flareBootstrap() {
|
|
return {
|
|
profile: flareProfile,
|
|
suggestions: flareProfile.discoverable ? flareSuggestions : [],
|
|
likes: flareLikes,
|
|
matches: flareMatches,
|
|
}
|
|
}
|
|
|
|
app.post('/api/:endpoint', (request, response) => {
|
|
console.log(`[NUI] ${request.params.endpoint}`, request.body)
|
|
const endpoint = request.params.endpoint
|
|
const bankingOverview = () => ({
|
|
bank: mockBankBalance,
|
|
cash: mockCashBalance,
|
|
currency: '$',
|
|
playerId: 42,
|
|
playerName: 'Alex Morgan',
|
|
transactions: mockBankTransactions,
|
|
})
|
|
if (endpoint === 'flare:bootstrap') {
|
|
response.json({ success: true, data: flareBootstrap() })
|
|
return
|
|
}
|
|
if (endpoint === 'flare:save-profile') {
|
|
const requestedPhotoIds = request.body.photoMediaIds
|
|
let photoUpdate = {}
|
|
if (requestedPhotoIds !== undefined) {
|
|
const validIds =
|
|
Array.isArray(requestedPhotoIds) &&
|
|
requestedPhotoIds.length <= 6 &&
|
|
new Set(requestedPhotoIds).size === requestedPhotoIds.length &&
|
|
requestedPhotoIds.every((id) => Number.isInteger(id) && id > 0)
|
|
const photos = validIds
|
|
? requestedPhotoIds.map((id) =>
|
|
mockMedia.find(
|
|
(item) => item.id === id && item.mediaType === 'photo',
|
|
),
|
|
)
|
|
: []
|
|
if (!validIds || photos.some((photo) => !photo)) {
|
|
response.json({ success: false, error: 'invalid_profile_photos' })
|
|
return
|
|
}
|
|
photoUpdate = {
|
|
photoMediaIds: [...requestedPhotoIds],
|
|
photoUrls: photos.map((photo) => photo.url),
|
|
}
|
|
}
|
|
flareProfile = {
|
|
...flareProfile,
|
|
...request.body,
|
|
discoverable: flareProfile.discoverable,
|
|
...photoUpdate,
|
|
}
|
|
response.json({ success: true, data: flareBootstrap() })
|
|
return
|
|
}
|
|
if (endpoint === 'flare:set-discovery') {
|
|
if (typeof request.body.enabled !== 'boolean') {
|
|
response.json({ success: false, error: 'invalid_discovery' })
|
|
return
|
|
}
|
|
flareProfile.discoverable = request.body.enabled
|
|
response.json({ success: true, data: flareBootstrap() })
|
|
return
|
|
}
|
|
if (endpoint === 'flare:swipe') {
|
|
if (!flareProfile.discoverable) {
|
|
response.json({ success: false, error: 'discovery_disabled' })
|
|
return
|
|
}
|
|
const target = flareSuggestions.find(
|
|
(profile) => profile.id === Number(request.body.targetId),
|
|
)
|
|
flareLastSwipe = target
|
|
? { choice: request.body.choice, profile: target }
|
|
: null
|
|
flareSuggestions = flareSuggestions.filter(
|
|
(profile) => profile.id !== Number(request.body.targetId),
|
|
)
|
|
flareLikes = flareLikes.filter(
|
|
(profile) => profile.id !== Number(request.body.targetId),
|
|
)
|
|
let match = null
|
|
if (
|
|
target &&
|
|
['like', 'superlike'].includes(request.body.choice) &&
|
|
target.id === 11
|
|
) {
|
|
match = {
|
|
id: 'flare-match-maya-0000-0000-0000000001',
|
|
profile: target,
|
|
lastMessage: '',
|
|
lastMessageAt: null,
|
|
lastMessageType: null,
|
|
unread: 0,
|
|
}
|
|
flareMatches.unshift(match)
|
|
flareMessages[match.id] = []
|
|
}
|
|
response.json({ success: true, data: { match } })
|
|
return
|
|
}
|
|
if (endpoint === 'flare:rewind') {
|
|
if (!flareLastSwipe) {
|
|
response.json({ success: false, error: 'nothing_to_rewind' })
|
|
return
|
|
}
|
|
const hasMatch = flareMatches.some(
|
|
(match) => match.profile.id === flareLastSwipe.profile.id,
|
|
)
|
|
if (hasMatch) {
|
|
response.json({ success: false, error: 'cannot_rewind_match' })
|
|
return
|
|
}
|
|
flareSuggestions.unshift(flareLastSwipe.profile)
|
|
flareLastSwipe = null
|
|
response.json({ success: true, data: flareBootstrap() })
|
|
return
|
|
}
|
|
if (endpoint === 'flare:unmatch') {
|
|
const index = flareMatches.findIndex(
|
|
(match) => match.id === request.body.matchId,
|
|
)
|
|
if (index < 0) {
|
|
response.json({ success: false, error: 'match_not_found' })
|
|
return
|
|
}
|
|
flareMatches.splice(index, 1)
|
|
delete flareMessages[request.body.matchId]
|
|
response.json({ success: true, data: { matches: flareMatches } })
|
|
return
|
|
}
|
|
if (endpoint === 'flare:thread') {
|
|
const match = flareMatches.find((item) => item.id === request.body.matchId)
|
|
if (!match) {
|
|
response.json({ success: false, error: 'match_not_found' })
|
|
return
|
|
}
|
|
match.unread = 0
|
|
response.json({
|
|
success: true,
|
|
data: { messages: flareMessages[match.id] ?? [] },
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'flare:send') {
|
|
const match = flareMatches.find((item) => item.id === request.body.matchId)
|
|
const messageType = request.body.messageType ?? 'text'
|
|
const body = String(request.body.body ?? '').trim()
|
|
const mediaUrl = String(request.body.mediaAssetId ?? '')
|
|
if (
|
|
!match ||
|
|
(messageType === 'text'
|
|
? !body
|
|
: !['image', 'gif', 'video'].includes(messageType) || !mediaUrl)
|
|
) {
|
|
response.json({ success: false, error: 'invalid_message' })
|
|
return
|
|
}
|
|
const message = {
|
|
id: `flare-message-${Date.now()}`,
|
|
direction: 'sent',
|
|
body: messageType === 'text' ? body : '',
|
|
createdAt: Date.now(),
|
|
mediaDurationMs: request.body.mediaDurationMs ?? null,
|
|
mediaUrl: messageType === 'text' ? null : mediaUrl,
|
|
messageType,
|
|
}
|
|
flareMessages[match.id] ??= []
|
|
flareMessages[match.id].push(message)
|
|
match.lastMessage = message.body
|
|
match.lastMessageAt = message.createdAt
|
|
match.lastMessageType = messageType
|
|
response.json({ success: true, data: message })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:bootstrap') {
|
|
if (!flipTokAuthenticated) {
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
authenticated: false,
|
|
musicTracks: flipTokMusicTracks,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
authenticated: true,
|
|
profile: flipTokProfile,
|
|
feed: { items: flipTokVideos, offset: 0, hasMore: false },
|
|
isAdmin: true,
|
|
musicTracks: flipTokMusicTracks,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:login' || endpoint === 'fliptok:register') {
|
|
flipTokAuthenticated = true
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:logout') {
|
|
flipTokAuthenticated = false
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:feed') {
|
|
const items =
|
|
request.body.mode === 'following'
|
|
? flipTokVideos.filter((video) => video.is_following)
|
|
: flipTokVideos
|
|
response.json({ success: true, data: { items, offset: 0, hasMore: false } })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:discover') {
|
|
const search = String(request.body.search ?? '').toLowerCase()
|
|
response.json({
|
|
success: true,
|
|
data: flipTokVideos.filter((video) =>
|
|
`${video.handle} ${video.display_name} ${video.caption}`
|
|
.toLowerCase()
|
|
.includes(search),
|
|
),
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:react') {
|
|
const video = flipTokVideos.find((item) => item.id === request.body.id)
|
|
if (video) {
|
|
const key = request.body.kind === 'like' ? 'is_liked' : 'is_saved'
|
|
video[key] = request.body.active
|
|
}
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:follow') {
|
|
flipTokVideos
|
|
.filter((video) => video.profile_id === request.body.profileId)
|
|
.forEach((video) => {
|
|
video.is_following = request.body.active
|
|
})
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:share') {
|
|
const video = flipTokVideos.find((item) => item.id === request.body.id)
|
|
if (video) video.share_count += 1
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:comments') {
|
|
response.json({ success: true, data: flipTokComments })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:comment') {
|
|
flipTokComments.unshift({
|
|
id: `comment-${Date.now()}`,
|
|
profile_id: 1,
|
|
handle: flipTokProfile.handle,
|
|
display_name: flipTokProfile.display_name,
|
|
verified: flipTokProfile.verified,
|
|
body: request.body.body,
|
|
created_at: Date.now(),
|
|
})
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:activities') {
|
|
response.json({ success: true, data: flipTokActivities })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:profile') {
|
|
const profileId = Number(request.body.profileId || 0)
|
|
const handle = String(request.body.handle || '').toLowerCase()
|
|
const own =
|
|
profileId === flipTokProfile.id || handle === flipTokProfile.handle
|
|
const videos = own
|
|
? flipTokVideos.filter((video) => video.profile_id === flipTokProfile.id)
|
|
: flipTokVideos.filter((video) =>
|
|
profileId ? video.profile_id === profileId : video.handle === handle,
|
|
)
|
|
const first = videos[0]
|
|
if (!first && !own) {
|
|
response.json({ success: false, error: 'profile_not_found' })
|
|
return
|
|
}
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
profile: own
|
|
? { ...flipTokProfile }
|
|
: {
|
|
id: first.profile_id,
|
|
handle: first.handle,
|
|
display_name: first.display_name,
|
|
bio: 'Creator in Los Santos.',
|
|
account_type: 'person',
|
|
verified: first.verified,
|
|
is_following: first.is_following,
|
|
is_owner: false,
|
|
followers: 12840,
|
|
following: 91,
|
|
video_count: videos.length,
|
|
},
|
|
videos,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:block') {
|
|
const profileId = Number(request.body.profileId)
|
|
flipTokVideos = flipTokVideos.filter(
|
|
(video) => video.profile_id !== profileId,
|
|
)
|
|
flipTokComments = flipTokComments.filter(
|
|
(comment) => comment.profile_id !== profileId,
|
|
)
|
|
flipTokActivities = flipTokActivities.filter(
|
|
(activity) => activity.profile_id !== profileId,
|
|
)
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:admin-reports') {
|
|
response.json({ success: true, data: flipTokReports })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:admin-resolve-report') {
|
|
const selected = flipTokReports.find(
|
|
(report) => report.id === request.body.id,
|
|
)
|
|
if (selected && request.body.action === 'remove')
|
|
flipTokVideos = flipTokVideos.filter(
|
|
(video) => video.id !== selected.video_id,
|
|
)
|
|
flipTokReports = flipTokReports.filter((report) =>
|
|
request.body.action === 'remove'
|
|
? report.video_id !== selected?.video_id
|
|
: report.id !== request.body.id,
|
|
)
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:update-profile') {
|
|
Object.assign(flipTokProfile, {
|
|
handle: request.body.handle,
|
|
display_name: request.body.displayName,
|
|
bio: request.body.bio,
|
|
account_type: request.body.accountType,
|
|
})
|
|
response.json({ success: true, data: flipTokProfile })
|
|
return
|
|
}
|
|
if (endpoint === 'fliptok:publish') {
|
|
const media = mockMedia.find(
|
|
(item) => item.id === request.body.mediaId && item.mediaType === 'video',
|
|
)
|
|
if (!media) {
|
|
response.json({ success: false, error: 'invalid_media' })
|
|
return
|
|
}
|
|
flipTokVideos.unshift({
|
|
id: `fliptok-${Date.now()}`,
|
|
profile_id: 1,
|
|
handle: flipTokProfile.handle,
|
|
display_name: flipTokProfile.display_name,
|
|
verified: flipTokProfile.verified,
|
|
caption: request.body.caption,
|
|
location: request.body.location,
|
|
trim_start_ms: request.body.trimStartMs || 0,
|
|
trim_end_ms: request.body.trimEndMs || null,
|
|
cover_time_ms: request.body.coverTimeMs || 0,
|
|
original_volume: request.body.originalVolume ?? 100,
|
|
music_volume: request.body.musicVolume || 0,
|
|
music_track: request.body.musicTrack || '',
|
|
music_title:
|
|
flipTokMusicTracks.find((track) => track.id === request.body.musicTrack)
|
|
?.title || '',
|
|
music_artist:
|
|
flipTokMusicTracks.find((track) => track.id === request.body.musicTrack)
|
|
?.artist || '',
|
|
music_url:
|
|
flipTokMusicTracks.find((track) => track.id === request.body.musicTrack)
|
|
?.url || '',
|
|
url: media.url,
|
|
comments_enabled: request.body.commentsEnabled,
|
|
is_liked: false,
|
|
is_saved: false,
|
|
is_following: false,
|
|
is_owner: true,
|
|
like_count: 0,
|
|
comment_count: 0,
|
|
view_count: 0,
|
|
share_count: 0,
|
|
created_at: Date.now(),
|
|
})
|
|
response.json({ success: true, data: { id: flipTokVideos[0].id } })
|
|
return
|
|
}
|
|
if (endpoint.startsWith('fliptok:')) {
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'banking:overview') {
|
|
response.json({ success: true, data: bankingOverview() })
|
|
return
|
|
}
|
|
if (endpoint === 'garage:vehicles') {
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
system: 'jg',
|
|
valet: {
|
|
account: 'bank',
|
|
enabled: true,
|
|
price: 750,
|
|
vehicleTypes: {
|
|
bike: true,
|
|
boat: false,
|
|
car: true,
|
|
helicopter: false,
|
|
plane: false,
|
|
},
|
|
},
|
|
vehicles: mockGarageVehicles,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'garage:valet-state') {
|
|
response.json({ success: true, data: mockGarageValet })
|
|
return
|
|
}
|
|
if (endpoint === 'garage:valet-request') {
|
|
const vehicle = mockGarageVehicles.find(
|
|
(item) => item.plate === request.body.plate,
|
|
)
|
|
if (!vehicle || vehicle.status !== 'garaged') {
|
|
response.json({ success: false, error: 'vehicle_not_garaged' })
|
|
return
|
|
}
|
|
if (!['car', 'bike'].includes(vehicle.kind)) {
|
|
response.json({ success: false, error: 'valet_vehicle_type' })
|
|
return
|
|
}
|
|
mockGarageValet = {
|
|
canCancel: true,
|
|
cost: 750,
|
|
distance: 1240,
|
|
etaSeconds: 62,
|
|
orderId: 'mock-valet-1',
|
|
plate: vehicle.plate,
|
|
status: 'en_route',
|
|
vehicleName: vehicle.nickname || vehicle.name,
|
|
}
|
|
response.json({ success: true, data: mockGarageValet })
|
|
return
|
|
}
|
|
if (endpoint === 'garage:valet-cancel') {
|
|
mockGarageValet = null
|
|
response.json({ success: true, data: null })
|
|
return
|
|
}
|
|
if (endpoint === 'banking:transfer') {
|
|
const amount = Number(request.body.amount)
|
|
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
response.json({ success: false, error: 'invalid_request' })
|
|
return
|
|
}
|
|
if (mockBankBalance < amount) {
|
|
response.json({ success: false, error: 'insufficient_funds' })
|
|
return
|
|
}
|
|
const kind = 'transfer_out'
|
|
mockBankBalance -= amount
|
|
mockBankTransactions.unshift({
|
|
amount,
|
|
createdAt: Date.now(),
|
|
id: nextBankTransactionId++,
|
|
kind,
|
|
label: kind === 'transfer_out' ? `Player #${request.body.target}` : '',
|
|
reference: `mock-${Date.now()}`,
|
|
})
|
|
response.json({ success: true, data: bankingOverview() })
|
|
return
|
|
}
|
|
if (endpoint === 'weather:get') {
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
clock: { year: 2026, month: 8, day: 5, hour: 17, minute: 20 },
|
|
condition: 'partly_cloudy',
|
|
nextCondition: 'rain',
|
|
rainLevel: 0.08,
|
|
region: 'los_santos',
|
|
windSpeed: 3.2,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'map:getPlayerCoords') {
|
|
response.json({
|
|
success: true,
|
|
data: { coords: { x: -75.2, y: -818.9, z: 326.2 } },
|
|
})
|
|
return
|
|
}
|
|
|
|
if (endpoint === 'map:setWaypoint') {
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'map:markers') {
|
|
response.json({ success: true, data: mockMapMarkers })
|
|
return
|
|
}
|
|
if (endpoint === 'map:create-marker') {
|
|
const label = String(request.body.label ?? '').trim()
|
|
const color = String(request.body.color ?? '')
|
|
const coords = request.body.coords
|
|
if (!label || label.length > 40 || !coords) {
|
|
response.json({ success: false, error: 'invalid_marker' })
|
|
return
|
|
}
|
|
const marker = {
|
|
color,
|
|
coords: {
|
|
x: Number(coords.x),
|
|
y: Number(coords.y),
|
|
z: Number(coords.z) || 0,
|
|
},
|
|
id: `mock-map-marker-${Date.now()}`,
|
|
label,
|
|
}
|
|
mockMapMarkers.push(marker)
|
|
response.json({ success: true, data: marker })
|
|
return
|
|
}
|
|
if (endpoint === 'map:delete-marker') {
|
|
const previousLength = mockMapMarkers.length
|
|
mockMapMarkers = mockMapMarkers.filter(
|
|
(marker) => marker.id !== request.body.id,
|
|
)
|
|
response.json(
|
|
mockMapMarkers.length === previousLength
|
|
? { success: false, error: 'marker_not_found' }
|
|
: { success: true },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:bootstrap') {
|
|
response.json({ success: true, data: darkChatBootstrap() })
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:update-profile') {
|
|
darkChatProfile.alias = String(
|
|
request.body.alias ?? darkChatProfile.alias,
|
|
).trim()
|
|
darkChatProfile.notificationMode =
|
|
request.body.notificationMode ?? 'private'
|
|
darkChatProfile.activityVisible = Boolean(request.body.activityVisible)
|
|
response.json({ success: true, data: darkChatProfile })
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:start') {
|
|
const identifier = String(request.body.identifier ?? '').toUpperCase()
|
|
const peer =
|
|
darkChatPeers.find((item) => item.darkId.toUpperCase() === identifier) ??
|
|
(identifier === 'DC-ECH0-77LM' ? darkChatPeers[1] : null)
|
|
if (!peer) {
|
|
response.json({ success: false, error: 'profile_not_found' })
|
|
return
|
|
}
|
|
let conversation = darkChatConversations.find(
|
|
(item) => item.peer.id === peer.id,
|
|
)
|
|
if (!conversation) {
|
|
conversation = {
|
|
id: `dc-conversation-${Date.now()}`,
|
|
peer,
|
|
disappearingSeconds: 0,
|
|
notificationsEnabled: true,
|
|
readReceipts: true,
|
|
blockedByPeer: false,
|
|
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
|
}
|
|
darkChatConversations.push(conversation)
|
|
}
|
|
response.json({ success: true, data: { conversationId: conversation.id } })
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:thread') {
|
|
const conversation = darkChatConversations.find(
|
|
(item) => item.id === request.body.conversationId,
|
|
)
|
|
if (!conversation) {
|
|
response.json({ success: false, error: 'conversation_not_found' })
|
|
return
|
|
}
|
|
const thread = darkChatMessages.filter(
|
|
(message) => message.conversationId === conversation.id,
|
|
)
|
|
for (const message of thread) {
|
|
if (message.direction === 'received')
|
|
message.readAt = message.readAt ?? new Date().toISOString()
|
|
}
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
conversation,
|
|
messages: thread.map(({ mediaSecret, ...message }) => message),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:send') {
|
|
const conversation = darkChatConversations.find(
|
|
(item) => item.id === request.body.conversationId,
|
|
)
|
|
if (!conversation || conversation.peer.blocked) {
|
|
response.json({
|
|
success: false,
|
|
error: conversation ? 'blocked' : 'conversation_not_found',
|
|
})
|
|
return
|
|
}
|
|
const messageType = request.body.messageType ?? 'text'
|
|
const body = String(request.body.body ?? '')
|
|
if ((messageType === 'text' || messageType === 'emoji') && !body.trim()) {
|
|
response.json({ success: false, error: 'invalid_message' })
|
|
return
|
|
}
|
|
const reply = darkChatMessages.find(
|
|
(message) => message.id === request.body.replyToId,
|
|
)
|
|
const message = {
|
|
id: `dc-message-${Date.now()}`,
|
|
conversationId: conversation.id,
|
|
direction: 'sent',
|
|
senderProfileId: darkChatProfile.id,
|
|
messageType,
|
|
body,
|
|
mediaPayload:
|
|
messageType === 'gif' ? request.body.mediaPayload : undefined,
|
|
mediaSecret:
|
|
messageType === 'voice' ? request.body.mediaPayload : undefined,
|
|
mediaMime: request.body.mediaMime,
|
|
mediaDurationMs: request.body.mediaDurationMs,
|
|
mediaWaveform: request.body.mediaWaveform,
|
|
replyToId: request.body.replyToId,
|
|
replyBody: reply?.body,
|
|
reactions: {},
|
|
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
|
readAt: null,
|
|
}
|
|
darkChatMessages.push(message)
|
|
const { mediaSecret, ...publicMessage } = message
|
|
response.json({ success: true, data: publicMessage })
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:media') {
|
|
const message = darkChatMessages.find(
|
|
(item) =>
|
|
item.id === request.body.messageId && item.messageType === 'voice',
|
|
)
|
|
response.json(
|
|
message?.mediaSecret
|
|
? {
|
|
success: true,
|
|
data: { mime: message.mediaMime, payload: message.mediaSecret },
|
|
}
|
|
: { success: false, error: 'message_not_found' },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:react') {
|
|
const message = darkChatMessages.find(
|
|
(item) => item.id === request.body.messageId,
|
|
)
|
|
if (message) {
|
|
const current = message.reactions[String(darkChatProfile.id)]
|
|
if (current === request.body.reaction)
|
|
delete message.reactions[String(darkChatProfile.id)]
|
|
else message.reactions[String(darkChatProfile.id)] = request.body.reaction
|
|
}
|
|
response.json({
|
|
success: Boolean(message),
|
|
error: message ? undefined : 'message_not_found',
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:message-action') {
|
|
const index = darkChatMessages.findIndex(
|
|
(item) => item.id === request.body.messageId,
|
|
)
|
|
if (index >= 0 && request.body.action === 'delete_me')
|
|
darkChatMessages.splice(index, 1)
|
|
else if (index >= 0 && request.body.action === 'delete_all') {
|
|
darkChatMessages[index].messageType = 'system'
|
|
darkChatMessages[index].body = 'message_deleted'
|
|
darkChatMessages[index].mediaPayload = undefined
|
|
}
|
|
response.json({ success: index >= 0 })
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:update-conversation') {
|
|
const conversation = darkChatConversations.find(
|
|
(item) => item.id === request.body.conversationId,
|
|
)
|
|
if (conversation) {
|
|
conversation.disappearingSeconds = Number(
|
|
request.body.disappearingSeconds,
|
|
)
|
|
conversation.notificationsEnabled = Boolean(
|
|
request.body.notificationsEnabled,
|
|
)
|
|
conversation.readReceipts = Boolean(request.body.readReceipts)
|
|
darkChatMessages.push({
|
|
id: `dc-system-${Date.now()}`,
|
|
conversationId: conversation.id,
|
|
direction: 'received',
|
|
messageType: 'system',
|
|
body: `timer_changed:${conversation.disappearingSeconds}`,
|
|
reactions: {},
|
|
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
|
})
|
|
}
|
|
response.json({ success: Boolean(conversation) })
|
|
return
|
|
}
|
|
if (
|
|
endpoint === 'darkchat:add-contact' ||
|
|
endpoint === 'darkchat:remove-contact'
|
|
) {
|
|
const conversation = darkChatConversations.find(
|
|
(item) => item.id === request.body.conversationId,
|
|
)
|
|
if (conversation) {
|
|
conversation.peer.isContact = endpoint === 'darkchat:add-contact'
|
|
if (request.body.alias)
|
|
conversation.peer.alias = String(request.body.alias)
|
|
}
|
|
response.json({ success: Boolean(conversation) })
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:block') {
|
|
const conversation = darkChatConversations.find(
|
|
(item) => item.id === request.body.conversationId,
|
|
)
|
|
if (conversation) conversation.peer.blocked = Boolean(request.body.blocked)
|
|
response.json({ success: Boolean(conversation) })
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:clear') {
|
|
for (let index = darkChatMessages.length - 1; index >= 0; index -= 1) {
|
|
if (
|
|
darkChatMessages[index].conversationId === request.body.conversationId
|
|
)
|
|
darkChatMessages.splice(index, 1)
|
|
}
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'darkchat:report') {
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'messages:conversations') {
|
|
const grouped = new Map()
|
|
for (const message of [...smsMessages].reverse()) {
|
|
const phoneNumber =
|
|
message.direction === 'sent'
|
|
? message.recipient_number
|
|
: message.sender_number
|
|
const conversation = grouped.get(phoneNumber)
|
|
if (conversation) {
|
|
if (message.direction === 'received' && !message.read_at)
|
|
conversation.unread += 1
|
|
continue
|
|
}
|
|
grouped.set(phoneNumber, {
|
|
lastMessage: message.body,
|
|
lastMessageAt: message.created_at,
|
|
lastMessageType: message.message_type,
|
|
phoneNumber,
|
|
unread: message.direction === 'received' && !message.read_at ? 1 : 0,
|
|
})
|
|
}
|
|
response.json({ success: true, data: [...grouped.values()] })
|
|
return
|
|
}
|
|
if (endpoint === 'messages:gifs') {
|
|
const offset = Math.max(0, Number(request.body.offset ?? 0))
|
|
const pageSize = 6
|
|
const results = gifMocks
|
|
.slice(offset, offset + pageSize)
|
|
.map(([id, title]) => ({
|
|
height: 200,
|
|
id,
|
|
previewUrl: `https://media.giphy.com/media/${id}/200w.gif`,
|
|
title,
|
|
url: `https://media.giphy.com/media/${id}/giphy.gif`,
|
|
width: 200,
|
|
}))
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
hasMore: offset + results.length < gifMocks.length,
|
|
nextOffset: offset + results.length,
|
|
results,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'development:bootstrap') {
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
account: linkedAccount,
|
|
device: {
|
|
data: deviceData,
|
|
imei: '356938035643809',
|
|
name: 'Personal iFruit Phone',
|
|
sim: {
|
|
id: 'development-sim',
|
|
number: '5551234567',
|
|
registered: true,
|
|
type: 'registered',
|
|
},
|
|
},
|
|
notes: mockNotes,
|
|
security: mockSecurity,
|
|
token: 'development',
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'messages:thread') {
|
|
const number = String(request.body.phoneNumber)
|
|
const thread = smsMessages.filter(
|
|
(message) =>
|
|
message.sender_number === number || message.recipient_number === number,
|
|
)
|
|
for (const message of thread) {
|
|
if (message.direction === 'received')
|
|
message.read_at = message.read_at ?? '2026-08-06 13:06:00'
|
|
}
|
|
response.json({
|
|
success: true,
|
|
data: thread.map(({ media_payload, ...message }) => ({
|
|
...message,
|
|
media_asset_id: ['image', 'gif', 'video'].includes(message.message_type)
|
|
? media_payload
|
|
: null,
|
|
})),
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'messages:media') {
|
|
const message = smsMessages.find(
|
|
(item) => item.id === request.body.id && item.message_type === 'voice',
|
|
)
|
|
response.json(
|
|
message
|
|
? {
|
|
success: true,
|
|
data: {
|
|
mime: message.media_mime,
|
|
payload: message.media_payload,
|
|
},
|
|
}
|
|
: { success: false, error: 'message_not_found' },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint === 'messages:send') {
|
|
const body = String(request.body.body ?? '').trim()
|
|
const phoneNumber = String(request.body.phoneNumber ?? '')
|
|
const messageType = request.body.messageType ?? 'text'
|
|
const isAttachment = ['image', 'gif', 'video'].includes(messageType)
|
|
const requestedAttachmentId = String(request.body.mediaAssetId ?? '')
|
|
const selectedMedia = /^\d+$/.test(requestedAttachmentId)
|
|
? mockMedia.find((item) => String(item.id) === requestedAttachmentId)
|
|
: null
|
|
const attachmentId = selectedMedia?.url ?? requestedAttachmentId
|
|
if (
|
|
!phoneNumber ||
|
|
(messageType === 'text' && !body) ||
|
|
(messageType === 'voice' && !request.body.mediaPayload) ||
|
|
(isAttachment &&
|
|
!attachmentAssets[messageType].has(attachmentId) &&
|
|
!attachmentId.startsWith('https://') &&
|
|
!attachmentId.startsWith('data:image/'))
|
|
) {
|
|
response.json({ success: false, error: 'invalid_message' })
|
|
return
|
|
}
|
|
const message = {
|
|
body,
|
|
created_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
|
direction: 'sent',
|
|
id: `sms-${Date.now()}`,
|
|
media_duration_ms: ['voice', 'video'].includes(messageType)
|
|
? (request.body.mediaDurationMs ?? null)
|
|
: null,
|
|
media_mime:
|
|
messageType === 'voice'
|
|
? request.body.mediaMime
|
|
: messageType === 'image'
|
|
? 'image/jpeg'
|
|
: messageType === 'gif'
|
|
? 'image/gif'
|
|
: messageType === 'video'
|
|
? 'video/mp4'
|
|
: null,
|
|
media_payload:
|
|
messageType === 'voice'
|
|
? request.body.mediaPayload
|
|
: isAttachment
|
|
? attachmentId
|
|
: null,
|
|
media_waveform:
|
|
messageType === 'voice' ? request.body.mediaWaveform : null,
|
|
message_type: messageType,
|
|
media_asset_id: isAttachment ? attachmentId : null,
|
|
read_at: null,
|
|
recipient_number: phoneNumber,
|
|
sender_number: '5551234567',
|
|
}
|
|
smsMessages.push(message)
|
|
const { media_payload, ...publicMessage } = message
|
|
response.json({ success: true, data: publicMessage })
|
|
return
|
|
}
|
|
if (endpoint === 'messages:delete') {
|
|
const phoneNumbers = new Set(
|
|
Array.isArray(request.body.phoneNumbers)
|
|
? request.body.phoneNumbers.map(String)
|
|
: [],
|
|
)
|
|
for (let index = smsMessages.length - 1; index >= 0; index -= 1) {
|
|
const message = smsMessages[index]
|
|
const otherNumber =
|
|
message.direction === 'sent'
|
|
? message.recipient_number
|
|
: message.sender_number
|
|
if (phoneNumbers.has(otherNumber)) smsMessages.splice(index, 1)
|
|
}
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'contacts:list') {
|
|
response.json({ success: true, data: contacts })
|
|
return
|
|
}
|
|
if (endpoint === 'contacts:save') {
|
|
const name = String(request.body.name ?? '').trim()
|
|
const phoneNumber = String(request.body.phoneNumber ?? '').trim()
|
|
if (!name || !phoneNumber) {
|
|
response.json({ success: false, error: 'invalid_contact' })
|
|
return
|
|
}
|
|
let contact = contacts.find((item) => item.id === request.body.id)
|
|
if (contact) {
|
|
contact.name = name
|
|
contact.phone_number = phoneNumber
|
|
contact.updated_at = new Date()
|
|
.toISOString()
|
|
.slice(0, 19)
|
|
.replace('T', ' ')
|
|
} else {
|
|
const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
contact = {
|
|
created_at: now,
|
|
id: `contact-${contactSequence++}`,
|
|
name,
|
|
phone_number: phoneNumber,
|
|
updated_at: now,
|
|
}
|
|
contacts.push(contact)
|
|
}
|
|
response.json({ success: true, data: contact })
|
|
return
|
|
}
|
|
if (endpoint === 'contacts:delete') {
|
|
const index = contacts.findIndex((item) => item.id === request.body.id)
|
|
if (index >= 0) contacts.splice(index, 1)
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'media:config') {
|
|
response.json({ success: true, data: { videoBitrateKbps: 1500 } })
|
|
return
|
|
}
|
|
if (endpoint === 'gallery:list') {
|
|
if (request.body.mockState === 'error') {
|
|
response.json({ success: false, error: 'service_unavailable' })
|
|
return
|
|
}
|
|
if (request.body.mockState === 'empty') {
|
|
response.json({ success: true, data: [] })
|
|
return
|
|
}
|
|
const filtered = request.body.mediaType
|
|
? mockMedia.filter((item) => item.mediaType === request.body.mediaType)
|
|
: mockMedia
|
|
const offset = Number(request.body.offset) || 0
|
|
const limit = Number(request.body.limit) || 30
|
|
response.json({
|
|
success: true,
|
|
data: filtered.slice(offset, offset + limit),
|
|
})
|
|
return
|
|
}
|
|
if (
|
|
endpoint === 'media:requestUpload' ||
|
|
endpoint === 'media:completeUpload' ||
|
|
endpoint === 'media:failUpload' ||
|
|
endpoint === 'media:cancelUpload'
|
|
) {
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'gallery:delete') {
|
|
mockMedia = mockMedia.filter((item) => item.id !== Number(request.body.id))
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'account:login' || endpoint === 'account:register') {
|
|
authenticated = true
|
|
linkedAccount = {
|
|
devices: accountDevices,
|
|
email: request.body.email.includes('@')
|
|
? request.body.email
|
|
: `${request.body.email}@ifruit.com`,
|
|
id: 1,
|
|
}
|
|
response.json({ success: true, data: linkedAccount })
|
|
return
|
|
}
|
|
if (endpoint === 'account:logout') {
|
|
authenticated = false
|
|
linkedAccount = null
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'account:devices') {
|
|
response.json({ success: true, data: accountDevices })
|
|
return
|
|
}
|
|
if (endpoint === 'account:remove-device') {
|
|
response.json({ success: true, data: accountDevices })
|
|
return
|
|
}
|
|
if (endpoint === 'device:save') {
|
|
const current = deviceData[request.body.namespace]
|
|
const revision = (current?.revision ?? 0) + 1
|
|
deviceData[request.body.namespace] = {
|
|
payload: request.body.payload,
|
|
revision,
|
|
}
|
|
response.json({ success: true, data: { revision } })
|
|
return
|
|
}
|
|
if (endpoint === 'security:unlock') {
|
|
response.json(
|
|
!mockSecurity.enabled || request.body.passcode === mockPasscode
|
|
? { success: true, data: { security: mockSecurity } }
|
|
: { success: false, error: 'invalid_passcode' },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint === 'security:set-passcode') {
|
|
mockPasscode = String(request.body.passcode)
|
|
mockSecurity = {
|
|
enabled: true,
|
|
length: mockPasscode.length,
|
|
lockedUntil: 0,
|
|
}
|
|
response.json({ success: true, data: { security: mockSecurity } })
|
|
return
|
|
}
|
|
if (endpoint === 'security:change-passcode') {
|
|
if (request.body.currentPasscode !== mockPasscode) {
|
|
response.json({ success: false, error: 'invalid_passcode' })
|
|
return
|
|
}
|
|
mockPasscode = String(request.body.newPasscode)
|
|
mockSecurity = {
|
|
enabled: true,
|
|
length: mockPasscode.length,
|
|
lockedUntil: 0,
|
|
}
|
|
response.json({ success: true, data: { security: mockSecurity } })
|
|
return
|
|
}
|
|
if (endpoint === 'security:disable-passcode') {
|
|
if (request.body.passcode !== mockPasscode) {
|
|
response.json({ success: false, error: 'invalid_passcode' })
|
|
return
|
|
}
|
|
mockPasscode = ''
|
|
mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
|
|
response.json({ success: true, data: { security: mockSecurity } })
|
|
return
|
|
}
|
|
if (endpoint === 'device:factory-reset') {
|
|
authenticated = false
|
|
linkedAccount = null
|
|
mockNotes = []
|
|
mockMedia = []
|
|
calendarEvents = []
|
|
mockPasscode = ''
|
|
mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
|
|
for (const key of Object.keys(deviceData)) delete deviceData[key]
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'contacts:list') {
|
|
response.json({ success: true, data: mockContacts })
|
|
return
|
|
}
|
|
if (endpoint === 'contacts:save') {
|
|
const now = new Date().toISOString()
|
|
const existing = mockContacts.find(
|
|
(contact) => contact.id === request.body.id,
|
|
)
|
|
if (existing) {
|
|
Object.assign(existing, {
|
|
name: request.body.name,
|
|
phone_number: request.body.phoneNumber,
|
|
updated_at: now,
|
|
})
|
|
response.json({ success: true, data: existing })
|
|
return
|
|
}
|
|
const contact = {
|
|
created_at: now,
|
|
id: `contact-${Date.now()}`,
|
|
name: request.body.name,
|
|
phone_number: request.body.phoneNumber,
|
|
updated_at: now,
|
|
}
|
|
mockContacts.push(contact)
|
|
response.json({ success: true, data: contact })
|
|
return
|
|
}
|
|
if (endpoint === 'contacts:delete') {
|
|
mockContacts = mockContacts.filter(
|
|
(contact) => contact.id !== request.body.id,
|
|
)
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'calls:recents') {
|
|
response.json({ success: true, data: recentCalls })
|
|
return
|
|
}
|
|
if (endpoint === 'calls:dial') {
|
|
const id = `call-${Date.now()}`
|
|
recentCalls.unshift({
|
|
call_id: id,
|
|
created_at: Date.now(),
|
|
direction: 'outgoing',
|
|
duration_seconds: 0,
|
|
id: recentCalls.length + 1,
|
|
other_number: request.body.phoneNumber,
|
|
status: 'completed',
|
|
})
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
direction: 'outgoing',
|
|
id,
|
|
otherNumber: request.body.phoneNumber,
|
|
startedAt: Date.now(),
|
|
state: 'ringing',
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (
|
|
endpoint === 'calls:answer' ||
|
|
endpoint === 'calls:decline' ||
|
|
endpoint === 'calls:hangup'
|
|
) {
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint.startsWith('calendar:') && !authenticated) {
|
|
response.json({ success: false, error: 'not_authenticated' })
|
|
return
|
|
}
|
|
if (endpoint === 'calendar:list') {
|
|
const startsAt = Number(request.body.startsAt) * 1000
|
|
const endsAt = Number(request.body.endsAt) * 1000
|
|
response.json({
|
|
success: true,
|
|
data: calendarEvents.filter(
|
|
(event) => event.endsAt >= startsAt && event.startsAt < endsAt,
|
|
),
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'calendar:create') {
|
|
const id = `calendar-${Date.now()}`
|
|
calendarEvents.push({
|
|
...request.body,
|
|
endsAt: Number(request.body.endsAt) * 1000,
|
|
id,
|
|
remindedAt: null,
|
|
revision: 1,
|
|
startsAt: Number(request.body.startsAt) * 1000,
|
|
})
|
|
response.json({ success: true, data: { id } })
|
|
return
|
|
}
|
|
if (endpoint === 'calendar:update') {
|
|
const index = calendarEvents.findIndex(
|
|
(event) => event.id === request.body.id,
|
|
)
|
|
if (index < 0 || calendarEvents[index].revision !== request.body.revision) {
|
|
response.json({ success: false, error: 'conflict' })
|
|
return
|
|
}
|
|
calendarEvents[index] = {
|
|
...calendarEvents[index],
|
|
...request.body,
|
|
endsAt: Number(request.body.endsAt) * 1000,
|
|
remindedAt: null,
|
|
revision: calendarEvents[index].revision + 1,
|
|
startsAt: Number(request.body.startsAt) * 1000,
|
|
}
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'calendar:delete') {
|
|
calendarEvents = calendarEvents.filter(
|
|
(event) => event.id !== request.body.id,
|
|
)
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'pages:list') {
|
|
const query = String(request.body.search ?? '').toLowerCase()
|
|
let items = pagesPosts
|
|
if (request.body.category && request.body.category !== 'all')
|
|
items = items.filter((item) => item.category === request.body.category)
|
|
if (query)
|
|
items = items.filter((item) =>
|
|
`${item.title} ${item.body}`.toLowerCase().includes(query),
|
|
)
|
|
if (request.body.saved)
|
|
items = items.filter((post) =>
|
|
pagesReactions.some(
|
|
(reaction) =>
|
|
reaction.post_id === post.id &&
|
|
reaction.account_id === 1 &&
|
|
reaction.kind === 'save',
|
|
),
|
|
)
|
|
response.json({
|
|
success: true,
|
|
data: { hasMore: false, items: items.map(pageView), offset: 0 },
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'pages:get') {
|
|
const post = pagesPosts.find((item) => item.id === request.body.id)
|
|
response.json(
|
|
post
|
|
? { success: true, data: pageView(post) }
|
|
: { success: false, error: 'post_not_found' },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint.startsWith('pages:') && !authenticated) {
|
|
response.json({ success: false, error: 'not_authenticated' })
|
|
return
|
|
}
|
|
if (endpoint === 'pages:list-own') {
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
hasMore: false,
|
|
items: pagesPosts.filter((item) => item.account_id === 1).map(pageView),
|
|
offset: 0,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'pages:create') {
|
|
const gradients = {
|
|
'sunset-drive': 'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)',
|
|
'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
|
|
'city-lights': 'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)',
|
|
'desert-road': 'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)',
|
|
}
|
|
const id = `pages-${Date.now()}`
|
|
const images = request.body.images.map((image, index) => ({
|
|
media_id: image.id,
|
|
gradient:
|
|
gradients[image.id] ??
|
|
'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)',
|
|
sort_order: index + 1,
|
|
}))
|
|
pagesPosts.unshift({
|
|
...request.body,
|
|
id,
|
|
account_id: 1,
|
|
author_name: 'demo',
|
|
source_type: 'personal',
|
|
citymarkt_listing_id: null,
|
|
created_at: new Date().toISOString(),
|
|
like_count: 0,
|
|
images,
|
|
})
|
|
response.json({ success: true, data: { id } })
|
|
return
|
|
}
|
|
if (endpoint === 'pages:share-citymarkt') {
|
|
const listing = marketplaceListings.find(
|
|
(item) =>
|
|
item.id === request.body.listingId &&
|
|
item.seller_account_id === 1 &&
|
|
['active', 'reserved'].includes(item.status),
|
|
)
|
|
if (!listing) {
|
|
response.json({ success: false, error: 'citymarkt_not_found' })
|
|
return
|
|
}
|
|
if (pagesPosts.some((item) => item.citymarkt_listing_id === listing.id)) {
|
|
response.json({ success: false, error: 'citymarkt_already_shared' })
|
|
return
|
|
}
|
|
if (
|
|
pagesPosts.some(
|
|
(item) =>
|
|
item.account_id === 1 &&
|
|
item.source_type === 'citymarkt' &&
|
|
item.created_at.slice(0, 10) === '2026-08-06',
|
|
)
|
|
) {
|
|
response.json({ success: false, error: 'citymarkt_daily_limit' })
|
|
return
|
|
}
|
|
const id = `pages-${Date.now()}`
|
|
pagesPosts.unshift({
|
|
id,
|
|
account_id: 1,
|
|
author_name: 'demo',
|
|
source_type: 'citymarkt',
|
|
citymarkt_listing_id: listing.id,
|
|
title: listing.title,
|
|
body: listing.description,
|
|
category: 'citymarkt',
|
|
district: listing.district,
|
|
created_at: '2026-08-06 13:30:00',
|
|
like_count: 0,
|
|
images: listing.images,
|
|
})
|
|
response.json({ success: true, data: { id } })
|
|
return
|
|
}
|
|
if (endpoint === 'pages:react') {
|
|
const index = pagesReactions.findIndex(
|
|
(item) =>
|
|
item.post_id === request.body.id &&
|
|
item.account_id === 1 &&
|
|
item.kind === request.body.kind,
|
|
)
|
|
if (request.body.active && index < 0)
|
|
pagesReactions.push({
|
|
post_id: request.body.id,
|
|
account_id: 1,
|
|
kind: request.body.kind,
|
|
})
|
|
if (!request.body.active && index >= 0) pagesReactions.splice(index, 1)
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'pages:delete') {
|
|
const index = pagesPosts.findIndex(
|
|
(item) => item.id === request.body.id && item.account_id === 1,
|
|
)
|
|
if (index < 0) {
|
|
response.json({ success: false, error: 'post_not_found' })
|
|
return
|
|
}
|
|
pagesPosts.splice(index, 1)
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:list') {
|
|
const query = String(request.body.search ?? '').toLowerCase()
|
|
let items = marketplaceListings.filter((item) =>
|
|
['active', 'reserved'].includes(item.status),
|
|
)
|
|
if (request.body.category && request.body.category !== 'all')
|
|
items = items.filter((item) => item.category === request.body.category)
|
|
if (request.body.district && request.body.district !== 'all')
|
|
items = items.filter((item) => item.district === request.body.district)
|
|
if (query)
|
|
items = items.filter((item) =>
|
|
`${item.title} ${item.description}`.toLowerCase().includes(query),
|
|
)
|
|
if (request.body.favorites) items = items.filter((item) => item.is_favorite)
|
|
response.json({ success: true, data: { hasMore: false, items, offset: 0 } })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:get') {
|
|
const item = marketplaceListings.find(
|
|
(listing) => listing.id === request.body.id,
|
|
)
|
|
response.json(
|
|
item
|
|
? {
|
|
success: true,
|
|
data: {
|
|
...item,
|
|
is_owner: authenticated && item.seller_account_id === 1,
|
|
},
|
|
}
|
|
: { success: false, error: 'listing_not_found' },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint.startsWith('marketplace:') && !authenticated) {
|
|
response.json({ success: false, error: 'not_authenticated' })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:counts') {
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
active: marketplaceListings.filter(
|
|
(item) =>
|
|
item.seller_account_id === 1 &&
|
|
['active', 'reserved'].includes(item.status),
|
|
).length,
|
|
unread: marketplaceInquiries.reduce(
|
|
(total, item) => total + item.unread,
|
|
0,
|
|
),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:list-own') {
|
|
response.json({
|
|
success: true,
|
|
data: {
|
|
hasMore: false,
|
|
items: marketplaceListings.filter(
|
|
(item) => item.seller_account_id === 1,
|
|
),
|
|
offset: 0,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:create') {
|
|
const id = `81bc9d37-20e1-4d8a-82f8-${String(Date.now()).slice(-12)}`
|
|
const selected = request.body.images.map((image) => {
|
|
const photo =
|
|
{
|
|
'sunset-drive':
|
|
'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)',
|
|
'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
|
|
'city-lights':
|
|
'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)',
|
|
'desert-road':
|
|
'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)',
|
|
}[image.id] ?? 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)'
|
|
return { media_id: image.id, gradient: photo, sort_order: 1 }
|
|
})
|
|
marketplaceListings.unshift({
|
|
...request.body,
|
|
id,
|
|
seller_account_id: 1,
|
|
seller_name: 'demo',
|
|
seller_since: '2026-08-04 12:00:00',
|
|
seller_active: 1,
|
|
item_condition: request.body.condition,
|
|
price_type: request.body.priceType,
|
|
show_phone: request.body.showPhone ? 1 : 0,
|
|
phone_number: null,
|
|
status: 'active',
|
|
revision: 1,
|
|
created_at: '2026-08-06 11:00:00',
|
|
updated_at: '2026-08-06 11:00:00',
|
|
expires_at: '2026-08-13 11:00:00',
|
|
images: selected,
|
|
image: selected[0]?.gradient ?? null,
|
|
is_favorite: false,
|
|
})
|
|
response.json({ success: true, data: { id } })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:update') {
|
|
const item = marketplaceListings.find(
|
|
(listing) => listing.id === request.body.id,
|
|
)
|
|
if (!item) {
|
|
response.json({ success: false, error: 'listing_not_found' })
|
|
return
|
|
}
|
|
const selected = request.body.images.map((image, index) => ({
|
|
media_id: image.id,
|
|
gradient:
|
|
{
|
|
'sunset-drive':
|
|
'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)',
|
|
'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)',
|
|
'city-lights':
|
|
'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)',
|
|
'desert-road':
|
|
'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)',
|
|
}[image.id] ?? item.image,
|
|
sort_order: index + 1,
|
|
}))
|
|
Object.assign(item, request.body, {
|
|
image: selected[0]?.gradient ?? null,
|
|
images: selected,
|
|
item_condition: request.body.condition,
|
|
price_type: request.body.priceType,
|
|
revision: item.revision + 1,
|
|
})
|
|
response.json({ success: true, data: { revision: item.revision } })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:favorite') {
|
|
const item = marketplaceListings.find(
|
|
(listing) => listing.id === request.body.id,
|
|
)
|
|
if (item) item.is_favorite = request.body.favorite
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:set-status') {
|
|
const item = marketplaceListings.find(
|
|
(listing) => listing.id === request.body.id,
|
|
)
|
|
if (item) item.status = request.body.status
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:list-inquiries') {
|
|
response.json({ success: true, data: marketplaceInquiries })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:send-message') {
|
|
let inquiry = marketplaceInquiries.find(
|
|
(item) =>
|
|
item.id === request.body.inquiryId ||
|
|
item.listing_id === request.body.listingId,
|
|
)
|
|
if (!inquiry) {
|
|
const listing = marketplaceListings.find(
|
|
(item) => item.id === request.body.listingId,
|
|
)
|
|
inquiry = {
|
|
id: '4903b923-409a-437e-971f-b7a2b10e9e31',
|
|
listing_id: listing.id,
|
|
seller_account_id: listing.seller_account_id,
|
|
buyer_account_id: 1,
|
|
title: listing.title,
|
|
price: listing.price,
|
|
price_type: listing.price_type,
|
|
status: listing.status,
|
|
image: listing.image,
|
|
other_name: listing.seller_name,
|
|
last_message: request.body.body,
|
|
offer_id: null,
|
|
offer_amount: null,
|
|
offer_proposer_account_id: null,
|
|
offer_status: null,
|
|
offer_revision: 0,
|
|
unread: 0,
|
|
updated_at: '2026-08-06 11:30:00',
|
|
}
|
|
marketplaceInquiries.push(inquiry)
|
|
}
|
|
marketplaceMessages.push({
|
|
id: marketplaceMessages.length + 1,
|
|
inquiry_id: inquiry.id,
|
|
sender_account_id: 1,
|
|
body: request.body.body,
|
|
created_at: '2026-08-06 12:00:00',
|
|
read_at: null,
|
|
})
|
|
inquiry.last_message = request.body.body
|
|
inquiry.unread = 0
|
|
inquiry.updated_at = '2026-08-06 12:00:00'
|
|
response.json({ success: true, data: { id: inquiry.id } })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:get-inquiry') {
|
|
const inquiry = marketplaceInquiries.find(
|
|
(item) => item.id === request.body.id,
|
|
)
|
|
if (inquiry) {
|
|
inquiry.unread = 0
|
|
for (const offer of marketplaceOffers) {
|
|
if (offer.inquiry_id === inquiry.id && offer.proposer_account_id !== 1)
|
|
offer.read_at = '2026-08-06 12:01:00'
|
|
if (
|
|
offer.inquiry_id === inquiry.id &&
|
|
offer.proposer_account_id === 1 &&
|
|
['accepted', 'rejected'].includes(offer.status)
|
|
)
|
|
offer.response_read_at = '2026-08-06 12:01:00'
|
|
}
|
|
}
|
|
const listing =
|
|
inquiry &&
|
|
marketplaceListings.find((item) => item.id === inquiry.listing_id)
|
|
response.json(
|
|
inquiry
|
|
? {
|
|
success: true,
|
|
data: {
|
|
accountId: 1,
|
|
inquiry: {
|
|
...inquiry,
|
|
reserved_account_id: listing.reserved_account_id ?? null,
|
|
status: listing.status,
|
|
},
|
|
messages: marketplaceMessages.filter(
|
|
(message) => message.inquiry_id === inquiry.id,
|
|
),
|
|
offers: marketplaceOffers.filter(
|
|
(offer) => offer.inquiry_id === inquiry.id,
|
|
),
|
|
},
|
|
}
|
|
: { success: false, error: 'inquiry_not_found' },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:make-offer') {
|
|
const inquiry = marketplaceInquiries.find(
|
|
(item) => item.id === request.body.inquiryId,
|
|
)
|
|
const amount = Number(request.body.amount)
|
|
if (!inquiry || !Number.isInteger(amount) || amount < 1) {
|
|
response.json({ success: false, error: 'invalid_offer' })
|
|
return
|
|
}
|
|
if (inquiry.offer_status === 'accepted') {
|
|
response.json({ success: false, error: 'offer_closed' })
|
|
return
|
|
}
|
|
if (
|
|
inquiry.offer_status === 'pending' &&
|
|
inquiry.offer_proposer_account_id === 1
|
|
) {
|
|
response.json({ success: false, error: 'offer_waiting' })
|
|
return
|
|
}
|
|
const previous = marketplaceOffers.find(
|
|
(offer) => offer.id === inquiry.offer_id,
|
|
)
|
|
if (previous?.status === 'pending') previous.status = 'countered'
|
|
const offer = {
|
|
id: marketplaceOffers.length + 1,
|
|
inquiry_id: inquiry.id,
|
|
proposer_account_id: 1,
|
|
amount,
|
|
status: 'pending',
|
|
read_at: null,
|
|
response_read_at: null,
|
|
created_at: '2026-08-06 12:02:00',
|
|
updated_at: '2026-08-06 12:02:00',
|
|
}
|
|
marketplaceOffers.push(offer)
|
|
inquiry.offer_id = offer.id
|
|
inquiry.offer_amount = amount
|
|
inquiry.offer_proposer_account_id = 1
|
|
inquiry.offer_status = 'pending'
|
|
inquiry.offer_revision += 1
|
|
inquiry.updated_at = offer.updated_at
|
|
response.json({ success: true, data: { id: offer.id } })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:respond-offer') {
|
|
const inquiry = marketplaceInquiries.find(
|
|
(item) => item.id === request.body.inquiryId,
|
|
)
|
|
const offer =
|
|
inquiry && marketplaceOffers.find((item) => item.id === inquiry.offer_id)
|
|
if (
|
|
!inquiry ||
|
|
!offer ||
|
|
offer.status !== 'pending' ||
|
|
offer.proposer_account_id === 1
|
|
) {
|
|
response.json({ success: false, error: 'offer_not_actionable' })
|
|
return
|
|
}
|
|
if (!['accepted', 'rejected'].includes(request.body.action)) {
|
|
response.json({ success: false, error: 'invalid_offer_response' })
|
|
return
|
|
}
|
|
offer.status = request.body.action
|
|
offer.updated_at = '2026-08-06 12:03:00'
|
|
inquiry.offer_status = request.body.action
|
|
inquiry.offer_revision += 1
|
|
inquiry.updated_at = offer.updated_at
|
|
if (request.body.action === 'accepted') {
|
|
const listing = marketplaceListings.find(
|
|
(item) => item.id === inquiry.listing_id,
|
|
)
|
|
listing.status = 'reserved'
|
|
listing.reserved_account_id = inquiry.buyer_account_id
|
|
inquiry.status = 'reserved'
|
|
}
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'marketplace:report' || endpoint === 'marketplace:block') {
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (
|
|
endpoint === 'sim:insert' &&
|
|
request.body.imei === '356938035643810' &&
|
|
!request.body.confirmed
|
|
) {
|
|
response.json({ success: false, error: 'confirmation_required' })
|
|
return
|
|
}
|
|
if (endpoint === 'notes:list') {
|
|
response.json({ success: true, data: mockNotes })
|
|
return
|
|
}
|
|
if (endpoint === 'notes:create') {
|
|
mockNotes.unshift({ ...request.body, revision: 1 })
|
|
response.json({ success: true, data: mockNotes })
|
|
return
|
|
}
|
|
if (endpoint === 'notes:update') {
|
|
const index = mockNotes.findIndex((note) => note.id === request.body.id)
|
|
if (index >= 0) {
|
|
mockNotes[index] = {
|
|
...request.body,
|
|
revision: mockNotes[index].revision + 1,
|
|
updatedAt: Date.now(),
|
|
}
|
|
}
|
|
response.json({ success: true, data: mockNotes })
|
|
return
|
|
}
|
|
if (endpoint === 'notes:delete') {
|
|
mockNotes = mockNotes.filter((note) => note.id !== request.body.id)
|
|
response.json({ success: true, data: mockNotes })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:login' || endpoint === 'mail:register') {
|
|
authenticated = true
|
|
linkedAccount = {
|
|
devices: accountDevices,
|
|
email: 'demo@ifruit.com',
|
|
id: 1,
|
|
}
|
|
response.json({ success: true, data: linkedAccount })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:logout') {
|
|
authenticated = false
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint.startsWith('mail:') && !authenticated) {
|
|
response.json({ success: false, error: 'not_authenticated' })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:counts') {
|
|
response.json({ success: true, data: counts() })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:list') {
|
|
const { folder, search = '' } = request.body
|
|
let items =
|
|
folder === 'drafts'
|
|
? draft
|
|
? [{ ...draft, created_at: draft.updated_at, preview: draft.body }]
|
|
: []
|
|
: messages.filter((item) =>
|
|
folder === 'trash'
|
|
? item.trashed_at
|
|
: item.folder === folder && !item.trashed_at,
|
|
)
|
|
const query = String(search).toLowerCase()
|
|
if (query) {
|
|
items = items.filter((item) =>
|
|
`${item.sender ?? ''} ${item.subject} ${item.preview}`
|
|
.toLowerCase()
|
|
.includes(query),
|
|
)
|
|
}
|
|
response.json({ success: true, data: { hasMore: false, items, offset: 0 } })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:get') {
|
|
const message = messages.find((item) => item.id === Number(request.body.id))
|
|
if (message) message.is_read = true
|
|
response.json(
|
|
message
|
|
? { success: true, data: message }
|
|
: { success: false, error: 'message_not_found' },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint === 'mail:get-draft') {
|
|
response.json(
|
|
draft
|
|
? { success: true, data: draft }
|
|
: { success: false, error: 'draft_not_found' },
|
|
)
|
|
return
|
|
}
|
|
if (endpoint === 'mail:save-draft') {
|
|
draft = {
|
|
...request.body,
|
|
id: request.body.id ?? '748836d2-2308-4c21-a4d8-3af6df6dd7eb',
|
|
created_at: '2026-08-04 11:40:00',
|
|
updated_at: '2026-08-04 11:40:00',
|
|
}
|
|
response.json({ success: true, data: { id: draft.id } })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:delete-draft') {
|
|
draft = null
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:send') {
|
|
draft = null
|
|
response.json({ success: true, data: { id: 'mock-sent-message' } })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:trash' || endpoint === 'mail:restore') {
|
|
const message = messages.find((item) => item.id === Number(request.body.id))
|
|
if (message)
|
|
message.trashed_at =
|
|
endpoint === 'mail:trash' ? '2026-08-04 12:00:00' : null
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:delete-forever') {
|
|
const index = messages.findIndex(
|
|
(item) => item.id === Number(request.body.id),
|
|
)
|
|
if (index >= 0) messages.splice(index, 1)
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:empty-trash') {
|
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
if (messages[index].trashed_at) messages.splice(index, 1)
|
|
}
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
if (endpoint === 'mail:set-read') {
|
|
const message = messages.find((item) => item.id === Number(request.body.id))
|
|
if (message) message.is_read = request.body.read
|
|
response.json({ success: true })
|
|
return
|
|
}
|
|
response.json({ success: true })
|
|
})
|
|
|
|
app.listen(port, () => {
|
|
console.log(`Mock NUI server listening on http://localhost:${port}`)
|
|
})
|