Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voice Inbound #474

Merged
merged 7 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/sweet-camels-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@sw-internal/playground-realtime-api': minor
'@signalwire/core': minor
'@signalwire/realtime-api': minor
---

Add ability to receive inbound Calls in the `Voice` namespace.
6 changes: 6 additions & 0 deletions internal/playground-realtime-api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
HOST=example.domain.com
PROJECT=xxx
TOKEN=yyy
RELAY_CONTEXT=default
FROM_NUMBER=+1xxx
TO_NUMBER=+1yyy
34 changes: 23 additions & 11 deletions internal/playground-realtime-api/src/voice/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,47 @@ import { Voice } from '@signalwire/realtime-api'
async function run() {
try {
const client = new Voice.Client({
// @ts-expect-error
host: process.env.HOST || 'relay.swire.io',
project: process.env.PROJECT as string,
token: process.env.TOKEN as string,
contexts: [process.env.RELAY_CONTEXT as string],
// logLevel: 'trace',
// debug: {
// logWsTraffic: true,
// },
})

// call.on('call.created', () => {})
client.on('call.received', async (call) => {
console.log('Got call', call.id, call.from, call.to, call.direction)

try {
await call.answer()
console.log('Inbound call answered', call)
setTimeout(async () => {
console.log('Terminating the call')
await call.hangup()
console.log('Call terminated!')
}, 3000)
} catch (error) {
console.error('Error answering inbound call', error)
}
})

try {
const call = await client.dial({
devices: [
[
{
type: 'phone',
to: '+12083660792',
from: '+15183601338',
to: process.env.TO_NUMBER as string,
from: process.env.FROM_NUMBER as string,
timeout: 30,
},
],
],
})

console.log('Dial resolved!')

setTimeout(async () => {
console.log('Terminating the call')
await call.hangup()
console.log('Call terminated!')
}, 3000)
console.log('Dial resolved!', call)
} catch (e) {
console.log('---> E', JSON.stringify(e, null, 2))
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/redux/features/shared/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const findNamespaceInPayload = (action: PubSubAction): string => {
} else if (isChatEvent(action)) {
return ''
} else if (isVoiceCallEvent(action)) {
return action.payload.tag
return action.payload.tag ?? ''
}

if ('development' === process.env.NODE_ENV) {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { CantinaEvent } from './cantina'
import type { ChatEvent } from './chat'
import type { TaskEvent } from './task'
import type { MessagingEvent } from './messaging'
import type { VoiceCallEvent } from './voice'

export interface SwEvent {
event_channel: string
Expand Down Expand Up @@ -119,6 +120,7 @@ export type SwEventParams =
| ChatEvent
| TaskEvent
| MessagingEvent
| VoiceCallEvent

// prettier-ignore
export type PubSubChannelEvents =
Expand Down
129 changes: 113 additions & 16 deletions packages/core/src/types/voiceCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import type {
type ToInternalVoiceEvent<T extends string> = `${VoiceNamespace}.${T}`
export type VoiceNamespace = typeof PRODUCT_PREFIX_VOICE_CALL

/**
* Private event types
*/
export type CallDial = 'call.dial'
export type CallState = 'call.state'
export type CallReceive = 'call.receive'

/**
* Public event types
*/
Expand Down Expand Up @@ -74,10 +81,17 @@ export type VoiceCallDisconnectReason =
*/
export interface VoiceCallContract<T = any> {
/** Unique id for this voice call */
id: string
readonly id: string
/** @ignore */
tag: string
/** @ignore */
callId: string
/** @ignore */
nodeId: string

dial(params?: VoiceCallDialMethodParams): Promise<T>
hangup(reason?: VoiceCallDisconnectReason): Promise<void>
answer(): Promise<T>
}

/**
Expand Down Expand Up @@ -107,38 +121,121 @@ export type InternalVoiceCallEntity = {
* ==========
*/

interface VoiceCallStateEvent {
interface CallingCallPhoneDevice {
type: 'phone'
params: {
from_number: string
to_number: string
timeout: number
max_duration: number
}
}

interface CallingCallSIPDevice {
type: 'sip'
params: {
from: string
from_name?: string
to: string
timeout?: number
max_duration?: number
headers?: SipHeader[]
codecs?: SipCodec[]
webrtc_media?: boolean
}
}

type CallingCallDevice = CallingCallPhoneDevice | CallingCallSIPDevice

interface CallingCall {
call_id: string
call_state: 'created' | 'ringing' | 'answered' | 'ending' | 'ended'
context?: string
tag?: string
direction: 'inbound' | 'outbound'
device: CallingCallDevice
node_id: string
segment_id: string
}

interface CallingCallDial extends CallingCall {
dial_winner: 'true' | 'false'
}

/**
* 'calling.call.dial'
*/
export interface CallingCallDialEventParams {
node_id: string
tag: string
dial_state: 'dialing' | 'answered' | 'failed'
call?: CallingCallDial
}

export interface CallingCallDialEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallDial>
params: CallingCallDialEventParams
}

/**
* 'voice.call.created'
* 'calling.call.state'
*/
export interface VoiceCallCreatedEventParams extends VoiceCallStateEvent {}
export interface CallingCallStateEventParams extends CallingCall {}

export interface VoiceCallCreatedEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallCreated>
params: VoiceCallCreatedEventParams
export interface CallingCallStateEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallState>
params: CallingCallStateEventParams
}

/**
* 'voice.call.ended'
* 'calling.call.receive'
*/
export interface VoiceCallEndedEventParams extends VoiceCallStateEvent {}
export interface CallingCallReceiveEventParams extends CallingCall {}

export interface VoiceCallEndedEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallEnded>
params: VoiceCallEndedEventParams
export interface CallingCallReceiveEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallReceive>
params: CallingCallReceiveEventParams
}

export type VoiceCallEvent = VoiceCallCreatedEvent | VoiceCallEndedEvent
// interface VoiceCallStateEvent {
// call_id: string
// node_id: string
// tag: string
// }

// /**
// * 'voice.call.created'
// */
// export interface VoiceCallCreatedEventParams extends VoiceCallStateEvent {}

// export interface VoiceCallCreatedEvent extends SwEvent {
// event_type: ToInternalVoiceEvent<CallCreated>
// params: VoiceCallCreatedEventParams
// }

// /**
// * 'voice.call.ended'
// */
// export interface VoiceCallEndedEventParams extends VoiceCallStateEvent {}

// export interface VoiceCallEndedEvent extends SwEvent {
// event_type: ToInternalVoiceEvent<CallEnded>
// params: VoiceCallEndedEventParams
// }

export type VoiceCallEvent =
| CallingCallDialEvent
| CallingCallStateEvent
| CallingCallReceiveEvent

export type VoiceCallEventParams =
| VoiceCallCreatedEventParams
| VoiceCallEndedEventParams
| CallingCallDialEventParams
| CallingCallStateEventParams
| CallingCallReceiveEventParams

export type VoiceCallAction = MapToPubSubShape<VoiceCallEvent>

export type VoiceCallJSONRPCMethod = 'calling.dial' | 'calling.end'
export type VoiceCallJSONRPCMethod =
| 'calling.dial'
| 'calling.end'
| 'calling.answer'
10 changes: 9 additions & 1 deletion packages/core/src/utils/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,12 +417,20 @@ export type InternalChannels = {
swEventChannel: SwEventChannel
}

export type SDKWorkerParams<T> = {
export interface SDKWorkerParams<T> {
channels: InternalChannels
instance: T
runSaga: any
/**
* TODO: rename these optional args with something more explicit or
* create derived types of `SDKWorkerParams` with specific arguments (?)
*/
payload?: any
resolve?(value: unknown): void
reject?(reason?: any): void
actionFilterHandler?(): boolean
}

export type SDKWorker<T> = (params: SDKWorkerParams<T>) => SagaIterator<any>

export interface SDKWorkerDefinition {
Expand Down
12 changes: 9 additions & 3 deletions packages/realtime-api/src/types/voice.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { CallCreated, CallEnded } from '@signalwire/core'
// import type { CallCreated, CallEnded } from '@signalwire/core'
import type { Call } from '../voice/Call'

// Record<
// CallCreated | CallEnded,
// (call: Call) => void
// >

// TODO: replace `any` with proper types.
export type RealTimeCallApiEventsHandlerMapping = Record<
CallCreated | CallEnded,
(params: any) => void
'call.received',
(call: Call) => void
>

export type RealTimeCallApiEvents = {
Expand Down
Loading