mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 00:01:29 +00:00
ADD - seed browser and ingame phone test data
This commit is contained in:
@@ -91,6 +91,20 @@ included in the NUI bundle.
|
||||
|
||||
Standalone FiveM phone built with Vue 3, TypeScript, Pinia, Vue Router, Konsta UI 5, and Tailwind CSS 4. The phone opens through the usable item; `/phone` is disabled unless `Config.Phone.DevelopmentCommand` is enabled explicitly. Phone identity and SIM-card behavior are selected independently through `Config.Phone.Unique` and `Config.Sim.Enabled`.
|
||||
|
||||
### Ingame test data
|
||||
|
||||
On development servers, enable `Config.TestData.Enabled` and run `/phonetestdata` as the player who
|
||||
owns the phone. The command creates or refreshes idempotent, player-scoped fixtures for contacts,
|
||||
calls, messages, mail, notes, gallery, banking history, billing, calendar, map markers, music, radio,
|
||||
EasyShare, CityMarkt, Local Pages, Picstagram, FlipTok, Feather, Flare, DarkChat, CrewLink, SkyRide,
|
||||
and company requests. It also creates a linked iFruit account and a registered SIM when the selected
|
||||
phone does not have them yet. Reopen the phone after the command completes.
|
||||
|
||||
Garage and Housing intentionally continue to use the real configured provider data. Apps without
|
||||
persistent content, such as Calculator, Camera, Clock, Weather, Settings, and Payphone, do not need
|
||||
database fixtures. Set `Config.TestData.AdminOnly = true` to restrict the command to the configured
|
||||
framework admin groups, and disable the feature outside development environments.
|
||||
|
||||
An iFruit account is optional. Unlinked devices retain local settings, alarms, media, apps, notes, contacts, and recent calls. Linking from Mail or Settings moves local data into an empty cloud account; an existing cloud dataset wins over local contacts and recents. Signing out keeps an editable local snapshot without deleting cloud data.
|
||||
|
||||
## Phone identity and SIM modes
|
||||
@@ -228,7 +242,24 @@ The homescreen is an original implementation inspired by the interaction and lay
|
||||
|
||||
## Development
|
||||
|
||||
From `frontend/`, run `pnpm dev` for browser development. The phone opens automatically and NUI callbacks are mocked. Feather can be opened directly with the following browser scenarios:
|
||||
From `frontend/`, run `pnpm dev` for browser development. The phone opens automatically and NUI callbacks are mocked with stateful data. Every built-in app can be opened directly by appending its id to `http://localhost:5174/?apiPort=3002#/apps/`:
|
||||
|
||||
| Area | App ids |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Communication | `phone`, `messages`, `mail`, `darkchat`, `radio` |
|
||||
| Social | `feather`, `fliptok`, `picstagram`, `flare`, `crewlink` |
|
||||
| Services | `companies`, `citymarkt`, `local-pages`, `banking`, `billing`, `garage`, `house`, `map`, `skyride`, `weather` |
|
||||
| Media and utilities | `camera`, `photos`, `music`, `calendar`, `notes`, `calculator`, `clock`, `app-store`, `settings` |
|
||||
| Games | `snake`, `memory`, `number-merge`, `minesweeper`, `tower-stack`, `sky-flappy`, `neon-drop` |
|
||||
|
||||
The browser bootstrap includes contacts, calls, messages, mail, invoices, transactions, vehicles, properties, companies, marketplace profiles and listings, social feeds, media, calendar entries, notes, alarms, game high scores, app settings, and persisted notifications. Mutating callbacks update the in-memory mock state until the mock server restarts. Unknown callbacks fail with `mock_endpoint_missing` instead of silently succeeding.
|
||||
|
||||
System overlays are available through dedicated preview parameters:
|
||||
|
||||
- SIM picker: `http://localhost:5174/?apiPort=3002&simPickerPreview=1`
|
||||
- Payphone: `http://localhost:5174/?apiPort=3002&payphonePreview=1` (dial `5551110001` for a connected call or `5550000000` for a busy line)
|
||||
|
||||
Feather can be opened directly with the following browser scenarios:
|
||||
|
||||
- Full data: `http://localhost:5174/?apiPort=3002#/apps/feather`
|
||||
- Login and registration: `http://localhost:5174/?apiPort=3002&testScenario=feather-login#/apps/feather`
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"typecheck": "vue-tsc --build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write src/ testserver/ build.cjs",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run && pnpm test:browser-mocks",
|
||||
"test:browser-mocks": "node testserver/smoke.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"emoji-picker-element-data": "^1.8.0",
|
||||
|
||||
@@ -425,6 +425,50 @@ async function hydrateDevelopmentPhone(): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
function openDevelopmentPayphonePreview(): void {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
data: {
|
||||
currency: '$',
|
||||
locales: {
|
||||
busy: 'LINE BUSY',
|
||||
call: 'CALL',
|
||||
callEnded: 'CALL ENDED',
|
||||
clear: 'Clear number',
|
||||
close: 'Close payphone',
|
||||
connected: 'CONNECTED',
|
||||
cost: 'COST',
|
||||
declined: 'CALL DECLINED',
|
||||
delete: 'Delete digit',
|
||||
dialing: 'DIALING',
|
||||
disconnected: 'DISCONNECTED',
|
||||
elapsed: 'TIME',
|
||||
hangup: 'HANG UP',
|
||||
insufficientFunds: 'OUT OF MONEY',
|
||||
invalidNumber: 'ENTER A VALID NUMBER',
|
||||
keypad: 'Dial pad',
|
||||
noAnswer: 'NO ANSWER',
|
||||
numberLabel: 'NUMBER TO CALL',
|
||||
numberPlaceholder: 'Enter a phone number',
|
||||
rate: '{currency}{price} / SEC',
|
||||
ready: 'READY',
|
||||
requestFailed: 'CALL COULD NOT BE STARTED',
|
||||
ringing: 'RINGING',
|
||||
subtitle: 'PUBLIC TELEPHONE',
|
||||
title: 'PAYPHONE',
|
||||
unavailable: 'NUMBER UNAVAILABLE',
|
||||
voiceUnavailable: 'VOICE SERVICE UNAVAILABLE',
|
||||
},
|
||||
maxNumberLength: 10,
|
||||
pricePerSecond: 2,
|
||||
},
|
||||
type: 'payphone:open',
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
if (!isTrustedRootMessageSource(event.source, window)) return
|
||||
|
||||
@@ -1056,6 +1100,9 @@ onMounted(() => {
|
||||
number: '5551234567',
|
||||
}
|
||||
}
|
||||
if (developmentParameters.has('payphonePreview')) {
|
||||
openDevelopmentPayphonePreview()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2543,7 +2543,19 @@ const defaultLocales: LocaleTree = {
|
||||
noMessagesBody: 'Messages about offers will appear here.',
|
||||
myListings: 'My listings',
|
||||
favorites: 'Favorites',
|
||||
addFavorite: 'Add to favorites',
|
||||
removeFavorite: 'Remove from favorites',
|
||||
noProfileListings: 'Nothing here yet',
|
||||
createProfile: 'Create your CityMarkt profile',
|
||||
editProfile: 'Edit profile',
|
||||
profileIntro: 'Your iFruit email stays linked to this profile.',
|
||||
profileEmail: 'iFruit email',
|
||||
displayName: 'Display name',
|
||||
profileBio: 'About you',
|
||||
saveProfile: 'Save profile',
|
||||
cancel: 'Cancel',
|
||||
profileSaved: 'Your profile was saved.',
|
||||
removeProfilePhoto: 'Remove photo',
|
||||
phone: 'Phone',
|
||||
contactSeller: 'Contact seller',
|
||||
messagePlaceholder: 'Hi, is this still available?',
|
||||
@@ -2683,13 +2695,16 @@ const defaultLocales: LocaleTree = {
|
||||
signInBody: 'Sign in to iFruit in Settings to publish and save posts.',
|
||||
authEyebrow: 'Local Pages account',
|
||||
authWelcome: 'Welcome to Local Pages',
|
||||
authBody: 'Sign in or create an iFruit account to build your local profile.',
|
||||
authBody:
|
||||
'Sign in or create an iFruit account to build your local profile.',
|
||||
login: 'Sign in',
|
||||
register: 'Register',
|
||||
loginTitle: 'Continue with iFruit',
|
||||
loginBody: 'Your posts, saved items and profile stay linked to your account.',
|
||||
loginBody:
|
||||
'Your posts, saved items and profile stay linked to your account.',
|
||||
registerTitle: 'Create an iFruit account',
|
||||
registerBody: 'Choose your new iFruit address. Your Local Pages profile comes next.',
|
||||
registerBody:
|
||||
'Choose your new iFruit address. Your Local Pages profile comes next.',
|
||||
authEmail: 'iFruit address',
|
||||
authEmailPlaceholder: 'your.name',
|
||||
authPassword: 'Password',
|
||||
@@ -2705,7 +2720,8 @@ const defaultLocales: LocaleTree = {
|
||||
profileEmail: 'iFruit email',
|
||||
profileHandle: 'Username',
|
||||
profileHandlePlaceholder: 'your.name',
|
||||
profileHandleHint: 'Use 3–24 lowercase letters, numbers, dots or underscores.',
|
||||
profileHandleHint:
|
||||
'Use 3–24 lowercase letters, numbers, dots or underscores.',
|
||||
profileBio: 'Bio',
|
||||
profileBioPlaceholder: 'Tell the city a little about yourself...',
|
||||
profilePhoto: 'Profile photo',
|
||||
|
||||
+244
-27
@@ -7,6 +7,22 @@ const port = Number(process.argv[2]) || 3001
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
|
||||
const lifecycleEndpoints = new Set([
|
||||
'camera:setActive',
|
||||
'camera:setFacing',
|
||||
'camera:setFlash',
|
||||
'camera:setFocus',
|
||||
'camera:setOrientation',
|
||||
'camera:setZoom',
|
||||
'close',
|
||||
'custom-app:lifecycle',
|
||||
'device:notification-open',
|
||||
'notification:focus',
|
||||
'sim:picker-close',
|
||||
'ui:opened',
|
||||
'ui:ready',
|
||||
])
|
||||
|
||||
function calendarTime(dayOffset, hour, minute = 0) {
|
||||
const value = new Date()
|
||||
value.setDate(value.getDate() + dayOffset)
|
||||
@@ -1878,6 +1894,17 @@ const marketplaceListings = [
|
||||
],
|
||||
},
|
||||
]
|
||||
let marketplaceProfile = {
|
||||
avatar_media_id: 1,
|
||||
avatar_url: 'https://picsum.photos/seed/citymarkt-demo-avatar/240/240',
|
||||
bio: 'Fair prices, quick replies, and meetups anywhere in Los Santos.',
|
||||
display_name: 'Skyline Deals',
|
||||
email: 'demo@ifruit.com',
|
||||
exists: true,
|
||||
listing_count: marketplaceListings.filter(
|
||||
(listing) => listing.seller_account_id === 1,
|
||||
).length,
|
||||
}
|
||||
let linkedAccount = {
|
||||
devices: accountDevices,
|
||||
email: 'demo@ifruit.com',
|
||||
@@ -2063,6 +2090,38 @@ const deviceData = {
|
||||
},
|
||||
revision: 2,
|
||||
},
|
||||
notifications: {
|
||||
payload: {
|
||||
items: [
|
||||
{
|
||||
appId: 'messages',
|
||||
id: 'demo-notification-message',
|
||||
route: '/apps/messages?phoneNumber=5551110001',
|
||||
subtitle: 'Alex Rivera',
|
||||
text: 'Meet us at the observatory after sunset.',
|
||||
title: 'Messages',
|
||||
},
|
||||
{
|
||||
appId: 'companies',
|
||||
id: 'demo-notification-company',
|
||||
route: '/apps/companies?area=requests',
|
||||
subtitle: 'Los Santos Customs',
|
||||
text: 'Your repair request has been accepted.',
|
||||
title: 'Companies',
|
||||
},
|
||||
{
|
||||
appId: 'billing',
|
||||
id: 'demo-notification-billing',
|
||||
route: '/apps/billing',
|
||||
subtitle: 'Los Santos Customs',
|
||||
text: 'A new invoice for $1,850 is ready.',
|
||||
title: 'Billing',
|
||||
},
|
||||
],
|
||||
version: 1,
|
||||
},
|
||||
revision: 1,
|
||||
},
|
||||
settings: {
|
||||
payload: {
|
||||
settings: {
|
||||
@@ -2085,6 +2144,14 @@ const deviceData = {
|
||||
}
|
||||
let mockPasscode = ''
|
||||
let mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
|
||||
let mockSim = {
|
||||
id: 'development-sim',
|
||||
number: '5551234567',
|
||||
removable: true,
|
||||
registered: true,
|
||||
type: 'registered',
|
||||
}
|
||||
let mockPayphoneCall = null
|
||||
const blockedCallNumbers = new Set()
|
||||
let recentCalls = [
|
||||
{
|
||||
@@ -3844,7 +3911,8 @@ const easyShareCatalog = [
|
||||
appId: 'citymarkt',
|
||||
copyText: 'Comet Retro Custom in excellent condition.',
|
||||
id: 'listing-easyshare-comet',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1503736334956-4c8f8e92946d?w=900',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1503736334956-4c8f8e92946d?w=900',
|
||||
kind: 'link',
|
||||
link: 'skyphone://citymarkt/listing/listing-easyshare-comet',
|
||||
subtitle: '$84,000',
|
||||
@@ -3868,7 +3936,8 @@ const easyShareCatalog = [
|
||||
appId: 'photos',
|
||||
copyText: 'Sunset over Los Santos.',
|
||||
id: 3,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=900',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=900',
|
||||
kind: 'photo',
|
||||
link: 'skyphone://media/3',
|
||||
title: 'Los Santos sunset',
|
||||
@@ -3923,7 +3992,8 @@ const easyShareCatalog = [
|
||||
appId: 'photos',
|
||||
copyText: 'Vehicle walkaround video.',
|
||||
id: 7,
|
||||
imageUrl: 'https://videos.pexels.com/video-files/3130284/3130284-hd_1920_1080_30fps.mp4',
|
||||
imageUrl:
|
||||
'https://videos.pexels.com/video-files/3130284/3130284-hd_1920_1080_30fps.mp4',
|
||||
kind: 'video',
|
||||
link: 'skyphone://media/7',
|
||||
title: 'Vehicle walkaround',
|
||||
@@ -3998,6 +4068,10 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
console.log(`[NUI] ${request.params.endpoint}`, request.body)
|
||||
const endpoint = request.params.endpoint
|
||||
const testScenario = String(request.body._testScenario ?? '')
|
||||
if (lifecycleEndpoints.has(endpoint)) {
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint.startsWith('companies:') && testScenario === 'companies-error') {
|
||||
response.json({ success: false, error: 'service_unavailable' })
|
||||
return
|
||||
@@ -5351,8 +5425,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
mediaDurationMs: request.body.mediaDurationMs ?? null,
|
||||
mediaUrl: messageType === 'text' ? null : mediaUrl,
|
||||
messageType,
|
||||
sharePayload:
|
||||
messageType === 'share' ? request.body.sharePayload : null,
|
||||
sharePayload: messageType === 'share' ? request.body.sharePayload : null,
|
||||
}
|
||||
flareMessages[match.id] ??= []
|
||||
flareMessages[match.id].push(message)
|
||||
@@ -5832,6 +5905,47 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: true, data: flipTokActivities })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'fliptok:mark-activities') {
|
||||
const readAt = new Date().toISOString()
|
||||
flipTokActivities = flipTokActivities.map((activity) => ({
|
||||
...activity,
|
||||
read_at: activity.read_at ?? readAt,
|
||||
}))
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'fliptok:view') {
|
||||
const video = flipTokVideos.find((item) => item.id === request.body.id)
|
||||
if (!video) {
|
||||
response.json({ success: false, error: 'video_not_found' })
|
||||
return
|
||||
}
|
||||
video.view_count += 1
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'fliptok:report') {
|
||||
const video = flipTokVideos.find((item) => item.id === request.body.id)
|
||||
if (!video) {
|
||||
response.json({ success: false, error: 'video_not_found' })
|
||||
return
|
||||
}
|
||||
flipTokReports.push({
|
||||
caption: video.caption,
|
||||
created_at: Date.now(),
|
||||
creator_display_name: video.display_name,
|
||||
creator_handle: video.handle,
|
||||
details: String(request.body.details ?? ''),
|
||||
id: `report-${Date.now()}`,
|
||||
reason: request.body.reason,
|
||||
reporter_display_name: flipTokProfile.display_name,
|
||||
reporter_handle: flipTokProfile.handle,
|
||||
url: video.url,
|
||||
video_id: video.id,
|
||||
})
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'fliptok:profile') {
|
||||
const profileId = Number(request.body.profileId || 0)
|
||||
const handle = String(request.body.handle || '').toLowerCase()
|
||||
@@ -6722,8 +6836,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
replyToId: request.body.replyToId,
|
||||
replyBody: reply?.body,
|
||||
reactions: {},
|
||||
sharePayload:
|
||||
messageType === 'share' ? request.body.sharePayload : null,
|
||||
sharePayload: messageType === 'share' ? request.body.sharePayload : null,
|
||||
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
||||
readAt: null,
|
||||
}
|
||||
@@ -6968,13 +7081,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
data: deviceData,
|
||||
imei: '356938035643809',
|
||||
name: 'Personal iFruit Phone',
|
||||
sim: {
|
||||
id: 'development-sim',
|
||||
number: '5551234567',
|
||||
removable: true,
|
||||
registered: true,
|
||||
type: 'registered',
|
||||
},
|
||||
sim: mockSim,
|
||||
},
|
||||
notes: mockNotes,
|
||||
security: mockSecurity,
|
||||
@@ -7091,11 +7198,11 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
organization: selectedContact.organization ?? null,
|
||||
phone_number: selectedContact.phone_number,
|
||||
}
|
||||
: messageType === 'share'
|
||||
? request.body.sharePayload
|
||||
: isAttachment
|
||||
? attachmentId
|
||||
: null,
|
||||
: messageType === 'share'
|
||||
? request.body.sharePayload
|
||||
: isAttachment
|
||||
? attachmentId
|
||||
: null,
|
||||
media_waveform:
|
||||
messageType === 'voice' ? request.body.mediaWaveform : null,
|
||||
message_type: messageType,
|
||||
@@ -7304,6 +7411,15 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: true, data: { revision } })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'notifications:save') {
|
||||
const revision = (deviceData.notifications?.revision ?? 0) + 1
|
||||
deviceData.notifications = {
|
||||
payload: request.body.payload,
|
||||
revision,
|
||||
}
|
||||
response.json({ success: true, data: { revision } })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'security:unlock') {
|
||||
response.json(
|
||||
!mockSecurity.enabled || request.body.passcode === mockPasscode
|
||||
@@ -7531,9 +7647,12 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
if (endpoint === 'pages:profile-save') {
|
||||
pagesOnboardingCompleted = true
|
||||
const avatarMediaId = Number(request.body.avatarMediaId) || 0
|
||||
const avatarMedia = avatarMediaId > 0
|
||||
? mockMedia.find((item) => item.id === avatarMediaId && item.mediaType === 'photo')
|
||||
: null
|
||||
const avatarMedia =
|
||||
avatarMediaId > 0
|
||||
? mockMedia.find(
|
||||
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
|
||||
)
|
||||
: null
|
||||
if (avatarMediaId > 0 && !avatarMedia) {
|
||||
response.json({ success: false, error: 'invalid_profile_image' })
|
||||
return
|
||||
@@ -7544,7 +7663,9 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
bio: String(request.body.bio ?? '').trim(),
|
||||
email: linkedAccount?.email ?? pagesProfile.email,
|
||||
exists: true,
|
||||
handle: String(request.body.handle ?? '').trim().toLowerCase(),
|
||||
handle: String(request.body.handle ?? '')
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
}
|
||||
pagesPosts.forEach((post) => {
|
||||
if (post.account_id === 1) post.author_name = pagesProfile.handle
|
||||
@@ -7709,6 +7830,42 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: false, error: 'not_authenticated' })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'marketplace:profile') {
|
||||
marketplaceProfile.listing_count = marketplaceListings.filter(
|
||||
(listing) => listing.seller_account_id === 1,
|
||||
).length
|
||||
response.json({ success: true, data: marketplaceProfile })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'marketplace:profile-save') {
|
||||
const displayName = String(request.body.displayName ?? '').trim()
|
||||
const bio = String(request.body.bio ?? '').trim()
|
||||
const avatarMediaId = Number(request.body.avatarMediaId)
|
||||
const avatar = mockMedia.find(
|
||||
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
|
||||
)
|
||||
if (
|
||||
displayName.length < 2 ||
|
||||
displayName.length > 40 ||
|
||||
bio.length > 160 ||
|
||||
!Number.isInteger(avatarMediaId) ||
|
||||
avatarMediaId < 0 ||
|
||||
(avatarMediaId > 0 && !avatar)
|
||||
) {
|
||||
response.json({ success: false, error: 'invalid_profile' })
|
||||
return
|
||||
}
|
||||
marketplaceProfile = {
|
||||
...marketplaceProfile,
|
||||
avatar_media_id: avatarMediaId || null,
|
||||
avatar_url: avatar?.url ?? null,
|
||||
bio,
|
||||
display_name: displayName,
|
||||
exists: true,
|
||||
}
|
||||
response.json({ success: true, data: marketplaceProfile })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'marketplace:counts') {
|
||||
response.json({
|
||||
success: true,
|
||||
@@ -8010,6 +8167,61 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: false, error: 'confirmation_required' })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'sim:insert') {
|
||||
mockSim = {
|
||||
id: `development-sim-${request.body.imei}`,
|
||||
number:
|
||||
request.body.imei === '356938035643810' ? '5559876543' : '5551234567',
|
||||
removable: true,
|
||||
registered: true,
|
||||
type: 'registered',
|
||||
}
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'sim:eject') {
|
||||
if (!mockSim) {
|
||||
response.json({ success: false, error: 'no_sim' })
|
||||
return
|
||||
}
|
||||
mockSim = null
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'payphone:dial') {
|
||||
const phoneNumber = String(request.body.phoneNumber ?? '').replace(
|
||||
/\D/g,
|
||||
'',
|
||||
)
|
||||
if (phoneNumber.length !== 10) {
|
||||
response.json({ success: false, error: 'invalid_number' })
|
||||
return
|
||||
}
|
||||
if (phoneNumber === '5550000000') {
|
||||
response.json({ success: false, error: 'busy' })
|
||||
return
|
||||
}
|
||||
mockPayphoneCall = {
|
||||
answeredAt: Math.floor(Date.now() / 1000),
|
||||
elapsedSeconds: 0,
|
||||
id: `payphone-${Date.now()}`,
|
||||
otherNumber: phoneNumber,
|
||||
state: 'connected',
|
||||
totalCost: 0,
|
||||
}
|
||||
response.json({ success: true, data: mockPayphoneCall })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'payphone:hangup') {
|
||||
mockPayphoneCall = null
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'payphone:close') {
|
||||
mockPayphoneCall = null
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'notes:list') {
|
||||
response.json({ success: true, data: mockNotes })
|
||||
return
|
||||
@@ -8149,9 +8361,14 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
response.json({ success: true })
|
||||
console.error(`[NUI] Missing browser mock for ${endpoint}`)
|
||||
response.json({ success: false, error: 'mock_endpoint_missing' })
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Mock NUI server listening on http://localhost:${port}`)
|
||||
})
|
||||
if (require.main === module) {
|
||||
app.listen(port, () => {
|
||||
console.log(`Mock NUI server listening on http://localhost:${port}`)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { app }
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const { once } = require('node:events')
|
||||
|
||||
const { app } = require('./index.cjs')
|
||||
|
||||
const browserDataRequests = [
|
||||
['development:bootstrap', {}],
|
||||
['account:devices', {}],
|
||||
['banking:overview', {}],
|
||||
['billing:overview', {}],
|
||||
['billing:list', { filter: 'all', limit: 20, offset: 0 }],
|
||||
['calendar:list', { endsAt: 4_102_444_800, startsAt: 0 }],
|
||||
['calls:recents', {}],
|
||||
['companies:list', {}],
|
||||
['companies:my-requests', { limit: 20, offset: 0 }],
|
||||
['companies:work-context', {}],
|
||||
['companies:work-queue', { limit: 20, offset: 0 }],
|
||||
['contacts:list', {}],
|
||||
['crewlink:bootstrap', {}],
|
||||
['crewlink:live', {}],
|
||||
['crewlink:nearby', {}],
|
||||
['darkchat:bootstrap', {}],
|
||||
['easyshare:bootstrap', {}],
|
||||
['easyshare:own-contact', {}],
|
||||
['feather:bootstrap', {}],
|
||||
['feather:feed', { limit: 20 }],
|
||||
['feather:explore', { limit: 20 }],
|
||||
['flare:bootstrap', {}],
|
||||
['fliptok:bootstrap', {}],
|
||||
['fliptok:feed', { limit: 20 }],
|
||||
['fliptok:discover', { limit: 20 }],
|
||||
['fliptok:activities', {}],
|
||||
['gallery:list', {}],
|
||||
['garage:vehicles', {}],
|
||||
['garage:valet-state', {}],
|
||||
['housing:overview', {}],
|
||||
['housing:key-candidates', { action: 'give' }],
|
||||
['mail:counts', {}],
|
||||
['mail:list', { folder: 'inbox' }],
|
||||
['map:getPlayerCoords', {}],
|
||||
['map:markers', {}],
|
||||
['marketplace:counts', {}],
|
||||
['marketplace:list', {}],
|
||||
['marketplace:list-own', {}],
|
||||
['marketplace:list-inquiries', {}],
|
||||
['marketplace:profile', {}],
|
||||
['messages:conversations', {}],
|
||||
['messages:gifs', { query: 'party' }],
|
||||
['music:bootstrap', {}],
|
||||
['notes:list', {}],
|
||||
['pages:list', {}],
|
||||
['pages:list-own', {}],
|
||||
['pages:profile', {}],
|
||||
['picstagram:bootstrap', {}],
|
||||
['picstagram:feed', { limit: 20 }],
|
||||
['picstagram:explore', { limit: 20 }],
|
||||
['picstagram:saved', {}],
|
||||
['picstagram:stories', {}],
|
||||
['picstagram:activities', {}],
|
||||
['radio:get', {}],
|
||||
['skyride:bootstrap', {}],
|
||||
['skyride:history', {}],
|
||||
['skyride:get-player-coords', {}],
|
||||
['weather:get', {}],
|
||||
]
|
||||
|
||||
async function post(baseUrl, endpoint, body = {}) {
|
||||
const response = await fetch(`${baseUrl}/api/${endpoint}`, {
|
||||
body: JSON.stringify(body),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
})
|
||||
assert.equal(response.status, 200, endpoint)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function expectSuccess(baseUrl, endpoint, body = {}, data = false) {
|
||||
const result = await post(baseUrl, endpoint, body)
|
||||
assert.equal(result.success, true, `${endpoint}: ${result.error ?? 'failed'}`)
|
||||
if (data) assert.notEqual(result.data, undefined, `${endpoint}: missing data`)
|
||||
return result.data
|
||||
}
|
||||
|
||||
async function verifyStatefulActions(baseUrl) {
|
||||
const noteId = `browser-note-${Date.now()}`
|
||||
let notes = await expectSuccess(
|
||||
baseUrl,
|
||||
'notes:create',
|
||||
{
|
||||
body: 'Created by the browser mock smoke test.',
|
||||
id: noteId,
|
||||
title: 'Browser test',
|
||||
},
|
||||
true,
|
||||
)
|
||||
assert(
|
||||
notes.some((note) => note.id === noteId),
|
||||
'notes:create did not persist',
|
||||
)
|
||||
notes = await expectSuccess(
|
||||
baseUrl,
|
||||
'notes:update',
|
||||
{
|
||||
body: 'Updated browser test note.',
|
||||
id: noteId,
|
||||
title: 'Browser test updated',
|
||||
},
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
notes.find((note) => note.id === noteId)?.title,
|
||||
'Browser test updated',
|
||||
)
|
||||
notes = await expectSuccess(baseUrl, 'notes:delete', { id: noteId }, true)
|
||||
assert(
|
||||
!notes.some((note) => note.id === noteId),
|
||||
'notes:delete did not persist',
|
||||
)
|
||||
|
||||
const contact = await expectSuccess(
|
||||
baseUrl,
|
||||
'contacts:save',
|
||||
{ name: 'Browser Tester', phoneNumber: '5552223333' },
|
||||
true,
|
||||
)
|
||||
await expectSuccess(
|
||||
baseUrl,
|
||||
'contacts:favorite',
|
||||
{ favorite: true, id: contact.id },
|
||||
true,
|
||||
)
|
||||
let contacts = await expectSuccess(baseUrl, 'contacts:list', {}, true)
|
||||
assert.equal(contacts.find((item) => item.id === contact.id)?.favorite, true)
|
||||
await expectSuccess(baseUrl, 'contacts:delete', { id: contact.id })
|
||||
contacts = await expectSuccess(baseUrl, 'contacts:list', {}, true)
|
||||
assert(
|
||||
!contacts.some((item) => item.id === contact.id),
|
||||
'contacts:delete did not persist',
|
||||
)
|
||||
|
||||
const event = await expectSuccess(
|
||||
baseUrl,
|
||||
'calendar:create',
|
||||
{
|
||||
allDay: false,
|
||||
description: 'Stateful browser mock check',
|
||||
endsAt: 2_000_003_600,
|
||||
location: 'Legion Square',
|
||||
reminderMinutes: 15,
|
||||
startsAt: 2_000_000_000,
|
||||
title: 'Browser test event',
|
||||
},
|
||||
true,
|
||||
)
|
||||
let events = await expectSuccess(
|
||||
baseUrl,
|
||||
'calendar:list',
|
||||
{ endsAt: 4_102_444_800, startsAt: 0 },
|
||||
true,
|
||||
)
|
||||
const storedEvent = events.find((item) => item.id === event.id)
|
||||
assert(storedEvent, 'calendar:create did not persist')
|
||||
await expectSuccess(baseUrl, 'calendar:update', {
|
||||
...storedEvent,
|
||||
endsAt: storedEvent.endsAt / 1000,
|
||||
startsAt: storedEvent.startsAt / 1000,
|
||||
title: 'Updated browser test event',
|
||||
})
|
||||
events = await expectSuccess(
|
||||
baseUrl,
|
||||
'calendar:list',
|
||||
{ endsAt: 4_102_444_800, startsAt: 0 },
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
events.find((item) => item.id === event.id)?.title,
|
||||
'Updated browser test event',
|
||||
)
|
||||
await expectSuccess(baseUrl, 'calendar:delete', { id: event.id })
|
||||
|
||||
const marker = await expectSuccess(
|
||||
baseUrl,
|
||||
'map:create-marker',
|
||||
{
|
||||
color: '#2dd4bf',
|
||||
coords: { x: 215.2, y: -810.1, z: 30.7 },
|
||||
icon: 'pin',
|
||||
label: 'Browser test marker',
|
||||
},
|
||||
true,
|
||||
)
|
||||
let markers = await expectSuccess(baseUrl, 'map:markers', {}, true)
|
||||
assert(
|
||||
markers.some((item) => item.id === marker.id),
|
||||
'map:create-marker did not persist',
|
||||
)
|
||||
await expectSuccess(baseUrl, 'map:delete-marker', { id: marker.id })
|
||||
markers = await expectSuccess(baseUrl, 'map:markers', {}, true)
|
||||
assert(
|
||||
!markers.some((item) => item.id === marker.id),
|
||||
'map:delete-marker did not persist',
|
||||
)
|
||||
|
||||
const bankingBefore = await expectSuccess(
|
||||
baseUrl,
|
||||
'banking:overview',
|
||||
{},
|
||||
true,
|
||||
)
|
||||
const bankingAfter = await expectSuccess(
|
||||
baseUrl,
|
||||
'banking:transfer',
|
||||
{ amount: 125, phoneNumber: '5551110001' },
|
||||
true,
|
||||
)
|
||||
assert.equal(bankingAfter.bank, bankingBefore.bank - 125)
|
||||
|
||||
let radio = await expectSuccess(
|
||||
baseUrl,
|
||||
'radio:connect',
|
||||
{ frequency: 42.5, secondaryFrequency: 7.25 },
|
||||
true,
|
||||
)
|
||||
assert.equal(radio.frequency, 42.5)
|
||||
radio = await expectSuccess(baseUrl, 'radio:set-volume', { volume: 44 }, true)
|
||||
assert.equal(radio.volume, 44)
|
||||
await expectSuccess(baseUrl, 'radio:disconnect')
|
||||
|
||||
const playlistState = await expectSuccess(
|
||||
baseUrl,
|
||||
'music:create-playlist',
|
||||
{ name: 'Browser Test Mix' },
|
||||
true,
|
||||
)
|
||||
const playlist = playlistState.playlists.find(
|
||||
(item) => item.name === 'Browser Test Mix',
|
||||
)
|
||||
assert(playlist, 'music:create-playlist did not persist')
|
||||
await expectSuccess(
|
||||
baseUrl,
|
||||
'music:rename-playlist',
|
||||
{ id: playlist.id, name: 'Updated Browser Mix' },
|
||||
true,
|
||||
)
|
||||
await expectSuccess(
|
||||
baseUrl,
|
||||
'music:delete-playlist',
|
||||
{ id: playlist.id },
|
||||
true,
|
||||
)
|
||||
|
||||
await expectSuccess(baseUrl, 'sim:eject')
|
||||
let bootstrap = await expectSuccess(
|
||||
baseUrl,
|
||||
'development:bootstrap',
|
||||
{},
|
||||
true,
|
||||
)
|
||||
assert.equal(bootstrap.device.sim, null)
|
||||
const simConfirmation = await post(baseUrl, 'sim:insert', {
|
||||
imei: '356938035643810',
|
||||
})
|
||||
assert.deepEqual(simConfirmation, {
|
||||
error: 'confirmation_required',
|
||||
success: false,
|
||||
})
|
||||
await expectSuccess(baseUrl, 'sim:insert', {
|
||||
confirmed: true,
|
||||
imei: '356938035643810',
|
||||
})
|
||||
bootstrap = await expectSuccess(baseUrl, 'development:bootstrap', {}, true)
|
||||
assert.equal(bootstrap.device.sim.number, '5559876543')
|
||||
|
||||
const payphoneCall = await expectSuccess(
|
||||
baseUrl,
|
||||
'payphone:dial',
|
||||
{ phoneNumber: '5551110001' },
|
||||
true,
|
||||
)
|
||||
assert.equal(payphoneCall.state, 'connected')
|
||||
await expectSuccess(baseUrl, 'payphone:hangup')
|
||||
|
||||
const draft = await expectSuccess(
|
||||
baseUrl,
|
||||
'mail:save-draft',
|
||||
{
|
||||
body: 'Browser test body',
|
||||
recipients: ['alex@ifruit.com'],
|
||||
subject: 'Browser test mail',
|
||||
},
|
||||
true,
|
||||
)
|
||||
const storedDraft = await expectSuccess(
|
||||
baseUrl,
|
||||
'mail:get-draft',
|
||||
{ id: draft.id },
|
||||
true,
|
||||
)
|
||||
assert.equal(storedDraft.subject, 'Browser test mail')
|
||||
await expectSuccess(baseUrl, 'mail:delete-draft', { id: draft.id })
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const server = app.listen(0, '127.0.0.1')
|
||||
await once(server, 'listening')
|
||||
const address = server.address()
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`
|
||||
|
||||
try {
|
||||
for (const [endpoint, body] of browserDataRequests) {
|
||||
await expectSuccess(baseUrl, endpoint, body, true)
|
||||
}
|
||||
|
||||
await verifyStatefulActions(baseUrl)
|
||||
|
||||
const lifecycleEndpoints = [
|
||||
'camera:setActive',
|
||||
'camera:setFacing',
|
||||
'camera:setFlash',
|
||||
'camera:setFocus',
|
||||
'camera:setOrientation',
|
||||
'camera:setZoom',
|
||||
'close',
|
||||
'custom-app:lifecycle',
|
||||
'device:notification-open',
|
||||
'notification:focus',
|
||||
'sim:picker-close',
|
||||
'ui:opened',
|
||||
'ui:ready',
|
||||
]
|
||||
for (const endpoint of lifecycleEndpoints) {
|
||||
await expectSuccess(baseUrl, endpoint)
|
||||
}
|
||||
|
||||
const unknown = await post(baseUrl, 'development:missing-mock', {})
|
||||
assert.deepEqual(unknown, {
|
||||
error: 'mock_endpoint_missing',
|
||||
success: false,
|
||||
})
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()))
|
||||
})
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Verified ${browserDataRequests.length} browser data endpoints and stateful app actions.`,
|
||||
)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -20,6 +20,13 @@ Config.Phone = {
|
||||
DeviceName = "iFruit Phone",
|
||||
}
|
||||
|
||||
Config.TestData = {
|
||||
Enabled = true,
|
||||
Command = "phonetestdata",
|
||||
AdminOnly = false, -- enable only on development servers; every run is scoped to the executing player's phone
|
||||
AdminGroups = { "admin", "superadmin" },
|
||||
}
|
||||
|
||||
Config.CustomApps = {
|
||||
Enabled = true,
|
||||
BundledApps = true,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
Locales["en"] = {
|
||||
CommandDescription = "Open your phone.",
|
||||
TestData = {
|
||||
CommandDescription = "Create or refresh test content in every data-driven phone app.",
|
||||
Success = "Test content is ready. Your iFruit login is {email}. Reopen the phone to refresh every app.",
|
||||
Failed = "Test content could not be created. Check the server console for details.",
|
||||
},
|
||||
FlipTokCommand = {
|
||||
usage = "Usage: /{command} <@handle> [on|off]",
|
||||
noPermission = "You do not have permission to manage FlipTok verification.",
|
||||
|
||||
@@ -89,6 +89,7 @@ server_scripts {
|
||||
'source/server/calendar.lua',
|
||||
'source/server/music.lua',
|
||||
'source/server/radio.lua',
|
||||
'source/server/testdata.lua',
|
||||
}
|
||||
|
||||
files {
|
||||
|
||||
@@ -328,6 +328,15 @@ if Config.Phone.DevelopmentCommand then
|
||||
end, false)
|
||||
end
|
||||
|
||||
RegisterNetEvent("sky_phone:testdata:feedback", function(success, detail)
|
||||
local locale = get_locale().TestData
|
||||
local message = success and locale.Success or locale.Failed
|
||||
if success and type(detail) == "string" and detail ~= "" then
|
||||
message = message:gsub("{email}", detail)
|
||||
end
|
||||
Bridge.Framework.Notify("iFruit", message, success and "success" or "error", 7000)
|
||||
end)
|
||||
|
||||
RegisterNUICallback("ui:ready", function(_, cb)
|
||||
Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true })
|
||||
TriggerEvent("sky_phone:client:nuiReady")
|
||||
@@ -765,6 +774,9 @@ CreateThread(function()
|
||||
if Config.Phone.DevelopmentCommand then
|
||||
TriggerEvent("chat:addSuggestion", "/" .. Config.Command, get_locale().CommandDescription)
|
||||
end
|
||||
if Config.TestData.Enabled then
|
||||
TriggerEvent("chat:addSuggestion", "/" .. Config.TestData.Command, get_locale().TestData.CommandDescription)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource_name)
|
||||
@@ -782,4 +794,7 @@ AddEventHandler("onResourceStop", function(resource_name)
|
||||
if Config.Phone.DevelopmentCommand then
|
||||
TriggerEvent("chat:removeSuggestion", "/" .. Config.Command)
|
||||
end
|
||||
if Config.TestData.Enabled then
|
||||
TriggerEvent("chat:removeSuggestion", "/" .. Config.TestData.Command)
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -0,0 +1,964 @@
|
||||
Bridge.Database.AfterMigration("sky_phone", function()
|
||||
|
||||
if not Config.TestData.Enabled then
|
||||
return
|
||||
end
|
||||
|
||||
local photo_urls = {
|
||||
city = "https://images.unsplash.com/photo-1519501025264-65ba15a82390?auto=format&fit=crop&w=1200&q=80",
|
||||
car = "https://images.unsplash.com/photo-1493238792000-8113da705763?auto=format&fit=crop&w=1200&q=80",
|
||||
beach = "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=1200&q=80",
|
||||
portrait = "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=800&q=80",
|
||||
}
|
||||
local video_url = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
|
||||
local seed_attempts = {}
|
||||
|
||||
local function affected_rows(result)
|
||||
if type(result) == "number" then
|
||||
return result
|
||||
end
|
||||
return type(result) == "table" and tonumber(result.affectedRows) or 0
|
||||
end
|
||||
|
||||
local function stable_uuid(seed)
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT LOWER(CONCAT(
|
||||
SUBSTR(MD5(?), 1, 8), '-', SUBSTR(MD5(?), 9, 4), '-',
|
||||
SUBSTR(MD5(?), 13, 4), '-', SUBSTR(MD5(?), 17, 4), '-', SUBSTR(MD5(?), 21, 12)
|
||||
)) AS `id`
|
||||
]], { seed, seed, seed, seed, seed })
|
||||
local id = rows[1] and rows[1].id
|
||||
if type(id) ~= "string" or #id ~= 36 then
|
||||
error("[sky_phone] Test data could not generate a stable UUID.")
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
local function seed_hash(seed)
|
||||
local rows = Bridge.Database.Query("SELECT LEFT(MD5(?), 12) AS `value`", { seed })
|
||||
local value = rows[1] and rows[1].value
|
||||
if type(value) ~= "string" or #value ~= 12 then
|
||||
error("[sky_phone] Test data could not generate a stable account suffix.")
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function database_uuid()
|
||||
local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
|
||||
local id = rows[1] and rows[1].id
|
||||
if type(id) ~= "string" then
|
||||
error("[sky_phone] Test data could not generate a database UUID.")
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
local function ensure_account(email)
|
||||
Bridge.Database.Query(
|
||||
"INSERT IGNORE INTO `sky_phone_accounts` (`email`, `password`) VALUES (?, ?)",
|
||||
{ email, "sky-phone-test" }
|
||||
)
|
||||
local rows = Bridge.Database.Query(
|
||||
"SELECT `id`, `email` FROM `sky_phone_accounts` WHERE `email` = ? LIMIT 1",
|
||||
{ email }
|
||||
)
|
||||
if not rows[1] then
|
||||
error(("[sky_phone] Test data account '%s' could not be loaded."):format(email))
|
||||
end
|
||||
rows[1].id = tonumber(rows[1].id)
|
||||
return rows[1]
|
||||
end
|
||||
|
||||
local function ensure_media(account_id, remote_id, url, media_type)
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT `id` FROM `sky_phone_media`
|
||||
WHERE `account_id` = ? AND `remote_id` = ?
|
||||
ORDER BY `id` LIMIT 1
|
||||
]], { account_id, remote_id })
|
||||
if rows[1] then
|
||||
Bridge.Database.Query(
|
||||
"UPDATE `sky_phone_media` SET `url` = ?, `media_type` = ? WHERE `id` = ?",
|
||||
{ url, media_type, rows[1].id }
|
||||
)
|
||||
return tonumber(rows[1].id)
|
||||
end
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_media` (`account_id`, `url`, `remote_id`, `media_type`)
|
||||
VALUES (?, ?, ?, ?)
|
||||
]], { account_id, url, remote_id, media_type })
|
||||
rows = Bridge.Database.Query([[
|
||||
SELECT `id` FROM `sky_phone_media`
|
||||
WHERE `account_id` = ? AND `remote_id` = ?
|
||||
ORDER BY `id` DESC LIMIT 1
|
||||
]], { account_id, remote_id })
|
||||
if not rows[1] then
|
||||
error("[sky_phone] Test media could not be created.")
|
||||
end
|
||||
return tonumber(rows[1].id)
|
||||
end
|
||||
|
||||
local function reserve_sim(owner_identifier, firstname, lastname)
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT `id`, `phone_number`, `sim_type`
|
||||
FROM `sky_phone_sims`
|
||||
WHERE `owner_identifier` = ? AND `is_virtual` = 0
|
||||
ORDER BY `created_at` LIMIT 1
|
||||
]], { owner_identifier })
|
||||
if rows[1] then
|
||||
return rows[1]
|
||||
end
|
||||
|
||||
local sim_id
|
||||
local number = SkyPhoneSimNumber.Reserve(database_uuid, function(candidate)
|
||||
if SkyPhoneCompanies.IsServiceNumber(candidate) then
|
||||
return false
|
||||
end
|
||||
sim_id = database_uuid()
|
||||
local result = Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_sims`
|
||||
(`id`, `phone_number`, `sim_type`, `is_virtual`, `owner_identifier`,
|
||||
`owner_firstname`, `owner_lastname`, `registered_at`)
|
||||
VALUES (?, ?, 'registered', 0, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
]], { sim_id, candidate, owner_identifier, firstname, lastname })
|
||||
return affected_rows(result) == 1
|
||||
end, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
|
||||
if not number then
|
||||
error("[sky_phone] Test data could not reserve a SIM number.")
|
||||
end
|
||||
return { id = sim_id, phone_number = number, sim_type = "registered" }
|
||||
end
|
||||
|
||||
local function ensure_bot(label, email_local, imei, firstname, lastname)
|
||||
local account = ensure_account(email_local .. "@" .. Config.Mail.Domain)
|
||||
local sim = reserve_sim("sky_phone:testbot:" .. label, firstname, lastname)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_devices` (`imei`, `account_id`, `sim_id`, `device_name`)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE `account_id` = VALUES(`account_id`), `sim_id` = VALUES(`sim_id`)
|
||||
]], { imei, account.id, sim.id, firstname .. "'s iFruit" })
|
||||
return {
|
||||
account = account,
|
||||
sim = sim,
|
||||
imei = imei,
|
||||
name = firstname .. " " .. lastname,
|
||||
}
|
||||
end
|
||||
|
||||
local function ensure_numeric_profile(table_name, account_id)
|
||||
local rows = Bridge.Database.Query(
|
||||
("SELECT `id` FROM `%s` WHERE `account_id` = ? LIMIT 1"):format(table_name),
|
||||
{ account_id }
|
||||
)
|
||||
if not rows[1] then
|
||||
error(("[sky_phone] Test profile in '%s' could not be loaded."):format(table_name))
|
||||
end
|
||||
return tonumber(rows[1].id)
|
||||
end
|
||||
|
||||
local function ensure_string_profile(table_name, account_id)
|
||||
local rows = Bridge.Database.Query(
|
||||
("SELECT `id` FROM `%s` WHERE `account_id` = ? LIMIT 1"):format(table_name),
|
||||
{ account_id }
|
||||
)
|
||||
if not rows[1] or type(rows[1].id) ~= "string" then
|
||||
error(("[sky_phone] Test profile in '%s' could not be loaded."):format(table_name))
|
||||
end
|
||||
return rows[1].id
|
||||
end
|
||||
|
||||
local function seed_core(context)
|
||||
local account_id = context.account.id
|
||||
local bot = context.bot_one
|
||||
local alarms = {
|
||||
{
|
||||
id = "test-weekday",
|
||||
enabled = true,
|
||||
time = "07:30",
|
||||
note = "Phone QA",
|
||||
sound = "radar",
|
||||
weekdays = { 1, 2, 3, 4, 5 },
|
||||
lastTriggeredMinute = nil,
|
||||
},
|
||||
{
|
||||
id = "test-weekend",
|
||||
enabled = false,
|
||||
time = "10:00",
|
||||
note = "Car Meet",
|
||||
sound = "chimes",
|
||||
weekdays = { 0, 6 },
|
||||
lastTriggeredMinute = nil,
|
||||
},
|
||||
}
|
||||
local games = {
|
||||
snake = { highScore = 42, speed = "fast" },
|
||||
memory = {
|
||||
best = {
|
||||
small = { moves = 12, timeMs = 42000 },
|
||||
medium = { moves = 28, timeMs = 96000 },
|
||||
},
|
||||
soundEnabled = true,
|
||||
},
|
||||
minesweeper = {
|
||||
best = { quick = { timeMs = 31000 }, classic = { timeMs = 124000 } },
|
||||
elapsedMs = 0,
|
||||
game = nil,
|
||||
soundEnabled = true,
|
||||
},
|
||||
["number-merge"] = { bestScore = 8192, game = nil, highestTile = 1024, soundEnabled = true },
|
||||
["tower-stack"] = { highHeight = 23, highScore = 4750, soundEnabled = true },
|
||||
["sky-flappy"] = { design = "neon", highScore = 18, soundEnabled = true },
|
||||
["neon-drop"] = { bestLines = 14, bestScore = 12600, soundEnabled = true },
|
||||
}
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_device_data` (`device_imei`, `namespace`, `payload`)
|
||||
VALUES (?, 'alarms', ?)
|
||||
ON DUPLICATE KEY UPDATE `payload` = VALUES(`payload`), `revision` = `revision` + 1
|
||||
]], { context.imei, json.encode(alarms) })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_device_data` (`device_imei`, `namespace`, `payload`)
|
||||
VALUES (?, 'games', ?)
|
||||
ON DUPLICATE KEY UPDATE `payload` = VALUES(`payload`), `revision` = `revision` + 1
|
||||
]], { context.imei, json.encode(games) })
|
||||
|
||||
local contact_id = stable_uuid(context.key .. ":contact:alex")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_contacts`
|
||||
(`id`, `contact_id`, `account_id`, `name`, `notes`, `organization`, `phone_number`, `favorite`)
|
||||
VALUES (?, ?, ?, 'Alex Rivera', 'Test contact for calls and messages.', 'Downtown Cab Co.', ?, 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `notes` = VALUES(`notes`),
|
||||
`organization` = VALUES(`organization`), `phone_number` = VALUES(`phone_number`), `favorite` = 1
|
||||
]], { contact_id, contact_id, account_id, bot.sim.phone_number })
|
||||
|
||||
local sms_one = stable_uuid(context.key .. ":sms:one")
|
||||
local sms_two = stable_uuid(context.key .. ":sms:two")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_sms_messages`
|
||||
(`id`, `sender_sim_id`, `recipient_sim_id`, `sender_number`, `recipient_number`, `body`, `read_at`, `created_at`)
|
||||
VALUES (?, ?, ?, ?, ?, 'Willkommen auf dem Testserver! Alle Apps sind jetzt befüllt.', NULL,
|
||||
DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 8 MINUTE))
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `read_at` = NULL
|
||||
]], { sms_one, bot.sim.id, context.sim.id, bot.sim.phone_number, context.sim.phone_number })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_sms_messages`
|
||||
(`id`, `sender_sim_id`, `recipient_sim_id`, `sender_number`, `recipient_number`, `body`, `read_at`, `created_at`)
|
||||
VALUES (?, ?, ?, ?, ?, 'Perfekt, ich teste gerade Nachrichten und Kontakte.', CURRENT_TIMESTAMP,
|
||||
DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE))
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `read_at` = CURRENT_TIMESTAMP
|
||||
]], { sms_two, context.sim.id, bot.sim.id, context.sim.phone_number, bot.sim.phone_number })
|
||||
|
||||
local call_id = stable_uuid(context.key .. ":call:missed")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_calls`
|
||||
(`id`, `caller_sim_id`, `callee_sim_id`, `caller_number`, `callee_number`, `status`,
|
||||
`started_at`, `ended_at`, `duration_seconds`)
|
||||
VALUES (?, ?, ?, ?, ?, 'missed', DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 2 HOUR),
|
||||
DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 2 HOUR), 0)
|
||||
ON DUPLICATE KEY UPDATE `status` = 'missed', `duration_seconds` = 0
|
||||
]], { call_id, bot.sim.id, context.sim.id, bot.sim.phone_number, context.sim.phone_number })
|
||||
Bridge.Database.Query("DELETE FROM `sky_phone_call_entries` WHERE `call_id` = ? AND `account_id` = ?", { call_id, account_id })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_call_entries`
|
||||
(`call_id`, `account_id`, `direction`, `status`, `other_number`, `created_at`)
|
||||
VALUES (?, ?, 'incoming', 'missed', ?, DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 2 HOUR))
|
||||
]], { call_id, account_id, bot.sim.phone_number })
|
||||
|
||||
local note_one = stable_uuid(context.key .. ":note:checklist")
|
||||
local note_two = stable_uuid(context.key .. ":note:ideas")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_notes` (`id`, `account_id`, `title`, `body`, `pinned`)
|
||||
VALUES (?, ?, 'Phone Test-Checkliste', 'Kontakte\nNachrichten\nAnrufe\nSocial Apps\nMarktplatz\nFirmen', 1)
|
||||
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `body` = VALUES(`body`), `pinned` = 1
|
||||
]], { note_one, account_id })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_notes` (`id`, `account_id`, `title`, `body`, `pinned`)
|
||||
VALUES (?, ?, 'Ideen für später', 'DarkChat testen und eine SkyRide-Fahrt bewerten.', 0)
|
||||
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `body` = VALUES(`body`)
|
||||
]], { note_two, account_id })
|
||||
|
||||
local mail_id = stable_uuid(context.key .. ":mail:welcome")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_mail_messages`
|
||||
(`id`, `sender_account_id`, `recipients`, `subject`, `body`, `created_at`)
|
||||
VALUES (?, ?, ?, 'Willkommen beim iFruit-Test',
|
||||
'Hallo! Diese Nachricht gehört zu deinem reproduzierbaren Ingame-Testdatensatz.',
|
||||
DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 HOUR))
|
||||
ON DUPLICATE KEY UPDATE `recipients` = VALUES(`recipients`), `subject` = VALUES(`subject`), `body` = VALUES(`body`)
|
||||
]], { mail_id, bot.account.id, json.encode({ context.account.email }) })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_mail_entries` (`message_id`, `account_id`, `folder`)
|
||||
VALUES (?, ?, 'inbox')
|
||||
]], { mail_id, account_id })
|
||||
local draft_id = stable_uuid(context.key .. ":mail:draft")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_mail_drafts` (`id`, `account_id`, `recipients`, `subject`, `body`)
|
||||
VALUES (?, ?, ?, 'Entwurf: Testfeedback', 'Hier kann ich später mein Testfeedback ergänzen.')
|
||||
ON DUPLICATE KEY UPDATE `recipients` = VALUES(`recipients`), `subject` = VALUES(`subject`), `body` = VALUES(`body`)
|
||||
]], { draft_id, account_id, json.encode({ bot.account.email }) })
|
||||
|
||||
Bridge.Database.Query(
|
||||
"DELETE FROM `sky_phone_bank_transactions` WHERE `owner_identifier` = ? AND `reference` LIKE 'sky-phone-test:%'",
|
||||
{ context.identifier }
|
||||
)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_bank_transactions` (`owner_identifier`, `kind`, `amount`, `label`, `reference`, `created_at`)
|
||||
VALUES (?, 'deposit', 2500, 'Test paycheck', 'sky-phone-test:paycheck', DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 DAY)),
|
||||
(?, 'withdrawal', 85, 'Los Santos Customs', 'sky-phone-test:repair', DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR)),
|
||||
(?, 'transfer_in', 420, 'Alex Rivera', 'sky-phone-test:transfer', DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 30 MINUTE))
|
||||
]], { context.identifier, context.identifier, context.identifier })
|
||||
|
||||
local invoice_id = stable_uuid(context.key .. ":invoice:repair")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_billing_invoices`
|
||||
(`id`, `recipient_identifier`, `issuer_identifier`, `issuer_account`, `issuer_label`,
|
||||
`title`, `description`, `amount`, `currency`, `status`, `due_at`)
|
||||
VALUES (?, ?, 'sky_phone:testbot:mechanic', 'society_mechanic', 'Los Santos Customs',
|
||||
'Vehicle inspection', 'Test invoice for the Billing app.', 750, '$', 'open',
|
||||
DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 3 DAY))
|
||||
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `description` = VALUES(`description`),
|
||||
`amount` = VALUES(`amount`), `status` = 'open', `read_at` = NULL
|
||||
]], { invoice_id, context.identifier })
|
||||
Bridge.Database.Query(
|
||||
"DELETE FROM `sky_phone_billing_events` WHERE `invoice_id` = ? AND `note` = 'Generated by the phone test data command.'",
|
||||
{ invoice_id }
|
||||
)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_billing_events` (`invoice_id`, `event`, `actor_identifier`, `note`)
|
||||
VALUES (?, 'created', 'sky_phone:testbot:mechanic', 'Generated by the phone test data command.')
|
||||
]], { invoice_id })
|
||||
|
||||
local event_id = stable_uuid(context.key .. ":calendar:meeting")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_calendar_events`
|
||||
(`id`, `account_id`, `title`, `note`, `starts_at`, `ends_at`, `reminder_minutes`)
|
||||
VALUES (?, ?, 'Phone QA Session', 'Alle Apps im Spiel durchtesten.',
|
||||
DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 1 DAY), DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 25 HOUR), 30)
|
||||
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `note` = VALUES(`note`),
|
||||
`starts_at` = VALUES(`starts_at`), `ends_at` = VALUES(`ends_at`), `reminder_minutes` = 30,
|
||||
`reminded_at` = NULL
|
||||
]], { event_id, account_id })
|
||||
|
||||
local marker_one = stable_uuid(context.key .. ":marker:lsc")
|
||||
local marker_two = stable_uuid(context.key .. ":marker:pier")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_map_markers` (`id`, `device_imei`, `label`, `color`, `position_x`, `position_y`, `position_z`)
|
||||
VALUES (?, ?, 'Los Santos Customs', 'orange', -337.3, -136.9, 39.0)
|
||||
ON DUPLICATE KEY UPDATE `label` = VALUES(`label`), `color` = VALUES(`color`),
|
||||
`position_x` = VALUES(`position_x`), `position_y` = VALUES(`position_y`), `position_z` = VALUES(`position_z`)
|
||||
]], { marker_one, context.imei })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_map_markers` (`id`, `device_imei`, `label`, `color`, `position_x`, `position_y`, `position_z`)
|
||||
VALUES (?, ?, 'Del Perro Pier', 'blue', -1649.7, -1078.3, 13.0)
|
||||
ON DUPLICATE KEY UPDATE `label` = VALUES(`label`), `color` = VALUES(`color`),
|
||||
`position_x` = VALUES(`position_x`), `position_y` = VALUES(`position_y`), `position_z` = VALUES(`position_z`)
|
||||
]], { marker_two, context.imei })
|
||||
|
||||
local song_id = stable_uuid(context.key .. ":music:song")
|
||||
local playlist_id = stable_uuid(context.key .. ":music:playlist")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_music_youtube_songs` (`id`, `account_id`, `video_id`, `title`, `artist`)
|
||||
VALUES (?, ?, 'dQw4w9WgXcQ', 'Never Gonna Give You Up', 'Rick Astley')
|
||||
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `artist` = VALUES(`artist`)
|
||||
]], { song_id, account_id })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_music_playlists` (`id`, `account_id`, `name`)
|
||||
VALUES (?, ?, 'Test Drive Mix')
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`)
|
||||
]], { playlist_id, account_id })
|
||||
Bridge.Database.Query("DELETE FROM `sky_phone_music_playlist_items` WHERE `playlist_id` = ?", { playlist_id })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_music_playlist_items` (`playlist_id`, `source`, `song_id`, `position`)
|
||||
VALUES (?, 'youtube', ?, 1)
|
||||
]], { playlist_id, song_id })
|
||||
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_radio_profiles`
|
||||
(`identifier`, `history`, `settings`, `primary_frequency`, `secondary_frequency`, `badge`, `display_name`)
|
||||
VALUES (?, ?, ?, 100.1, 101.5, 'QA', ?)
|
||||
ON DUPLICATE KEY UPDATE `history` = VALUES(`history`), `settings` = VALUES(`settings`),
|
||||
`primary_frequency` = VALUES(`primary_frequency`), `secondary_frequency` = VALUES(`secondary_frequency`)
|
||||
]], {
|
||||
context.identifier,
|
||||
json.encode({ 100.1, 101.5, 99.9 }),
|
||||
json.encode({ volume = 65, notifications = true, autoRejoin = false }),
|
||||
context.player_name,
|
||||
})
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_easyshare_preferences` (`device_imei`, `visibility`)
|
||||
VALUES (?, 'everyone') ON DUPLICATE KEY UPDATE `visibility` = 'everyone'
|
||||
]], { context.imei })
|
||||
local transfer_id = stable_uuid(context.key .. ":easyshare:transfer")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_easyshare_transfers`
|
||||
(`id`, `sender_imei`, `recipient_imei`, `sender_name`, `recipient_name`, `content_type`,
|
||||
`payload`, `status`, `progress`, `completed_at`)
|
||||
VALUES (?, ?, ?, 'Alex Rivera', ?, 'contact', ?, 'completed', 100, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE `payload` = VALUES(`payload`), `status` = 'completed',
|
||||
`progress` = 100, `completed_at` = CURRENT_TIMESTAMP
|
||||
]], {
|
||||
transfer_id,
|
||||
bot.imei,
|
||||
context.imei,
|
||||
context.player_name,
|
||||
json.encode({ name = "Mia Chen", phoneNumber = context.bot_two.sim.phone_number }),
|
||||
})
|
||||
end
|
||||
|
||||
local function seed_marketplace_and_pages(context)
|
||||
local account_id = context.account.id
|
||||
local bot_id = context.bot_one.account.id
|
||||
local own_handle = "tester" .. tostring(account_id)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_marketplace_profiles` (`account_id`, `display_name`, `bio`, `avatar_media_id`)
|
||||
VALUES (?, ?, 'QA profile generated in game.', ?)
|
||||
ON DUPLICATE KEY UPDATE `display_name` = VALUES(`display_name`), `bio` = VALUES(`bio`),
|
||||
`avatar_media_id` = VALUES(`avatar_media_id`)
|
||||
]], { account_id, context.player_name, context.media.user_portrait })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_marketplace_profiles` (`account_id`, `display_name`, `bio`, `avatar_media_id`)
|
||||
VALUES (?, 'Alex Rivera', 'Trusted test seller.', ?)
|
||||
ON DUPLICATE KEY UPDATE `display_name` = VALUES(`display_name`), `bio` = VALUES(`bio`),
|
||||
`avatar_media_id` = VALUES(`avatar_media_id`)
|
||||
]], { bot_id, context.media.bot_portrait })
|
||||
|
||||
local bot_listing = stable_uuid(context.key .. ":market:bot-listing")
|
||||
local own_listing = stable_uuid(context.key .. ":market:own-listing")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_marketplace_listings`
|
||||
(`id`, `seller_account_id`, `title`, `description`, `category`, `item_condition`,
|
||||
`price_type`, `price`, `district`, `show_phone`, `phone_number`, `status`, `expires_at`)
|
||||
VALUES (?, ?, 'Sultan RS - Test Vehicle', 'Clean test listing with negotiable price.',
|
||||
'vehicles', 'very_good', 'negotiable', 42000, 'los_santos', 1, ?, 'active', DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 30 DAY))
|
||||
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `description` = VALUES(`description`),
|
||||
`status` = 'active', `expires_at` = VALUES(`expires_at`)
|
||||
]], { bot_listing, bot_id, context.bot_one.sim.phone_number })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_marketplace_listings`
|
||||
(`id`, `seller_account_id`, `title`, `description`, `category`, `item_condition`,
|
||||
`price_type`, `price`, `district`, `status`, `expires_at`)
|
||||
VALUES (?, ?, 'QA Headset', 'A listing owned by the test user.', 'electronics', 'used',
|
||||
'fixed', 350, 'los_santos', 'active', DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 30 DAY))
|
||||
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `description` = VALUES(`description`),
|
||||
`status` = 'active', `expires_at` = VALUES(`expires_at`)
|
||||
]], { own_listing, account_id })
|
||||
local car_gradient = ("url(%s)"):format(json.encode(photo_urls.car))
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_marketplace_images` (`listing_id`, `media_id`, `gradient`, `sort_order`)
|
||||
VALUES (?, ?, ?, 1)
|
||||
ON DUPLICATE KEY UPDATE `media_id` = VALUES(`media_id`), `gradient` = VALUES(`gradient`)
|
||||
]], { bot_listing, tostring(context.media.bot_car), car_gradient })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_marketplace_favorites` (`account_id`, `listing_id`) VALUES (?, ?)
|
||||
]], { account_id, bot_listing })
|
||||
local inquiry_id = stable_uuid(context.key .. ":market:inquiry")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_marketplace_inquiries`
|
||||
(`id`, `listing_id`, `seller_account_id`, `buyer_account_id`, `offer_amount`,
|
||||
`offer_proposer_account_id`, `offer_status`, `offer_revision`)
|
||||
VALUES (?, ?, ?, ?, 40000, ?, 'pending', 1)
|
||||
ON DUPLICATE KEY UPDATE `offer_amount` = 40000, `offer_proposer_account_id` = VALUES(`offer_proposer_account_id`),
|
||||
`offer_status` = 'pending', `offer_revision` = 1
|
||||
]], { inquiry_id, bot_listing, bot_id, account_id, account_id })
|
||||
Bridge.Database.Query("DELETE FROM `sky_phone_marketplace_messages` WHERE `inquiry_id` = ?", { inquiry_id })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_marketplace_messages` (`inquiry_id`, `sender_account_id`, `body`, `read_at`)
|
||||
VALUES (?, ?, 'Ist der Sultan noch verfügbar?', CURRENT_TIMESTAMP),
|
||||
(?, ?, 'Ja, gerne Probefahrt in Burton.', NULL)
|
||||
]], { inquiry_id, account_id, inquiry_id, bot_id })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_marketplace_offers` (`inquiry_id`, `proposer_account_id`, `amount`)
|
||||
VALUES (?, ?, 40000)
|
||||
]], { inquiry_id, account_id })
|
||||
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_pages_profiles` (`account_id`, `handle`, `bio`, `avatar_media_id`)
|
||||
VALUES (?, ?, 'Lokale Tests, Events und Angebote.', ?)
|
||||
ON DUPLICATE KEY UPDATE `handle` = VALUES(`handle`), `bio` = VALUES(`bio`),
|
||||
`avatar_media_id` = VALUES(`avatar_media_id`)
|
||||
]], { account_id, own_handle, context.media.user_portrait })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_pages_profiles` (`account_id`, `handle`, `bio`, `avatar_media_id`)
|
||||
VALUES (?, 'alex.local', 'News aus Los Santos.', ?)
|
||||
ON DUPLICATE KEY UPDATE `bio` = VALUES(`bio`), `avatar_media_id` = VALUES(`avatar_media_id`)
|
||||
]], { bot_id, context.media.bot_portrait })
|
||||
local page_post = stable_uuid(context.key .. ":pages:post")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_pages_posts`
|
||||
(`id`, `account_id`, `source_type`, `title`, `body`, `category`, `district`)
|
||||
VALUES (?, ?, 'personal', 'Car Meet am Pier', 'Heute Abend findet ein offenes Test-Car-Meet statt.',
|
||||
'event', 'los_santos')
|
||||
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `body` = VALUES(`body`)
|
||||
]], { page_post, bot_id })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_pages_images` (`post_id`, `media_id`, `gradient`, `sort_order`)
|
||||
VALUES (?, ?, ?, 1)
|
||||
ON DUPLICATE KEY UPDATE `media_id` = VALUES(`media_id`), `gradient` = VALUES(`gradient`)
|
||||
]], { page_post, tostring(context.media.bot_car), car_gradient })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_pages_reactions` (`post_id`, `account_id`, `kind`)
|
||||
VALUES (?, ?, 'like'), (?, ?, 'save')
|
||||
]], { page_post, account_id, page_post, account_id })
|
||||
end
|
||||
|
||||
local function seed_social_apps(context)
|
||||
local account_id = context.account.id
|
||||
local bot_id = context.bot_one.account.id
|
||||
local bot_two_id = context.bot_two.account.id
|
||||
local suffix = tostring(account_id)
|
||||
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_picstagram_profiles`
|
||||
(`id`, `account_id`, `handle`, `display_name`, `bio`, `avatar_media_id`)
|
||||
VALUES (?, ?, ?, ?, 'Ingame QA account', ?)
|
||||
ON DUPLICATE KEY UPDATE `handle` = VALUES(`handle`), `display_name` = VALUES(`display_name`),
|
||||
`bio` = VALUES(`bio`), `avatar_media_id` = VALUES(`avatar_media_id`)
|
||||
]], {
|
||||
stable_uuid(context.key .. ":pic:user"), account_id, "tester" .. suffix, context.player_name,
|
||||
context.media.user_portrait,
|
||||
})
|
||||
local pic_user = ensure_string_profile("sky_phone_picstagram_profiles", account_id)
|
||||
local pic_bot_seed = stable_uuid("sky_phone:testbot:pic:alex")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_picstagram_profiles`
|
||||
(`id`, `account_id`, `handle`, `display_name`, `bio`, `avatar_media_id`, `verified`)
|
||||
VALUES (?, ?, 'alex.rivera', 'Alex Rivera', 'Cars, city lights and test content.', ?, 1)
|
||||
ON DUPLICATE KEY UPDATE `display_name` = VALUES(`display_name`), `bio` = VALUES(`bio`),
|
||||
`avatar_media_id` = VALUES(`avatar_media_id`), `verified` = 1
|
||||
]], { pic_bot_seed, bot_id, context.media.bot_portrait })
|
||||
local pic_bot = ensure_string_profile("sky_phone_picstagram_profiles", bot_id)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_picstagram_sessions` (`device_imei`, `profile_id`)
|
||||
VALUES (?, ?) ON DUPLICATE KEY UPDATE `profile_id` = VALUES(`profile_id`)
|
||||
]], { context.imei, pic_user })
|
||||
local pic_post = stable_uuid(context.key .. ":pic:post")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_picstagram_posts` (`id`, `profile_id`, `caption`, `location`)
|
||||
VALUES (?, ?, 'Night drive through Los Santos #test', 'Vinewood')
|
||||
ON DUPLICATE KEY UPDATE `caption` = VALUES(`caption`), `location` = VALUES(`location`), `status` = 'published'
|
||||
]], { pic_post, pic_bot })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_picstagram_post_media` (`post_id`, `media_id`, `position`)
|
||||
VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE `media_id` = VALUES(`media_id`)
|
||||
]], { pic_post, context.media.bot_city })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_picstagram_follows` (`follower_id`, `following_id`, `status`)
|
||||
VALUES (?, ?, 'accepted')
|
||||
]], { pic_user, pic_bot })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_picstagram_reactions` (`post_id`, `profile_id`, `kind`)
|
||||
VALUES (?, ?, 'like'), (?, ?, 'save')
|
||||
]], { pic_post, pic_user, pic_post, pic_user })
|
||||
local pic_comment = stable_uuid(context.key .. ":pic:comment")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_picstagram_comments` (`id`, `post_id`, `profile_id`, `body`)
|
||||
VALUES (?, ?, ?, 'Sieht richtig gut aus!')
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `status` = 'visible'
|
||||
]], { pic_comment, pic_post, pic_user })
|
||||
local story_id = stable_uuid(context.key .. ":pic:story")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_picstagram_stories` (`id`, `profile_id`, `media_id`, `body`, `expires_at`)
|
||||
VALUES (?, ?, ?, 'Test story live', DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 24 HOUR))
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `status` = 'active', `expires_at` = VALUES(`expires_at`)
|
||||
]], { story_id, pic_bot, context.media.bot_city })
|
||||
local pic_activity = stable_uuid(context.key .. ":pic:activity")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_picstagram_activities` (`id`, `recipient_id`, `actor_id`, `post_id`, `kind`)
|
||||
VALUES (?, ?, ?, ?, 'like')
|
||||
ON DUPLICATE KEY UPDATE `read_at` = NULL
|
||||
]], { pic_activity, pic_user, pic_bot, pic_post })
|
||||
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_fliptok_profiles` (`account_id`, `handle`, `display_name`, `bio`)
|
||||
VALUES (?, ?, ?, 'Testing every FlipTok feature.')
|
||||
ON DUPLICATE KEY UPDATE `handle` = VALUES(`handle`), `display_name` = VALUES(`display_name`), `bio` = VALUES(`bio`)
|
||||
]], { account_id, "tester" .. suffix, context.player_name })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_fliptok_profiles`
|
||||
(`account_id`, `handle`, `display_name`, `bio`, `account_type`, `verified`)
|
||||
VALUES (?, 'mia.motion', 'Mia Chen', 'Short videos from Los Santos.', 'media', 1)
|
||||
ON DUPLICATE KEY UPDATE `display_name` = VALUES(`display_name`), `bio` = VALUES(`bio`), `verified` = 1
|
||||
]], { bot_two_id })
|
||||
local flip_user = ensure_numeric_profile("sky_phone_fliptok_profiles", account_id)
|
||||
local flip_bot = ensure_numeric_profile("sky_phone_fliptok_profiles", bot_two_id)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_fliptok_sessions` (`device_imei`, `profile_id`)
|
||||
VALUES (?, ?) ON DUPLICATE KEY UPDATE `profile_id` = VALUES(`profile_id`)
|
||||
]], { context.imei, flip_user })
|
||||
local flip_video = stable_uuid(context.key .. ":flip:video")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_fliptok_videos`
|
||||
(`id`, `profile_id`, `media_id`, `caption`, `location`, `view_count`, `share_count`)
|
||||
VALUES (?, ?, ?, 'Flowers in motion #fyp #test', 'Mirror Park', 1842, 37)
|
||||
ON DUPLICATE KEY UPDATE `caption` = VALUES(`caption`), `view_count` = 1842,
|
||||
`share_count` = 37, `status` = 'published'
|
||||
]], { flip_video, flip_bot, context.media.bot_video })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_fliptok_follows` (`follower_id`, `following_id`) VALUES (?, ?)
|
||||
]], { flip_user, flip_bot })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_fliptok_reactions` (`video_id`, `profile_id`, `kind`)
|
||||
VALUES (?, ?, 'like'), (?, ?, 'save')
|
||||
]], { flip_video, flip_user, flip_video, flip_user })
|
||||
local flip_comment = stable_uuid(context.key .. ":flip:comment")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_fliptok_comments` (`id`, `video_id`, `profile_id`, `body`)
|
||||
VALUES (?, ?, ?, 'Der Test-Clip läuft flüssig.')
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `status` = 'visible'
|
||||
]], { flip_comment, flip_video, flip_user })
|
||||
local flip_notification = stable_uuid(context.key .. ":flip:notification")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_fliptok_notifications` (`id`, `recipient_id`, `actor_id`, `video_id`, `kind`)
|
||||
VALUES (?, ?, ?, ?, 'follow') ON DUPLICATE KEY UPDATE `read_at` = NULL
|
||||
]], { flip_notification, flip_user, flip_bot, flip_video })
|
||||
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_feather_profiles` (`account_id`, `handle`, `display_name`, `bio`, `avatar_media_id`)
|
||||
VALUES (?, ?, ?, 'Testing Feather in game.', ?)
|
||||
ON DUPLICATE KEY UPDATE `handle` = VALUES(`handle`), `display_name` = VALUES(`display_name`),
|
||||
`bio` = VALUES(`bio`), `avatar_media_id` = VALUES(`avatar_media_id`)
|
||||
]], { account_id, "tester" .. suffix, context.player_name, context.media.user_portrait })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_feather_profiles`
|
||||
(`account_id`, `handle`, `display_name`, `bio`, `avatar_media_id`, `verified`)
|
||||
VALUES (?, 'alex_updates', 'Alex Rivera', 'Los Santos live updates.', ?, 1)
|
||||
ON DUPLICATE KEY UPDATE `display_name` = VALUES(`display_name`), `bio` = VALUES(`bio`),
|
||||
`avatar_media_id` = VALUES(`avatar_media_id`), `verified` = 1
|
||||
]], { bot_id, context.media.bot_portrait })
|
||||
local feather_user = ensure_numeric_profile("sky_phone_feather_profiles", account_id)
|
||||
local feather_bot = ensure_numeric_profile("sky_phone_feather_profiles", bot_id)
|
||||
local feather_post = stable_uuid(context.key .. ":feather:post")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_feather_posts` (`id`, `profile_id`, `body`)
|
||||
VALUES (?, ?, 'Der neue iFruit-Testdatensatz ist live. #LosSantos #PhoneQA')
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `status` = 'published'
|
||||
]], { feather_post, feather_bot })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_feather_hashtags` (`post_id`, `tag`)
|
||||
VALUES (?, 'lossantos'), (?, 'phoneqa')
|
||||
]], { feather_post, feather_post })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_feather_follows` (`follower_id`, `following_id`) VALUES (?, ?)
|
||||
]], { feather_user, feather_bot })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_feather_reactions` (`post_id`, `profile_id`, `kind`)
|
||||
VALUES (?, ?, 'like'), (?, ?, 'bookmark')
|
||||
]], { feather_post, feather_user, feather_post, feather_user })
|
||||
local feather_notification = stable_uuid(context.key .. ":feather:notification")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_feather_notifications` (`id`, `recipient_id`, `actor_id`, `post_id`, `kind`)
|
||||
VALUES (?, ?, ?, ?, 'follow') ON DUPLICATE KEY UPDATE `read_at` = NULL
|
||||
]], { feather_notification, feather_user, feather_bot, feather_post })
|
||||
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_flare_profiles`
|
||||
(`account_id`, `name`, `age`, `bio`, `gender`, `interested_in`, `min_age`, `max_age`,
|
||||
`avatar`, `interests`, `looking_for`)
|
||||
VALUES (?, ?, 27, 'Testing Flare conversations.', 'nonbinary', 'everyone', 21, 40, 0, ?, 'friends')
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `bio` = VALUES(`bio`), `interests` = VALUES(`interests`)
|
||||
]], { account_id, context.player_name:sub(1, 32), json.encode({ "cars", "music", "gaming" }) })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_flare_profiles`
|
||||
(`account_id`, `name`, `age`, `bio`, `gender`, `interested_in`, `min_age`, `max_age`,
|
||||
`avatar`, `interests`, `looking_for`)
|
||||
VALUES (?, 'Mia', 26, 'Coffee, sunsets and good conversations.', 'woman', 'everyone', 21, 40, 1, ?, 'friends')
|
||||
ON DUPLICATE KEY UPDATE `bio` = VALUES(`bio`), `interests` = VALUES(`interests`)
|
||||
]], { bot_two_id, json.encode({ "coffee", "travel", "photography" }) })
|
||||
local flare_user = ensure_numeric_profile("sky_phone_flare_profiles", account_id)
|
||||
local flare_bot = ensure_numeric_profile("sky_phone_flare_profiles", bot_two_id)
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_flare_profile_photos` (`profile_id`, `media_id`, `sort_order`)
|
||||
VALUES (?, ?, 1), (?, ?, 1)
|
||||
]], { flare_user, context.media.user_portrait, flare_bot, context.media.bot_two_portrait })
|
||||
local match_id = stable_uuid(context.key .. ":flare:match")
|
||||
local account_a = math.min(account_id, bot_two_id)
|
||||
local account_b = math.max(account_id, bot_two_id)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_flare_matches` (`id`, `account_a_id`, `account_b_id`)
|
||||
VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `created_at` = `created_at`
|
||||
]], { match_id, account_a, account_b })
|
||||
local flare_message = stable_uuid(context.key .. ":flare:message")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_flare_messages` (`id`, `match_id`, `sender_account_id`, `body`, `read_at`)
|
||||
VALUES (?, ?, ?, 'Hey! Bereit für einen vollständigen App-Test?', NULL)
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `read_at` = NULL
|
||||
]], { flare_message, match_id, bot_two_id })
|
||||
end
|
||||
|
||||
local function seed_private_and_services(context)
|
||||
local account_id = context.account.id
|
||||
local bot_id = context.bot_one.account.id
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_darkchat_profiles`
|
||||
(`account_id`, `dark_id`, `invite_code`, `alias`, `avatar_seed`, `notification_mode`, `activity_visible`)
|
||||
VALUES (?, ?, ?, 'NightTester', 42, 'private', 1)
|
||||
ON DUPLICATE KEY UPDATE `alias` = VALUES(`alias`), `notification_mode` = VALUES(`notification_mode`)
|
||||
]], { account_id, ("DARK%010d"):format(account_id % 10000000000), ("INV%08d"):format(account_id % 100000000) })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_darkchat_profiles`
|
||||
(`account_id`, `dark_id`, `invite_code`, `alias`, `avatar_seed`, `notification_mode`, `activity_visible`)
|
||||
VALUES (?, 'DARK0000000001', 'INV00000001', 'GhostAlex', 17, 'full', 1)
|
||||
ON DUPLICATE KEY UPDATE `alias` = VALUES(`alias`)
|
||||
]], { bot_id })
|
||||
local dark_user = ensure_numeric_profile("sky_phone_darkchat_profiles", account_id)
|
||||
local dark_bot = ensure_numeric_profile("sky_phone_darkchat_profiles", bot_id)
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_darkchat_contacts` (`profile_id`, `contact_profile_id`, `alias_override`)
|
||||
VALUES (?, ?, 'Ghost')
|
||||
]], { dark_user, dark_bot })
|
||||
local conversation_id = stable_uuid(context.key .. ":dark:conversation")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_darkchat_conversations` (`id`, `disappearing_seconds`)
|
||||
VALUES (?, 0) ON DUPLICATE KEY UPDATE `disappearing_seconds` = 0
|
||||
]], { conversation_id })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_darkchat_members` (`conversation_id`, `profile_id`, `last_read_at`)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP), (?, ?, NULL)
|
||||
]], { conversation_id, dark_user, conversation_id, dark_bot })
|
||||
local dark_one = stable_uuid(context.key .. ":dark:message:one")
|
||||
local dark_two = stable_uuid(context.key .. ":dark:message:two")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_darkchat_messages` (`id`, `conversation_id`, `sender_profile_id`, `body`)
|
||||
VALUES (?, ?, ?, 'Willkommen im privaten Testchat.')
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `deleted_for_everyone` = 0
|
||||
]], { dark_one, conversation_id, dark_bot })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_darkchat_messages`
|
||||
(`id`, `conversation_id`, `sender_profile_id`, `message_type`, `body`, `reactions`)
|
||||
VALUES (?, ?, ?, 'emoji', '🔥', ?)
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `reactions` = VALUES(`reactions`)
|
||||
]], { dark_two, conversation_id, dark_user, json.encode({ ["🔥"] = { dark_bot } }) })
|
||||
|
||||
local crew_user_seed = stable_uuid(context.key .. ":crew:user")
|
||||
local crew_bot_seed = stable_uuid("sky_phone:testbot:crew:alex")
|
||||
local group_id = stable_uuid(context.key .. ":crew:group")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_crewlink_profiles` (`id`, `account_id`, `username`, `active_group_id`)
|
||||
VALUES (?, ?, ?, NULL)
|
||||
ON DUPLICATE KEY UPDATE `username` = VALUES(`username`)
|
||||
]], { crew_user_seed, account_id, ("tester%s"):format(account_id):sub(1, 20) })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_crewlink_profiles` (`id`, `account_id`, `username`, `active_group_id`)
|
||||
VALUES (?, ?, 'alexcrew', NULL)
|
||||
ON DUPLICATE KEY UPDATE `username` = VALUES(`username`)
|
||||
]], { crew_bot_seed, bot_id })
|
||||
local crew_user = ensure_string_profile("sky_phone_crewlink_profiles", account_id)
|
||||
local crew_bot = ensure_string_profile("sky_phone_crewlink_profiles", bot_id)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_crewlink_groups`
|
||||
(`id`, `name`, `colour`, `owner_profile_id`, `invite_code`, `allow_member_pings`, `overhead_allowed`)
|
||||
VALUES (?, 'Phone QA Crew', 'violet', ?, ?, 1, 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `colour` = VALUES(`colour`)
|
||||
]], { group_id, crew_user, ("QA%s"):format(account_id):sub(1, 12) })
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_crewlink_memberships` (`group_id`, `profile_id`, `role`)
|
||||
VALUES (?, ?, 'owner'), (?, ?, 'member')
|
||||
]], { group_id, crew_user, group_id, crew_bot })
|
||||
Bridge.Database.Query(
|
||||
"UPDATE `sky_phone_crewlink_profiles` SET `active_group_id` = ? WHERE `id` IN (?, ?)",
|
||||
{ group_id, crew_user, crew_bot }
|
||||
)
|
||||
local ping_id = stable_uuid(context.key .. ":crew:ping")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_crewlink_pings`
|
||||
(`id`, `group_id`, `creator_profile_id`, `type`, `label`, `position_x`, `position_y`, `position_z`, `expires_at`)
|
||||
VALUES (?, ?, ?, 'meeting', 'QA Treffpunkt', -337.3, -136.9, 39.0,
|
||||
DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 12 HOUR))
|
||||
ON DUPLICATE KEY UPDATE `label` = VALUES(`label`), `expires_at` = VALUES(`expires_at`)
|
||||
]], { ping_id, group_id, crew_bot })
|
||||
|
||||
local ride_user_seed = stable_uuid(context.key .. ":skyride:user")
|
||||
local ride_bot_seed = stable_uuid("sky_phone:testbot:skyride:alex")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_skyride_profiles` (`id`, `owner_identifier`)
|
||||
VALUES (?, ?) ON DUPLICATE KEY UPDATE `owner_identifier` = VALUES(`owner_identifier`)
|
||||
]], { ride_user_seed, context.identifier })
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_skyride_profiles` (`id`, `owner_identifier`)
|
||||
VALUES (?, 'sky_phone:testbot:alex') ON DUPLICATE KEY UPDATE `owner_identifier` = VALUES(`owner_identifier`)
|
||||
]], { ride_bot_seed })
|
||||
local ride_user_rows = Bridge.Database.Query(
|
||||
"SELECT `id` FROM `sky_phone_skyride_profiles` WHERE `owner_identifier` = ? LIMIT 1",
|
||||
{ context.identifier }
|
||||
)
|
||||
local ride_bot_rows = Bridge.Database.Query(
|
||||
"SELECT `id` FROM `sky_phone_skyride_profiles` WHERE `owner_identifier` = 'sky_phone:testbot:alex' LIMIT 1",
|
||||
{}
|
||||
)
|
||||
local ride_user = ride_user_rows[1] and ride_user_rows[1].id
|
||||
local ride_bot = ride_bot_rows[1] and ride_bot_rows[1].id
|
||||
if type(ride_user) ~= "string" or type(ride_bot) ~= "string" then
|
||||
error("[sky_phone] Test SkyRide profiles could not be loaded.")
|
||||
end
|
||||
local ride_id = stable_uuid(context.key .. ":skyride:completed")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_skyride_rides`
|
||||
(`id`, `passenger_profile_id`, `driver_profile_id`, `passenger_name`, `driver_name`,
|
||||
`status`, `service_class`, `pickup_label`, `pickup_x`, `pickup_y`, `pickup_z`,
|
||||
`destination_label`, `destination_x`, `destination_y`, `destination_z`, `distance_meters`,
|
||||
`duration_seconds`, `price`, `payout_amount`, `currency`, `driver_vehicle_model`,
|
||||
`driver_vehicle_color`, `driver_vehicle_plate`, `passenger_rating`, `rating_comment`,
|
||||
`tip_amount`, `tip_status`, `accepted_at`, `arrived_at`, `started_at`, `completed_at`, `paid_out_at`)
|
||||
VALUES (?, ?, ?, ?, 'Alex Rivera', 'completed', 'comfort', 'Legion Square', 215.8, -810.1, 30.7,
|
||||
'Del Perro Pier', -1649.7, -1078.3, 13.0, 6200, 540, 320, 240, '$', 'Sultan',
|
||||
'Midnight Blue', 'QA 2026', 5, 'Saubere Testfahrt.', 25, 'completed',
|
||||
DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR), DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 170 MINUTE),
|
||||
DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 165 MINUTE), DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 2 HOUR),
|
||||
DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 2 HOUR))
|
||||
ON DUPLICATE KEY UPDATE `status` = 'completed', `passenger_rating` = 5,
|
||||
`rating_comment` = 'Saubere Testfahrt.', `tip_amount` = 25, `tip_status` = 'completed'
|
||||
]], { ride_id, ride_user, ride_bot, context.player_name })
|
||||
|
||||
local company_rows = Bridge.Database.Query(
|
||||
"SELECT `company_id` FROM `sky_phone_company_profiles` WHERE `company_id` = 'mechanic' LIMIT 1",
|
||||
{}
|
||||
)
|
||||
if company_rows[1] then
|
||||
local request_id = stable_uuid(context.key .. ":company:request")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_company_requests`
|
||||
(`id`, `company_id`, `service_id`, `customer_sim_id`, `subject`, `description`,
|
||||
`status`, `customer_unread`, `company_activity_revision`, `revision`)
|
||||
VALUES (?, 'mechanic', 'mechanic-repair', ?, 'Motor verliert Leistung',
|
||||
'Testauftrag mit Chatverlauf und Status.', 'waiting_customer', 1, 3, 3)
|
||||
ON DUPLICATE KEY UPDATE `subject` = VALUES(`subject`), `description` = VALUES(`description`),
|
||||
`status` = 'waiting_customer', `customer_unread` = 1, `company_activity_revision` = 3, `revision` = 3
|
||||
]], { request_id, context.sim.id })
|
||||
local request_message = stable_uuid(context.key .. ":company:message")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_company_request_messages`
|
||||
(`id`, `request_id`, `sender_type`, `sender_identifier`, `body`)
|
||||
VALUES (?, ?, 'company', 'sky_phone:testbot:mechanic',
|
||||
'Bitte komm für eine Diagnose bei Los Santos Customs vorbei.')
|
||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`)
|
||||
]], { request_message, request_id })
|
||||
local request_event = stable_uuid(context.key .. ":company:event")
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_company_request_events`
|
||||
(`id`, `request_id`, `event_type`, `actor_type`, `actor_identifier`, `from_status`, `to_status`, `detail`)
|
||||
VALUES (?, ?, 'status', 'company', 'sky_phone:testbot:mechanic', 'in_progress', 'waiting_customer',
|
||||
'Waiting for the test customer.')
|
||||
ON DUPLICATE KEY UPDATE `detail` = VALUES(`detail`)
|
||||
]], { request_event, request_id })
|
||||
end
|
||||
end
|
||||
|
||||
local function seed_for_source(source)
|
||||
local slots = Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)
|
||||
local phone_slot = slots[1]
|
||||
if not phone_slot then
|
||||
error(("[sky_phone] Test data source %s has no phone item."):format(source))
|
||||
end
|
||||
local imei, device_error = SkyPhone.EnsureDevice(source, phone_slot)
|
||||
if not imei then
|
||||
error(("[sky_phone] Test data could not resolve the phone device: %s"):format(tostring(device_error)))
|
||||
end
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
if type(identifier) ~= "string" or identifier == "" then
|
||||
error("[sky_phone] Test data could not resolve the player identifier.")
|
||||
end
|
||||
|
||||
local player_name = ((Bridge.Framework.GetFirstname(source) or "") .. " "
|
||||
.. (Bridge.Framework.GetLastname(source) or "")):match("^%s*(.-)%s*$")
|
||||
if player_name == "" then
|
||||
player_name = GetPlayerName(source) or ("Player %s"):format(source)
|
||||
end
|
||||
local device = SkyPhone.LoadDevice(imei)
|
||||
local account
|
||||
if device and device.account_id then
|
||||
local rows = Bridge.Database.Query(
|
||||
"SELECT `id`, `email` FROM `sky_phone_accounts` WHERE `id` = ? LIMIT 1",
|
||||
{ device.account_id }
|
||||
)
|
||||
account = rows[1]
|
||||
if not account then
|
||||
error("[sky_phone] Test data found a device with a missing iFruit account.")
|
||||
end
|
||||
account.id = tonumber(account.id)
|
||||
else
|
||||
account = ensure_account("tester_" .. seed_hash(identifier) .. "@" .. Config.Mail.Domain)
|
||||
Bridge.Database.Query("UPDATE `sky_phone_devices` SET `account_id` = ? WHERE `imei` = ?", { account.id, imei })
|
||||
end
|
||||
|
||||
device = SkyPhone.LoadDevice(imei)
|
||||
local sim
|
||||
if device and device.sim_id then
|
||||
local rows = Bridge.Database.Query("SELECT * FROM `sky_phone_sims` WHERE `id` = ? LIMIT 1", { device.sim_id })
|
||||
sim = rows[1]
|
||||
else
|
||||
sim = reserve_sim(identifier, Bridge.Framework.GetFirstname(source), Bridge.Framework.GetLastname(source))
|
||||
Bridge.Database.Query("UPDATE `sky_phone_devices` SET `sim_id` = ? WHERE `imei` = ?", { sim.id, imei })
|
||||
local metadata = phone_slot.metadata or {}
|
||||
metadata.sim_id = sim.id
|
||||
metadata.phone_number = sim.phone_number
|
||||
metadata.formatted_number = SkyPhoneSimNumber.Format(
|
||||
sim.phone_number,
|
||||
Config.Sim.NumberGroups,
|
||||
Config.Sim.NumberLength,
|
||||
Config.Sim.NumberPrefix
|
||||
)
|
||||
if not Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, metadata) then
|
||||
error("[sky_phone] Test data could not update the phone item's SIM metadata.")
|
||||
end
|
||||
end
|
||||
|
||||
local bot_one = ensure_bot("alex", "phone.bot.alex", "990000000000001", "Alex", "Rivera")
|
||||
local bot_two = ensure_bot("mia", "phone.bot.mia", "990000000000002", "Mia", "Chen")
|
||||
local context = {
|
||||
source = source,
|
||||
identifier = identifier,
|
||||
player_name = player_name,
|
||||
account = account,
|
||||
imei = imei,
|
||||
sim = sim,
|
||||
bot_one = bot_one,
|
||||
bot_two = bot_two,
|
||||
key = identifier .. ":" .. imei,
|
||||
media = {},
|
||||
}
|
||||
context.media.user_portrait = ensure_media(account.id, "test-user-portrait", photo_urls.portrait, "photo")
|
||||
context.media.user_city = ensure_media(account.id, "test-user-city", photo_urls.city, "photo")
|
||||
context.media.user_video = ensure_media(account.id, "test-user-video", video_url, "video")
|
||||
context.media.bot_portrait = ensure_media(bot_one.account.id, "test-bot-alex-portrait", photo_urls.car, "photo")
|
||||
context.media.bot_car = ensure_media(bot_one.account.id, "test-bot-alex-car", photo_urls.car, "photo")
|
||||
context.media.bot_city = ensure_media(bot_one.account.id, "test-bot-alex-city", photo_urls.city, "photo")
|
||||
context.media.bot_two_portrait = ensure_media(bot_two.account.id, "test-bot-mia-portrait", photo_urls.portrait, "photo")
|
||||
context.media.bot_video = ensure_media(bot_two.account.id, "test-bot-mia-video", video_url, "video")
|
||||
|
||||
seed_core(context)
|
||||
seed_marketplace_and_pages(context)
|
||||
seed_social_apps(context)
|
||||
seed_private_and_services(context)
|
||||
SkyPhone.RefreshSource(source)
|
||||
return account.email
|
||||
end
|
||||
|
||||
RegisterCommand(Config.TestData.Command, function(source)
|
||||
if source <= 0 then
|
||||
Bridge.Debug("warn", "[sky_phone] The test data command must be run by an in-game player.")
|
||||
return
|
||||
end
|
||||
if Config.TestData.AdminOnly and not Bridge.Framework.HasAdminGroup(source, Config.TestData.AdminGroups) then
|
||||
Bridge.Debug("warn", "[sky_phone] Source %s attempted to run the restricted test data command.", tostring(source))
|
||||
TriggerClientEvent("sky_phone:testdata:feedback", source, false)
|
||||
return
|
||||
end
|
||||
local now = os.time()
|
||||
if seed_attempts[source] and now - seed_attempts[source] < 30 then
|
||||
Bridge.Debug("warn", "[sky_phone] Source %s repeated the test data command too quickly.", tostring(source))
|
||||
TriggerClientEvent("sky_phone:testdata:feedback", source, false)
|
||||
return
|
||||
end
|
||||
seed_attempts[source] = now
|
||||
local success, result = pcall(seed_for_source, source)
|
||||
if not success then
|
||||
Bridge.Debug("error", "[sky_phone] Test data seeding failed for source %s: %s", tostring(source), tostring(result))
|
||||
TriggerClientEvent("sky_phone:testdata:feedback", source, false)
|
||||
return
|
||||
end
|
||||
Bridge.Debug("info", "[sky_phone] Test data seeded for source %s.", tostring(source), { always = true })
|
||||
TriggerClientEvent("sky_phone:testdata:feedback", source, true, result)
|
||||
end, false)
|
||||
|
||||
AddEventHandler("playerDropped", function()
|
||||
seed_attempts[source] = nil
|
||||
end)
|
||||
|
||||
end)
|
||||
Reference in New Issue
Block a user