mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
FIX - route company service-line messages
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
function source(path: string): string {
|
||||||
|
return readFileSync(new URL(path, import.meta.url), 'utf8').replace(
|
||||||
|
/\r\n/g,
|
||||||
|
'\n',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceBlock(
|
||||||
|
value: string,
|
||||||
|
startMarker: string,
|
||||||
|
endMarker: string,
|
||||||
|
): string {
|
||||||
|
const start = value.indexOf(startMarker)
|
||||||
|
const end = value.indexOf(endMarker, start)
|
||||||
|
|
||||||
|
expect(start).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(end).toBeGreaterThan(start)
|
||||||
|
return value.slice(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
const companiesServer = source('../../sky_phone/source/server/companies.lua')
|
||||||
|
const messagesServer = source('../../sky_phone/source/server/messages.lua')
|
||||||
|
const databaseMigration = source('../../sky_phone/source/server/db_migrate.lua')
|
||||||
|
const configDefaults = source(
|
||||||
|
'../../sky_phone/source/shared/config_default.lua',
|
||||||
|
)
|
||||||
|
const messagesApp = source('./views/apps/MessagesApp.vue')
|
||||||
|
const phoneFallbacks = source('./stores/phone.ts')
|
||||||
|
const locales = ['en', 'de', 'es'].map((locale) =>
|
||||||
|
source(`../../sky_phone/config/locales/${locale}.lua`),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe('Companies service-line message routing contract', () => {
|
||||||
|
it('enables the police service line and persists an indexed route channel', () => {
|
||||||
|
const police = sourceBlock(
|
||||||
|
configDefaults,
|
||||||
|
' police = {',
|
||||||
|
' ambulance = {',
|
||||||
|
)
|
||||||
|
const schema = sourceBlock(
|
||||||
|
databaseMigration,
|
||||||
|
' name = "sky_phone_company_requests",',
|
||||||
|
' name = "sky_phone_company_request_reads",',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(police).toContain('Number = "911"')
|
||||||
|
expect(police).toContain('CanMessage = true')
|
||||||
|
expect(schema).toContain(
|
||||||
|
"name = \"channel\", type = \"ENUM('app','service_line') NOT NULL DEFAULT 'app'\"",
|
||||||
|
)
|
||||||
|
expect(schema).toContain(
|
||||||
|
'name = "idx_sky_phone_company_requests_service_line"',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves short service numbers before the normal SIM-number path', () => {
|
||||||
|
const resolver = sourceBlock(
|
||||||
|
messagesServer,
|
||||||
|
'local function resolve_message_recipient(value)',
|
||||||
|
'\n\nlocal function shared_contact',
|
||||||
|
)
|
||||||
|
const send = sourceBlock(
|
||||||
|
messagesServer,
|
||||||
|
'Bridge.Callbacks.Register("sky_phone:messages:send"',
|
||||||
|
'\n\nend)',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(resolver).toContain('SkyPhoneCompanies.GetServiceLine(value)')
|
||||||
|
expect(resolver).toContain('return service_line.number, service_line')
|
||||||
|
expect(messagesServer.match(/resolve_message_recipient\(/g)?.length).toBe(4)
|
||||||
|
expect(send).toContain('service_line_text_only')
|
||||||
|
expect(send).toContain('SkyPhoneCompanies.RouteServiceLineMessage')
|
||||||
|
expect(send.indexOf('RouteServiceLineMessage')).toBeLessThan(
|
||||||
|
send.indexOf('SELECT s.`id`, s.`phone_number`'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('atomically mirrors inbound SMS into one active company request', () => {
|
||||||
|
const route = sourceBlock(
|
||||||
|
companiesServer,
|
||||||
|
'function SkyPhoneCompanies.RouteServiceLineMessage(source, data)',
|
||||||
|
'Bridge.Callbacks.Register("sky_phone:companies:create-request"',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(route).toContain('current_device(source, false)')
|
||||||
|
expect(route).toContain('service_line.canMessage')
|
||||||
|
expect(route).toContain('UPDATE `sky_phone_sims`')
|
||||||
|
expect(route).toContain("`channel` = 'service_line'")
|
||||||
|
expect(route).toContain('AND NOT EXISTS (')
|
||||||
|
expect(route).toContain('INSERT INTO `sky_phone_company_request_messages`')
|
||||||
|
expect(route).toContain('INSERT INTO `sky_phone_sms_messages`')
|
||||||
|
expect(route).toContain('Bridge.Database.Transaction(statements)')
|
||||||
|
expect(route).toContain('emit_request_change(row, true, true, source)')
|
||||||
|
expect(route).toContain('"sky_phone:companies:notification"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('mirrors authorized company replies back to the customer SMS thread', () => {
|
||||||
|
const sendMessage = sourceBlock(
|
||||||
|
companiesServer,
|
||||||
|
'Bridge.Callbacks.Register("sky_phone:companies:send-message"',
|
||||||
|
'Bridge.Callbacks.Register("sky_phone:companies:claim-request"',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(sendMessage).toContain('access.row.channel == "service_line"')
|
||||||
|
expect(
|
||||||
|
sendMessage.match(/INSERT INTO `sky_phone_sms_messages`/g)?.length,
|
||||||
|
).toBe(2)
|
||||||
|
expect(sendMessage).toContain("message.`sender_type` = 'customer'")
|
||||||
|
expect(sendMessage).toContain("message.`sender_type` = 'company'")
|
||||||
|
expect(sendMessage).toContain('"sky_phone:messages:changed"')
|
||||||
|
expect(sendMessage).toContain('"sky_phone:messages:new"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps service-line composition text-only in every shipped locale', () => {
|
||||||
|
expect(messagesApp).toContain(
|
||||||
|
"() => activeContact.value?.source === 'company'",
|
||||||
|
)
|
||||||
|
expect(messagesApp).toContain(
|
||||||
|
'activeCanMessage && !activeServiceLine && attachmentMenuOpen',
|
||||||
|
)
|
||||||
|
expect(messagesApp).toContain('v-if="!activeServiceLine"')
|
||||||
|
expect(messagesApp).toContain('v-else-if="!activeServiceLine"')
|
||||||
|
expect(messagesApp).toContain("'service_line_text_only'")
|
||||||
|
expect(phoneFallbacks).toContain('service_line_text_only:')
|
||||||
|
for (const locale of locales) {
|
||||||
|
expect(locale).toContain('service_line_text_only =')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -107,10 +107,10 @@ describe('company configurator creation contract', () => {
|
|||||||
expect(blankRegistration).toBeLessThan(policeRegistration)
|
expect(blankRegistration).toBeLessThan(policeRegistration)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('repairs persisted service-line messaging before Companies starts', () => {
|
it('enables persisted police service-line messaging before Companies starts', () => {
|
||||||
const migration = configuratorServer.slice(
|
const migration = configuratorServer.slice(
|
||||||
configuratorServer.indexOf(
|
configuratorServer.indexOf(
|
||||||
'local function migrate_unsupported_company_message_defaults()',
|
'local function migrate_police_service_line_messaging()',
|
||||||
),
|
),
|
||||||
configuratorServer.indexOf('\n\ndefault_config = {}'),
|
configuratorServer.indexOf('\n\ndefault_config = {}'),
|
||||||
)
|
)
|
||||||
@@ -118,19 +118,22 @@ describe('company configurator creation contract', () => {
|
|||||||
'Bridge.Database.AfterMigration("sky_phone", migrate_police_request_defaults)',
|
'Bridge.Database.AfterMigration("sky_phone", migrate_police_request_defaults)',
|
||||||
)
|
)
|
||||||
const messageRegistration = configuratorServer.indexOf(
|
const messageRegistration = configuratorServer.indexOf(
|
||||||
'Bridge.Database.AfterMigration("sky_phone", migrate_unsupported_company_message_defaults)',
|
'Bridge.Database.AfterMigration("sky_phone", migrate_police_service_line_messaging)',
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(migration).toContain(
|
expect(migration).toContain(
|
||||||
'sky-phone:configurator:unsupported-company-message-defaults:v1',
|
'sky-phone:configurator:service-line-messaging:v2',
|
||||||
)
|
)
|
||||||
expect(migration).toContain('line.CanMessage == true')
|
expect(migration).toContain('line.CanMessage ~= true')
|
||||||
expect(migration).toContain('line.CanMessage = false')
|
expect(migration).toContain('line.CanMessage = true')
|
||||||
expect(migration).toContain('Bridge.Database.Transaction(statements)')
|
expect(migration).toContain('Bridge.Database.Transaction(statements)')
|
||||||
expect(migration).toContain('SET `config_payload` = ?')
|
expect(migration).toContain('SET `config_payload` = ?')
|
||||||
expect(migration).toContain('`revision` = `revision` + 1')
|
expect(migration).toContain('`revision` = `revision` + 1')
|
||||||
expect(migration).toContain('apply_stored_row(read_stored_row())')
|
expect(migration).toContain('apply_stored_row(read_stored_row())')
|
||||||
expect(migration).toContain('apply_runtime_configuration()')
|
expect(migration).toContain('apply_runtime_configuration()')
|
||||||
expect(messageRegistration).toBeGreaterThan(policeRegistration)
|
expect(messageRegistration).toBeGreaterThan(policeRegistration)
|
||||||
|
expect(companiesServer).not.toContain(
|
||||||
|
'enables messaging without a virtual service-line message router',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2712,6 +2712,8 @@ const defaultLocales: LocaleTree = {
|
|||||||
recipient_not_found: 'That number is unavailable.',
|
recipient_not_found: 'That number is unavailable.',
|
||||||
blocked: 'This contact has blocked calls and messages from your SIM.',
|
blocked: 'This contact has blocked calls and messages from your SIM.',
|
||||||
messaging_unavailable: 'This company contact does not accept messages.',
|
messaging_unavailable: 'This company contact does not accept messages.',
|
||||||
|
service_line_text_only:
|
||||||
|
'Company service lines currently accept text messages only.',
|
||||||
no_sim: 'This phone has no SIM card.',
|
no_sim: 'This phone has no SIM card.',
|
||||||
rate_limited: 'Too many messages. Try again in a minute.',
|
rate_limited: 'Too many messages. Try again in a minute.',
|
||||||
request_failed: 'Messages are temporarily unavailable.',
|
request_failed: 'Messages are temporarily unavailable.',
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ describe('MessagesApp Sky UI contract', () => {
|
|||||||
expect(sheetStart).toBeGreaterThan(-1)
|
expect(sheetStart).toBeGreaterThan(-1)
|
||||||
expect(sheetEnd).toBeGreaterThan(sheetStart)
|
expect(sheetEnd).toBeGreaterThan(sheetStart)
|
||||||
expect(sheet).toContain(
|
expect(sheet).toContain(
|
||||||
':opened="activeCanMessage && attachmentPicker !== null"',
|
'activeCanMessage && !activeServiceLine && attachmentPicker !== null',
|
||||||
)
|
)
|
||||||
expect(sheet).toContain('swipe-to-close')
|
expect(sheet).toContain('swipe-to-close')
|
||||||
expect(sheet).toContain('grabber-clickable')
|
expect(sheet).toContain('grabber-clickable')
|
||||||
|
|||||||
@@ -215,6 +215,9 @@ const activeContact = computed(() =>
|
|||||||
const activeCanMessage = computed(
|
const activeCanMessage = computed(
|
||||||
() => activeContact.value?.canMessage !== false,
|
() => activeContact.value?.canMessage !== false,
|
||||||
)
|
)
|
||||||
|
const activeServiceLine = computed(
|
||||||
|
() => activeContact.value?.source === 'company',
|
||||||
|
)
|
||||||
const activeContactEmail = computed(() =>
|
const activeContactEmail = computed(() =>
|
||||||
normalizeMailAddress(activeContact.value?.email ?? ''),
|
normalizeMailAddress(activeContact.value?.email ?? ''),
|
||||||
)
|
)
|
||||||
@@ -255,12 +258,15 @@ const inboxMenuItems = computed(() => [
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
const attachmentPanelOpen = computed(
|
const attachmentPanelOpen = computed(
|
||||||
() => emojiOpen.value || attachmentPicker.value !== null,
|
() =>
|
||||||
|
emojiOpen.value ||
|
||||||
|
(!activeServiceLine.value && attachmentPicker.value !== null),
|
||||||
)
|
)
|
||||||
const composerHasContent = computed(
|
const composerHasContent = computed(
|
||||||
() =>
|
() =>
|
||||||
Boolean(draft.value.trim() || shareDraft.value) ||
|
Boolean(draft.value.trim()) ||
|
||||||
pendingAttachments.value.length > 0,
|
(!activeServiceLine.value &&
|
||||||
|
(Boolean(shareDraft.value) || pendingAttachments.value.length > 0)),
|
||||||
)
|
)
|
||||||
function contactName(number: string): string {
|
function contactName(number: string): string {
|
||||||
return (
|
return (
|
||||||
@@ -417,6 +423,8 @@ function errorText(error?: string): string {
|
|||||||
'gif_provider_failed',
|
'gif_provider_failed',
|
||||||
'self_message',
|
'self_message',
|
||||||
'recipient_not_found',
|
'recipient_not_found',
|
||||||
|
'messaging_unavailable',
|
||||||
|
'service_line_text_only',
|
||||||
'no_sim',
|
'no_sim',
|
||||||
'rate_limited',
|
'rate_limited',
|
||||||
'blocked',
|
'blocked',
|
||||||
@@ -614,6 +622,7 @@ async function callActiveContact(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleAttachmentMenu(): void {
|
function toggleAttachmentMenu(): void {
|
||||||
|
if (activeServiceLine.value) return
|
||||||
attachmentMenuOpen.value = !attachmentMenuOpen.value
|
attachmentMenuOpen.value = !attachmentMenuOpen.value
|
||||||
emojiOpen.value = false
|
emojiOpen.value = false
|
||||||
attachmentPicker.value = null
|
attachmentPicker.value = null
|
||||||
@@ -646,7 +655,7 @@ function openMediaApp(
|
|||||||
app: 'camera' | 'photos',
|
app: 'camera' | 'photos',
|
||||||
mediaType: 'photo' | 'video',
|
mediaType: 'photo' | 'video',
|
||||||
): void {
|
): void {
|
||||||
if (!messages.activeNumber) return
|
if (!messages.activeNumber || activeServiceLine.value) return
|
||||||
const remainingSlots =
|
const remainingSlots =
|
||||||
MAX_PENDING_ATTACHMENTS - pendingAttachments.value.length
|
MAX_PENDING_ATTACHMENTS - pendingAttachments.value.length
|
||||||
if (remainingSlots < 1) {
|
if (remainingSlots < 1) {
|
||||||
@@ -707,7 +716,14 @@ async function sendAttachment(
|
|||||||
mediaAssetId: string,
|
mediaAssetId: string,
|
||||||
mediaDurationMs?: number,
|
mediaDurationMs?: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!messages.activeNumber || !activeCanMessage.value || sending.value) return
|
if (
|
||||||
|
!messages.activeNumber ||
|
||||||
|
!activeCanMessage.value ||
|
||||||
|
activeServiceLine.value ||
|
||||||
|
sending.value
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
attachmentMenuOpen.value = false
|
attachmentMenuOpen.value = false
|
||||||
attachmentPicker.value = null
|
attachmentPicker.value = null
|
||||||
sending.value = true
|
sending.value = true
|
||||||
@@ -722,7 +738,7 @@ async function sendAttachment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendContact(contact: PhoneContact): Promise<void> {
|
async function sendContact(contact: PhoneContact): Promise<void> {
|
||||||
if (!messages.activeNumber || sending.value) return
|
if (!messages.activeNumber || activeServiceLine.value || sending.value) return
|
||||||
attachmentMenuOpen.value = false
|
attachmentMenuOpen.value = false
|
||||||
attachmentPicker.value = null
|
attachmentPicker.value = null
|
||||||
sending.value = true
|
sending.value = true
|
||||||
@@ -885,7 +901,12 @@ function sampleMicrophone(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startVoiceRecording(): Promise<void> {
|
async function startVoiceRecording(): Promise<void> {
|
||||||
if (!activeCanMessage.value || recording.value || recordingStarting.value) {
|
if (
|
||||||
|
!activeCanMessage.value ||
|
||||||
|
activeServiceLine.value ||
|
||||||
|
recording.value ||
|
||||||
|
recordingStarting.value
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emojiOpen.value = false
|
emojiOpen.value = false
|
||||||
@@ -1041,6 +1062,27 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(activeServiceLine, (serviceLine) => {
|
||||||
|
if (!serviceLine) return
|
||||||
|
const discarded = Boolean(
|
||||||
|
shareDraft.value ||
|
||||||
|
pendingAttachments.value.length ||
|
||||||
|
recording.value ||
|
||||||
|
recordingStarting.value,
|
||||||
|
)
|
||||||
|
attachmentMenuOpen.value = false
|
||||||
|
attachmentPicker.value = null
|
||||||
|
shareDraft.value = null
|
||||||
|
pendingAttachments.value = []
|
||||||
|
if (recording.value) {
|
||||||
|
discardRecording = true
|
||||||
|
cancelVoiceRecording()
|
||||||
|
} else if (recordingStarting.value) {
|
||||||
|
cleanupRecorder()
|
||||||
|
}
|
||||||
|
if (discarded) showToast(errorText('service_line_text_only'))
|
||||||
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
discardRecording = true
|
discardRecording = true
|
||||||
cleanupRecorder()
|
cleanupRecorder()
|
||||||
@@ -1614,7 +1656,7 @@ onBeforeUnmount(() => {
|
|||||||
</SkyScrollArea>
|
</SkyScrollArea>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
v-if="activeCanMessage && attachmentMenuOpen"
|
v-if="activeCanMessage && !activeServiceLine && attachmentMenuOpen"
|
||||||
class="messages-attachment-menu"
|
class="messages-attachment-menu"
|
||||||
:aria-hidden="contactDetailsOpen"
|
:aria-hidden="contactDetailsOpen"
|
||||||
:inert="contactDetailsOpen || undefined"
|
:inert="contactDetailsOpen || undefined"
|
||||||
@@ -1659,7 +1701,9 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<SkySheet
|
<SkySheet
|
||||||
class="messages-media-picker-sheet"
|
class="messages-media-picker-sheet"
|
||||||
:opened="activeCanMessage && attachmentPicker !== null"
|
:opened="
|
||||||
|
activeCanMessage && !activeServiceLine && attachmentPicker !== null
|
||||||
|
"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
phone.t(
|
phone.t(
|
||||||
attachmentPicker === 'contacts'
|
attachmentPicker === 'contacts'
|
||||||
@@ -1794,7 +1838,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="activeCanMessage && shareDraft && !recording"
|
v-if="activeCanMessage && !activeServiceLine && shareDraft && !recording"
|
||||||
class="shared-composer-preview"
|
class="shared-composer-preview"
|
||||||
:aria-hidden="contactDetailsOpen"
|
:aria-hidden="contactDetailsOpen"
|
||||||
:inert="contactDetailsOpen || undefined"
|
:inert="contactDetailsOpen || undefined"
|
||||||
@@ -1810,7 +1854,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
v-if="activeCanMessage && recording"
|
v-if="activeCanMessage && !activeServiceLine && recording"
|
||||||
class="messages-recorder"
|
class="messages-recorder"
|
||||||
:aria-hidden="contactDetailsOpen"
|
:aria-hidden="contactDetailsOpen"
|
||||||
:inert="contactDetailsOpen || undefined"
|
:inert="contactDetailsOpen || undefined"
|
||||||
@@ -1898,6 +1942,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<div class="messages-sky-composer-row">
|
<div class="messages-sky-composer-row">
|
||||||
<SkyGlass
|
<SkyGlass
|
||||||
|
v-if="!activeServiceLine"
|
||||||
component="button"
|
component="button"
|
||||||
type="button"
|
type="button"
|
||||||
class="messages-sky-messagebar__action messages-sky-messagebar__plus"
|
class="messages-sky-messagebar__action messages-sky-messagebar__plus"
|
||||||
@@ -1934,7 +1979,7 @@ onBeforeUnmount(() => {
|
|||||||
<ArrowUpCircle :size="29" :stroke-width="2.4" />
|
<ArrowUpCircle :size="29" :stroke-width="2.4" />
|
||||||
</SkyLink>
|
</SkyLink>
|
||||||
<SkyLink
|
<SkyLink
|
||||||
v-else
|
v-else-if="!activeServiceLine"
|
||||||
icon-only
|
icon-only
|
||||||
class="messages-sky-messagebar__send"
|
class="messages-sky-messagebar__send"
|
||||||
:disabled="sending || recordingStarting"
|
:disabled="sending || recordingStarting"
|
||||||
|
|||||||
@@ -1978,7 +1978,7 @@ const contacts = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
canCall: true,
|
canCall: true,
|
||||||
canMessage: false,
|
canMessage: true,
|
||||||
companyId: 'police',
|
companyId: 'police',
|
||||||
avatar_url: 'https://picsum.photos/seed/companies-police-logo/180/180',
|
avatar_url: 'https://picsum.photos/seed/companies-police-logo/180/180',
|
||||||
id: 'company:police',
|
id: 'company:police',
|
||||||
@@ -4187,7 +4187,7 @@ const companyProfiles = [
|
|||||||
availability: 'available',
|
availability: 'available',
|
||||||
availabilityUpdatedAt: isoTime(-12 * 60 * 1000),
|
availabilityUpdatedAt: isoTime(-12 * 60 * 1000),
|
||||||
canCall: true,
|
canCall: true,
|
||||||
canMessage: false,
|
canMessage: true,
|
||||||
categoryId: 'public_services',
|
categoryId: 'public_services',
|
||||||
categoryName: 'Public Services',
|
categoryName: 'Public Services',
|
||||||
coverUrl: 'https://picsum.photos/seed/companies-police-cover/900/360',
|
coverUrl: 'https://picsum.photos/seed/companies-police-cover/900/360',
|
||||||
|
|||||||
@@ -1190,7 +1190,7 @@ if IsDuplicityVersion() then
|
|||||||
Number = "911",
|
Number = "911",
|
||||||
AutoContact = true,
|
AutoContact = true,
|
||||||
CanCall = true,
|
CanCall = true,
|
||||||
CanMessage = false,
|
CanMessage = true,
|
||||||
Routing = "round_robin",
|
Routing = "round_robin",
|
||||||
MinimumGrade = 0,
|
MinimumGrade = 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -695,6 +695,7 @@ Locales["de"] = {
|
|||||||
gif_provider_rate_limited = "GIPHY ist ausgelastet. Versuch es gleich erneut.", gif_provider_failed = "GIF-Suche ist vorübergehend nicht verfügbar.",
|
gif_provider_rate_limited = "GIPHY ist ausgelastet. Versuch es gleich erneut.", gif_provider_failed = "GIF-Suche ist vorübergehend nicht verfügbar.",
|
||||||
self_message = "Du kannst deine eigene Nummer nicht melden.", recipient_not_found = "Diese Nummer ist nicht verfügbar.", blocked = "Dieser Kontakt hat Anrufe und Nachrichten von deinem SIM blockiert.",
|
self_message = "Du kannst deine eigene Nummer nicht melden.", recipient_not_found = "Diese Nummer ist nicht verfügbar.", blocked = "Dieser Kontakt hat Anrufe und Nachrichten von deinem SIM blockiert.",
|
||||||
messaging_unavailable = "Dieser Firmenkontakt akzeptiert keine Nachrichten.",
|
messaging_unavailable = "Dieser Firmenkontakt akzeptiert keine Nachrichten.",
|
||||||
|
service_line_text_only = "Firmen-Serviceleitungen akzeptieren derzeit nur Textnachrichten.",
|
||||||
no_sim = "Dieses Handy hat keine SIM-Karte.", rate_limited = "Zu viele Nachrichten. Versuch es in einer Minute erneut.",
|
no_sim = "Dieses Handy hat keine SIM-Karte.", rate_limited = "Zu viele Nachrichten. Versuch es in einer Minute erneut.",
|
||||||
request_failed = "Nachrichten sind vorübergehend nicht verfügbar.", default = "Die Nachricht konnte nicht gesendet werden.",
|
request_failed = "Nachrichten sind vorübergehend nicht verfügbar.", default = "Die Nachricht konnte nicht gesendet werden.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -695,6 +695,7 @@ Locales["en"] = {
|
|||||||
gif_provider_rate_limited = "GIPHY is busy. Try again in a moment.", gif_provider_failed = "GIF search is temporarily unavailable.",
|
gif_provider_rate_limited = "GIPHY is busy. Try again in a moment.", gif_provider_failed = "GIF search is temporarily unavailable.",
|
||||||
self_message = "You cannot message your own number.", recipient_not_found = "That number is unavailable.", blocked = "This contact has blocked calls and messages from your SIM.",
|
self_message = "You cannot message your own number.", recipient_not_found = "That number is unavailable.", blocked = "This contact has blocked calls and messages from your SIM.",
|
||||||
messaging_unavailable = "This company contact does not accept messages.",
|
messaging_unavailable = "This company contact does not accept messages.",
|
||||||
|
service_line_text_only = "Company service lines currently accept text messages only.",
|
||||||
no_sim = "This phone has no SIM card.", rate_limited = "Too many messages. Try again in a minute.",
|
no_sim = "This phone has no SIM card.", rate_limited = "Too many messages. Try again in a minute.",
|
||||||
request_failed = "Messages are temporarily unavailable.", default = "The message could not be sent.",
|
request_failed = "Messages are temporarily unavailable.", default = "The message could not be sent.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -695,6 +695,7 @@ Locales["es"] = {
|
|||||||
gif_provider_rate_limited = "GIPHY está ocupado. Inténtalo de nuevo en un momento.", gif_provider_failed = "La búsqueda de GIFs está temporalmente inaccesible.",
|
gif_provider_rate_limited = "GIPHY está ocupado. Inténtalo de nuevo en un momento.", gif_provider_failed = "La búsqueda de GIFs está temporalmente inaccesible.",
|
||||||
self_message = "No puedes enviar mensajes a tu propio número.", recipient_not_found = "Ese número no está disponible.", blocked = "Este contacto ha bloqueado las llamadas y los mensajes de esta SIM.",
|
self_message = "No puedes enviar mensajes a tu propio número.", recipient_not_found = "Ese número no está disponible.", blocked = "Este contacto ha bloqueado las llamadas y los mensajes de esta SIM.",
|
||||||
messaging_unavailable = "Este contacto de empresa no acepta mensajes.",
|
messaging_unavailable = "Este contacto de empresa no acepta mensajes.",
|
||||||
|
service_line_text_only = "Las líneas de servicio de empresa solo aceptan mensajes de texto por ahora.",
|
||||||
no_sim = "Este teléfono no tiene tarjeta SIM.", rate_limited = "Demasiados mensajes, inténtalo de nuevo en un minuto.",
|
no_sim = "Este teléfono no tiene tarjeta SIM.", rate_limited = "Demasiados mensajes, inténtalo de nuevo en un minuto.",
|
||||||
request_failed = "Los mensajes están temporalmente inaccesibles.", default = "El mensaje no se pudo enviar.",
|
request_failed = "Los mensajes están temporalmente inaccesibles.", default = "El mensaje no se pudo enviar.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -337,11 +337,6 @@ local function validate_configuration(configuration)
|
|||||||
tostring(line.Routing)
|
tostring(line.Routing)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
if line.CanMessage then
|
|
||||||
return nil, ("[sky_phone] Company '%s' enables messaging without a virtual service-line message router."):format(
|
|
||||||
company_id
|
|
||||||
)
|
|
||||||
end
|
|
||||||
line.Number = number
|
line.Number = number
|
||||||
validated_definitions[company_id] = definition
|
validated_definitions[company_id] = definition
|
||||||
validated_definition_ids[#validated_definition_ids + 1] = company_id
|
validated_definition_ids[#validated_definition_ids + 1] = company_id
|
||||||
@@ -1193,7 +1188,7 @@ end
|
|||||||
|
|
||||||
local function request_row(request_id)
|
local function request_row(request_id)
|
||||||
local rows = Bridge.Database.Query([[
|
local rows = Bridge.Database.Query([[
|
||||||
SELECT r.`id`, r.`company_id`, r.`service_id`, r.`customer_sim_id`, r.`subject`, r.`description`,
|
SELECT r.`id`, r.`company_id`, r.`service_id`, r.`channel`, r.`customer_sim_id`, r.`subject`, r.`description`,
|
||||||
r.`status`, r.`assigned_identifier`, r.`customer_unread`,
|
r.`status`, r.`assigned_identifier`, r.`customer_unread`,
|
||||||
r.`company_activity_revision`, r.`revision`,
|
r.`company_activity_revision`, r.`revision`,
|
||||||
UNIX_TIMESTAMP(r.`created_at`) AS `created_at_unix`,
|
UNIX_TIMESTAMP(r.`created_at`) AS `created_at_unix`,
|
||||||
@@ -1895,6 +1890,175 @@ local function notification_payload(kind, area, row)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function SkyPhoneCompanies.RouteServiceLineMessage(source, data)
|
||||||
|
if not Config.Companies.Enabled or type(data) ~= "table"
|
||||||
|
or data.messageType ~= "text" or not valid_uuid(data.id)
|
||||||
|
then
|
||||||
|
return { success = false, error = "invalid_request" }
|
||||||
|
end
|
||||||
|
local device, device_error = current_device(source, false)
|
||||||
|
if not device then
|
||||||
|
return device_error
|
||||||
|
end
|
||||||
|
local service_line = SkyPhoneCompanies.GetServiceLine(data.phoneNumber)
|
||||||
|
if not service_line or not service_line.canMessage then
|
||||||
|
return { success = false, error = "messaging_unavailable" }
|
||||||
|
end
|
||||||
|
local body = valid_text(
|
||||||
|
data.body,
|
||||||
|
math.min(Config.Messages.BodyMaxLength, Config.Companies.MessageMaxLength),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
if not body then
|
||||||
|
return { success = false, error = "invalid_message" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local request_id = uuid()
|
||||||
|
local created_event_id = uuid()
|
||||||
|
local status_event_id = uuid()
|
||||||
|
local mutation_token = uuid()
|
||||||
|
local statements = {
|
||||||
|
{
|
||||||
|
query = "UPDATE `sky_phone_sims` SET `updated_at` = `updated_at` WHERE `id` = ?",
|
||||||
|
params = { device.sim_id },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query = [[
|
||||||
|
INSERT INTO `sky_phone_company_requests`
|
||||||
|
(`id`, `company_id`, `channel`, `customer_sim_id`, `subject`, `description`,
|
||||||
|
`company_activity_revision`)
|
||||||
|
SELECT ?, profile.`company_id`, 'service_line', sim.`id`, sim.`phone_number`, ?, 0
|
||||||
|
FROM `sky_phone_sims` sim
|
||||||
|
INNER JOIN `sky_phone_company_profiles` profile ON profile.`company_id` = ?
|
||||||
|
WHERE sim.`id` = ? AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sky_phone_company_requests` existing
|
||||||
|
WHERE existing.`company_id` = profile.`company_id`
|
||||||
|
AND existing.`customer_sim_id` = sim.`id`
|
||||||
|
AND existing.`channel` = 'service_line'
|
||||||
|
AND existing.`status` NOT IN ('completed', 'cancelled')
|
||||||
|
)
|
||||||
|
]],
|
||||||
|
params = {
|
||||||
|
request_id,
|
||||||
|
body,
|
||||||
|
service_line.companyId,
|
||||||
|
device.sim_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query = [[
|
||||||
|
INSERT INTO `sky_phone_company_request_events`
|
||||||
|
(`id`, `request_id`, `event_type`, `actor_type`, `to_status`, `detail`)
|
||||||
|
SELECT ?, `id`, 'created', 'customer', 'new', 'service_line'
|
||||||
|
FROM `sky_phone_company_requests` WHERE `id` = ?
|
||||||
|
]],
|
||||||
|
params = { created_event_id, request_id },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query = [[
|
||||||
|
INSERT INTO `sky_phone_company_request_events`
|
||||||
|
(`id`, `request_id`, `event_type`, `actor_type`, `from_status`, `to_status`, `detail`)
|
||||||
|
SELECT ?, `id`, 'status', 'customer', 'waiting_customer', 'in_progress', 'service_line_message'
|
||||||
|
FROM `sky_phone_company_requests`
|
||||||
|
WHERE `company_id` = ? AND `customer_sim_id` = ?
|
||||||
|
AND `channel` = 'service_line' AND `status` = 'waiting_customer'
|
||||||
|
ORDER BY `updated_at` DESC, `id` DESC LIMIT 1
|
||||||
|
]],
|
||||||
|
params = { status_event_id, service_line.companyId, device.sim_id },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query = [[
|
||||||
|
UPDATE `sky_phone_company_requests`
|
||||||
|
SET `status` = IF(`status` = 'waiting_customer', 'in_progress', `status`),
|
||||||
|
`company_activity_revision` = `company_activity_revision` + 1,
|
||||||
|
`revision` = `revision` + 1, `mutation_token` = ?
|
||||||
|
WHERE `company_id` = ? AND `customer_sim_id` = ?
|
||||||
|
AND `channel` = 'service_line'
|
||||||
|
AND `status` NOT IN ('completed', 'cancelled')
|
||||||
|
ORDER BY `updated_at` DESC, `id` DESC LIMIT 1
|
||||||
|
]],
|
||||||
|
params = { mutation_token, service_line.companyId, device.sim_id },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query = [[
|
||||||
|
INSERT INTO `sky_phone_company_request_messages`
|
||||||
|
(`id`, `request_id`, `sender_type`, `sender_sim_id`, `body`)
|
||||||
|
SELECT ?, `id`, 'customer', `customer_sim_id`, ?
|
||||||
|
FROM `sky_phone_company_requests`
|
||||||
|
WHERE `mutation_token` = ? LIMIT 1
|
||||||
|
]],
|
||||||
|
params = { data.id, body, mutation_token },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query = [[
|
||||||
|
INSERT INTO `sky_phone_sms_messages`
|
||||||
|
(`id`, `sender_sim_id`, `recipient_sim_id`, `sender_number`, `recipient_number`,
|
||||||
|
`message_type`, `body`)
|
||||||
|
SELECT ?, sim.`id`, NULL, sim.`phone_number`, ?, 'text', ?
|
||||||
|
FROM `sky_phone_sims` sim
|
||||||
|
INNER JOIN `sky_phone_company_request_messages` message ON message.`id` = ?
|
||||||
|
WHERE sim.`id` = ?
|
||||||
|
]],
|
||||||
|
params = {
|
||||||
|
data.id,
|
||||||
|
service_line.number,
|
||||||
|
body,
|
||||||
|
data.id,
|
||||||
|
device.sim_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if not Bridge.Database.Transaction(statements) then
|
||||||
|
return { success = false, error = "request_failed" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local routed = Bridge.Database.Query([[
|
||||||
|
SELECT request.`id` AS `request_id`, created.`id` AS `created_event_id`
|
||||||
|
FROM `sky_phone_sms_messages` sms
|
||||||
|
INNER JOIN `sky_phone_company_request_messages` message ON message.`id` = sms.`id`
|
||||||
|
INNER JOIN `sky_phone_company_requests` request ON request.`id` = message.`request_id`
|
||||||
|
LEFT JOIN `sky_phone_company_request_events` created
|
||||||
|
ON created.`id` = ? AND created.`request_id` = request.`id`
|
||||||
|
WHERE sms.`id` = ? LIMIT 1
|
||||||
|
]], { created_event_id, data.id })
|
||||||
|
if not routed[1] then
|
||||||
|
Bridge.Debug(
|
||||||
|
"error",
|
||||||
|
"[sky_phone] Service-line message %s committed without a complete route.",
|
||||||
|
tostring(data.id),
|
||||||
|
{ always = true }
|
||||||
|
)
|
||||||
|
return { success = false, error = "request_failed" }
|
||||||
|
end
|
||||||
|
local row = request_row(routed[1].request_id)
|
||||||
|
if not row then
|
||||||
|
return { success = false, error = "request_failed" }
|
||||||
|
end
|
||||||
|
|
||||||
|
emit_request_change(row, true, true, source)
|
||||||
|
if routed[1].created_event_id then
|
||||||
|
notify_company(row.company_id, "sky_phone:companies:notification", notification_payload(
|
||||||
|
"newRequest",
|
||||||
|
"work",
|
||||||
|
row
|
||||||
|
), source)
|
||||||
|
elseif row.assigned_identifier then
|
||||||
|
notify_identifier(
|
||||||
|
row.assigned_identifier,
|
||||||
|
row.company_id,
|
||||||
|
"sky_phone:companies:notification",
|
||||||
|
notification_payload("newMessage", "work", row)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
success = true,
|
||||||
|
data = {
|
||||||
|
messageId = data.id,
|
||||||
|
requestId = row.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
Bridge.Callbacks.Register("sky_phone:companies:create-request", function(source, data)
|
Bridge.Callbacks.Register("sky_phone:companies:create-request", function(source, data)
|
||||||
local allowed, rate_error = allow_mutation(source, "create_request", "CreateRequest")
|
local allowed, rate_error = allow_mutation(source, "create_request", "CreateRequest")
|
||||||
if not allowed then
|
if not allowed then
|
||||||
@@ -2160,6 +2324,11 @@ Bridge.Callbacks.Register("sky_phone:companies:send-message", function(source, d
|
|||||||
then
|
then
|
||||||
return { success = false, error = "invalid_status" }
|
return { success = false, error = "invalid_status" }
|
||||||
end
|
end
|
||||||
|
local service_line = access.row.channel == "service_line"
|
||||||
|
and SkyPhoneCompanies.GetServiceLineForCompany(access.row.company_id) or nil
|
||||||
|
if access.row.channel == "service_line" and (not service_line or not service_line.canMessage) then
|
||||||
|
return { success = false, error = "messaging_unavailable" }
|
||||||
|
end
|
||||||
local message_id = uuid()
|
local message_id = uuid()
|
||||||
local mutation_token = uuid()
|
local mutation_token = uuid()
|
||||||
local new_status = access.audience == "customer" and access.row.status == "waiting_customer"
|
local new_status = access.audience == "customer" and access.row.status == "waiting_customer"
|
||||||
@@ -2199,6 +2368,37 @@ Bridge.Callbacks.Register("sky_phone:companies:send-message", function(source, d
|
|||||||
params = { message_id, access.member.identifier, body, request_id, revision + 1, mutation_token },
|
params = { message_id, access.member.identifier, body, request_id, revision + 1, mutation_token },
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
if service_line then
|
||||||
|
if access.audience == "customer" then
|
||||||
|
statements[#statements + 1] = {
|
||||||
|
query = [[
|
||||||
|
INSERT INTO `sky_phone_sms_messages`
|
||||||
|
(`id`, `sender_sim_id`, `recipient_sim_id`, `sender_number`, `recipient_number`,
|
||||||
|
`message_type`, `body`)
|
||||||
|
SELECT message.`id`, request.`customer_sim_id`, NULL, sim.`phone_number`, ?, 'text', message.`body`
|
||||||
|
FROM `sky_phone_company_request_messages` message
|
||||||
|
INNER JOIN `sky_phone_company_requests` request ON request.`id` = message.`request_id`
|
||||||
|
INNER JOIN `sky_phone_sims` sim ON sim.`id` = request.`customer_sim_id`
|
||||||
|
WHERE message.`id` = ? AND message.`sender_type` = 'customer'
|
||||||
|
]],
|
||||||
|
params = { service_line.number, message_id },
|
||||||
|
}
|
||||||
|
else
|
||||||
|
statements[#statements + 1] = {
|
||||||
|
query = [[
|
||||||
|
INSERT INTO `sky_phone_sms_messages`
|
||||||
|
(`id`, `sender_sim_id`, `recipient_sim_id`, `sender_number`, `recipient_number`,
|
||||||
|
`message_type`, `body`)
|
||||||
|
SELECT message.`id`, NULL, request.`customer_sim_id`, ?, sim.`phone_number`, 'text', message.`body`
|
||||||
|
FROM `sky_phone_company_request_messages` message
|
||||||
|
INNER JOIN `sky_phone_company_requests` request ON request.`id` = message.`request_id`
|
||||||
|
INNER JOIN `sky_phone_sims` sim ON sim.`id` = request.`customer_sim_id`
|
||||||
|
WHERE message.`id` = ? AND message.`sender_type` = 'company'
|
||||||
|
]],
|
||||||
|
params = { service_line.number, message_id },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
if new_status ~= access.row.status then
|
if new_status ~= access.row.status then
|
||||||
statements[#statements + 1] = {
|
statements[#statements + 1] = {
|
||||||
query = [[
|
query = [[
|
||||||
@@ -2213,16 +2413,32 @@ Bridge.Callbacks.Register("sky_phone:companies:send-message", function(source, d
|
|||||||
if not Bridge.Database.Transaction(statements) then
|
if not Bridge.Database.Transaction(statements) then
|
||||||
return { success = false, error = "request_failed" }
|
return { success = false, error = "request_failed" }
|
||||||
end
|
end
|
||||||
local inserted = Bridge.Database.Query(
|
local inserted = Bridge.Database.Query([[
|
||||||
"SELECT `id` FROM `sky_phone_company_request_messages` WHERE `id` = ? LIMIT 1",
|
SELECT message.`id`, sms.`id` AS `sms_id`
|
||||||
{ message_id }
|
FROM `sky_phone_company_request_messages` message
|
||||||
)
|
LEFT JOIN `sky_phone_sms_messages` sms ON sms.`id` = message.`id`
|
||||||
|
WHERE message.`id` = ? LIMIT 1
|
||||||
|
]], { message_id })
|
||||||
if not inserted[1] then
|
if not inserted[1] then
|
||||||
return { success = false, error = "revision_conflict" }
|
return { success = false, error = "revision_conflict" }
|
||||||
end
|
end
|
||||||
|
if service_line and not inserted[1].sms_id then
|
||||||
|
Bridge.Debug(
|
||||||
|
"error",
|
||||||
|
"[sky_phone] Company request message %s committed without its service-line SMS.",
|
||||||
|
tostring(message_id),
|
||||||
|
{ always = true }
|
||||||
|
)
|
||||||
|
return { success = false, error = "request_failed" }
|
||||||
|
end
|
||||||
local row = request_row(request_id)
|
local row = request_row(request_id)
|
||||||
emit_request_change(row, true, true, source)
|
emit_request_change(row, true, true, source)
|
||||||
if access.audience == "customer" then
|
if access.audience == "customer" then
|
||||||
|
if service_line then
|
||||||
|
TriggerClientEvent("sky_phone:messages:changed", source, {
|
||||||
|
phoneNumber = service_line.number,
|
||||||
|
})
|
||||||
|
end
|
||||||
local notification = notification_payload("newMessage", "work", row)
|
local notification = notification_payload("newMessage", "work", row)
|
||||||
if row.assigned_identifier then
|
if row.assigned_identifier then
|
||||||
notify_identifier(
|
notify_identifier(
|
||||||
@@ -2232,6 +2448,12 @@ Bridge.Callbacks.Register("sky_phone:companies:send-message", function(source, d
|
|||||||
notification
|
notification
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
elseif service_line then
|
||||||
|
notify_sim(row.customer_sim_id, "sky_phone:messages:new", {
|
||||||
|
phoneNumber = service_line.number,
|
||||||
|
sender = service_line.name,
|
||||||
|
voice = false,
|
||||||
|
})
|
||||||
else
|
else
|
||||||
notify_sim(row.customer_sim_id, "sky_phone:companies:notification", notification_payload(
|
notify_sim(row.customer_sim_id, "sky_phone:companies:notification", notification_payload(
|
||||||
"newMessage",
|
"newMessage",
|
||||||
|
|||||||
@@ -2710,6 +2710,7 @@ local schema = {
|
|||||||
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||||
{ name = "company_id", type = "VARCHAR(64) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
{ name = "company_id", type = "VARCHAR(64) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||||
{ name = "service_id", type = "VARCHAR(64) NULL", characterSet = "ascii", collation = "ascii_bin" },
|
{ name = "service_id", type = "VARCHAR(64) NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||||
|
{ name = "channel", type = "ENUM('app','service_line') NOT NULL DEFAULT 'app'" },
|
||||||
{ name = "customer_sim_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
{ name = "customer_sim_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||||
{ name = "subject", type = "VARCHAR(120) NOT NULL" },
|
{ name = "subject", type = "VARCHAR(120) NOT NULL" },
|
||||||
{ name = "description", type = "VARCHAR(2000) NOT NULL" },
|
{ name = "description", type = "VARCHAR(2000) NOT NULL" },
|
||||||
@@ -2729,6 +2730,7 @@ local schema = {
|
|||||||
{ name = "idx_sky_phone_company_requests_customer", columns = "(`customer_sim_id`, `updated_at`, `id`)" },
|
{ name = "idx_sky_phone_company_requests_customer", columns = "(`customer_sim_id`, `updated_at`, `id`)" },
|
||||||
{ name = "idx_sky_phone_company_requests_queue", columns = "(`company_id`, `status`, `updated_at`, `id`)" },
|
{ name = "idx_sky_phone_company_requests_queue", columns = "(`company_id`, `status`, `updated_at`, `id`)" },
|
||||||
{ name = "idx_sky_phone_company_requests_assignee", columns = "(`company_id`, `assigned_identifier`, `status`)" },
|
{ name = "idx_sky_phone_company_requests_assignee", columns = "(`company_id`, `assigned_identifier`, `status`)" },
|
||||||
|
{ name = "idx_sky_phone_company_requests_service_line", columns = "(`company_id`, `customer_sim_id`, `channel`, `status`, `updated_at`, `id`)" },
|
||||||
},
|
},
|
||||||
foreignKeys = {
|
foreignKeys = {
|
||||||
{ column = "company_id", references = "`sky_phone_company_profiles` (`company_id`) ON DELETE CASCADE" },
|
{ column = "company_id", references = "`sky_phone_company_profiles` (`company_id`) ON DELETE CASCADE" },
|
||||||
|
|||||||
@@ -97,6 +97,14 @@ local function current_device(source)
|
|||||||
return device
|
return device
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function resolve_message_recipient(value)
|
||||||
|
local service_line = SkyPhoneCompanies.GetServiceLine(value)
|
||||||
|
if service_line then
|
||||||
|
return service_line.number, service_line
|
||||||
|
end
|
||||||
|
return SkyPhoneSimNumber.Normalize(value, Config.Sim.NumberLength, Config.Sim.NumberPrefix), nil
|
||||||
|
end
|
||||||
|
|
||||||
local function shared_contact(device, contact_id)
|
local function shared_contact(device, contact_id)
|
||||||
if type(contact_id) ~= "string" or contact_id == "" or #contact_id > 36 then
|
if type(contact_id) ~= "string" or contact_id == "" or #contact_id > 36 then
|
||||||
return nil, "invalid_contact"
|
return nil, "invalid_contact"
|
||||||
@@ -366,7 +374,7 @@ Bridge.Callbacks.Register("sky_phone:messages:thread", function(source, data)
|
|||||||
if not device then
|
if not device then
|
||||||
return error_response
|
return error_response
|
||||||
end
|
end
|
||||||
local number = SkyPhoneSimNumber.Normalize(data.phoneNumber, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
|
local number = resolve_message_recipient(data.phoneNumber)
|
||||||
if not number then
|
if not number then
|
||||||
return { success = false, error = "invalid_number" }
|
return { success = false, error = "invalid_number" }
|
||||||
end
|
end
|
||||||
@@ -407,11 +415,7 @@ Bridge.Callbacks.Register("sky_phone:messages:delete", function(source, data)
|
|||||||
local numbers = {}
|
local numbers = {}
|
||||||
local seen = {}
|
local seen = {}
|
||||||
for index = 1, #data.phoneNumbers do
|
for index = 1, #data.phoneNumbers do
|
||||||
local number = SkyPhoneSimNumber.Normalize(
|
local number = resolve_message_recipient(data.phoneNumbers[index])
|
||||||
data.phoneNumbers[index],
|
|
||||||
Config.Sim.NumberLength,
|
|
||||||
Config.Sim.NumberPrefix
|
|
||||||
)
|
|
||||||
if not number then
|
if not number then
|
||||||
return { success = false, error = "invalid_number" }
|
return { success = false, error = "invalid_number" }
|
||||||
end
|
end
|
||||||
@@ -483,7 +487,7 @@ Bridge.Callbacks.Register("sky_phone:messages:send", function(source, data)
|
|||||||
if not device then
|
if not device then
|
||||||
return error_response
|
return error_response
|
||||||
end
|
end
|
||||||
local number = SkyPhoneSimNumber.Normalize(data.phoneNumber, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
|
local number, service_line = resolve_message_recipient(data.phoneNumber)
|
||||||
if not number then
|
if not number then
|
||||||
return { success = false, error = "invalid_number" }
|
return { success = false, error = "invalid_number" }
|
||||||
end
|
end
|
||||||
@@ -536,6 +540,42 @@ Bridge.Callbacks.Register("sky_phone:messages:send", function(source, data)
|
|||||||
else
|
else
|
||||||
return { success = false, error = "invalid_request" }
|
return { success = false, error = "invalid_request" }
|
||||||
end
|
end
|
||||||
|
if service_line then
|
||||||
|
if not service_line.canMessage then
|
||||||
|
return { success = false, error = "messaging_unavailable" }
|
||||||
|
end
|
||||||
|
if message_type ~= "text" then
|
||||||
|
return { success = false, error = "service_line_text_only" }
|
||||||
|
end
|
||||||
|
local id = uuid()
|
||||||
|
local routed = SkyPhoneCompanies.RouteServiceLineMessage(source, {
|
||||||
|
id = id,
|
||||||
|
body = body,
|
||||||
|
messageType = message_type,
|
||||||
|
phoneNumber = number,
|
||||||
|
})
|
||||||
|
if not routed or not routed.success then
|
||||||
|
return routed or { success = false, error = "request_failed" }
|
||||||
|
end
|
||||||
|
local rows = Bridge.Database.Query([[
|
||||||
|
SELECT `id`, `sender_number`, `recipient_number`, `message_type`, `body`,
|
||||||
|
`media_payload`, `media_mime`, `media_duration_ms`, `media_waveform`,
|
||||||
|
`read_at`, `created_at`, 'sent' AS `direction`
|
||||||
|
FROM `sky_phone_sms_messages` WHERE `id` = ? LIMIT 1
|
||||||
|
]], { id })
|
||||||
|
if not rows[1] then
|
||||||
|
Bridge.Debug(
|
||||||
|
"error",
|
||||||
|
"[sky_phone] Routed service-line SMS %s could not be read back.",
|
||||||
|
tostring(id),
|
||||||
|
{ always = true }
|
||||||
|
)
|
||||||
|
return { success = false, error = "request_failed" }
|
||||||
|
end
|
||||||
|
local message = format_message(rows[1])
|
||||||
|
TriggerClientEvent("sky_phone:messages:changed", source, { phoneNumber = number })
|
||||||
|
return { success = true, data = message }
|
||||||
|
end
|
||||||
local recipients = Bridge.Database.Query([[
|
local recipients = Bridge.Database.Query([[
|
||||||
SELECT s.`id`, s.`phone_number`
|
SELECT s.`id`, s.`phone_number`
|
||||||
FROM `sky_phone_sims` s
|
FROM `sky_phone_sims` s
|
||||||
|
|||||||
@@ -1289,8 +1289,8 @@ local function migrate_police_request_defaults()
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function migrate_unsupported_company_message_defaults()
|
local function migrate_police_service_line_messaging()
|
||||||
local migration_name = "sky-phone:configurator:unsupported-company-message-defaults:v1"
|
local migration_name = "sky-phone:configurator:service-line-messaging:v2"
|
||||||
local completed = Bridge.Database.Query(
|
local completed = Bridge.Database.Query(
|
||||||
"SELECT 1 FROM `sky_phone_migrations` WHERE `name` = ? LIMIT 1",
|
"SELECT 1 FROM `sky_phone_migrations` WHERE `name` = ? LIMIT 1",
|
||||||
{ migration_name }
|
{ migration_name }
|
||||||
@@ -1301,22 +1301,17 @@ local function migrate_unsupported_company_message_defaults()
|
|||||||
|
|
||||||
local row = read_stored_row()
|
local row = read_stored_row()
|
||||||
local config_payload = decode_payload(row.config_payload, "config")
|
local config_payload = decode_payload(row.config_payload, "config")
|
||||||
local definitions = config_payload.Companies
|
local police = config_payload.Companies
|
||||||
and config_payload.Companies.Definitions
|
and config_payload.Companies.Definitions
|
||||||
local migrated_companies = {}
|
and config_payload.Companies.Definitions.police
|
||||||
if type(definitions) == "table" then
|
local line = type(police) == "table" and police.ServiceLine or nil
|
||||||
for company_id, definition in pairs(definitions) do
|
local migrated = type(line) == "table" and line.CanMessage ~= true
|
||||||
local line = type(definition) == "table" and definition.ServiceLine or nil
|
if migrated then
|
||||||
if type(line) == "table" and line.CanMessage == true then
|
line.CanMessage = true
|
||||||
line.CanMessage = false
|
|
||||||
migrated_companies[#migrated_companies + 1] = tostring(company_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
table.sort(migrated_companies)
|
|
||||||
|
|
||||||
local statements = {}
|
local statements = {}
|
||||||
if #migrated_companies > 0 then
|
if migrated then
|
||||||
statements[#statements + 1] = {
|
statements[#statements + 1] = {
|
||||||
query = ([[
|
query = ([[
|
||||||
UPDATE `%s`
|
UPDATE `%s`
|
||||||
@@ -1334,13 +1329,13 @@ local function migrate_unsupported_company_message_defaults()
|
|||||||
params = {
|
params = {
|
||||||
migration_name,
|
migration_name,
|
||||||
"sky-phone",
|
"sky-phone",
|
||||||
json.encode({ companies = migrated_companies }),
|
json.encode({ police = migrated }),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if not Bridge.Database.Transaction(statements) then
|
if not Bridge.Database.Transaction(statements) then
|
||||||
error("[sky_phone] Could not migrate unsupported Phone Configurator service-line messaging defaults.")
|
error("[sky_phone] Could not enable Phone Configurator police service-line messaging.")
|
||||||
end
|
end
|
||||||
if #migrated_companies == 0 then
|
if not migrated then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1350,8 +1345,7 @@ local function migrate_unsupported_company_message_defaults()
|
|||||||
SkyPhoneConfigurator.Broadcast(-1)
|
SkyPhoneConfigurator.Broadcast(-1)
|
||||||
Bridge.Debug(
|
Bridge.Debug(
|
||||||
"info",
|
"info",
|
||||||
"[sky_phone] Disabled unsupported Phone Configurator service-line messaging for: %s",
|
"[sky_phone] Enabled Phone Configurator police service-line messaging.",
|
||||||
table.concat(migrated_companies, ", "),
|
|
||||||
{ always = true }
|
{ always = true }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -1378,7 +1372,7 @@ apply_stored_row(read_stored_row())
|
|||||||
apply_runtime_configuration()
|
apply_runtime_configuration()
|
||||||
Bridge.Database.AfterMigration("sky_phone", migrate_blank_company_definitions)
|
Bridge.Database.AfterMigration("sky_phone", migrate_blank_company_definitions)
|
||||||
Bridge.Database.AfterMigration("sky_phone", migrate_police_request_defaults)
|
Bridge.Database.AfterMigration("sky_phone", migrate_police_request_defaults)
|
||||||
Bridge.Database.AfterMigration("sky_phone", migrate_unsupported_company_message_defaults)
|
Bridge.Database.AfterMigration("sky_phone", migrate_police_service_line_messaging)
|
||||||
|
|
||||||
function SkyPhoneConfigurator.GetAdminData()
|
function SkyPhoneConfigurator.GetAdminData()
|
||||||
local data = build_admin_data()
|
local data = build_admin_data()
|
||||||
|
|||||||
@@ -1175,7 +1175,7 @@ if IsDuplicityVersion() then
|
|||||||
Number = "911",
|
Number = "911",
|
||||||
AutoContact = true,
|
AutoContact = true,
|
||||||
CanCall = true,
|
CanCall = true,
|
||||||
CanMessage = false,
|
CanMessage = true,
|
||||||
Routing = "round_robin",
|
Routing = "round_robin",
|
||||||
MinimumGrade = 0,
|
MinimumGrade = 0,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user