ENH - migrate DarkChat account flow to Sky UI

This commit is contained in:
Leon.Schmidt
2026-08-13 21:27:49 +02:00
parent 477b61e0cb
commit 712d96eaa6
10 changed files with 1375 additions and 1406 deletions
+101 -15
View File
@@ -2,7 +2,11 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useDarkChatStore } from '@/stores/darkchat'
import type { DarkChatMessage, DarkChatThread } from '@/types/darkchat'
import type {
DarkChatMessage,
DarkChatProfile,
DarkChatThread,
} from '@/types/darkchat'
import { nuiCall } from '@/utils/nui'
vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() }))
@@ -18,6 +22,17 @@ const conversation: DarkChatThread['conversation'] = {
readReceipts: true,
}
const profile: DarkChatProfile = {
activityVisible: false,
alias: 'Nightshade',
avatarSeed: 267,
createdAt: '2026-08-01 21:20:00',
darkId: 'dark:7X4K-P92D',
id: 1,
inviteCode: 'DC-7X4K-NOVA',
notificationMode: 'private',
}
function message(id: string): DarkChatMessage {
return {
body: 'Quiet channel',
@@ -37,28 +52,53 @@ describe('darkchat store', () => {
})
it('optimistically sends and confirms a private message', async () => {
let resolveSend: ((value: { data: DarkChatMessage; success: true }) => void) | undefined
const pending = new Promise<{ data: DarkChatMessage; success: true }>((resolve) => (resolveSend = resolve))
let resolveSend:
| ((value: { data: DarkChatMessage; success: true }) => void)
| undefined
const pending = new Promise<{ data: DarkChatMessage; success: true }>(
(resolve) => (resolveSend = resolve),
)
mockNuiCall
.mockResolvedValueOnce({ data: { conversation, messages: [] }, success: true })
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
.mockResolvedValueOnce({
data: { conversation, messages: [] },
success: true,
})
.mockResolvedValueOnce({
data: { contacts: [], conversations: [], profile: null },
success: true,
})
.mockImplementationOnce(() => pending)
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
.mockResolvedValueOnce({
data: { contacts: [], conversations: [], profile: null },
success: true,
})
const store = useDarkChatStore()
await store.openThread(conversation.id)
const sending = store.send({ body: 'Quiet channel', messageType: 'text' })
expect(store.messages[0]).toMatchObject({ deliveryStatus: 'sending', body: 'Quiet channel' })
expect(store.messages[0]).toMatchObject({
deliveryStatus: 'sending',
body: 'Quiet channel',
})
resolveSend?.({ data: message('server-message'), success: true })
await sending
expect(store.messages[0]).toMatchObject({ deliveryStatus: 'delivered', id: 'server-message' })
expect(store.messages[0]).toMatchObject({
deliveryStatus: 'delivered',
id: 'server-message',
})
})
it('keeps failed messages visible for delivery feedback', async () => {
mockNuiCall
.mockResolvedValueOnce({ data: { conversation, messages: [] }, success: true })
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
.mockResolvedValueOnce({
data: { conversation, messages: [] },
success: true,
})
.mockResolvedValueOnce({
data: { contacts: [], conversations: [], profile: null },
success: true,
})
.mockResolvedValueOnce({ error: 'blocked', success: false })
const store = useDarkChatStore()
@@ -69,10 +109,22 @@ describe('darkchat store', () => {
it('uses a local media preview without sending that URL to the server', async () => {
mockNuiCall
.mockResolvedValueOnce({ data: { conversation, messages: [] }, success: true })
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
.mockResolvedValueOnce({ data: { ...message('media-id'), messageType: 'image' }, success: true })
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
.mockResolvedValueOnce({
data: { conversation, messages: [] },
success: true,
})
.mockResolvedValueOnce({
data: { contacts: [], conversations: [], profile: null },
success: true,
})
.mockResolvedValueOnce({
data: { ...message('media-id'), messageType: 'image' },
success: true,
})
.mockResolvedValueOnce({
data: { contacts: [], conversations: [], profile: null },
success: true,
})
const store = useDarkChatStore()
await store.openThread(conversation.id)
@@ -102,7 +154,41 @@ describe('darkchat store', () => {
const store = useDarkChatStore()
expect(await store.loadMedia('voice-id')).toBe(true)
expect(await store.loadMedia('voice-id')).toBe(true)
expect(store.mediaSources['voice-id']).toBe('data:audio/webm;codecs=opus;base64,ZmFrZQ==')
expect(store.mediaSources['voice-id']).toBe(
'data:audio/webm;codecs=opus;base64,ZmFrZQ==',
)
expect(mockNuiCall).toHaveBeenCalledTimes(1)
})
it('creates a private profile only through the explicit profile action', async () => {
mockNuiCall.mockResolvedValueOnce({
data: { contacts: [], conversations: [], profile },
success: true,
})
const store = useDarkChatStore()
expect(await store.createProfile()).toBe(true)
expect(store.profile).toEqual(profile)
expect(mockNuiCall).toHaveBeenCalledWith('darkchat:create-profile')
})
it('clears all private state after deleting the profile', async () => {
mockNuiCall
.mockResolvedValueOnce({
data: { contacts: [], conversations: [], profile },
success: true,
})
.mockResolvedValueOnce({ success: true })
const store = useDarkChatStore()
await store.bootstrap()
store.activeConversation = conversation
store.messages = [message('message-id')]
expect(await store.deleteProfile()).toBe(true)
expect(store.profile).toBeNull()
expect(store.activeConversation).toBeNull()
expect(store.messages).toEqual([])
expect(mockNuiCall).toHaveBeenLastCalledWith('darkchat:delete-profile')
})
})
+75 -14
View File
@@ -23,7 +23,10 @@ export const useDarkChatStore = defineStore('darkchat', () => {
const loading = ref(false)
const lastError = ref<string | null>(null)
const unreadCount = computed(() =>
conversations.value.reduce((total, conversation) => total + conversation.unread, 0),
conversations.value.reduce(
(total, conversation) => total + conversation.unread,
0,
),
)
async function bootstrap(): Promise<boolean> {
@@ -37,15 +40,37 @@ export const useDarkChatStore = defineStore('darkchat', () => {
conversations.value = []
return false
}
profile.value = response.data.profile
profile.value = response.data.profile ?? null
contacts.value = response.data.contacts
conversations.value = response.data.conversations
return true
}
async function createProfile(): Promise<boolean> {
loading.value = true
const response = await nuiCall<DarkChatBootstrap>('darkchat:create-profile')
loading.value = false
lastError.value = response.error ?? null
if (!response.success || !response.data) return false
profile.value = response.data.profile ?? null
contacts.value = response.data.contacts
conversations.value = response.data.conversations
return true
}
async function deleteProfile(): Promise<boolean> {
const response = await nuiCall('darkchat:delete-profile')
lastError.value = response.error ?? null
if (!response.success) return false
reset()
return true
}
async function openThread(conversationId: string): Promise<boolean> {
loading.value = true
const response = await nuiCall<DarkChatThread>('darkchat:thread', { conversationId })
const response = await nuiCall<DarkChatThread>('darkchat:thread', {
conversationId,
})
loading.value = false
lastError.value = response.error ?? null
if (!response.success || !response.data) return false
@@ -61,20 +86,28 @@ export const useDarkChatStore = defineStore('darkchat', () => {
async function refreshInbox(): Promise<boolean> {
const response = await nuiCall<DarkChatBootstrap>('darkchat:bootstrap')
if (!response.success || !response.data) return false
profile.value = response.data.profile
profile.value = response.data.profile ?? null
contacts.value = response.data.contacts
conversations.value = response.data.conversations
return true
}
async function start(identifier: string): Promise<NuiResponse<{ conversationId: string }>> {
const response = await nuiCall<{ conversationId: string }>('darkchat:start', { identifier })
async function start(
identifier: string,
): Promise<NuiResponse<{ conversationId: string }>> {
const response = await nuiCall<{ conversationId: string }>(
'darkchat:start',
{ identifier },
)
if (response.success) await refreshInbox()
return response
}
async function send(outgoing: DarkChatOutgoing): Promise<NuiResponse<DarkChatMessage>> {
if (!activeConversation.value) return { success: false, error: 'invalid_conversation' }
async function send(
outgoing: DarkChatOutgoing,
): Promise<NuiResponse<DarkChatMessage>> {
if (!activeConversation.value)
return { success: false, error: 'invalid_conversation' }
const clientId = `dark-pending-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const { mediaPreviewUrl, ...request } = outgoing
const optimistic: DarkChatMessage = {
@@ -100,13 +133,16 @@ export const useDarkChatStore = defineStore('darkchat', () => {
}
messages.value.push(optimistic)
if (outgoing.messageType === 'voice' && outgoing.mediaPayload) {
mediaSources.value[clientId] = `data:${outgoing.mediaMime};base64,${outgoing.mediaPayload}`
mediaSources.value[clientId] =
`data:${outgoing.mediaMime};base64,${outgoing.mediaPayload}`
}
const response = await nuiCall<DarkChatMessage>('darkchat:send', {
...request,
conversationId: activeConversation.value.id,
})
const index = messages.value.findIndex((message) => message.clientId === clientId)
const index = messages.value.findIndex(
(message) => message.clientId === clientId,
)
if (!response.success || !response.data) {
if (index >= 0) messages.value[index].deliveryStatus = 'failed'
return response
@@ -115,7 +151,11 @@ export const useDarkChatStore = defineStore('darkchat', () => {
delete mediaSources.value[clientId]
if (source) mediaSources.value[response.data.id] = source
if (index >= 0) {
messages.value[index] = { ...response.data, clientId, deliveryStatus: 'delivered' }
messages.value[index] = {
...response.data,
clientId,
deliveryStatus: 'delivered',
}
}
await refreshInbox()
return response
@@ -123,13 +163,20 @@ export const useDarkChatStore = defineStore('darkchat', () => {
async function loadMedia(messageId: string): Promise<boolean> {
if (mediaSources.value[messageId]) return true
const response = await nuiCall<{ mime: string; payload: string }>('darkchat:media', { messageId })
const response = await nuiCall<{ mime: string; payload: string }>(
'darkchat:media',
{ messageId },
)
if (!response.success || !response.data) return false
mediaSources.value[messageId] = `data:${response.data.mime};base64,${response.data.payload}`
mediaSources.value[messageId] =
`data:${response.data.mime};base64,${response.data.payload}`
return true
}
async function mutate(endpoint: string, data: Record<string, unknown>): Promise<boolean> {
async function mutate(
endpoint: string,
data: Record<string, unknown>,
): Promise<boolean> {
const response = await nuiCall(`darkchat:${endpoint}`, data)
return response.success
}
@@ -140,12 +187,25 @@ export const useDarkChatStore = defineStore('darkchat', () => {
mediaSources.value = {}
}
function reset(): void {
profile.value = null
contacts.value = []
conversations.value = []
activeConversation.value = null
messages.value = []
mediaSources.value = {}
loading.value = false
lastError.value = null
}
return {
activeConversation,
bootstrap,
closeThread,
createProfile,
contacts,
conversations,
deleteProfile,
lastError,
loadMedia,
loading,
@@ -155,6 +215,7 @@ export const useDarkChatStore = defineStore('darkchat', () => {
openThread,
profile,
refreshInbox,
reset,
send,
start,
unreadCount,
+25
View File
@@ -1483,6 +1483,9 @@ const defaultLocales: LocaleTree = {
signInBody:
'DarkChat identities are linked to your private Sky Cloud account.',
signInHint: 'Sign in through Settings to continue.',
createIdentity: 'Create Dark Identity',
createIdentityBody:
'Create a private identity before starting a DarkChat conversation.',
security: 'Security',
privateNetwork: 'Private invitation-only network',
noResults: 'No Results',
@@ -1497,6 +1500,10 @@ const defaultLocales: LocaleTree = {
darkIdOrInvite: 'Dark-ID or invitation code',
continue: 'Continue',
contacts: 'DarkChat Contacts',
contactsHint:
'Saved private identities appear only on your DarkChat account.',
noContacts: 'No saved DarkChat contacts',
private: 'Private',
shareIdentity: 'Tap to share your private identity',
message: 'Dark message',
activeNow: 'Activity shared',
@@ -1535,7 +1542,10 @@ const defaultLocales: LocaleTree = {
deleteForMe: 'Delete for me',
deleteForBoth: 'Delete for both',
report: 'Report',
messageActions: 'Message actions',
contactSecurity: 'Contact & Security',
chatSettings: 'Chat Settings',
contactActions: 'Contact Actions',
chatSince: 'Private chat since {date}',
notifications: 'Notifications',
readReceipts: 'Read receipts',
@@ -1552,6 +1562,7 @@ const defaultLocales: LocaleTree = {
chatCleared: 'Chat cleared',
myIdentity: 'My Dark Identity',
alias: 'Alias',
privacySettings: 'Privacy Settings',
notificationPrivacy: 'Notification privacy',
notificationFull: 'Full · alias and message',
notificationPrivate: 'Private · generic message',
@@ -1561,6 +1572,19 @@ const defaultLocales: LocaleTree = {
copyInvite: 'Copy Invite',
privacyDisclaimer:
'DarkChat stores messages on the server and does not claim end-to-end encryption.',
profileActions: 'Account Actions',
signOut: 'Sign Out',
signOutTitle: 'Sign out of Sky Cloud?',
signOutBody:
'This signs the whole phone out of Sky Cloud. Your DarkChat profile and messages remain stored.',
signingOut: 'Signing Out...',
signOutHint:
'Signing out affects every app that uses Sky Cloud on this phone.',
deleteProfile: 'Delete DarkChat Profile',
deleteProfileTitle: 'Delete DarkChat profile?',
deleteProfileBody:
'Your identity, contacts and DarkChat conversations will be permanently deleted.',
deletingProfile: 'Deleting...',
unknownIdentity: 'Unknown Identity',
unknownIdentityBody:
'Only continue if you expected this identity. The account is not discoverable through public search.',
@@ -1588,6 +1612,7 @@ const defaultLocales: LocaleTree = {
invalid_voice: 'This voice message is invalid.',
invalid_attachment: 'This photo or video is unavailable.',
invalid_profile: 'Check your alias and privacy settings.',
sign_out_failed: 'Sky Cloud could not sign out.',
rate_limited: 'Too many requests. Try again shortly.',
gif_provider_unconfigured: 'GIF search is not configured.',
gif_provider_unauthorized: 'The GIF provider key is invalid.',
+1 -1
View File
@@ -79,7 +79,7 @@ export type DarkChatMessage = {
}
export type DarkChatBootstrap = {
profile: DarkChatProfile
profile: DarkChatProfile | null
contacts: DarkChatContact[]
conversations: DarkChatConversationSummary[]
}
@@ -0,0 +1,59 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const source = readFileSync(
new URL('./DarkChatApp.vue', import.meta.url),
'utf8',
)
describe('DarkChatApp Sky UI contract', () => {
it('uses first-party Sky UI without direct Konsta markup', () => {
expect(source).not.toContain("from 'konsta/vue'")
expect(source).not.toMatch(/<\/?k-[a-z]/)
expect(source).toContain('<SkyAppPage')
expect(source).toContain('<SkyNavbar')
expect(source).toContain('<SkyScrollArea')
expect(source).toContain('<SkySettingsGroup')
expect(source).toContain('<SkyMessagebar')
expect(source).toContain('<SkyPillNavigation')
})
it('keeps one identity action in the inbox header', () => {
const inboxStart = source.indexOf("screen === 'inbox'")
const newChatStart = source.indexOf("screen === 'new'")
const inbox = source.slice(inboxStart, newChatStart)
expect(inbox.match(/@click="openProfile"/g)).toHaveLength(1)
})
it('bottom-aligns short threads and exposes profile lifecycle actions', () => {
expect(source).toContain('ref="messagesArea"')
expect(source).toMatch(/\.dc-day\s*\{[^}]*margin:\s*auto 0 8px/s)
expect(source).toContain('@click="signOut"')
expect(source).toContain('@click="deleteProfile"')
})
it('separates the round attachment action from the floating message pill', () => {
expect(source).toContain('class="dc-composer-row"')
expect(source).toContain('class="dc-composer-action"')
expect(source).toContain('class="dc-composer-pill"')
expect(source).toMatch(
/<div v-else class="dc-composer-row">[\s\S]*?<SkyGlass[\s\S]*?class="dc-composer-action"[\s\S]*?<SkyGlass class="dc-composer-pill">[\s\S]*?<SkyMessagebar/,
)
expect(source).not.toContain('<template #left>')
expect(source).toMatch(
/\.dc-composer-pill\s*\{[^}]*border-radius:\s*var\(--sky-radius-pill\)/s,
)
expect(source).toMatch(
/\.dc-composer-action\s*\{[^}]*border-radius:\s*50%/s,
)
})
it('raises only the DarkChat inbox title block', () => {
expect(source).toContain('class="dc-inbox-navbar"')
expect(source).toMatch(
/\.dc-inbox-navbar :deep\(\.sky-navbar__title-container > div\)\s*\{[^}]*translateY\(-6px\)/s,
)
})
})
File diff suppressed because it is too large Load Diff
+14
View File
@@ -1666,6 +1666,7 @@ const darkChatProfile = {
activityVisible: false,
createdAt: '2026-08-01 21:20:00',
}
let darkChatProfileActive = true
const darkChatPeers = [
{
id: 2,
@@ -1758,6 +1759,9 @@ const darkChatMessages = [
]
function darkChatBootstrap() {
if (!darkChatProfileActive) {
return { profile: null, contacts: [], conversations: [] }
}
return {
profile: darkChatProfile,
contacts: darkChatPeers
@@ -7304,6 +7308,16 @@ app.post('/api/:endpoint', (request, response) => {
response.json({ success: true, data: darkChatBootstrap() })
return
}
if (endpoint === 'darkchat:create-profile') {
darkChatProfileActive = true
response.json({ success: true, data: darkChatBootstrap() })
return
}
if (endpoint === 'darkchat:delete-profile') {
darkChatProfileActive = false
response.json({ success: true })
return
}
if (endpoint === 'darkchat:update-profile') {
darkChatProfile.alias = String(
request.body.alias ?? darkChatProfile.alias,
+5 -1
View File
@@ -482,9 +482,10 @@ Locales["en"] = {
darkchat = {
name = "DarkChat", newMessage = "New DarkChat message from {sender}", privateNotification = "New DarkChat message",
signInBody = "DarkChat identities are linked to your private Sky Cloud account.", signInHint = "Sign in through Settings to continue.",
createIdentity = "Create Dark Identity", createIdentityBody = "Create a private identity before starting a DarkChat conversation.",
security = "Security", privateNetwork = "Private invitation-only network", noResults = "No Results", noResultsBody = "Try another alias or Dark-ID.",
noChats = "No Private Chats", noChatsBody = "Connect with a Dark-ID or invitation code. There is no public search.", newChat = "New Chat",
connectPrivately = "Connect privately", newChatBody = "Enter an exact Dark-ID or invitation code. Unknown identities require confirmation.", darkIdOrInvite = "Dark-ID or invitation code", continue = "Continue", contacts = "DarkChat Contacts", shareIdentity = "Tap to share your private identity",
connectPrivately = "Connect privately", newChatBody = "Enter an exact Dark-ID or invitation code. Unknown identities require confirmation.", darkIdOrInvite = "Dark-ID or invitation code", continue = "Continue", contacts = "DarkChat Contacts", contactsHint = "Saved private identities appear only on your DarkChat account.", noContacts = "No saved DarkChat contacts", private = "Private", shareIdentity = "Tap to share your private identity",
message = "Dark message", activeNow = "Activity shared", encryptedSession = "Private session", serverPrivate = "Private server-stored conversation",
emoji = "Emoji", gif = "GIF", gifs = "GIFs", photo = "Photo", video = "Video", attachPhoto = "Attach Photo", takePhoto = "Take Photo", attachGif = "Attach GIF", attachVideo = "Attach Video", searchGifs = "Search GIFs", loadMore = "Load More",
voiceMessage = "Voice message", sending = "Sending", failed = "Not delivered", delivered = "Delivered", read = "Read", replying = "Replying to",
@@ -493,10 +494,13 @@ Locales["en"] = {
copied = "Copied", reply = "Reply", copy = "Copy", deleteForMe = "Delete for me", deleteForBoth = "Delete for both", report = "Report",
contactSecurity = "Contact & Security", chatSince = "Private chat since {date}", notifications = "Notifications", readReceipts = "Read receipts", disappearing = "Disappearing messages", contactAlias = "Contact alias", saveContact = "Save Contact", addContact = "Add Contact", contactSaved = "Contact saved", removeContact = "Remove Contact", block = "Block User", unblock = "Unblock User", clearChat = "Clear Chat", chatCleared = "Chat cleared",
myIdentity = "My Dark Identity", alias = "Alias", notificationPrivacy = "Notification privacy", notificationFull = "Full · alias and message", notificationPrivate = "Private · generic message", notificationHidden = "Invisible · badge only", shareActivity = "Share activity status", inviteCode = "Private invitation code", copyInvite = "Copy Invite", privacyDisclaimer = "DarkChat stores messages on the server and does not claim end-to-end encryption.",
messageActions = "Message actions", chatSettings = "Chat Settings", contactActions = "Contact Actions", privacySettings = "Privacy Settings",
profileActions = "Account Actions", signOut = "Sign Out", signOutTitle = "Sign out of Sky Cloud?", signOutBody = "This signs the whole phone out of Sky Cloud. Your DarkChat profile and messages remain stored.", signingOut = "Signing Out...", signOutHint = "Signing out affects every app that uses Sky Cloud on this phone.", deleteProfile = "Delete DarkChat Profile", deleteProfileTitle = "Delete DarkChat profile?", deleteProfileBody = "Your identity, contacts and DarkChat conversations will be permanently deleted.", deletingProfile = "Deleting...",
unknownIdentity = "Unknown Identity", unknownIdentityBody = "Only continue if you expected this identity. The account is not discoverable through public search.", openSecureChat = "Open Private Chat",
reportUser = "Report User", reportSpam = "Spam", reportHarassment = "Harassment", reportThreats = "Threats", reportIllegal = "Illegal content", reportOther = "Other", reportDetails = "Optional details", submitReport = "Submit Report", reported = "Report submitted",
microphoneUnavailable = "The microphone is unavailable.", recordingTooLarge = "The voice message is too large.",
errors = {
sign_out_failed = "Sky Cloud could not sign out.",
not_authenticated = "Sign in to your Sky Cloud account first.", invalid_dark_id = "Enter a valid Dark-ID or invitation code.", profile_not_found = "This private identity was not found.", self_chat = "You cannot message your own identity.", conversation_not_found = "This conversation is unavailable.", blocked = "Messages are blocked in this conversation.", invalid_message = "Enter a valid message.", invalid_gif = "This GIF is invalid.", invalid_voice = "This voice message is invalid.", invalid_attachment = "This photo or video is unavailable.", invalid_profile = "Check your alias and privacy settings.", rate_limited = "Too many requests. Try again shortly.", gif_provider_unconfigured = "GIF search is not configured.", gif_provider_unauthorized = "The GIF provider key is invalid.", gif_provider_rate_limited = "GIF search is busy. Try again shortly.", gif_provider_failed = "GIFs are temporarily unavailable.", default = "DarkChat could not complete the request.",
},
},
+2
View File
@@ -246,6 +246,8 @@ local server_callbacks = {
"easyshare:respond",
"easyshare:cancel",
"darkchat:bootstrap",
"darkchat:create-profile",
"darkchat:delete-profile",
"darkchat:update-profile",
"darkchat:start",
"darkchat:thread",
+63 -3
View File
@@ -117,7 +117,10 @@ local function require_profile(source)
if not account then
return nil, nil, error_response
end
local profile = load_profile(account.id) or create_profile(account.id)
local profile = load_profile(account.id)
if not profile then
return nil, account, { success = false, error = "profile_not_found" }
end
return profile, account
end
@@ -340,10 +343,21 @@ local function valid_voice(data)
end
Bridge.Callbacks.Register("sky_phone:darkchat:bootstrap", function(source)
local profile, _, error_response = require_profile(source)
if not profile then
local account, error_response = SkyPhone.RequireAccount(source)
if not account then
return error_response
end
local profile = load_profile(account.id)
if not profile then
return {
success = true,
data = {
profile = nil,
contacts = {},
conversations = {},
},
}
end
return {
success = true,
data = {
@@ -354,6 +368,52 @@ Bridge.Callbacks.Register("sky_phone:darkchat:bootstrap", function(source)
}
end)
Bridge.Callbacks.Register("sky_phone:darkchat:create-profile", function(source)
if not SkyPhone.AllowOperation(source, "darkchat_profile_create", 5, 60) then
return { success = false, error = "rate_limited" }
end
local account, error_response = SkyPhone.RequireAccount(source)
if not account then
return error_response
end
local profile = load_profile(account.id) or create_profile(account.id)
return {
success = true,
data = {
profile = profile_payload(profile),
contacts = list_contacts(profile.id),
conversations = list_conversations(profile.id),
},
}
end)
Bridge.Callbacks.Register("sky_phone:darkchat:delete-profile", function(source)
if not SkyPhone.AllowOperation(source, "darkchat_profile_delete", 3, 60) then
return { success = false, error = "rate_limited" }
end
local profile, account, error_response = require_profile(source)
if not profile then
return error_response
end
if not Bridge.Database.Transaction({
{
query = [[
DELETE conversation FROM `sky_phone_darkchat_conversations` conversation
JOIN `sky_phone_darkchat_members` member ON member.`conversation_id` = conversation.`id`
WHERE member.`profile_id` = ?
]],
params = { profile.id },
},
{
query = "DELETE FROM `sky_phone_darkchat_profiles` WHERE `id` = ? AND `account_id` = ?",
params = { profile.id, account.id },
},
}) then
error("[sky_phone] Failed to delete a DarkChat profile transaction.")
end
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:darkchat:update-profile", function(source, data)
if not SkyPhone.AllowOperation(source, "darkchat_profile", 10, 60) then
return { success = false, error = "rate_limited" }